diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 1f1bb353e..26be26036 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -110,7 +110,7 @@ declare module "@fastify/request-context" { }; }; identityPermissionMetadata?: Record; // filled by permission service - projectAssumeRole?: { userId: string; actorId: string; actorType: ActorType; projectId: string }; + assumedProjectRole: { requesterId: string; actorId: string; actorType: ActorType; projectId: string }; } } diff --git a/backend/src/ee/routes/v1/assume-privilege-router.ts b/backend/src/ee/routes/v1/assume-privilege-router.ts index 31ca3c542..1f5e3e2ee 100644 --- a/backend/src/ee/routes/v1/assume-privilege-router.ts +++ b/backend/src/ee/routes/v1/assume-privilege-router.ts @@ -6,6 +6,7 @@ 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"; +import { requestContext } from "@fastify/request-context"; export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -32,10 +33,10 @@ export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) handler: async (req, res) => { if (req.auth.authMode === AuthMode.JWT) { const payload = await server.services.assumePrivileges.assumeProjectPrivileges({ - actorType: req.body.actorType, - actorId: req.body.actorId, + targetActorType: req.body.actorType, + targetActorId: req.body.actorId, projectId: req.params.projectId, - projectPermission: req.permission, + actorPermissionDetails: req.permission, tokenVersionId: req.auth.tokenVersionId }); @@ -44,20 +45,22 @@ export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) httpOnly: true, path: "/", sameSite: "strict", - secure: appCfg.HTTPS_ENABLED + 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, + 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 + targetActorId: req.body.actorId, + duration: "1hr" } } }); @@ -87,24 +90,28 @@ export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req, res) => { - if (req.auth.authMode === AuthMode.JWT) { + const assumePrivilege = requestContext.get("assumedProjectRole"); + if (req.auth.authMode === AuthMode.JWT && assumePrivilege) { const appCfg = getConfig(); void res.setCookie("infisical-project-assume-privileges", "", { httpOnly: true, path: "/", sameSite: "strict", - secure: appCfg.HTTPS_ENABLED + 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_EXIT, + type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END, metadata: { projectId: req.params.projectId, requesterEmail: req.auth.user.username, - requesterId: req.auth.user.id + requesterId: req.auth.user.id, + targetActorId: assumePrivilege.actorId, + targetActorType: assumePrivilege.actorType } } }); diff --git a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts index b2c513af9..709ce44b6 100644 --- a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts +++ b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts @@ -3,7 +3,7 @@ import jwt from "jsonwebtoken"; import { ActionProjectType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { NotFoundError } from "@app/lib/errors"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -24,23 +24,24 @@ export type TAssumePrivilegeServiceFactory = ReturnType { const assumeProjectPrivileges = async ({ - actorType, - actorId, + targetActorType, + targetActorId, projectId, - projectPermission, + 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: projectPermission.type, - actorId: projectPermission.id, + actor: actorPermissionDetails.type, + actorId: actorPermissionDetails.id, projectId, - actorAuthMethod: projectPermission.authMethod, - actorOrgId: projectPermission.orgId, + actorAuthMethod: actorPermissionDetails.authMethod, + actorOrgId: actorPermissionDetails.orgId, actionProjectType: ActionProjectType.Any }); - if (actorType === ActorType.USER) { + + if (targetActorType === ActorType.USER) { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionMemberActions.AssumePrivileges, ProjectPermissionSub.Member @@ -54,11 +55,11 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: // check entity is part of project await permissionService.getProjectPermission({ - actor: actorType, - actorId, + actor: targetActorType, + actorId: targetActorId, projectId, - actorAuthMethod: projectPermission.authMethod, - actorOrgId: projectPermission.orgId, + actorAuthMethod: actorPermissionDetails.authMethod, + actorOrgId: actorPermissionDetails.orgId, actionProjectType: ActionProjectType.Any }); @@ -66,16 +67,16 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: const assumePrivilegesToken = jwt.sign( { tokenVersionId, - actorType, - actorId, + actorType: targetActorType, + actorId: targetActorId, projectId, - userId: projectPermission.id + requesterId: actorPermissionDetails.id }, appCfg.AUTH_SECRET, { expiresIn: "1hr" } ); - return { actorType, actorId, projectId, assumePrivilegesToken }; + return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken }; }; const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => { @@ -83,11 +84,13 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as { tokenVersionId: string; projectId: string; - userId: string; + requesterId: string; actorType: ActorType; actorId: string; }; - if (decodedToken.tokenVersionId !== tokenVersionId) return; + if (decodedToken.tokenVersionId !== tokenVersionId) { + throw new ForbiddenRequestError({ message: "Invalid token version" }); + } return decodedToken; }; diff --git a/backend/src/ee/services/assume-privilege/assume-privilege-types.ts b/backend/src/ee/services/assume-privilege/assume-privilege-types.ts index 774cb2de5..55b6c8449 100644 --- a/backend/src/ee/services/assume-privilege/assume-privilege-types.ts +++ b/backend/src/ee/services/assume-privilege/assume-privilege-types.ts @@ -2,9 +2,9 @@ import { OrgServiceActor } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; export type TAssumeProjectPrivilegeDTO = { - actorType: ActorType.USER | ActorType.IDENTITY; - actorId: string; + targetActorType: ActorType.USER | ActorType.IDENTITY; + targetActorId: string; projectId: string; tokenVersionId: string; - projectPermission: OrgServiceActor; + actorPermissionDetails: OrgServiceActor; }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index e36061e24..b10e449b0 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -318,8 +318,8 @@ export enum EventType { SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets", PROJECT_ACCESS_REQUEST = "project-access-request", - PROJECT_ASSUME_PRIVILEGE = "project-assume-privileges", - PROJECT_ASSUME_PRIVILEGE_EXIT = "project-assume-privileges-exit" + PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start", + PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end" } export const filterableSecretEvents: EventType[] = [ @@ -2428,22 +2428,25 @@ interface ProjectAccessRequestEvent { } interface ProjectAssumePrivilegesEvent { - type: EventType.PROJECT_ASSUME_PRIVILEGE; + 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_EXIT; + type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END; metadata: { projectId: string; requesterId: string; requesterEmail: string; + targetActorType: ActorType; + targetActorId: string; }; } diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index ed672f956..67d553d59 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -560,15 +560,15 @@ export const permissionServiceFactory = ({ }: TGetProjectPermissionArg): Promise> => { let actor = inputActor; let actorId = inputActorId; - const projectAssumeRole = requestContext.get("projectAssumeRole"); + const assumedProjectRole = requestContext.get("assumedProjectRole"); if ( - projectAssumeRole && + assumedProjectRole && actor === ActorType.USER && - actorId === projectAssumeRole.userId && - projectId === projectAssumeRole.projectId + actorId === assumedProjectRole.requesterId && + projectId === assumedProjectRole.projectId ) { - actor = projectAssumeRole.actorType; - actorId = projectAssumeRole.actorId; + actor = assumedProjectRole.actorType; + actorId = assumedProjectRole.actorId; } switch (actor) { diff --git a/backend/src/server/plugins/auth/inject-assume-privilege.ts b/backend/src/server/plugins/auth/inject-assume-privilege.ts index a77d688c1..99fca57c0 100644 --- a/backend/src/server/plugins/auth/inject-assume-privilege.ts +++ b/backend/src/server/plugins/auth/inject-assume-privilege.ts @@ -4,16 +4,21 @@ 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) => { + server.addHook("onRequest", async (req, res) => { const assumeRoleCookie = req.cookies["infisical-project-assume-privileges"]; - if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) { - const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken( - assumeRoleCookie, - req.auth.tokenVersionId - ); - if (decodedToken) { - requestContext.set("projectAssumeRole", decodedToken); + try { + if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) { + const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken( + assumeRoleCookie, + req.auth.tokenVersionId + ); + if (decodedToken) { + requestContext.set("assumedProjectRole", decodedToken); + } } + } catch (error) { + req.log.error({ error }, "Failed to verify assume privilege token"); + void res.clearCookie("infisical-project-assume-privileges"); } }); }); diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 9a2c1a519..717c6f1b6 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -37,7 +37,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { httpOnly: true, path: "/", sameSite: "strict", - secure: appCfg.HTTPS_ENABLED + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 }); return { message: "Successfully logged out" }; diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 97bd8ec83..af2d97b8e 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { httpOnly: true, path: "/", sameSite: "strict", - secure: cfg.HTTPS_ENABLED + secure: cfg.HTTPS_ENABLED, + maxAge: 0 }); return { token: tokens.access, isMfaEnabled: false }; @@ -142,7 +143,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { httpOnly: true, path: "/", sameSite: "strict", - secure: appCfg.HTTPS_ENABLED + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 }); return { diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index ed49c0ffe..bace8839a 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -238,21 +238,23 @@ export const projectRoleServiceFactory = ({ // just to satisfy ts if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" }); - const projectAssumeRole = requestContext.get("projectAssumeRole"); - const isImpersonating = projectAssumeRole?.projectId === projectId; + const assumedProjectRole = requestContext.get("assumedProjectRole"); + const isImpersonating = assumedProjectRole?.projectId === projectId; const impersonation = isImpersonating ? { - actorId: projectAssumeRole?.actorId, - actorType: projectAssumeRole?.actorType, + actorId: assumedProjectRole?.actorId, + actorType: assumedProjectRole?.actorType, actorName: "", actorEmail: "" } : undefined; if (impersonation?.actorType === ActorType.IDENTITY) { const identityDetails = await identityDAL.findById(impersonation.actorId); + if (!identityDetails) throw new NotFoundError({ message: `Identity with ID ${impersonation.actorId} not found` }); impersonation.actorName = identityDetails.name; } else if (impersonation?.actorType === ActorType.USER) { const userDetails = await userDAL.findById(impersonation?.actorId); + if (!userDetails) throw new NotFoundError({ message: `User with ID ${impersonation.actorId} not found` }); impersonation.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`; impersonation.actorEmail = userDetails?.email || ""; } diff --git a/frontend/src/components/v2/ConfirmActionModal/ConfirmActionModal.tsx b/frontend/src/components/v2/ConfirmActionModal/ConfirmActionModal.tsx index 154334474..c6bbec305 100644 --- a/frontend/src/components/v2/ConfirmActionModal/ConfirmActionModal.tsx +++ b/frontend/src/components/v2/ConfirmActionModal/ConfirmActionModal.tsx @@ -45,8 +45,6 @@ export const ConfirmActionModal = ({ setIsLoading.on(); try { await onConfirmed(); - } catch { - setIsLoading.off(); } finally { setIsLoading.off(); } @@ -77,7 +75,7 @@ export const ConfirmActionModal = ({ - {" "} + } onClose={onClose} diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index dd8d76795..cd67b1276 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -49,7 +49,6 @@ const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "deleteIdentity", - "upgradePlan", "assumePrivileges" ] as const); const assumePrivileges = useAssumeProjectPrivileges(); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index 1b2569457..ced486b45 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -136,7 +136,7 @@ export const Page = () => { variant="outline_bg" size="xs" isDisabled={!isAllowed} - isLoading={isRemovingUserFromWorkspace} + isLoading={assumePrivileges.isPending} onClick={() => handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id }) }