feat: updated code by rabbit, reptile and maidul changes

This commit is contained in:
=
2025-04-21 16:27:56 +05:30
parent 649f7b560f
commit 2ee423174a
13 changed files with 83 additions and 63 deletions

View File

@@ -110,7 +110,7 @@ declare module "@fastify/request-context" {
}; };
}; };
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
projectAssumeRole?: { userId: string; actorId: string; actorType: ActorType; projectId: string }; assumedProjectRole: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
} }
} }

View File

@@ -6,6 +6,7 @@ import { BadRequestError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { requestContext } from "@fastify/request-context";
export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) => { export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -32,10 +33,10 @@ export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider)
handler: async (req, res) => { handler: async (req, res) => {
if (req.auth.authMode === AuthMode.JWT) { if (req.auth.authMode === AuthMode.JWT) {
const payload = await server.services.assumePrivileges.assumeProjectPrivileges({ const payload = await server.services.assumePrivileges.assumeProjectPrivileges({
actorType: req.body.actorType, targetActorType: req.body.actorType,
actorId: req.body.actorId, targetActorId: req.body.actorId,
projectId: req.params.projectId, projectId: req.params.projectId,
projectPermission: req.permission, actorPermissionDetails: req.permission,
tokenVersionId: req.auth.tokenVersionId tokenVersionId: req.auth.tokenVersionId
}); });
@@ -44,20 +45,22 @@ export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider)
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: appCfg.HTTPS_ENABLED secure: appCfg.HTTPS_ENABLED,
maxAge: 3600 // 1 hour in seconds
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
event: { event: {
type: EventType.PROJECT_ASSUME_PRIVILEGE, type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START,
metadata: { metadata: {
projectId: req.params.projectId, projectId: req.params.projectId,
requesterEmail: req.auth.user.username, requesterEmail: req.auth.user.username,
requesterId: req.auth.user.id, requesterId: req.auth.user.id,
targetActorType: req.body.actorType, 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]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => { 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(); const appCfg = getConfig();
void res.setCookie("infisical-project-assume-privileges", "", { void res.setCookie("infisical-project-assume-privileges", "", {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: appCfg.HTTPS_ENABLED secure: appCfg.HTTPS_ENABLED,
expires: new Date(0)
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
event: { event: {
type: EventType.PROJECT_ASSUME_PRIVILEGE_EXIT, type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END,
metadata: { metadata: {
projectId: req.params.projectId, projectId: req.params.projectId,
requesterEmail: req.auth.user.username, requesterEmail: req.auth.user.username,
requesterId: req.auth.user.id requesterId: req.auth.user.id,
targetActorId: assumePrivilege.actorId,
targetActorType: assumePrivilege.actorType
} }
} }
}); });

View File

@@ -3,7 +3,7 @@ import jwt from "jsonwebtoken";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; 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 { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -24,23 +24,24 @@ export type TAssumePrivilegeServiceFactory = ReturnType<typeof assumePrivilegeSe
export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => { export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => {
const assumeProjectPrivileges = async ({ const assumeProjectPrivileges = async ({
actorType, targetActorType,
actorId, targetActorId,
projectId, projectId,
projectPermission, actorPermissionDetails,
tokenVersionId tokenVersionId
}: TAssumeProjectPrivilegeDTO) => { }: TAssumeProjectPrivilegeDTO) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: projectPermission.type, actor: actorPermissionDetails.type,
actorId: projectPermission.id, actorId: actorPermissionDetails.id,
projectId, projectId,
actorAuthMethod: projectPermission.authMethod, actorAuthMethod: actorPermissionDetails.authMethod,
actorOrgId: projectPermission.orgId, actorOrgId: actorPermissionDetails.orgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
if (actorType === ActorType.USER) {
if (targetActorType === ActorType.USER) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionMemberActions.AssumePrivileges, ProjectPermissionMemberActions.AssumePrivileges,
ProjectPermissionSub.Member ProjectPermissionSub.Member
@@ -54,11 +55,11 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }:
// check entity is part of project // check entity is part of project
await permissionService.getProjectPermission({ await permissionService.getProjectPermission({
actor: actorType, actor: targetActorType,
actorId, actorId: targetActorId,
projectId, projectId,
actorAuthMethod: projectPermission.authMethod, actorAuthMethod: actorPermissionDetails.authMethod,
actorOrgId: projectPermission.orgId, actorOrgId: actorPermissionDetails.orgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
@@ -66,16 +67,16 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }:
const assumePrivilegesToken = jwt.sign( const assumePrivilegesToken = jwt.sign(
{ {
tokenVersionId, tokenVersionId,
actorType, actorType: targetActorType,
actorId, actorId: targetActorId,
projectId, projectId,
userId: projectPermission.id requesterId: actorPermissionDetails.id
}, },
appCfg.AUTH_SECRET, appCfg.AUTH_SECRET,
{ expiresIn: "1hr" } { expiresIn: "1hr" }
); );
return { actorType, actorId, projectId, assumePrivilegesToken }; return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken };
}; };
const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => { const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => {
@@ -83,11 +84,13 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }:
const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as { const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as {
tokenVersionId: string; tokenVersionId: string;
projectId: string; projectId: string;
userId: string; requesterId: string;
actorType: ActorType; actorType: ActorType;
actorId: string; actorId: string;
}; };
if (decodedToken.tokenVersionId !== tokenVersionId) return; if (decodedToken.tokenVersionId !== tokenVersionId) {
throw new ForbiddenRequestError({ message: "Invalid token version" });
}
return decodedToken; return decodedToken;
}; };

View File

@@ -2,9 +2,9 @@ import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
export type TAssumeProjectPrivilegeDTO = { export type TAssumeProjectPrivilegeDTO = {
actorType: ActorType.USER | ActorType.IDENTITY; targetActorType: ActorType.USER | ActorType.IDENTITY;
actorId: string; targetActorId: string;
projectId: string; projectId: string;
tokenVersionId: string; tokenVersionId: string;
projectPermission: OrgServiceActor; actorPermissionDetails: OrgServiceActor;
}; };

View File

@@ -318,8 +318,8 @@ export enum EventType {
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 = "project-assume-privileges", PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
PROJECT_ASSUME_PRIVILEGE_EXIT = "project-assume-privileges-exit" PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
} }
export const filterableSecretEvents: EventType[] = [ export const filterableSecretEvents: EventType[] = [
@@ -2428,22 +2428,25 @@ interface ProjectAccessRequestEvent {
} }
interface ProjectAssumePrivilegesEvent { interface ProjectAssumePrivilegesEvent {
type: EventType.PROJECT_ASSUME_PRIVILEGE; type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START;
metadata: { metadata: {
projectId: string; projectId: string;
requesterId: string; requesterId: string;
requesterEmail: string; requesterEmail: string;
targetActorType: ActorType; targetActorType: ActorType;
targetActorId: string; targetActorId: string;
duration: string;
}; };
} }
interface ProjectAssumePrivilegesExitEvent { interface ProjectAssumePrivilegesExitEvent {
type: EventType.PROJECT_ASSUME_PRIVILEGE_EXIT; type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END;
metadata: { metadata: {
projectId: string; projectId: string;
requesterId: string; requesterId: string;
requesterEmail: string; requesterEmail: string;
targetActorType: ActorType;
targetActorId: string;
}; };
} }

View File

@@ -560,15 +560,15 @@ export const permissionServiceFactory = ({
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => { }: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
let actor = inputActor; let actor = inputActor;
let actorId = inputActorId; let actorId = inputActorId;
const projectAssumeRole = requestContext.get("projectAssumeRole"); const assumedProjectRole = requestContext.get("assumedProjectRole");
if ( if (
projectAssumeRole && assumedProjectRole &&
actor === ActorType.USER && actor === ActorType.USER &&
actorId === projectAssumeRole.userId && actorId === assumedProjectRole.requesterId &&
projectId === projectAssumeRole.projectId projectId === assumedProjectRole.projectId
) { ) {
actor = projectAssumeRole.actorType; actor = assumedProjectRole.actorType;
actorId = projectAssumeRole.actorId; actorId = assumedProjectRole.actorId;
} }
switch (actor) { switch (actor) {

View File

@@ -4,16 +4,21 @@ import fp from "fastify-plugin";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const injectAssumePrivilege = fp(async (server: FastifyZodProvider) => { 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"]; const assumeRoleCookie = req.cookies["infisical-project-assume-privileges"];
if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) { try {
const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken( if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) {
assumeRoleCookie, const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken(
req.auth.tokenVersionId assumeRoleCookie,
); req.auth.tokenVersionId
if (decodedToken) { );
requestContext.set("projectAssumeRole", decodedToken); 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");
} }
}); });
}); });

View File

@@ -37,7 +37,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: appCfg.HTTPS_ENABLED secure: appCfg.HTTPS_ENABLED,
maxAge: 0
}); });
return { message: "Successfully logged out" }; return { message: "Successfully logged out" };

View File

@@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: cfg.HTTPS_ENABLED secure: cfg.HTTPS_ENABLED,
maxAge: 0
}); });
return { token: tokens.access, isMfaEnabled: false }; return { token: tokens.access, isMfaEnabled: false };
@@ -142,7 +143,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: appCfg.HTTPS_ENABLED secure: appCfg.HTTPS_ENABLED,
maxAge: 0
}); });
return { return {

View File

@@ -238,21 +238,23 @@ export const projectRoleServiceFactory = ({
// just to satisfy ts // just to satisfy ts
if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" }); if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" });
const projectAssumeRole = requestContext.get("projectAssumeRole"); const assumedProjectRole = requestContext.get("assumedProjectRole");
const isImpersonating = projectAssumeRole?.projectId === projectId; const isImpersonating = assumedProjectRole?.projectId === projectId;
const impersonation = isImpersonating const impersonation = isImpersonating
? { ? {
actorId: projectAssumeRole?.actorId, actorId: assumedProjectRole?.actorId,
actorType: projectAssumeRole?.actorType, actorType: assumedProjectRole?.actorType,
actorName: "", actorName: "",
actorEmail: "" actorEmail: ""
} }
: undefined; : undefined;
if (impersonation?.actorType === ActorType.IDENTITY) { if (impersonation?.actorType === ActorType.IDENTITY) {
const identityDetails = await identityDAL.findById(impersonation.actorId); const identityDetails = await identityDAL.findById(impersonation.actorId);
if (!identityDetails) throw new NotFoundError({ message: `Identity with ID ${impersonation.actorId} not found` });
impersonation.actorName = identityDetails.name; impersonation.actorName = identityDetails.name;
} else if (impersonation?.actorType === ActorType.USER) { } else if (impersonation?.actorType === ActorType.USER) {
const userDetails = await userDAL.findById(impersonation?.actorId); 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.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`;
impersonation.actorEmail = userDetails?.email || ""; impersonation.actorEmail = userDetails?.email || "";
} }

View File

@@ -45,8 +45,6 @@ export const ConfirmActionModal = ({
setIsLoading.on(); setIsLoading.on();
try { try {
await onConfirmed(); await onConfirmed();
} catch {
setIsLoading.off();
} finally { } finally {
setIsLoading.off(); setIsLoading.off();
} }
@@ -77,7 +75,7 @@ export const ConfirmActionModal = ({
<Button variant="plain" colorSchema="secondary" onClick={onClose}> <Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel Cancel
</Button> </Button>
</ModalClose>{" "} </ModalClose>
</div> </div>
} }
onClose={onClose} onClose={onClose}

View File

@@ -49,7 +49,6 @@ const Page = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteIdentity", "deleteIdentity",
"upgradePlan",
"assumePrivileges" "assumePrivileges"
] as const); ] as const);
const assumePrivileges = useAssumeProjectPrivileges(); const assumePrivileges = useAssumeProjectPrivileges();

View File

@@ -136,7 +136,7 @@ export const Page = () => {
variant="outline_bg" variant="outline_bg"
size="xs" size="xs"
isDisabled={!isAllowed} isDisabled={!isAllowed}
isLoading={isRemovingUserFromWorkspace} isLoading={assumePrivileges.isPending}
onClick={() => onClick={() =>
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id }) handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
} }