merge + final tweaks

This commit is contained in:
x032205
2025-05-21 15:46:36 -04:00
parent dc6a94ccda
commit e3725dd3ab
12 changed files with 134 additions and 92 deletions
@@ -23,6 +23,7 @@ import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-poli
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
@@ -340,17 +341,7 @@ export const accessApprovalRequestServiceFactory = ({
}); });
} }
if ( const { membership, hasRole, permission } = await permissionService.getProjectPermission({
!policy.allowedSelfApprovals &&
actorId === accessApprovalRequest.requestedByUserId &&
policy.enforcementLevel !== EnforcementLevel.Soft
) {
throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request."
});
}
const { membership, hasRole } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: accessApprovalRequest.projectId, projectId: accessApprovalRequest.projectId,
@@ -363,6 +354,19 @@ export const accessApprovalRequestServiceFactory = ({
throw new ForbiddenRequestError({ message: "You are not a member of this project" }); throw new ForbiddenRequestError({ message: "You are not a member of this project" });
} }
if (
!policy.allowedSelfApprovals &&
actorId === accessApprovalRequest.requestedByUserId &&
!(
policy.enforcementLevel === EnforcementLevel.Soft &&
permission.can(ProjectPermissionApprovalActions.AllowAccessBypass, ProjectPermissionSub.SecretApproval)
)
) {
throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request."
});
}
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
+25 -25
View File
@@ -18,33 +18,33 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0, environmentsUsed: 0,
identityLimit: null, identityLimit: null,
identitiesUsed: 0, identitiesUsed: 0,
dynamicSecret: true, dynamicSecret: false,
secretVersioning: true, secretVersioning: true,
pitRecovery: true, pitRecovery: false,
ipAllowlisting: true, ipAllowlisting: false,
rbac: true, rbac: false,
githubOrgSync: true, githubOrgSync: false,
customRateLimits: false, customRateLimits: false,
customAlerts: true, customAlerts: false,
secretAccessInsights: true, secretAccessInsights: false,
auditLogs: true, auditLogs: false,
auditLogsRetentionDays: 3, auditLogsRetentionDays: 0,
auditLogStreams: true, auditLogStreams: false,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: true, samlSSO: false,
hsm: true, hsm: false,
oidcSSO: true, oidcSSO: false,
scim: true, scim: false,
ldap: true, ldap: false,
groups: true, groups: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
secretApproval: true, secretApproval: false,
secretRotation: true, secretRotation: false,
caCrl: true, caCrl: false,
instanceUserManagement: true, instanceUserManagement: false,
externalKms: true, externalKms: false,
rateLimits: { rateLimits: {
readLimit: 60, readLimit: 60,
writeLimit: 200, writeLimit: 200,
@@ -52,10 +52,10 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
}, },
pkiEst: true, pkiEst: true,
enforceMfa: true, enforceMfa: true,
projectTemplates: true, projectTemplates: false,
kmip: true, kmip: false,
gateway: true, gateway: false,
sshHostGroups: true sshHostGroups: false
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -61,7 +61,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionApprovalActions.Edit, ProjectPermissionApprovalActions.Edit,
ProjectPermissionApprovalActions.Create, ProjectPermissionApprovalActions.Create,
ProjectPermissionApprovalActions.Delete, ProjectPermissionApprovalActions.Delete,
ProjectPermissionApprovalActions.AllowChangeBypass ProjectPermissionApprovalActions.AllowChangeBypass,
ProjectPermissionApprovalActions.AllowAccessBypass
], ],
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -39,7 +39,8 @@ export enum ProjectPermissionApprovalActions {
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
AllowChangeBypass = "allow-change-bypass" AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
} }
export enum ProjectPermissionCmekActions { export enum ProjectPermissionCmekActions {
@@ -3,10 +3,10 @@ title: "Access Requests"
description: "Learn how to request access to sensitive resources in Infisical." description: "Learn how to request access to sensitive resources in Infisical."
--- ---
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality. In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
This functionality works in the following way: This functionality works in the following way:
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment. 1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png) ![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png)
![Access Request Policies](/images/platform/access-controls/access-request-policies.png) ![Access Request Policies](/images/platform/access-controls/access-request-policies.png)
@@ -19,9 +19,8 @@ This functionality works in the following way:
![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png) ![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png)
<Info> <Info>
If the access request matches with a policy that has a **Soft** enforcement level, the requester may bypass the policy and get access to the resource without full approval. If the access request matches with a policy that has allows break-glass approval bypasses, the requester may bypass the policy and get access to the resource without full approval.
</Info> </Info>
5. As soon as the request is approved, developer is able to access the sought resources. 5. As soon as the request is approved, developer is able to access the sought resources.
![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png) ![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png)
@@ -33,6 +33,8 @@ First, you would need to create a set of policies for a certain environment. In
The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email. The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email.
You can use the **Soft** enforcement level by enabling "Bypass Approvals".
### Self approvals ### Self approvals
If the **Self Approvals** option is enabled, users who are designated as approvers on the policy can approve requests that they themselves have submitted. If the **Self Approvals** option is enabled, users who are designated as approvers on the policy can approve requests that they themselves have submitted.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 129 KiB

@@ -185,6 +185,7 @@ Supports conditions and permission inversion
| `edit` | Modify approval policies | | `edit` | Modify approval policies |
| `delete` | Remove approval policies | | `delete` | Remove approval policies |
| `allow-change-bypass` | Allow request creators to bypass policy in break-glass situations | | `allow-change-bypass` | Allow request creators to bypass policy in break-glass situations |
| `allow-access-bypass` | Allow request creators to bypass policy in break-glass situations |
#### Subject: `secret-rotation` #### Subject: `secret-rotation`
@@ -29,7 +29,8 @@ export enum ProjectPermissionApprovalActions {
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
AllowChangeBypass = "allow-change-bypass" AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
} }
export enum ProjectPermissionDynamicSecretActions { export enum ProjectPermissionDynamicSecretActions {
@@ -58,7 +58,8 @@ const ApprovalPolicyActionSchema = z.object({
[ProjectPermissionApprovalActions.Edit]: z.boolean().optional(), [ProjectPermissionApprovalActions.Edit]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Delete]: z.boolean().optional(), [ProjectPermissionApprovalActions.Delete]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Create]: z.boolean().optional(), [ProjectPermissionApprovalActions.Create]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional() [ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowAccessBypass]: z.boolean().optional()
}); });
const CmekPolicyActionSchema = z.object({ const CmekPolicyActionSchema = z.object({
@@ -578,6 +579,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canEdit = action.includes(ProjectPermissionApprovalActions.Edit); const canEdit = action.includes(ProjectPermissionApprovalActions.Edit);
const canRead = action.includes(ProjectPermissionApprovalActions.Read); const canRead = action.includes(ProjectPermissionApprovalActions.Read);
const canChangeBypass = action.includes(ProjectPermissionApprovalActions.AllowChangeBypass); const canChangeBypass = action.includes(ProjectPermissionApprovalActions.AllowChangeBypass);
const canAccessBypass = action.includes(ProjectPermissionApprovalActions.AllowAccessBypass);
if (!formVal[subject]) formVal[subject] = [{}]; if (!formVal[subject]) formVal[subject] = [{}];
@@ -588,6 +590,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canRead) formVal[subject]![0][ProjectPermissionApprovalActions.Read] = true; if (canRead) formVal[subject]![0][ProjectPermissionApprovalActions.Read] = true;
if (canChangeBypass) if (canChangeBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowChangeBypass] = true; formVal[subject]![0][ProjectPermissionApprovalActions.AllowChangeBypass] = true;
if (canAccessBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowAccessBypass] = true;
return; return;
} }
@@ -1212,7 +1216,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionApprovalActions.Create }, { label: "Create", value: ProjectPermissionApprovalActions.Create },
{ label: "Modify", value: ProjectPermissionApprovalActions.Edit }, { label: "Modify", value: ProjectPermissionApprovalActions.Edit },
{ label: "Remove", value: ProjectPermissionApprovalActions.Delete }, { label: "Remove", value: ProjectPermissionApprovalActions.Delete },
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass } { label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass },
{ label: "Allow Access Bypass", value: ProjectPermissionApprovalActions.AllowAccessBypass }
] ]
}, },
[ProjectPermissionSub.SecretRotation]: { [ProjectPermissionSub.SecretRotation]: {
@@ -25,6 +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 {
ProjectPermissionApprovalActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission, useProjectPermission,
@@ -100,6 +101,11 @@ export const AccessApprovalRequest = ({
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const canBypassApprovalPermission = permission.can(
ProjectPermissionApprovalActions.AllowAccessBypass,
ProjectPermissionSub.SecretApproval
);
const { data: members } = useGetWorkspaceUsers(projectId, true); const { data: members } = useGetWorkspaceUsers(projectId, true);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>( const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(prev, curr) => ({ ...prev, [curr.user.id]: curr }), (prev, curr) => ({ ...prev, [curr.user.id]: curr }),
@@ -350,7 +356,11 @@ export const AccessApprovalRequest = ({
details.isReviewedByUser || details.isReviewedByUser ||
details.isRejectedByAnyone || details.isRejectedByAnyone ||
(!details.isApprover && (!details.isApprover &&
!(details.isSoftEnforcement && details.isRequestedByCurrentUser)) !(
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
canBypassApprovalPermission
))
) )
return; return;
@@ -360,9 +370,11 @@ export const AccessApprovalRequest = ({
) { ) {
setSelectedRequest({ setSelectedRequest({
...request, ...request,
user: details.isRequestedByCurrentUser user:
? user details.isRequestedByCurrentUser ||
: membersGroupById?.[request.requestedByUserId].user!, !membersGroupById?.[request.requestedByUserId].user
? user
: membersGroupById?.[request.requestedByUserId].user,
isRequestedByCurrentUser: details.isRequestedByCurrentUser, isRequestedByCurrentUser: details.isRequestedByCurrentUser,
isApprover: details.isApprover isApprover: details.isApprover
}); });
@@ -376,7 +388,11 @@ export const AccessApprovalRequest = ({
details.isReviewedByUser || details.isReviewedByUser ||
details.isRejectedByAnyone || details.isRejectedByAnyone ||
(!details.isApprover && (!details.isApprover &&
!(details.isSoftEnforcement && details.isRequestedByCurrentUser)) !(
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
canBypassApprovalPermission
))
) )
return; return;
@@ -387,9 +403,11 @@ export const AccessApprovalRequest = ({
) { ) {
setSelectedRequest({ setSelectedRequest({
...request, ...request,
user: details.isRequestedByCurrentUser user:
? user details.isRequestedByCurrentUser ||
: membersGroupById?.[request.requestedByUserId].user!, !membersGroupById?.[request.requestedByUserId].user
? user
: membersGroupById?.[request.requestedByUserId].user,
isRequestedByCurrentUser: details.isRequestedByCurrentUser, isRequestedByCurrentUser: details.isRequestedByCurrentUser,
isApprover: details.isApprover isApprover: details.isApprover
}); });
@@ -463,6 +481,7 @@ export const AccessApprovalRequest = ({
setSelectedRequest(null); setSelectedRequest(null);
refetchRequests(); refetchRequests();
}} }}
canBypassApprovalPermission={canBypassApprovalPermission}
/> />
)} )}
@@ -1,5 +1,8 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms"; import ms from "ms";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { Button, Checkbox, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
@@ -8,9 +11,6 @@ import { ProjectPermissionActions } from "@app/context";
import { useReviewAccessRequest } from "@app/hooks/api"; import { useReviewAccessRequest } from "@app/hooks/api";
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
import { EnforcementLevel } from "@app/hooks/api/policies/enums"; import { EnforcementLevel } from "@app/hooks/api/policies/enums";
import { twMerge } from "tailwind-merge";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
export const ReviewAccessRequestModal = ({ export const ReviewAccessRequestModal = ({
isOpen, isOpen,
@@ -18,7 +18,8 @@ export const ReviewAccessRequestModal = ({
request, request,
projectSlug, projectSlug,
selectedRequester, selectedRequester,
selectedEnvSlug selectedEnvSlug,
canBypassApprovalPermission
}: { }: {
isOpen: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
@@ -30,6 +31,7 @@ export const ReviewAccessRequestModal = ({
projectSlug: string; projectSlug: string;
selectedRequester: string | undefined; selectedRequester: string | undefined;
selectedEnvSlug: string | undefined; selectedEnvSlug: string | undefined;
canBypassApprovalPermission: boolean;
}) => { }) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
const [bypassApproval, setBypassApproval] = useState(false); const [bypassApproval, setBypassApproval] = useState(false);
@@ -182,7 +184,11 @@ export const ReviewAccessRequestModal = ({
<Button <Button
isLoading={isLoading === "approved"} isLoading={isLoading === "approved"}
isDisabled={ isDisabled={
!!isLoading || (!request.isApprover && !bypassApproval && isSoftEnforcement) !!isLoading ||
(!request.isApprover &&
!bypassApproval &&
isSoftEnforcement &&
canBypassApprovalPermission)
} }
onClick={() => handleReview("approved")} onClick={() => handleReview("approved")}
className="mt-4" className="mt-4"
@@ -202,39 +208,42 @@ export const ReviewAccessRequestModal = ({
</Button> </Button>
</div> </div>
{isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && ( {isSoftEnforcement &&
<div className="mt-2 flex flex-col space-y-2"> request.isRequestedByCurrentUser &&
<Checkbox !request.isApprover &&
onCheckedChange={(checked) => setBypassApproval(checked === true)} canBypassApprovalPermission && (
isChecked={bypassApproval} <div className="mt-2 flex flex-col space-y-2">
id="byPassApproval" <Checkbox
checkIndicatorBg="text-white" onCheckedChange={(checked) => setBypassApproval(checked === true)}
className={twMerge( isChecked={bypassApproval}
"mr-2", id="byPassApproval"
bypassApproval ? "border-red bg-red hover:bg-red-600" : "" checkIndicatorBg="text-white"
)} className={twMerge(
> "mr-2",
<span className="text-xs text-red"> bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
Approve without waiting for requirements to be met (bypass policy protection) )}
</span>
</Checkbox>
{bypassApproval && (
<FormControl
label="Reason for bypass"
className="mt-2"
isRequired
tooltipText="Enter a reason for bypassing the secret change policy"
> >
<Input <span className="text-xs text-red">
value={bypassReason} Approve without waiting for requirements to be met (bypass policy protection)
onChange={(e) => setBypassReason(e.currentTarget.value)} </span>
placeholder="Enter reason for bypass (min 10 chars)" </Checkbox>
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />} {bypassApproval && (
/> <FormControl
</FormControl> label="Reason for bypass"
)} className="mt-2"
</div> isRequired
)} tooltipText="Enter a reason for bypassing the secret change policy"
>
<Input
value={bypassReason}
onChange={(e) => setBypassReason(e.currentTarget.value)}
placeholder="Enter reason for bypass (min 10 chars)"
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
/>
</FormControl>
)}
</div>
)}
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>