diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 83378c16e..2cfc90564 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -133,6 +133,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv approvals: z.number(), approvers: z .object({ + isOrgMembershipActive: z.boolean().nullable().optional(), userId: z.string().nullable().optional(), sequence: z.number().nullable().optional(), approvalsRequired: z.number().nullable().optional(), @@ -150,6 +151,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv }), reviewers: z .object({ + isOrgMembershipActive: z.boolean().nullable().optional(), userId: z.string(), status: z.string() }) diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 3d07af562..104738140 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -294,12 +294,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv 200: z.object({ approval: SecretApprovalRequestsSchema.merge( z.object({ - // secretPath: z.string(), policy: z.object({ id: z.string(), name: z.string(), approvals: z.number(), - approvers: approvalRequestUser.array(), + approvers: approvalRequestUser + .extend({ isOrgMembershipActive: z.boolean().nullable().optional() }) + .array(), bypassers: approvalRequestUser.array(), secretPath: z.string().optional().nullable(), enforcementLevel: z.string(), @@ -309,7 +310,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), committerUser: approvalRequestUser.nullish(), - reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), + reviewers: approvalRequestUser + .extend({ + status: z.string(), + comment: z.string().optional(), + isOrgMembershipActive: z.boolean().nullable().optional() + }) + .array(), secretPath: z.string(), commits: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true }) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index 68f9240a6..f3972fc1c 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -5,6 +5,7 @@ import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests, + TOrgMemberships, TUserGroupMembership, TUsers } from "@app/db/schemas"; @@ -144,6 +145,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit( + db(TableName.OrgMembership).as("approverOrgMembership"), + `${TableName.AccessApprovalPolicyApprover}.approverUserId`, + `approverOrgMembership.userId` + ) + + .leftJoin( + db(TableName.OrgMembership).as("approverGroupOrgMembership"), + `${TableName.Users}.id`, + `approverGroupOrgMembership.userId` + ) + + .leftJoin( + db(TableName.OrgMembership).as("reviewerOrgMembership"), + `${TableName.AccessApprovalRequestReviewer}.reviewerUserId`, + `reviewerOrgMembership.userId` + ) + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .select(selectAllTableCols(TableName.AccessApprovalRequest)) @@ -300,6 +324,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"), db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"), db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"), + + db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"), + db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"), + db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"), db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod") ) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) @@ -396,17 +424,26 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR { key: "reviewerUserId", label: "reviewers" as const, - mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined) + mapper: ({ reviewerUserId: userId, reviewerStatus: status, reviewerIsOrgMembershipActive }) => + userId ? { userId, status, isOrgMembershipActive: reviewerIsOrgMembershipActive } : undefined }, { key: "approverUserId", label: "approvers" as const, - mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({ + mapper: ({ + approverUserId, + approverSequence, + approvalsRequired, + approverUsername, + approverEmail, + approverIsOrgMembershipActive + }) => ({ userId: approverUserId, sequence: approverSequence, approvalsRequired, email: approverEmail, - username: approverUsername + username: approverUsername, + isOrgMembershipActive: approverIsOrgMembershipActive }) }, { @@ -417,13 +454,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR approverSequence, approvalsRequired, approverGroupEmail, - approverGroupUsername + approverGroupUsername, + approverGroupIsOrgMembershipActive }) => ({ userId: approverGroupUserId, sequence: approverSequence, approvalsRequired, email: approverGroupEmail, - username: approverGroupUsername + username: approverGroupUsername, + isOrgMembershipActive: approverGroupIsOrgMembershipActive }) }, { key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId }, diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index ed027835a..f1d3102d6 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -87,6 +87,7 @@ export interface TAccessApprovalRequestServiceFactory { approvalsRequired: number | null | undefined; email: string | null | undefined; username: string; + isOrgMembershipActive: boolean; } | { userId: string; @@ -94,6 +95,7 @@ export interface TAccessApprovalRequestServiceFactory { approvalsRequired: number | null | undefined; email: string | null | undefined; username: string; + isOrgMembershipActive: boolean; } )[]; bypassers: string[]; @@ -145,6 +147,7 @@ export interface TAccessApprovalRequestServiceFactory { reviewers: { userId: string; status: string; + isOrgMembershipActive: boolean; }[]; approvers: ( | { @@ -153,6 +156,7 @@ export interface TAccessApprovalRequestServiceFactory { approvalsRequired: number | null | undefined; email: string | null | undefined; username: string; + isOrgMembershipActive: boolean; } | { userId: string; @@ -160,6 +164,7 @@ export interface TAccessApprovalRequestServiceFactory { approvalsRequired: number | null | undefined; email: string | null | undefined; username: string; + isOrgMembershipActive: boolean; } )[]; bypassers: string[]; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 49f31bdf6..49512768c 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { SecretApprovalRequestsSchema, TableName, + TOrgMemberships, TSecretApprovalRequests, TSecretApprovalRequestsSecrets, TUserGroupMembership, @@ -107,11 +108,32 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, `secretApprovalReviewerUser.id` ) + + .leftJoin( + db(TableName.OrgMembership).as("approverOrgMembership"), + `${TableName.SecretApprovalPolicyApprover}.approverUserId`, + `approverOrgMembership.userId` + ) + + .leftJoin( + db(TableName.OrgMembership).as("approverGroupOrgMembership"), + `secretApprovalPolicyGroupApproverUser.id`, + `approverGroupOrgMembership.userId` + ) + + .leftJoin( + db(TableName.OrgMembership).as("reviewerOrgMembership"), + `${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, + `reviewerOrgMembership.userId` + ) + .select(selectAllTableCols(TableName.SecretApprovalRequest)) .select( tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), + tx.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"), + tx.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"), tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"), tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"), @@ -148,6 +170,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"), tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"), tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"), + tx.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"), tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"), tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"), tx.ref("projectId").withSchema(TableName.Environment), @@ -211,9 +234,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { reviewerLastName: lastName, reviewerUsername: username, reviewerFirstName: firstName, - reviewerComment: comment + reviewerComment: comment, + reviewerIsOrgMembershipActive: isOrgMembershipActive }) => - userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined + userId + ? { + userId, + status, + email, + firstName, + lastName, + username, + comment: comment ?? "", + isOrgMembershipActive + } + : undefined }, { key: "approverUserId", @@ -223,13 +258,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { approverEmail: email, approverUsername: username, approverLastName: lastName, - approverFirstName: firstName + approverFirstName: firstName, + approverIsOrgMembershipActive: isOrgMembershipActive }) => ({ userId, email, firstName, lastName, - username + username, + isOrgMembershipActive }) }, { @@ -240,13 +277,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { approverGroupEmail: email, approverGroupUsername: username, approverGroupLastName: lastName, - approverGroupFirstName: firstName + approverGroupFirstName: firstName, + approverGroupIsOrgMembershipActive: isOrgMembershipActive }) => ({ userId, email, firstName, lastName, - username + username, + isOrgMembershipActive }) }, { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 90a7b0e1f..e8bdd5ba2 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -258,6 +258,7 @@ export const secretApprovalRequestServiceFactory = ({ if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); const secretApprovalRequest = await secretApprovalRequestDAL.findById(id); + if (!secretApprovalRequest) throw new NotFoundError({ message: `Secret approval request with ID '${id}' not found` }); diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 7a5a9341f..5e7dce76e 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -108,7 +108,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { firstName: true, lastName: true, id: true - }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + }) + .merge(UserEncryptionKeysSchema.pick({ publicKey: true })) + .extend({ + isOrgMembershipActive: z.boolean() + }), project: SanitizedProjectSchema.pick({ name: true, id: true }), roles: z.array( z.object({ diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index a776f7245..263838a9d 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -156,6 +156,7 @@ export const groupProjectDALFactory = (db: TDbClient) => { `${TableName.GroupProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .join(TableName.OrgMembership, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("createdAt").withSchema(TableName.UserGroupMembership), @@ -176,7 +177,8 @@ export const groupProjectDALFactory = (db: TDbClient) => { db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole), - db.ref("name").as("projectName").withSchema(TableName.Project) + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("isActive").withSchema(TableName.OrgMembership) ) .where({ isGhost: false }); @@ -192,7 +194,8 @@ export const groupProjectDALFactory = (db: TDbClient) => { id, userId, projectName, - createdAt + createdAt, + isActive }) => ({ isGroupMember: true, id, @@ -202,7 +205,7 @@ export const groupProjectDALFactory = (db: TDbClient) => { id: projectId, name: projectName }, - user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + user: { email, username, firstName, lastName, id: userId, publicKey, isGhost, isOrgMembershipActive: isActive }, createdAt }), key: "id", diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 7dfd1f65c..dd503c2a3 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -21,6 +21,14 @@ export const projectMembershipDALFactory = (db: TDbClient) => { .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .join(TableName.OrgMembership, (qb) => { + qb.on(`${TableName.Users}.id`, "=", `${TableName.OrgMembership}.userId`).andOn( + `${TableName.OrgMembership}.orgId`, + "=", + `${TableName.Project}.orgId` + ); + }) + .where((qb) => { if (filter.usernames) { void qb.whereIn("username", filter.usernames); @@ -90,7 +98,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").as("projectName").withSchema(TableName.Project) + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("isActive").withSchema(TableName.OrgMembership) ) .where({ isGhost: false }) .orderBy(`${TableName.Users}.username` as "username"); @@ -107,12 +116,22 @@ export const projectMembershipDALFactory = (db: TDbClient) => { id, userId, projectName, - createdAt + createdAt, + isActive }) => ({ id, userId, projectId, - user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + user: { + email, + username, + firstName, + lastName, + id: userId, + publicKey, + isGhost, + isOrgMembershipActive: isActive + }, project: { id: projectId, name: projectName diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index b9e502922..7cf665141 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -97,7 +97,6 @@ export const projectMembershipServiceFactory = ({ const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles }); - // projectMembers[0].project if (includeGroupMembers) { const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId); const allMembers = [ diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 0e4c01350..5063f4fff 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -36,6 +36,7 @@ export type Approver = { type: ApproverType; sequence?: number; approvalsRequired?: number; + isOrgMembershipActive: boolean; }; export type Bypasser = { @@ -82,6 +83,7 @@ export type TAccessApprovalRequest = { name: string; approvals: number; approvers: { + isOrgMembershipActive: boolean; userId: string; sequence?: number; approvalsRequired?: number; @@ -98,6 +100,7 @@ export type TAccessApprovalRequest = { }; reviewers: { + isOrgMembershipActive: boolean; userId: string; status: string; }[]; @@ -177,7 +180,7 @@ export type TCreateAccessPolicyDTO = { projectSlug: string; name?: string; environments: string[]; - approvers?: Approver[]; + approvers?: Omit[]; bypassers?: Bypasser[]; approvals?: number; secretPath: string; @@ -190,7 +193,7 @@ export type TCreateAccessPolicyDTO = { export type TUpdateAccessPolicyDTO = { id: string; name?: string; - approvers?: Approver[]; + approvers?: Omit[]; bypassers?: Bypasser[]; secretPath?: string; environments?: string[]; diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 8fd86624d..1785ffa8a 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -20,6 +20,7 @@ export enum ApproverType { } export type Approver = { + isOrgMembershipActive: boolean; id: string; type: ApproverType; }; @@ -49,7 +50,7 @@ export type TCreateSecretPolicyDTO = { name?: string; environments: string[]; secretPath: string; - approvers?: Approver[]; + approvers?: Omit[]; bypassers?: Bypasser[]; approvals?: number; enforcementLevel: EnforcementLevel; @@ -59,7 +60,7 @@ export type TCreateSecretPolicyDTO = { export type TUpdateSecretPolicyDTO = { id: string; name?: string; - approvers?: Approver[]; + approvers?: Omit[]; bypassers?: Bypasser[]; secretPath?: string; approvals?: number; diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index caf1df320..0eef3fa3d 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -53,6 +53,7 @@ export type TSecretApprovalRequest = { firstName: string; lastName: string; username: string; + isOrgMembershipActive: boolean; }[]; workspace: string; environment: string; @@ -62,6 +63,7 @@ export type TSecretApprovalRequest = { status: "open" | "close"; policy: Omit & { approvers: { + isOrgMembershipActive: boolean; userId: string; email: string; firstName: string; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 3af283b7e..0a43e850e 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -83,6 +83,7 @@ export type TProjectMembership = { export type TWorkspaceUser = { id: string; user: { + isOrgMembershipActive: boolean; email: string; username: string; firstName: string; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index de568931a..53df91974 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -1,5 +1,6 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -13,7 +14,6 @@ import { } from "@app/context"; import { useLogoutUser, useUpdateOrg } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -import { twMerge } from "tailwind-merge"; enum EnforceAuthType { SAML = "saml", diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx index e87054f93..b1cd906bb 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -4,7 +4,9 @@ import { faCheck, faEdit, faHourglass, - faTriangleExclamation + faTriangleExclamation, + faUser, + faUserSlash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ms from "ms"; @@ -37,7 +39,7 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types"; import { groupBy } from "@app/lib/fn/array"; import { EditAccessRequestModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal"; -const getReviewedStatusSymbol = (status?: ApprovalStatus) => { +const getReviewedStatusSymbol = (status?: ApprovalStatus, isOrgMembershipActive?: boolean) => { if (status === ApprovalStatus.APPROVED) return ( @@ -50,6 +52,17 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => { ); + + if (!isOrgMembershipActive) { + return ( + // Can't do a tooltip here because nested tooltips doesn't work properly as of yet. + // TODO(daniel): Fix nested tooltips in the future. + + + + + ); + } return ( @@ -87,6 +100,7 @@ export const ReviewAccessRequestModal = ({ }) => { const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); const [bypassApproval, setBypassApproval] = useState(false); + const [bypassReason, setBypassReason] = useState(""); const { currentWorkspace } = useWorkspace(); const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || ""); @@ -192,6 +206,7 @@ export const ReviewAccessRequestModal = ({ (acc, curr) => { if (acc.length && acc[acc.length - 1].sequence === curr.sequence) { acc[acc.length - 1][curr.type]?.push(curr); + return acc; } @@ -203,6 +218,7 @@ export const ReviewAccessRequestModal = ({ ? { user: [curr], group: [], sequence, approvals } : { group: [curr], user: [], sequence, approvals } ); + return acc; }, [] as { @@ -216,7 +232,10 @@ export const ReviewAccessRequestModal = ({ const approvers = approversBySequence?.map((approverChain) => { const reviewers = request.policy.approvers .filter((el) => (el.sequence || 1) === approverChain.sequence) - .map((el) => ({ ...el, status: reviewesGroupById?.[el.userId]?.[0]?.status })); + .map((el) => ({ + ...el, + status: reviewesGroupById?.[el.userId]?.[0]?.status + })); const hasApproved = reviewers.filter((el) => el.status === "approved").length >= (approverChain?.approvals || 1); @@ -410,12 +429,39 @@ export const ReviewAccessRequestModal = ({ )}
- - {approver?.user - ?.map( - (el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username - ) - .join(", ")} + + {Boolean(approver.user.length) && ( +
+ {approver?.user?.map((el, idx) => { + const member = approverSequence?.membersGroupById?.[el.id]?.[0]; + if (!member) return null; + + return member.user.isOrgMembershipActive ? ( +
+ {member.user.username} + {idx < approver.user.length - 1 && ","} +
+ ) : ( +
+ + {member.user.username} + + +
+ + + Inactive + +
+
+
+
+ {idx < approver.user.length - 1 && ","} +
+ ); + })} +
+ )}
{approver?.group @@ -440,8 +486,18 @@ export const ReviewAccessRequestModal = ({ key={`reviewer-${idx + 1}`} className="flex items-center gap-2 px-2 py-2 text-sm" > -
{el.username}
- {getReviewedStatusSymbol(el?.status as ApprovalStatus)} +
+ {el.username} +
+ {getReviewedStatusSymbol( + el?.status as ApprovalStatus, + el.isOrgMembershipActive + )}
))} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 30f77d2e1..856c16ee1 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -43,6 +43,8 @@ import { import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums"; import { TWorkspaceUser } from "@app/hooks/api/users/types"; +import { PolicyMemberOption } from "./PolicyMemberOption"; + type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; @@ -59,7 +61,11 @@ const formSchema = z secretPath: z.string().trim().min(1), approvals: z.number().min(1).default(1), userApprovers: z - .object({ type: z.literal(ApproverType.User), id: z.string() }) + .object({ + type: z.literal(ApproverType.User), + id: z.string(), + isOrgMembershipActive: z.boolean().optional() + }) .array() .default([]), groupApprovers: z @@ -67,7 +73,11 @@ const formSchema = z .array() .default([]), userBypassers: z - .object({ type: z.literal(BypasserType.User), id: z.string() }) + .object({ + type: z.literal(BypasserType.User), + id: z.string(), + isOrgMembershipActive: z.boolean().optional() + }) .array() .default([]), groupBypassers: z @@ -80,7 +90,11 @@ const formSchema = z sequenceApprovers: z .object({ user: z - .object({ type: z.literal(ApproverType.User), id: z.string() }) + .object({ + type: z.literal(ApproverType.User), + id: z.string(), + isOrgMembershipActive: z.boolean().optional() + }) .array() .default([]), group: z @@ -139,7 +153,11 @@ const Form = ({ userApprovers: editValues?.approvers ?.filter((approver) => approver.type === ApproverType.User) - .map(({ id, type }) => ({ id, type: type as ApproverType.User })) || [], + .map(({ id, type, isOrgMembershipActive }) => ({ + id, + type: type as ApproverType.User, + isOrgMembershipActive + })) || [], groupApprovers: editValues?.approvers ?.filter((approver) => approver.type === ApproverType.Group) @@ -235,7 +253,9 @@ const Form = ({ ...data, approvers: sequenceApprovers?.flatMap((approvers, index) => approvers.user - .map((el) => ({ ...el, sequence: index + 1 }) as Approver) + .map( + (el) => ({ ...el, sequence: index + 1 }) as Omit + ) .concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 }))) ), approvalsRequired: sequenceApprovers?.map((el, index) => ({ @@ -291,7 +311,9 @@ const Form = ({ ...data, approvers: sequenceApprovers?.flatMap((approvers, index) => approvers.user - .map((el) => ({ ...el, sequence: index + 1 }) as Approver) + .map( + (el) => ({ ...el, sequence: index + 1 }) as Omit + ) .concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 }))) ), approvalsRequired: sequenceApprovers?.map((el, index) => ({ @@ -329,7 +351,8 @@ const Form = ({ () => members.map((member) => ({ id: member.user.id, - type: ApproverType.User + type: ApproverType.User, + isOrgMembershipActive: member.user.isOrgMembershipActive })), [members] ); @@ -347,7 +370,8 @@ const Form = ({ () => members.map((member) => ({ id: member.user.id, - type: BypasserType.User + type: BypasserType.User, + isOrgMembershipActive: member.user.isOrgMembershipActive })), [members] ); @@ -608,6 +632,7 @@ const Form = ({ isMulti placeholder="Select members..." options={memberOptions} + components={{ Option: PolicyMemberOption }} getOptionValue={(option) => option.id} getOptionLabel={(option) => { const member = members?.find((m) => m.user.id === option.id); @@ -685,6 +710,7 @@ const Form = ({ menuPlacement="top" isMulti placeholder="Select members..." + components={{ Option: PolicyMemberOption }} options={memberOptions} getOptionValue={(option) => option.id} getOptionLabel={(option) => { @@ -783,6 +809,7 @@ const Form = ({ menuPlacement="top" isMulti placeholder="Select members..." + components={{ Option: PolicyMemberOption }} options={bypasserMemberOptions} getOptionValue={(option) => option.id} getOptionLabel={(option) => { diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx index b19ace7ea..89a97003e 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx @@ -1,5 +1,6 @@ import { useMemo } from "react"; import { + faBan, faClipboardCheck, faEdit, faEllipsisV, @@ -19,6 +20,7 @@ import { GenericFieldLabel, IconButton, Td, + Tooltip, Tr } from "@app/components/v2"; import { Badge } from "@app/components/v2/Badge"; @@ -86,10 +88,9 @@ export const ApprovalPolicyRow = ({ return entityInSameSequence?.map((el) => { return { sequence: el.sequence || policy.approvals, - userLabels: members - ?.filter((member) => el.user.find((i) => i.id === member.user.id)) - .map((member) => getMemberLabel(member)) - .join(", "), + + users: members.filter((member) => el.user.find((i) => i.id === member.user.id)), + groupLabels: groups ?.filter(({ group }) => el.group.find((i) => i.id === group.id)) .map(({ group }) => group.name) @@ -212,7 +213,35 @@ export const ApprovalPolicyRow = ({ )}
- {el.userLabels} + {Boolean(el.users.length) && ( +
+ {el.users.map((u, idx) => { + return u.user.isOrgMembershipActive ? ( +
+ {getMemberLabel(u)} + {idx < el.users.length - 1 && ","} +
+ ) : ( +
+ + {getMemberLabel(u)} + + +
+ + + Inactive + +
+
+
+
+ {idx < el.users.length - 1 && ","} +
+ ); + })} +
+ )}
{el.groupLabels} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/PolicyMemberOption.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/PolicyMemberOption.tsx new file mode 100644 index 000000000..50376521c --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/PolicyMemberOption.tsx @@ -0,0 +1,40 @@ +import { components, OptionProps } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { faBan } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { Badge } from "@app/components/v2"; +import { BypasserType } from "@app/hooks/api/accessApproval/types"; +import { ApproverType } from "@app/hooks/api/secretApproval/types"; + +export const PolicyMemberOption = ({ + isSelected, + children, + ...props +}: OptionProps<{ + id: string; + type: BypasserType | ApproverType; + isOrgMembershipActive?: boolean; +}>) => { + return ( + +
+

+ {children} +

+ {!props.data.isOrgMembershipActive && ( + + + Inactive + + )} + {isSelected && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 0f2b00299..6a0c3cd6d 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ import { ReactNode } from "react"; import { Controller, useForm } from "react-hook-form"; import { @@ -8,7 +9,8 @@ import { faCodeBranch, faComment, faFolder, - faHourglass + faHourglass, + faUserSlash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -85,6 +87,7 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => { return ; if (status === ApprovalStatus.REJECTED) return ; + return ; }; @@ -162,11 +165,15 @@ export const SecretApprovalRequestChanges = ({ secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id); const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce< - Record + Record >( (prev, curr) => ({ ...prev, - [curr.userId]: { status: curr.status, comment: curr.comment } + [curr.userId]: { + status: curr.status, + comment: curr.comment, + isOrgMembershipActive: curr.isOrgMembershipActive + } }), {} ); @@ -533,26 +540,44 @@ export const SecretApprovalRequestChanges = ({ ) .map((requiredApprover) => { const reviewer = reviewedUsers?.[requiredApprover.userId]; + const { isOrgMembershipActive } = requiredApprover; + return (
- -
-
{requiredApprover?.email}
- * -
-
-
+ +
+
{requiredApprover?.email}
+ * + {!isOrgMembershipActive && ( + + )} +
+
+
+
{reviewer?.comment && ( )} - - {getReviewedStatusSymbol(reviewer?.status)} - +
+ + Status:{" "} + + {reviewer?.status || ApprovalStatus.PENDING} + + + } + > + {getReviewedStatusSymbol(reviewer?.status)} + +
); @@ -578,20 +615,42 @@ export const SecretApprovalRequestChanges = ({ ) .map((reviewer) => { const status = reviewedUsers?.[reviewer.userId].status; + const { isOrgMembershipActive } = reviewer; return (
-
- - {reviewer?.email} +
+ +
+ {reviewer?.email} + {!isOrgMembershipActive && ( + + )} +
- *
+
{reviewer.comment && ( - + )} - + + Status:{" "} + {status || ApprovalStatus.PENDING} + + } + > {getReviewedStatusSymbol(status)}