+
+
+
- Approval Contraints
+ Approval Conditions
{policy.conditions.conditions.map((step, index) => (
@@ -235,7 +239,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
))}
-
+
Approval Sequence
diff --git a/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicyModal.tsx b/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicyModal.tsx
index 0d92981d8..790ade93f 100644
--- a/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicyModal.tsx
+++ b/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicyModal.tsx
@@ -26,8 +26,8 @@ type Props = {
};
const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [
- { name: "Details", key: "details", fields: ["name", "maxRequestTtl"] },
- { name: "Constraints", key: "constraints", fields: ["conditions", "constraints"] },
+ { name: "Details", key: "details", fields: ["name", "maxRequestTtl", "constraints"] },
+ { name: "Conditions", key: "conditions", fields: ["conditions"] },
{ name: "Approvals", key: "approvals", fields: ["steps"] },
{ name: "Review", key: "review", fields: [] }
];
diff --git a/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyConstraintsStep.tsx b/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyConstraintsStep.tsx
index bfc17223d..52289ef14 100644
--- a/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyConstraintsStep.tsx
+++ b/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyConstraintsStep.tsx
@@ -66,7 +66,7 @@ export const PolicyConstraintsStep = () => {
label="Account Paths"
isError={Boolean(error)}
errorText={error?.message}
- helperText="Comma-separated account paths this condition applies to"
+ helperText="Matches any of the comma-separated account paths this condition applies to"
>
{
}
+ label={ }
helperText="Maximum time-to-live for requests. Must be between 1 hour and 30 days. Leave empty for no limit."
>
@@ -41,10 +41,10 @@ export const PolicyDetailsStep = () => {
- Request Duration Constraints
+ PAM Account Access Duration TTL
- Set minimum and maximum duration (in seconds) for access requests
+ Set minimum and maximum duration (in seconds) for pam account access
diff --git a/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyReviewStep.tsx b/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyReviewStep.tsx
index 10595015d..7dacb73c9 100644
--- a/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyReviewStep.tsx
+++ b/frontend/src/pages/pam/ApprovalsPage/components/PolicyTab/components/PolicySteps/PolicyReviewStep.tsx
@@ -49,13 +49,15 @@ export const PolicyReviewStep = () => {
-
+
- Request Duration Constraints
+
+ PAM Account Access Duration TTL
+
diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx
index f19a1a308..61a09c1b0 100644
--- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx
+++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx
@@ -8,6 +8,7 @@ import {
faFilter,
faFolderPlus,
faMagnifyingGlass,
+ faPen,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
@@ -67,6 +68,7 @@ import { PamAddFolderModal } from "./PamAddFolderModal";
import { PamDeleteAccountModal } from "./PamDeleteAccountModal";
import { PamDeleteFolderModal } from "./PamDeleteFolderModal";
import { PamFolderRow } from "./PamFolderRow";
+import { PamRequestAccountAccessModal } from "./PamRequestAccountAccessModal";
import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
import { useAccessAwsIamAccount } from "./useAccessAwsIamAccount";
@@ -90,6 +92,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
"deleteFolder",
"addAccount",
"accessAccount",
+ "requestAccount",
"updateAccount",
"deleteAccount"
] as const);
@@ -323,6 +326,14 @@ export const PamAccountsTable = ({ projectId }: Props) => {
)}
+ }
+ onClick={() => handlePopUpOpen("requestAccount")}
+ className="h-10 transition-colors"
+ >
+ Request Account
+
{
}
projectId={projectId}
/>
+ handlePopUpToggle("requestAccount", isOpen)}
+ />
handlePopUpToggle("deleteAccount", isOpen)}
diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamRequestAccountAccessModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamRequestAccountAccessModal.tsx
new file mode 100644
index 000000000..0e37d8af6
--- /dev/null
+++ b/frontend/src/pages/pam/PamAccountsPage/components/PamRequestAccountAccessModal.tsx
@@ -0,0 +1,187 @@
+import { useMemo } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import ms from "ms";
+import { z } from "zod";
+
+import { TtlFormLabel } from "@app/components/features";
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ FormControl,
+ Input,
+ Modal,
+ ModalClose,
+ ModalContent,
+ TextArea
+} from "@app/components/v2";
+import { useProject } from "@app/context";
+import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
+import { useCreateApprovalRequest } from "@app/hooks/api/approvalRequests/mutations";
+import { TPamAccount } from "@app/hooks/api/pam";
+
+type Props = {
+ account?: TPamAccount;
+ accountPath?: string;
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+};
+
+const formSchema = z.object({
+ accountPath: z.string().min(1, "Account path is required"),
+ accessDuration: z
+ .string()
+ .min(1, "Access duration is required")
+ .refine(
+ (value) => {
+ try {
+ const duration = ms(value);
+ return duration > 0;
+ } catch {
+ return false;
+ }
+ },
+ { message: "Invalid duration format. Use formats like: 1h, 3d, 30m" }
+ ),
+ justification: z.string().max(512).optional()
+});
+
+type FormData = z.infer;
+
+const Content = ({ onOpenChange, account, accountPath }: Props) => {
+ const { projectId } = useProject();
+ const { mutateAsync: createApprovalRequest, isPending: isSubmitting } =
+ useCreateApprovalRequest();
+
+ const fullAccountPath = useMemo(() => {
+ const accountName = account?.name ?? "";
+ if (accountPath) {
+ const path = accountPath.replace(/^\/+|\/+$/g, "");
+ return `${path}/${accountName}`;
+ }
+ return accountName;
+ }, [account, accountPath]);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ accountPath: fullAccountPath,
+ accessDuration: "4h",
+ justification: ""
+ }
+ });
+
+ const {
+ control,
+ handleSubmit,
+ formState: { isDirty }
+ } = form;
+
+ const onSubmit = async (formData: FormData) => {
+ try {
+ await createApprovalRequest({
+ policyType: ApprovalPolicyType.PamAccess,
+ projectId,
+ justification: formData.justification || null,
+ requestData: {
+ accountPath: formData.accountPath,
+ accessDuration: formData.accessDuration
+ }
+ });
+
+ createNotification({
+ text: "Access request submitted successfully",
+ type: "success"
+ });
+
+ onOpenChange(false);
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to submit access request",
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+ );
+};
+
+export const PamRequestAccountAccessModal = (props: Props) => {
+ const { isOpen, onOpenChange } = props;
+
+ return (
+
+
+
+
+
+ );
+};
|