mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 08:26:29 +00:00
Merge pull request #3447 from akhilmhdh/feat/assume-role
Implemented project permission impersonation
This commit is contained in:
Vendored
+3
@@ -5,6 +5,7 @@ import { Redis } from "ioredis";
|
|||||||
import { TUsers } from "@app/db/schemas";
|
import { TUsers } from "@app/db/schemas";
|
||||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||||
|
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service";
|
||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
@@ -109,6 +110,7 @@ declare module "@fastify/request-context" {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||||
|
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +243,7 @@ declare module "fastify" {
|
|||||||
kmipOperation: TKmipOperationServiceFactory;
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
gateway: TGatewayServiceFactory;
|
gateway: TGatewayServiceFactory;
|
||||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||||
|
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:projectId/assume-privileges",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
actorType: z.enum([ActorType.USER, ActorType.IDENTITY]),
|
||||||
|
actorId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req, res) => {
|
||||||
|
if (req.auth.authMode === AuthMode.JWT) {
|
||||||
|
const payload = await server.services.assumePrivileges.assumeProjectPrivileges({
|
||||||
|
targetActorType: req.body.actorType,
|
||||||
|
targetActorId: req.body.actorId,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
actorPermissionDetails: req.permission,
|
||||||
|
tokenVersionId: req.auth.tokenVersionId
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
void res.setCookie("infisical-project-assume-privileges", payload.assumePrivilegesToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 3600 // 1 hour in seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START,
|
||||||
|
metadata: {
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
requesterEmail: req.auth.user.username,
|
||||||
|
requesterId: req.auth.user.id,
|
||||||
|
targetActorType: req.body.actorType,
|
||||||
|
targetActorId: req.body.actorId,
|
||||||
|
duration: "1hr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully assumed role" };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: "Invalid auth mode" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:projectId/assume-privileges",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const assumedPrivilegeDetails = requestContext.get("assumedPrivilegeDetails");
|
||||||
|
if (req.auth.authMode === AuthMode.JWT && assumedPrivilegeDetails) {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
void res.setCookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
expires: new Date(0)
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END,
|
||||||
|
metadata: {
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
requesterEmail: req.auth.user.username,
|
||||||
|
requesterId: req.auth.user.id,
|
||||||
|
targetActorId: assumedPrivilegeDetails.actorId,
|
||||||
|
targetActorType: assumedPrivilegeDetails.actorType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { message: "Successfully exited assumed role" };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: "Invalid auth mode" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat
|
|||||||
|
|
||||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||||
|
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
@@ -45,6 +46,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await projectRouter.register(registerProjectRoleRouter);
|
await projectRouter.register(registerProjectRoleRouter);
|
||||||
await projectRouter.register(registerProjectRouter);
|
await projectRouter.register(registerProjectRouter);
|
||||||
await projectRouter.register(registerTrustedIpRouter);
|
await projectRouter.register(registerTrustedIpRouter);
|
||||||
|
await projectRouter.register(registerAssumePrivilegeRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { packRules } from "@casl/ability/extra";
|
import { packRules } from "@casl/ability/extra";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
import {
|
import {
|
||||||
backfillPermissionV1SchemaToV2Schema,
|
backfillPermissionV1SchemaToV2Schema,
|
||||||
ProjectPermissionV1Schema
|
ProjectPermissionV1Schema
|
||||||
@@ -245,13 +245,22 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
membership: ProjectMembershipsSchema.extend({
|
membership: z.object({
|
||||||
|
id: z.string(),
|
||||||
roles: z
|
roles: z
|
||||||
.object({
|
.object({
|
||||||
role: z.string()
|
role: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
}),
|
}),
|
||||||
|
assumedPrivilegeDetails: z
|
||||||
|
.object({
|
||||||
|
actorId: z.string(),
|
||||||
|
actorType: z.string(),
|
||||||
|
actorName: z.string(),
|
||||||
|
actorEmail: z.string().optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -259,14 +268,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permissions, membership } = await server.services.projectRole.getUserPermission(
|
const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission(
|
||||||
req.permission.id,
|
req.permission.id,
|
||||||
req.params.projectId,
|
req.params.projectId,
|
||||||
req.permission.authMethod,
|
req.permission.authMethod,
|
||||||
req.permission.orgId
|
req.permission.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data: { permissions, membership } };
|
return {
|
||||||
|
data: {
|
||||||
|
permissions,
|
||||||
|
membership,
|
||||||
|
assumedPrivilegeDetails
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionIdentityActions,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "../permission/project-permission";
|
||||||
|
import { TAssumeProjectPrivilegeDTO } from "./assume-privilege-types";
|
||||||
|
|
||||||
|
type TAssumePrivilegeServiceFactoryDep = {
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAssumePrivilegeServiceFactory = ReturnType<typeof assumePrivilegeServiceFactory>;
|
||||||
|
|
||||||
|
export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => {
|
||||||
|
const assumeProjectPrivileges = async ({
|
||||||
|
targetActorType,
|
||||||
|
targetActorId,
|
||||||
|
projectId,
|
||||||
|
actorPermissionDetails,
|
||||||
|
tokenVersionId
|
||||||
|
}: TAssumeProjectPrivilegeDTO) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actorPermissionDetails.type,
|
||||||
|
actorId: actorPermissionDetails.id,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod: actorPermissionDetails.authMethod,
|
||||||
|
actorOrgId: actorPermissionDetails.orgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetActorType === ActorType.USER) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionMemberActions.AssumePrivileges,
|
||||||
|
ProjectPermissionSub.Member
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionIdentityActions.AssumePrivileges,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check entity is part of project
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor: targetActorType,
|
||||||
|
actorId: targetActorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod: actorPermissionDetails.authMethod,
|
||||||
|
actorOrgId: actorPermissionDetails.orgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const assumePrivilegesToken = jwt.sign(
|
||||||
|
{
|
||||||
|
tokenVersionId,
|
||||||
|
actorType: targetActorType,
|
||||||
|
actorId: targetActorId,
|
||||||
|
projectId,
|
||||||
|
requesterId: actorPermissionDetails.id
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{ expiresIn: "1hr" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as {
|
||||||
|
tokenVersionId: string;
|
||||||
|
projectId: string;
|
||||||
|
requesterId: string;
|
||||||
|
actorType: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
if (decodedToken.tokenVersionId !== tokenVersionId) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Invalid token version" });
|
||||||
|
}
|
||||||
|
return decodedToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
assumeProjectPrivileges,
|
||||||
|
verifyAssumePrivilegeToken
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export type TAssumeProjectPrivilegeDTO = {
|
||||||
|
targetActorType: ActorType.USER | ActorType.IDENTITY;
|
||||||
|
targetActorId: string;
|
||||||
|
projectId: string;
|
||||||
|
tokenVersionId: string;
|
||||||
|
actorPermissionDetails: OrgServiceActor;
|
||||||
|
};
|
||||||
@@ -320,7 +320,9 @@ export enum EventType {
|
|||||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||||
|
|
||||||
PROJECT_ACCESS_REQUEST = "project-access-request"
|
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||||
|
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||||
|
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterableSecretEvents: EventType[] = [
|
export const filterableSecretEvents: EventType[] = [
|
||||||
@@ -2454,6 +2456,29 @@ interface ProjectAccessRequestEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProjectAssumePrivilegesEvent {
|
||||||
|
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START;
|
||||||
|
metadata: {
|
||||||
|
projectId: string;
|
||||||
|
requesterId: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
targetActorType: ActorType;
|
||||||
|
targetActorId: string;
|
||||||
|
duration: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectAssumePrivilegesExitEvent {
|
||||||
|
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END;
|
||||||
|
metadata: {
|
||||||
|
projectId: string;
|
||||||
|
requesterId: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
targetActorType: ActorType;
|
||||||
|
targetActorId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SetupKmipEvent {
|
interface SetupKmipEvent {
|
||||||
type: EventType.SETUP_KMIP;
|
type: EventType.SETUP_KMIP;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2759,6 +2784,8 @@ export type Event =
|
|||||||
| KmipOperationLocateEvent
|
| KmipOperationLocateEvent
|
||||||
| KmipOperationRegisterEvent
|
| KmipOperationRegisterEvent
|
||||||
| ProjectAccessRequestEvent
|
| ProjectAccessRequestEvent
|
||||||
|
| ProjectAssumePrivilegesEvent
|
||||||
|
| ProjectAssumePrivilegesExitEvent
|
||||||
| CreateSecretRequestEvent
|
| CreateSecretRequestEvent
|
||||||
| SecretApprovalRequestReview
|
| SecretApprovalRequestReview
|
||||||
| GetSecretRotationsEvent
|
| GetSecretRotationsEvent
|
||||||
|
|||||||
@@ -551,13 +551,26 @@ export const permissionServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getProjectPermission = async <T extends ActorType>({
|
const getProjectPermission = async <T extends ActorType>({
|
||||||
actor,
|
actor: inputActor,
|
||||||
actorId,
|
actorId: inputActorId,
|
||||||
projectId,
|
projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType
|
actionProjectType
|
||||||
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
|
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
|
||||||
|
let actor = inputActor;
|
||||||
|
let actorId = inputActorId;
|
||||||
|
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
|
||||||
|
if (
|
||||||
|
assumedPrivilegeDetailsCtx &&
|
||||||
|
actor === ActorType.USER &&
|
||||||
|
actorId === assumedPrivilegeDetailsCtx.requesterId &&
|
||||||
|
projectId === assumedPrivilegeDetailsCtx.projectId
|
||||||
|
) {
|
||||||
|
actor = assumedPrivilegeDetailsCtx.actorType;
|
||||||
|
actorId = assumedPrivilegeDetailsCtx.actorId;
|
||||||
|
}
|
||||||
|
|
||||||
switch (actor) {
|
switch (actor) {
|
||||||
case ActorType.USER:
|
case ActorType.USER:
|
||||||
return getUserProjectPermission({
|
return getUserProjectPermission({
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export enum ProjectPermissionIdentityActions {
|
|||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges",
|
||||||
|
AssumePrivileges = "assume-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionMemberActions {
|
export enum ProjectPermissionMemberActions {
|
||||||
@@ -58,7 +59,8 @@ export enum ProjectPermissionMemberActions {
|
|||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges",
|
||||||
|
AssumePrivileges = "assume-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionGroupActions {
|
export enum ProjectPermissionGroupActions {
|
||||||
@@ -714,7 +716,8 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionMemberActions.Edit,
|
ProjectPermissionMemberActions.Edit,
|
||||||
ProjectPermissionMemberActions.Delete,
|
ProjectPermissionMemberActions.Delete,
|
||||||
ProjectPermissionMemberActions.Read,
|
ProjectPermissionMemberActions.Read,
|
||||||
ProjectPermissionMemberActions.GrantPrivileges
|
ProjectPermissionMemberActions.GrantPrivileges,
|
||||||
|
ProjectPermissionMemberActions.AssumePrivileges
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Member
|
ProjectPermissionSub.Member
|
||||||
);
|
);
|
||||||
@@ -736,7 +739,8 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionIdentityActions.Edit,
|
ProjectPermissionIdentityActions.Edit,
|
||||||
ProjectPermissionIdentityActions.Delete,
|
ProjectPermissionIdentityActions.Delete,
|
||||||
ProjectPermissionIdentityActions.Read,
|
ProjectPermissionIdentityActions.Read,
|
||||||
ProjectPermissionIdentityActions.GrantPrivileges
|
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||||
|
ProjectPermissionIdentityActions.AssumePrivileges
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Identity
|
ProjectPermissionSub.Identity
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
import fp from "fastify-plugin";
|
||||||
|
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const injectAssumePrivilege = fp(async (server: FastifyZodProvider) => {
|
||||||
|
server.addHook("onRequest", async (req, res) => {
|
||||||
|
const assumeRoleCookie = req.cookies["infisical-project-assume-privileges"];
|
||||||
|
try {
|
||||||
|
if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) {
|
||||||
|
const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken(
|
||||||
|
assumeRoleCookie,
|
||||||
|
req.auth.tokenVersionId
|
||||||
|
);
|
||||||
|
if (decodedToken) {
|
||||||
|
requestContext.set("assumedPrivilegeDetails", decodedToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
req.log.error({ error }, "Failed to verify assume privilege token");
|
||||||
|
void res.clearCookie("infisical-project-assume-privileges");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-appr
|
|||||||
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
||||||
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
||||||
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||||
|
import { assumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service";
|
||||||
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||||
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
@@ -248,6 +249,7 @@ import { workflowIntegrationDALFactory } from "@app/services/workflow-integratio
|
|||||||
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||||
|
|
||||||
import { injectAuditLogInfo } from "../plugins/audit-log";
|
import { injectAuditLogInfo } from "../plugins/audit-log";
|
||||||
|
import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege";
|
||||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||||
@@ -427,6 +429,11 @@ export const registerRoutes = async (
|
|||||||
serviceTokenDAL,
|
serviceTokenDAL,
|
||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
const assumePrivilegeService = assumePrivilegeServiceFactory({
|
||||||
|
projectDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const licenseService = licenseServiceFactory({
|
const licenseService = licenseServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
@@ -1022,7 +1029,9 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
identityDAL,
|
||||||
|
userDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotService = secretSnapshotServiceFactory({
|
const snapshotService = secretSnapshotServiceFactory({
|
||||||
@@ -1675,7 +1684,8 @@ export const registerRoutes = async (
|
|||||||
kmip: kmipService,
|
kmip: kmipService,
|
||||||
kmipOperation: kmipOperationService,
|
kmipOperation: kmipOperationService,
|
||||||
gateway: gatewayService,
|
gateway: gatewayService,
|
||||||
secretRotationV2: secretRotationV2Service
|
secretRotationV2: secretRotationV2Service,
|
||||||
|
assumePrivileges: assumePrivilegeService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
@@ -1696,6 +1706,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||||
|
await server.register(injectAssumePrivilege);
|
||||||
await server.register(injectPermission);
|
await server.register(injectPermission);
|
||||||
await server.register(injectRateLimits);
|
await server.register(injectRateLimits);
|
||||||
await server.register(injectAuditLogInfo);
|
await server.register(injectAuditLogInfo);
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
secure: appCfg.HTTPS_ENABLED
|
secure: appCfg.HTTPS_ENABLED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void res.cookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
return { message: "Successfully logged out" };
|
return { message: "Successfully logged out" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,6 +77,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
secure: cfg.HTTPS_ENABLED
|
secure: cfg.HTTPS_ENABLED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void res.cookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: cfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
return { token: tokens.access, isMfaEnabled: false };
|
return { token: tokens.access, isMfaEnabled: false };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -131,6 +139,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
secure: appCfg.HTTPS_ENABLED
|
secure: appCfg.HTTPS_ENABLED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void res.cookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptionVersion: data.user.encryptionVersion,
|
encryptionVersion: data.user.encryptionVersion,
|
||||||
token: data.token.access,
|
token: data.token.access,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -12,10 +13,12 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
|
|
||||||
import { ActorAuthMethod } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||||
import { getPredefinedRoles } from "./project-role-fns";
|
import { getPredefinedRoles } from "./project-role-fns";
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +32,8 @@ import {
|
|||||||
|
|
||||||
type TProjectRoleServiceFactoryDep = {
|
type TProjectRoleServiceFactoryDep = {
|
||||||
projectRoleDAL: TProjectRoleDALFactory;
|
projectRoleDAL: TProjectRoleDALFactory;
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "findById">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||||
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
||||||
@@ -47,7 +52,9 @@ export const projectRoleServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
identityDAL,
|
||||||
|
userDAL
|
||||||
}: TProjectRoleServiceFactoryDep) => {
|
}: TProjectRoleServiceFactoryDep) => {
|
||||||
const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => {
|
const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => {
|
||||||
let projectId = "";
|
let projectId = "";
|
||||||
@@ -220,14 +227,42 @@ export const projectRoleServiceFactory = ({
|
|||||||
actorAuthMethod: ActorAuthMethod,
|
actorAuthMethod: ActorAuthMethod,
|
||||||
actorOrgId: string | undefined
|
actorOrgId: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const { permission, membership } = await permissionService.getUserProjectPermission({
|
const { permission, membership } = await permissionService.getProjectPermission({
|
||||||
userId,
|
actor: ActorType.USER,
|
||||||
|
actorId: userId,
|
||||||
projectId,
|
projectId,
|
||||||
authMethod: actorAuthMethod,
|
actorAuthMethod,
|
||||||
userOrgId: actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
return { permissions: packRules(permission.rules), membership };
|
// just to satisfy ts
|
||||||
|
if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" });
|
||||||
|
|
||||||
|
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
|
||||||
|
const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === projectId;
|
||||||
|
const assumedPrivilegeDetails = isAssumingPrivilege
|
||||||
|
? {
|
||||||
|
actorId: assumedPrivilegeDetailsCtx?.actorId,
|
||||||
|
actorType: assumedPrivilegeDetailsCtx?.actorType,
|
||||||
|
actorName: "",
|
||||||
|
actorEmail: ""
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) {
|
||||||
|
const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId);
|
||||||
|
if (!identityDetails)
|
||||||
|
throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` });
|
||||||
|
assumedPrivilegeDetails.actorName = identityDetails.name;
|
||||||
|
} else if (assumedPrivilegeDetails?.actorType === ActorType.USER) {
|
||||||
|
const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId);
|
||||||
|
if (!userDetails)
|
||||||
|
throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` });
|
||||||
|
assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`;
|
||||||
|
assumedPrivilegeDetails.actorEmail = userDetails?.email || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { permissions: packRules(permission.rules), membership, assumedPrivilegeDetails };
|
||||||
};
|
};
|
||||||
|
|
||||||
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
|
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { FormControl } from "../FormControl";
|
||||||
|
import { Input } from "../Input";
|
||||||
|
import { Modal, ModalClose, ModalContent } from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
onChange?: (isOpen: boolean) => void;
|
||||||
|
confirmKey: string;
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
onConfirmed: () => Promise<void>;
|
||||||
|
buttonText?: string;
|
||||||
|
formContent?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
confirmationMessage?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfirmActionModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onChange,
|
||||||
|
confirmKey,
|
||||||
|
onConfirmed,
|
||||||
|
title,
|
||||||
|
subTitle = "This action is irreversible.",
|
||||||
|
buttonText = "Yes",
|
||||||
|
formContent,
|
||||||
|
confirmationMessage,
|
||||||
|
children
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const [inputData, setInputData] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useToggle();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputData("");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const onDelete = async () => {
|
||||||
|
setIsLoading.on();
|
||||||
|
try {
|
||||||
|
await onConfirmed();
|
||||||
|
} finally {
|
||||||
|
setIsLoading.off();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={(isOpenState) => {
|
||||||
|
setInputData("");
|
||||||
|
if (onChange) onChange(isOpenState);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={title}
|
||||||
|
subTitle={subTitle}
|
||||||
|
footerContent={
|
||||||
|
<div className="mx-2 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
isDisabled={!(confirmKey === inputData) || isLoading}
|
||||||
|
onClick={onDelete}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{formContent}
|
||||||
|
<form
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (confirmKey === inputData) onDelete();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
label={
|
||||||
|
<div className="break-words pb-2 text-sm">
|
||||||
|
{confirmationMessage || (
|
||||||
|
<>
|
||||||
|
Type <span className="font-bold">{confirmKey}</span> to perform this action
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={inputData}
|
||||||
|
onChange={(e) => setInputData(e.target.value)}
|
||||||
|
placeholder={`Type ${confirmKey} here`}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ConfirmActionModal } from "./ConfirmActionModal";
|
||||||
@@ -14,7 +14,7 @@ export const PageHeader = ({ title, description, children, className }: Props) =
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
|
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">{children}</div>
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-gray-400">{description}</div>
|
<div className="mt-2 text-gray-400">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export * from "./Breadcrumb";
|
|||||||
export * from "./Button";
|
export * from "./Button";
|
||||||
export * from "./Card";
|
export * from "./Card";
|
||||||
export * from "./Checkbox";
|
export * from "./Checkbox";
|
||||||
|
export * from "./ConfirmActionModal";
|
||||||
export * from "./ContentLoader";
|
export * from "./ContentLoader";
|
||||||
export * from "./DatePicker";
|
export * from "./DatePicker";
|
||||||
export * from "./DeleteActionModal";
|
export * from "./DeleteActionModal";
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ export const useProjectPermission = () => {
|
|||||||
strict: false,
|
strict: false,
|
||||||
select: (el) => el?.projectId
|
select: (el) => el?.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { permission, membership }
|
data: { permission, membership, assumedPrivilegeDetails }
|
||||||
} = useSuspenseQuery({
|
} = useSuspenseQuery({
|
||||||
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId: projectId }),
|
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId: projectId }),
|
||||||
queryFn: () => fetchUserProjectPermissions({ workspaceId: projectId }),
|
queryFn: () => fetchUserProjectPermissions({ workspaceId: projectId }),
|
||||||
@@ -29,6 +30,7 @@ export const useProjectPermission = () => {
|
|||||||
const ability = evaluatePermissionsAbility(rule);
|
const ability = evaluatePermissionsAbility(rule);
|
||||||
return {
|
return {
|
||||||
permission: ability,
|
permission: ability,
|
||||||
|
assumedPrivilegeDetails: data.assumedPrivilegeDetails,
|
||||||
membership: {
|
membership: {
|
||||||
...data.membership,
|
...data.membership,
|
||||||
roles: data.membership.roles.map(({ role }) => role)
|
roles: data.membership.roles.map(({ role }) => role)
|
||||||
@@ -42,5 +44,5 @@ export const useProjectPermission = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { permission, membership, hasProjectRole };
|
return { permission, membership, hasProjectRole, assumedPrivilegeDetails };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export enum ProjectPermissionIdentityActions {
|
|||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges",
|
||||||
|
AssumePrivileges = "assume-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionMemberActions {
|
export enum ProjectPermissionMemberActions {
|
||||||
@@ -66,7 +67,8 @@ export enum ProjectPermissionMemberActions {
|
|||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges",
|
||||||
|
AssumePrivileges = "assume-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionGroupActions {
|
export enum ProjectPermissionGroupActions {
|
||||||
@@ -247,7 +249,7 @@ export type ProjectPermissionSet =
|
|||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
| [ProjectPermissionMemberActions, ProjectPermissionSub.Member]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||||
@@ -258,7 +260,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionIdentityActions,
|
||||||
(
|
(
|
||||||
| ProjectPermissionSub.Identity
|
| ProjectPermissionSub.Identity
|
||||||
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useAssumeProjectPrivileges, useRemoveAssumeProjectPrivilege } from "./mutations";
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TProjectAssumePrivilegesDTO } from "./types";
|
||||||
|
|
||||||
|
export const useAssumeProjectPrivileges = () =>
|
||||||
|
useMutation({
|
||||||
|
mutationFn: async ({ projectId, actorId, actorType }: TProjectAssumePrivilegesDTO) => {
|
||||||
|
const { data } = await apiRequest.post<{ message: string }>(
|
||||||
|
`/api/v1/workspace/${projectId}/assume-privileges`,
|
||||||
|
{ actorId, actorType }
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useRemoveAssumeProjectPrivilege = () =>
|
||||||
|
useMutation({
|
||||||
|
mutationFn: async ({ projectId }: { projectId: string }) => {
|
||||||
|
const { data } = await apiRequest.delete<{ message: string }>(
|
||||||
|
`/api/v1/workspace/${projectId}/assume-privileges`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ActorType } from "../auditLogs/enums";
|
||||||
|
|
||||||
|
export type TProjectAssumePrivilegesDTO = {
|
||||||
|
projectId: string;
|
||||||
|
actorType: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from "./accessApproval";
|
export * from "./accessApproval";
|
||||||
export * from "./admin";
|
export * from "./admin";
|
||||||
export * from "./apiKeys";
|
export * from "./apiKeys";
|
||||||
|
export * from "./assumePrivileges";
|
||||||
export * from "./auditLogs";
|
export * from "./auditLogs";
|
||||||
export * from "./auditLogStreams";
|
export * from "./auditLogStreams";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/type
|
|||||||
import { groupBy } from "@app/lib/fn/array";
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
import { omit } from "@app/lib/fn/object";
|
import { omit } from "@app/lib/fn/object";
|
||||||
|
|
||||||
|
import { ActorType } from "../auditLogs/enums";
|
||||||
import { OrgUser, TProjectMembership } from "../users/types";
|
import { OrgUser, TProjectMembership } from "../users/types";
|
||||||
import {
|
import {
|
||||||
TGetUserOrgPermissionsDTO,
|
TGetUserOrgPermissionsDTO,
|
||||||
@@ -137,6 +138,12 @@ export const fetchUserProjectPermissions = async ({
|
|||||||
data: {
|
data: {
|
||||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||||
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
|
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
|
||||||
|
assumedPrivilegeDetails?: {
|
||||||
|
actorId: string;
|
||||||
|
actorType: ActorType;
|
||||||
|
actorEmail: string;
|
||||||
|
actorName: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
|
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
TBreadcrumbFormat
|
TBreadcrumbFormat
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
|
useProjectPermission,
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
|
||||||
import { ProjectSelect } from "./components/ProjectSelect";
|
import { ProjectSelect } from "./components/ProjectSelect";
|
||||||
|
|
||||||
// This is a generic layout shared by all types of projects.
|
// This is a generic layout shared by all types of projects.
|
||||||
@@ -35,6 +37,7 @@ export const ProjectLayout = () => {
|
|||||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
@@ -68,6 +71,7 @@ export const ProjectLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||||
|
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||||
|
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
||||||
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
|
||||||
|
export const AssumePrivilegeModeBanner = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege();
|
||||||
|
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||||
|
|
||||||
|
if (!assumedPrivilegeDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="z-10 -mx-4 flex items-center justify-center gap-2 rounded border border-mineshaft-600 bg-primary-400 p-2 text-mineshaft-800 shadow">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="mr-2" />
|
||||||
|
You are currently viewing the project with privileges of{" "}
|
||||||
|
<b>
|
||||||
|
{assumedPrivilegeDetails?.actorType === ActorType.IDENTITY ? "identity" : "user"}{" "}
|
||||||
|
{assumedPrivilegeDetails?.actorName}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="hover:bg-mineshaft-500"
|
||||||
|
onClick={() => {
|
||||||
|
exitAssumePrivilegeMode.mutate(
|
||||||
|
{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Click to exit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AssumePrivilegeModeBanner } from "./AssumePrivilegeModeBanner";
|
||||||
+2
-1
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
@@ -68,7 +69,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
|||||||
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionMemberActions.Edit,
|
||||||
ProjectPermissionSub.Member
|
ProjectPermissionSub.Member
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -37,6 +37,7 @@ import {
|
|||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -88,7 +89,8 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
] as const);
|
] as const);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled =
|
const isMemberEditDisabled =
|
||||||
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege;
|
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member) &&
|
||||||
|
Boolean(privilege);
|
||||||
|
|
||||||
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
||||||
const requestAccess = useCreateAccessRequest();
|
const requestAccess = useCreateAccessRequest();
|
||||||
|
|||||||
@@ -6,13 +6,27 @@ import { formatRelative } from "date-fns";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
|
import {
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
Button,
|
||||||
|
ConfirmActionModal,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
PageHeader,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionIdentityActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
|
useAssumeProjectPrivileges,
|
||||||
useDeleteIdentityFromWorkspace,
|
useDeleteIdentityFromWorkspace,
|
||||||
useGetWorkspaceIdentityMembershipDetails
|
useGetWorkspaceIdentityMembershipDetails
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
|
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
|
||||||
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection";
|
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection";
|
||||||
@@ -35,8 +49,28 @@ const Page = () => {
|
|||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"deleteIdentity",
|
"deleteIdentity",
|
||||||
"upgradePlan"
|
"assumePrivileges"
|
||||||
] as const);
|
] as const);
|
||||||
|
const assumePrivileges = useAssumeProjectPrivileges();
|
||||||
|
|
||||||
|
const handleAssumePrivileges = async () => {
|
||||||
|
assumePrivileges.mutate(
|
||||||
|
{
|
||||||
|
actorId: identityId,
|
||||||
|
actorType: ActorType.IDENTITY,
|
||||||
|
projectId: workspaceId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Identity privilege assumption has started"
|
||||||
|
});
|
||||||
|
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const onRemoveIdentitySubmit = async () => {
|
const onRemoveIdentitySubmit = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -100,6 +134,24 @@ const Page = () => {
|
|||||||
>
|
>
|
||||||
Copy Membership ID
|
Copy Membership ID
|
||||||
</Button>
|
</Button>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionIdentityActions.AssumePrivileges}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Assume privileges of the user"
|
||||||
|
passThrough={false}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => handlePopUpOpen("assumePrivileges")}
|
||||||
|
>
|
||||||
|
Assume Privileges
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Identity, {
|
a={subject(ProjectPermissionSub.Identity, {
|
||||||
@@ -137,6 +189,15 @@ const Page = () => {
|
|||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
onDeleteApproved={() => onRemoveIdentitySubmit()}
|
onDeleteApproved={() => onRemoveIdentitySubmit()}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmActionModal
|
||||||
|
isOpen={popUp.assumePrivileges.isOpen}
|
||||||
|
confirmKey="assume"
|
||||||
|
title="Do you want to assume privileges of this identity?"
|
||||||
|
subTitle="This will set your privileges to those of the identity for the next hour."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
|
||||||
|
onConfirmed={handleAssumePrivileges}
|
||||||
|
buttonText="Confirm"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState title="Error: Unable to find the identity." className="py-12" />
|
<EmptyState title="Error: Unable to find the identity." className="py-12" />
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -97,7 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
|||||||
});
|
});
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isIdentityEditDisabled = permission.cannot(
|
const isIdentityEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionIdentityActions.Edit,
|
||||||
subject(ProjectPermissionSub.Identity, { identityId })
|
subject(ProjectPermissionSub.Identity, { identityId })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -22,7 +22,12 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionIdentityActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission
|
||||||
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteIdentityProjectAdditionalPrivilege } from "@app/hooks/api";
|
import { useDeleteIdentityProjectAdditionalPrivilege } from "@app/hooks/api";
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
@@ -83,7 +88,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
|||||||
identityId={identityId}
|
identityId={identityId}
|
||||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||||
isDisabled={permission.cannot(
|
isDisabled={permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionIdentityActions.Edit,
|
||||||
subject(ProjectPermissionSub.Identity, {
|
subject(ProjectPermissionSub.Identity, {
|
||||||
identityId
|
identityId
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-1
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -66,7 +67,7 @@ export const IdentityRoleModify = ({ identityProjectMembership }: Props) => {
|
|||||||
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isIdentityEditDisabled = permission.cannot(
|
const isIdentityEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionIdentityActions.Edit,
|
||||||
ProjectPermissionSub.Identity
|
ProjectPermissionSub.Identity
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,28 @@ import { formatRelative } from "date-fns";
|
|||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
ConfirmActionModal,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
PageHeader,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteUserFromWorkspace, useGetWorkspaceUserDetails } from "@app/hooks/api";
|
import {
|
||||||
|
useAssumeProjectPrivileges,
|
||||||
|
useDeleteUserFromWorkspace,
|
||||||
|
useGetWorkspaceUserDetails
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
|
||||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||||
@@ -35,12 +48,34 @@ export const Page = () => {
|
|||||||
|
|
||||||
const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } =
|
const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } =
|
||||||
useDeleteUserFromWorkspace();
|
useDeleteUserFromWorkspace();
|
||||||
|
const assumePrivileges = useAssumeProjectPrivileges();
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"removeMember",
|
"removeMember",
|
||||||
"upgradePlan"
|
"upgradePlan",
|
||||||
|
"assumePrivileges"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const handleAssumePrivileges = async () => {
|
||||||
|
const { userId } = popUp?.assumePrivileges?.data as { userId: string };
|
||||||
|
assumePrivileges.mutate(
|
||||||
|
{
|
||||||
|
actorId: userId,
|
||||||
|
actorType: ActorType.USER,
|
||||||
|
projectId: workspaceId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "User privilege assumption has started"
|
||||||
|
});
|
||||||
|
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemoveUser = async () => {
|
const handleRemoveUser = async () => {
|
||||||
if (!currentOrg?.id || !currentWorkspace?.id || !membershipDetails?.user?.username) return;
|
if (!currentOrg?.id || !currentWorkspace?.id || !membershipDetails?.user?.username) return;
|
||||||
|
|
||||||
@@ -91,7 +126,29 @@ export const Page = () => {
|
|||||||
description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`}
|
description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`}
|
||||||
>
|
>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Assume privileges of the user"
|
||||||
|
passThrough={false}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
isLoading={assumePrivileges.isPending}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Assume Privileges
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionMemberActions.Delete}
|
||||||
a={ProjectPermissionSub.Member}
|
a={ProjectPermissionSub.Member}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Remove from project"
|
allowedLabel="Remove from project"
|
||||||
@@ -128,6 +185,15 @@ export const Page = () => {
|
|||||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||||
onDeleteApproved={handleRemoveUser}
|
onDeleteApproved={handleRemoveUser}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmActionModal
|
||||||
|
isOpen={popUp.assumePrivileges.isOpen}
|
||||||
|
confirmKey="assume"
|
||||||
|
title="Do you want to assume privileges of this user?"
|
||||||
|
subTitle="This will set your privileges to those of the user for the next hour."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
|
||||||
|
onConfirmed={handleAssumePrivileges}
|
||||||
|
buttonText="Confirm"
|
||||||
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useUser
|
useUser
|
||||||
@@ -90,7 +91,7 @@ export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: P
|
|||||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
isOwnProjectMembershipDetails ||
|
isOwnProjectMembershipDetails ||
|
||||||
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member)
|
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
+2
-2
@@ -31,7 +31,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -95,7 +95,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
|||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionMemberActions.Edit,
|
||||||
ProjectPermissionSub.Member
|
ProjectPermissionSub.Member
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
@@ -69,7 +70,7 @@ export const MemberRoleModify = ({ projectMember, onOpenUpgradeModal }: Props) =
|
|||||||
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionMemberActions.Edit,
|
||||||
ProjectPermissionSub.Member
|
ProjectPermissionSub.Member
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -92,7 +92,8 @@ const MemberPolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionMemberActions.Create]: z.boolean().optional(),
|
[ProjectPermissionMemberActions.Create]: z.boolean().optional(),
|
||||||
[ProjectPermissionMemberActions.Edit]: z.boolean().optional(),
|
[ProjectPermissionMemberActions.Edit]: z.boolean().optional(),
|
||||||
[ProjectPermissionMemberActions.Delete]: z.boolean().optional(),
|
[ProjectPermissionMemberActions.Delete]: z.boolean().optional(),
|
||||||
[ProjectPermissionMemberActions.GrantPrivileges]: z.boolean().optional()
|
[ProjectPermissionMemberActions.GrantPrivileges]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionMemberActions.AssumePrivileges]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const IdentityPolicyActionSchema = z.object({
|
const IdentityPolicyActionSchema = z.object({
|
||||||
@@ -100,7 +101,8 @@ const IdentityPolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionIdentityActions.Create]: z.boolean().optional(),
|
[ProjectPermissionIdentityActions.Create]: z.boolean().optional(),
|
||||||
[ProjectPermissionIdentityActions.Edit]: z.boolean().optional(),
|
[ProjectPermissionIdentityActions.Edit]: z.boolean().optional(),
|
||||||
[ProjectPermissionIdentityActions.Delete]: z.boolean().optional(),
|
[ProjectPermissionIdentityActions.Delete]: z.boolean().optional(),
|
||||||
[ProjectPermissionIdentityActions.GrantPrivileges]: z.boolean().optional()
|
[ProjectPermissionIdentityActions.GrantPrivileges]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionIdentityActions.AssumePrivileges]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const GroupPolicyActionSchema = z.object({
|
const GroupPolicyActionSchema = z.object({
|
||||||
@@ -577,6 +579,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const canEdit = action.includes(ProjectPermissionMemberActions.Edit);
|
const canEdit = action.includes(ProjectPermissionMemberActions.Edit);
|
||||||
const canDelete = action.includes(ProjectPermissionMemberActions.Delete);
|
const canDelete = action.includes(ProjectPermissionMemberActions.Delete);
|
||||||
const canGrantPrivileges = action.includes(ProjectPermissionMemberActions.GrantPrivileges);
|
const canGrantPrivileges = action.includes(ProjectPermissionMemberActions.GrantPrivileges);
|
||||||
|
const canAssumePrivileges = action.includes(ProjectPermissionMemberActions.AssumePrivileges);
|
||||||
|
|
||||||
if (!formVal[subject]) formVal[subject] = [{}];
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
@@ -587,6 +590,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
if (canDelete) formVal[subject]![0][ProjectPermissionMemberActions.Delete] = true;
|
if (canDelete) formVal[subject]![0][ProjectPermissionMemberActions.Delete] = true;
|
||||||
if (canGrantPrivileges)
|
if (canGrantPrivileges)
|
||||||
formVal[subject]![0][ProjectPermissionMemberActions.GrantPrivileges] = true;
|
formVal[subject]![0][ProjectPermissionMemberActions.GrantPrivileges] = true;
|
||||||
|
if (canAssumePrivileges)
|
||||||
|
formVal[subject]![0][ProjectPermissionMemberActions.AssumePrivileges] = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +601,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const canEdit = action.includes(ProjectPermissionIdentityActions.Edit);
|
const canEdit = action.includes(ProjectPermissionIdentityActions.Edit);
|
||||||
const canDelete = action.includes(ProjectPermissionIdentityActions.Delete);
|
const canDelete = action.includes(ProjectPermissionIdentityActions.Delete);
|
||||||
const canGrantPrivileges = action.includes(ProjectPermissionIdentityActions.GrantPrivileges);
|
const canGrantPrivileges = action.includes(ProjectPermissionIdentityActions.GrantPrivileges);
|
||||||
|
const canAssumePrivileges = action.includes(
|
||||||
|
ProjectPermissionIdentityActions.AssumePrivileges
|
||||||
|
);
|
||||||
|
|
||||||
if (!formVal[subject]) formVal[subject] = [{ conditions: [] }];
|
if (!formVal[subject]) formVal[subject] = [{ conditions: [] }];
|
||||||
|
|
||||||
@@ -606,6 +614,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
if (canDelete) formVal[subject]![0][ProjectPermissionIdentityActions.Delete] = true;
|
if (canDelete) formVal[subject]![0][ProjectPermissionIdentityActions.Delete] = true;
|
||||||
if (canGrantPrivileges)
|
if (canGrantPrivileges)
|
||||||
formVal[subject]![0][ProjectPermissionIdentityActions.GrantPrivileges] = true;
|
formVal[subject]![0][ProjectPermissionIdentityActions.GrantPrivileges] = true;
|
||||||
|
if (canAssumePrivileges)
|
||||||
|
formVal[subject]![0][ProjectPermissionIdentityActions.AssumePrivileges] = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,7 +914,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Add", value: ProjectPermissionMemberActions.Create },
|
{ label: "Add", value: ProjectPermissionMemberActions.Create },
|
||||||
{ label: "Modify", value: ProjectPermissionMemberActions.Edit },
|
{ label: "Modify", value: ProjectPermissionMemberActions.Edit },
|
||||||
{ label: "Remove", value: ProjectPermissionMemberActions.Delete },
|
{ label: "Remove", value: ProjectPermissionMemberActions.Delete },
|
||||||
{ label: "Grant Privileges", value: ProjectPermissionMemberActions.GrantPrivileges }
|
{ label: "Grant Privileges", value: ProjectPermissionMemberActions.GrantPrivileges },
|
||||||
|
{ label: "Assume Privileges", value: ProjectPermissionMemberActions.AssumePrivileges }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.Identity]: {
|
[ProjectPermissionSub.Identity]: {
|
||||||
@@ -914,7 +925,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Add", value: ProjectPermissionIdentityActions.Create },
|
{ label: "Add", value: ProjectPermissionIdentityActions.Create },
|
||||||
{ label: "Modify", value: ProjectPermissionIdentityActions.Edit },
|
{ label: "Modify", value: ProjectPermissionIdentityActions.Edit },
|
||||||
{ label: "Remove", value: ProjectPermissionIdentityActions.Delete },
|
{ label: "Remove", value: ProjectPermissionIdentityActions.Delete },
|
||||||
{ label: "Grant Privileges", value: ProjectPermissionIdentityActions.GrantPrivileges }
|
{ label: "Grant Privileges", value: ProjectPermissionIdentityActions.GrantPrivileges },
|
||||||
|
{ label: "Assume Privileges", value: ProjectPermissionIdentityActions.AssumePrivileges }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.Groups]: {
|
[ProjectPermissionSub.Groups]: {
|
||||||
|
|||||||
+5
-2
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
@@ -289,7 +289,10 @@ export const AccessApprovalRequest = ({
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
|
{!!permission.can(
|
||||||
|
ProjectPermissionMemberActions.Read,
|
||||||
|
ProjectPermissionSub.Member
|
||||||
|
) && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
+5
-2
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useUser,
|
useUser,
|
||||||
@@ -171,7 +171,10 @@ export const SecretApprovalRequest = () => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
|
{!!permission.can(
|
||||||
|
ProjectPermissionMemberActions.Read,
|
||||||
|
ProjectPermissionSub.Member
|
||||||
|
) && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user