feat: resolved changes made in backend

This commit is contained in:
=
2025-12-07 16:58:38 +05:30
parent 273a899500
commit 5d580f9d4b
21 changed files with 234 additions and 198 deletions
-4
View File
@@ -366,10 +366,6 @@ export const ROUTE_PATHS = Object.freeze({
PamSessionByIDPage: setRoute( PamSessionByIDPage: setRoute(
"/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId", "/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId" "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
),
ApprovalRequestDetailPage: setRoute(
"/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/approval-requests/$approvalRequestId"
) )
}, },
Public: { Public: {
@@ -0,0 +1,10 @@
export { useRevokeApprovalGrant } from "./mutations";
export { approvalGrantQuery } from "./queries";
export {
ApprovalGrantStatus,
type PamAccessGrantAttributes,
type TApprovalGrant,
type TGetApprovalGrantByIdDTO,
type TListApprovalGrantsDTO,
type TRevokeApprovalGrantDTO
} from "./types";
@@ -0,0 +1,23 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { approvalGrantQuery } from "./queries";
import { TApprovalGrant, TRevokeApprovalGrantDTO } from "./types";
export const useRevokeApprovalGrant = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ policyType, grantId, revocationReason }: TRevokeApprovalGrantDTO) => {
const { data } = await apiRequest.post<{ grant: TApprovalGrant }>(
`/api/v1/approval-policies/${policyType}/grants/${grantId}/revoke`,
{ revocationReason }
);
return data.grant;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: approvalGrantQuery.allKey() });
}
});
};
@@ -0,0 +1,37 @@
import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TApprovalGrant, TGetApprovalGrantByIdDTO, TListApprovalGrantsDTO } from "./types";
export const approvalGrantQuery = {
allKey: () => ["approval-grants"] as const,
getByIdKey: (params: TGetApprovalGrantByIdDTO) =>
[...approvalGrantQuery.allKey(), "by-id", params] as const,
listKey: (params: TListApprovalGrantsDTO) =>
[...approvalGrantQuery.allKey(), "list", params] as const,
getById: (params: TGetApprovalGrantByIdDTO) =>
queryOptions({
queryKey: approvalGrantQuery.getByIdKey(params),
queryFn: async () => {
const { data } = await apiRequest.get<{ grant: TApprovalGrant }>(
`/api/v1/approval-policies/${params.policyType}/grants/${params.grantId}`
);
return data.grant;
}
}),
list: (params: TListApprovalGrantsDTO) =>
queryOptions({
queryKey: approvalGrantQuery.listKey(params),
queryFn: async () => {
const { data } = await apiRequest.get<{
grants: TApprovalGrant[];
}>(`/api/v1/approval-policies/${params.policyType}/grants`, {
params: {
projectId: params.projectId
}
});
return data.grants;
}
})
};
@@ -0,0 +1,46 @@
import { ApprovalPolicyType } from "../approvalPolicies";
export enum ApprovalGrantStatus {
Active = "active",
Expired = "expired",
Revoked = "revoked"
}
// PAM Access Grant Attributes
export type PamAccessGrantAttributes = {
accountPath: string;
accessDuration: string;
};
// Base Grant Type
export type TApprovalGrant = {
id: string;
projectId: string;
requestId: string | null;
granteeUserId: string | null;
revokedByUserId: string | null;
revocationReason: string | null;
status: ApprovalGrantStatus;
type: ApprovalPolicyType;
attributes: PamAccessGrantAttributes;
createdAt: string;
expiresAt: string | null;
revokedAt: string | null;
};
// DTOs
export type TListApprovalGrantsDTO = {
policyType: ApprovalPolicyType;
projectId: string;
};
export type TGetApprovalGrantByIdDTO = {
policyType: ApprovalPolicyType;
grantId: string;
};
export type TRevokeApprovalGrantDTO = {
policyType: ApprovalPolicyType;
grantId: string;
revocationReason?: string;
};
@@ -18,14 +18,13 @@ export type ApprovalPolicyStep = {
}; };
export type PamAccessPolicyConditions = { export type PamAccessPolicyConditions = {
resourceIds: string[];
accountPaths: string[]; accountPaths: string[];
}[]; }[];
export type PamAccessPolicyConstraints = { export type PamAccessPolicyConstraints = {
requestDurationSeconds: { accessDuration: {
min: number; min: string;
max: number; max: string;
}; };
}; };
@@ -33,7 +32,7 @@ export type TApprovalPolicy = {
id: string; id: string;
projectId: string; projectId: string;
name: string; name: string;
maxRequestTtlSeconds?: number | null; maxRequestTtl?: string | null;
type: ApprovalPolicyType; type: ApprovalPolicyType;
conditions: { conditions: {
version: number; version: number;
@@ -52,7 +51,7 @@ export type TCreateApprovalPolicyDTO = {
policyType: ApprovalPolicyType; policyType: ApprovalPolicyType;
projectId: string; projectId: string;
name: string; name: string;
maxRequestTtlSeconds?: number | null; maxRequestTtl?: string | null;
conditions: PamAccessPolicyConditions; conditions: PamAccessPolicyConditions;
constraints: PamAccessPolicyConstraints; constraints: PamAccessPolicyConstraints;
steps: ApprovalPolicyStep[]; steps: ApprovalPolicyStep[];
@@ -62,7 +61,7 @@ export type TUpdateApprovalPolicyDTO = {
policyType: ApprovalPolicyType; policyType: ApprovalPolicyType;
policyId: string; policyId: string;
name?: string; name?: string;
maxRequestTtlSeconds?: number | null; maxRequestTtl?: string | null;
conditions?: PamAccessPolicyConditions; conditions?: PamAccessPolicyConditions;
constraints?: PamAccessPolicyConstraints; constraints?: PamAccessPolicyConstraints;
steps?: ApprovalPolicyStep[]; steps?: ApprovalPolicyStep[];
@@ -1,5 +1,6 @@
export { export {
useApproveApprovalRequest, useApproveApprovalRequest,
useCancelApprovalRequest,
useCreateApprovalRequest, useCreateApprovalRequest,
useRejectApprovalRequest useRejectApprovalRequest
} from "./mutations"; } from "./mutations";
@@ -6,6 +6,7 @@ import { approvalRequestQuery } from "./queries";
import { import {
TApprovalRequest, TApprovalRequest,
TApproveApprovalRequestDTO, TApproveApprovalRequestDTO,
TCancelApprovalRequestDTO,
TCreateApprovalRequestDTO, TCreateApprovalRequestDTO,
TRejectApprovalRequestDTO TRejectApprovalRequestDTO
} from "./types"; } from "./types";
@@ -57,3 +58,18 @@ export const useRejectApprovalRequest = () => {
} }
}); });
}; };
export const useCancelApprovalRequest = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ policyType, requestId }: TCancelApprovalRequestDTO) => {
const { data } = await apiRequest.post<{ request: TApprovalRequest }>(
`/api/v1/approval-policies/${policyType}/requests/${requestId}/cancel`
);
return data.request;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() });
}
});
};
@@ -49,9 +49,8 @@ export type ApprovalRequestStep = {
}; };
export type PamAccessRequestData = { export type PamAccessRequestData = {
resourceId: string;
accountPath: string; accountPath: string;
requestDurationSeconds: number; accessDuration: number;
}; };
export type TApprovalRequest = { export type TApprovalRequest = {
@@ -78,7 +77,7 @@ export type TCreateApprovalRequestDTO = {
policyType: ApprovalPolicyType; policyType: ApprovalPolicyType;
projectId: string; projectId: string;
justification?: string | null; justification?: string | null;
expiresAt?: Date | null; requestDuration?: string | null;
requestData: PamAccessRequestData; requestData: PamAccessRequestData;
}; };
@@ -103,3 +102,8 @@ export type TRejectApprovalRequestDTO = {
requestId: string; requestId: string;
comment?: string; comment?: string;
}; };
export type TCancelApprovalRequestDTO = {
policyType: ApprovalPolicyType;
requestId: string;
};
+3
View File
@@ -1,6 +1,9 @@
export * from "./accessApproval"; export * from "./accessApproval";
export * from "./admin"; export * from "./admin";
export * from "./apiKeys"; export * from "./apiKeys";
export * from "./approvalGrants";
export * from "./approvalPolicies";
export * from "./approvalRequests";
export * from "./assumePrivileges"; export * from "./assumePrivileges";
export * from "./auditLogs"; export * from "./auditLogs";
export * from "./auditLogStreams"; export * from "./auditLogStreams";
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useParams } from "@tanstack/react-router";
import { ContentLoader, EmptyState, PageHeader } from "@app/components/v2"; import { ContentLoader, EmptyState, PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -61,7 +61,7 @@ const PageContent = () => {
<PageHeader <PageHeader
scope={ProjectType.PAM} scope={ProjectType.PAM}
title="Approval Request" title="Approval Request"
description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.requestDurationSeconds} by ${request.requesterName || "Unknown"}`} description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.accessDuration} by ${request.requesterName || "Unknown"}`}
/> />
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<div className="flex w-96 flex-col gap-4"> <div className="flex w-96 flex-col gap-4">
@@ -5,7 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
FormControl,
FormLabel, FormLabel,
Popover, Popover,
PopoverContent, PopoverContent,
@@ -23,18 +23,8 @@ const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
} }
}; };
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
}
return `${minutes}m`;
};
export const RequestDetailsSection = ({ request }: Props) => { export const RequestDetailsSection = ({ request }: Props) => {
const { accountPath, requestDurationSeconds } = request.requestData.requestData; const { accountPath, accessDuration } = request.requestData.requestData;
return ( return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3"> <div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
@@ -53,9 +43,7 @@ export const RequestDetailsSection = ({ request }: Props) => {
</GenericFieldLabel> </GenericFieldLabel>
<GenericFieldLabel label="Requester Email">{request.requesterEmail}</GenericFieldLabel> <GenericFieldLabel label="Requester Email">{request.requesterEmail}</GenericFieldLabel>
<GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel> <GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel>
<GenericFieldLabel label="Duration"> <GenericFieldLabel label="Duration">{accessDuration}</GenericFieldLabel>
{formatDuration(requestDurationSeconds)}
</GenericFieldLabel>
{request.justification && ( {request.justification && (
<GenericFieldLabel label="Justification"> <GenericFieldLabel label="Justification">
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words"> <p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
@@ -63,16 +63,6 @@ const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
} }
}; };
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
}
return `${minutes}m`;
};
const checkIfUserNeedsToApprove = ( const checkIfUserNeedsToApprove = (
request: TApprovalRequest, request: TApprovalRequest,
userId: string, userId: string,
@@ -131,7 +121,6 @@ export const RequestsTable = () => {
request.requesterName?.toLowerCase().includes(search.toLowerCase()) || request.requesterName?.toLowerCase().includes(search.toLowerCase()) ||
request.requesterEmail?.toLowerCase().includes(search.toLowerCase()) || request.requesterEmail?.toLowerCase().includes(search.toLowerCase()) ||
request.justification?.toLowerCase().includes(search.toLowerCase()) || request.justification?.toLowerCase().includes(search.toLowerCase()) ||
request.requestData.requestData.resourceId.toLowerCase().includes(search.toLowerCase()) ||
request.requestData.requestData.accountPath.toLowerCase().includes(search.toLowerCase()) request.requestData.requestData.accountPath.toLowerCase().includes(search.toLowerCase())
); );
} }
@@ -272,7 +261,7 @@ export const RequestsTable = () => {
{!isRequestsLoading && {!isRequestsLoading &&
paginatedRequests.map((request) => { paginatedRequests.map((request) => {
const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups); const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups);
const { accountPath, requestDurationSeconds } = request.requestData.requestData; const { accountPath, accessDuration } = request.requestData.requestData;
return ( return (
<Tr <Tr
@@ -297,14 +286,12 @@ export const RequestsTable = () => {
</div> </div>
</Td> </Td>
<Td> <Td>
<span className="text-sm text-mineshaft-200"> <span className="text-sm text-mineshaft-200">{accessDuration}</span>
{formatDuration(requestDurationSeconds)}
</span>
</Td> </Td>
<Td> <Td>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant={getStatusBadgeColor(request.status)}> <Badge className="capitalize" variant={getStatusBadgeColor(request.status)}>
{request.status} {request.status.split("-").join(" ")}
</Badge> </Badge>
{needsApproval && ( {needsApproval && (
<div className="flex items-center gap-1 rounded bg-primary/20 px-2 py-0.5 text-xs text-primary"> <div className="flex items-center gap-1 rounded bg-primary/20 px-2 py-0.5 text-xs text-primary">
@@ -37,6 +37,8 @@ import {
ApproverType ApproverType
} from "@app/hooks/api/approvalPolicies"; } from "@app/hooks/api/approvalPolicies";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { Badge } from "@app/components/v3";
import { User, Users } from "lucide-react";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
@@ -113,9 +115,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{!isPoliciesLoading && {!isPoliciesLoading &&
policies.map((policy) => { policies.map((policy) => {
const isExpanded = expandedRows.has(policy.id); const isExpanded = expandedRows.has(policy.id);
const maxTtl = policy.maxRequestTtlSeconds const maxTtl = policy.maxRequestTtl ? policy.maxRequestTtl : "No limit";
? `${Math.floor(policy.maxRequestTtlSeconds / 3600)}h`
: "No limit";
const conditionsCount = policy.conditions.conditions.length; const conditionsCount = policy.conditions.conditions.length;
return ( return (
@@ -198,8 +198,8 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{isExpanded && ( {isExpanded && (
<Tr className="bg-mineshaft-800"> <Tr className="bg-mineshaft-800">
<Td colSpan={5} className="p-0"> <Td colSpan={5} className="p-0">
<div className="max-h-80 overflow-auto overflow-x-hidden"> <div className="flex max-h-80 w-full gap-2 overflow-auto overflow-x-hidden">
<div className="p-4"> <div className="flex-1 p-4">
<div className="mb-2 text-sm font-medium text-mineshaft-300"> <div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Contraints Approval Contraints
</div> </div>
@@ -211,27 +211,6 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
)} )}
> >
<div className="space-y-2"> <div className="space-y-2">
<div>
<span className="text-sm font-medium text-mineshaft-300">
Resources:
</span>
<p className="text-sm text-mineshaft-100">
{step.resourceIds.join(", ")}
</p>
</div>
<div className="flex items-center justify-center">
<div
style={{ height: "1px" }}
className="w-1/5 bg-mineshaft-500"
/>
<span className="px-2 text-xs font-medium text-mineshaft-400">
AND
</span>
<div
style={{ height: "1px" }}
className="w-1/5 bg-mineshaft-500"
/>
</div>
<div> <div>
<span className="text-sm font-medium text-mineshaft-300"> <span className="text-sm font-medium text-mineshaft-300">
Account Paths: Account Paths:
@@ -256,7 +235,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Fragment> </Fragment>
))} ))}
</div> </div>
<div className="p-4"> <div className="flex-2 p-4">
<div className="mb-2 text-sm font-medium text-mineshaft-300"> <div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Sequence Approval Sequence
</div> </div>
@@ -264,11 +243,11 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
<div <div
key={`${policy.id}-step-${index + 1}`} key={`${policy.id}-step-${index + 1}`}
className={twMerge( className={twMerge(
"mb-3 rounded border border-mineshaft-600 bg-mineshaft-900 p-3", "mb-3 rounded border border-mineshaft-600 bg-mineshaft-900",
index === policy.steps.length - 1 && "mb-0" index === policy.steps.length - 1 && "mb-0"
)} )}
> >
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between bg-mineshaft-700 p-3">
<div className="text-sm font-medium text-mineshaft-200"> <div className="text-sm font-medium text-mineshaft-200">
Step {index + 1} Step {index + 1}
{step.name && ( {step.name && (
@@ -282,21 +261,19 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{step.requiredApprovals !== 1 ? "s" : ""} {step.requiredApprovals !== 1 ? "s" : ""}
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2 p-3">
{step.approvers.map((approver, approverIndex) => ( {step.approvers.map((approver, approverIndex) => (
<div <Badge
variant="neutral"
key={`${policy.id}-step-${index + 1}-approver-${approverIndex + 1}`} key={`${policy.id}-step-${index + 1}-approver-${approverIndex + 1}`}
className="rounded bg-mineshaft-700 px-2 py-1 text-xs text-mineshaft-300"
> >
<FontAwesomeIcon {approver.type === ApproverType.Group ? (
icon={faUsers} <User />
className="mr-1.5 text-mineshaft-400" ) : (
/> <Users />
<span className="capitalize">{approver.type}:</span>{" "} )}
<span className="text-mineshaft-200"> {getApproverLabel(approver.id, approver.type)}
{getApproverLabel(approver.id, approver.type)} </Badge>
</span>
</div>
))} ))}
</div> </div>
</div> </div>
@@ -26,7 +26,7 @@ type Props = {
}; };
const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [ const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [
{ name: "Details", key: "details", fields: ["name", "maxRequestTtlSeconds"] }, { name: "Details", key: "details", fields: ["name", "maxRequestTtl"] },
{ name: "Constraints", key: "constraints", fields: ["conditions", "constraints"] }, { name: "Constraints", key: "constraints", fields: ["conditions", "constraints"] },
{ name: "Approvals", key: "approvals", fields: ["steps"] }, { name: "Approvals", key: "approvals", fields: ["steps"] },
{ name: "Review", key: "review", fields: [] } { name: "Review", key: "review", fields: [] }
@@ -45,12 +45,12 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
resolver: zodResolver(PolicyFormSchema), resolver: zodResolver(PolicyFormSchema),
defaultValues: { defaultValues: {
name: "", name: "",
maxRequestTtlSeconds: null, maxRequestTtl: null,
conditions: [{ resourceIds: [], accountPaths: [] }], conditions: [{ accountPaths: [] }],
constraints: { constraints: {
requestDurationSeconds: { accessDuration: {
min: 30, min: "30s",
max: 604800 max: "7d"
} }
}, },
steps: [ steps: [
@@ -74,7 +74,7 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
if (policyData?.policy) { if (policyData?.policy) {
reset({ reset({
name: policyData.policy.name, name: policyData.policy.name,
maxRequestTtlSeconds: policyData.policy.maxRequestTtlSeconds, maxRequestTtl: policyData.policy.maxRequestTtl,
conditions: policyData.policy.conditions.conditions, conditions: policyData.policy.conditions.conditions,
constraints: policyData.policy.constraints.constraints, constraints: policyData.policy.constraints.constraints,
steps: policyData.policy.steps.map((step) => ({ steps: policyData.policy.steps.map((step) => ({
@@ -85,12 +85,12 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
} else { } else {
reset({ reset({
name: "", name: "",
maxRequestTtlSeconds: null, maxRequestTtl: null,
conditions: [{ resourceIds: [], accountPaths: [] }], conditions: [{ accountPaths: [] }],
constraints: { constraints: {
requestDurationSeconds: { accessDuration: {
min: 30, min: "30s",
max: 604800 max: "7d"
} }
}, },
steps: [ steps: [
@@ -1,21 +1,41 @@
import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/hooks/api/approvalPolicies"; import { ApproverType } from "@app/hooks/api/approvalPolicies";
// 30 to 7 days
const DurationSchema = (
min = 30,
max = 604800,
msg = "Duration must be between 30 seconds and 7 days"
) =>
z.string().refine(
(val) => {
const duration = ms(val) / 1000;
// 30 seconds to 7 days
return duration >= min && duration <= max;
},
{ message: msg }
);
export const PolicyFormSchema = z.object({ export const PolicyFormSchema = z.object({
name: z.string().min(1, "Policy name is required").max(128), name: z.string().min(1, "Policy name is required").max(128),
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(), maxRequestTtl: DurationSchema(
3600,
2592000,
"Duration must be between 1 hour and 30 days"
).nullish(),
conditions: z conditions: z
.object({ .object({
resourceIds: z.array(z.string().uuid()),
accountPaths: z.array(z.string().min(1)) accountPaths: z.array(z.string().min(1))
}) })
.array() .array()
.min(1, "At least one condition is required"), .min(1, "At least one condition is required"),
constraints: z.object({ constraints: z.object({
requestDurationSeconds: z.object({ accessDuration: z.object({
min: z.number().min(0).max(604800), min: DurationSchema(),
max: z.number().min(1).max(604800) max: DurationSchema()
}) })
}), }),
steps: z steps: z
@@ -33,7 +33,7 @@ export const PolicyConstraintsStep = () => {
variant="outline_bg" variant="outline_bg"
size="xs" size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => appendCondition({ resourceIds: [], accountPaths: [] })} onClick={() => appendCondition({ accountPaths: [] })}
> >
Add Condition Add Condition
</Button> </Button>
@@ -58,35 +58,6 @@ export const PolicyConstraintsStep = () => {
)} )}
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<Controller
control={control}
name={`conditions.${index}.resourceIds`}
render={({ field: resourceField, fieldState: { error } }) => (
<FormControl
label="Resource IDs"
isError={Boolean(error)}
errorText={error?.message}
helperText="Comma-separated UUIDs of resources this condition applies to"
>
<Input
value={resourceField.value.join(", ")}
onChange={(e) => {
const ids = e.target.value
.split(",")
.map((id) => id.trim())
.filter(Boolean);
resourceField.onChange(ids);
}}
placeholder="e.g., 550e8400-e29b-41d4-a716-446655440000, ..."
/>
</FormControl>
)}
/>
<div className="flex items-center justify-center">
<div style={{ height: "1px" }} className="w-1/5 bg-mineshaft-500" />
<span className="px-2 text-xs font-medium text-mineshaft-400">AND</span>
<div style={{ height: "1px" }} className="w-1/5 bg-mineshaft-500" />
</div>
<Controller <Controller
control={control} control={control}
name={`conditions.${index}.accountPaths`} name={`conditions.${index}.accountPaths`}
@@ -106,7 +77,7 @@ export const PolicyConstraintsStep = () => {
.filter(Boolean); .filter(Boolean);
pathField.onChange(paths); pathField.onChange(paths);
}} }}
placeholder="e.g., /admin/*, /users/john" placeholder="e.g., /admin/**, /users/john, /**"
/> />
</FormControl> </FormControl>
)} )}
@@ -1,5 +1,6 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { TtlFormLabel } from "@app/components/features";
import { FormControl, Input } from "@app/components/v2"; import { FormControl, Input } from "@app/components/v2";
import { TPolicyForm } from "../PolicySchema"; import { TPolicyForm } from "../PolicySchema";
@@ -25,24 +26,15 @@ export const PolicyDetailsStep = () => {
/> />
<Controller <Controller
control={control} control={control}
name="maxRequestTtlSeconds" name="maxRequestTtl"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Max Request TTL (seconds)"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
helperText="Maximum time-to-live for requests. Must be between 1 hour (3600s) and 30 days (2592000s). Leave empty for no limit." label={<TtlFormLabel label="Max Request TTL" />}
helperText="Maximum time-to-live for requests. Must be between 1 hour and 30 days. Leave empty for no limit."
> >
<Input <Input {...field} value={field.value ?? ""} placeholder="1h" />
{...field}
type="number"
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value;
field.onChange(val === "" ? null : parseInt(val, 10));
}}
placeholder="e.g., 86400 (24 hours)"
/>
</FormControl> </FormControl>
)} )}
/> />
@@ -58,41 +50,29 @@ export const PolicyDetailsStep = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Controller <Controller
control={control} control={control}
name="constraints.requestDurationSeconds.min" name="constraints.accessDuration.min"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Minimum Hours" label={<TtlFormLabel label="Minimum TTL" />}
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
helperText="30-604800 hours" helperText="Must be between 30s and 7 days"
> >
<Input <Input {...field} />
{...field}
type="number"
min={0}
max={168}
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
/>
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="constraints.requestDurationSeconds.max" name="constraints.accessDuration.max"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Maximum Hours" label={<TtlFormLabel label="Maximum TTL" />}
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
helperText="30-604800 hours (7 days)" helperText="Must be between 30s and 7 days"
> >
<Input <Input {...field} />
{...field}
type="number"
min={1}
max={168}
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
/>
</FormControl> </FormControl>
)} )}
/> />
@@ -24,17 +24,7 @@ export const PolicyReviewStep = () => {
const { data: members = [] } = useGetWorkspaceUsers(projectId); const { data: members = [] } = useGetWorkspaceUsers(projectId);
const { data: groups = [] } = useListWorkspaceGroups(projectId); const { data: groups = [] } = useListWorkspaceGroups(projectId);
const { name, maxRequestTtlSeconds, conditions, constraints, steps } = watch(); const { name, maxRequestTtl, conditions, constraints, steps } = watch();
const formatTtl = (seconds: number | null | undefined) => {
if (!seconds) return "No limit";
const hours = Math.floor(seconds / 3600);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days} day${days !== 1 ? "s" : ""} (${seconds}s)`;
}
return `${hours} hour${hours !== 1 ? "s" : ""} (${seconds}s)`;
};
const getApproverLabel = (approverId: string, approverType: ApproverType) => { const getApproverLabel = (approverId: string, approverType: ApproverType) => {
if (approverType === ApproverType.User) { if (approverType === ApproverType.User) {
@@ -59,7 +49,7 @@ export const PolicyReviewStep = () => {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<ReviewField label="Policy Name" value={name || "Not set"} /> <ReviewField label="Policy Name" value={name || "Not set"} />
<ReviewField label="Max Request TTL" value={formatTtl(maxRequestTtlSeconds)} /> <ReviewField label="Max Request TTL" value={maxRequestTtl || "No Limit"} />
</div> </div>
</div> </div>
@@ -68,14 +58,8 @@ export const PolicyReviewStep = () => {
<h3 className="text-sm font-medium text-mineshaft-200">Request Duration Constraints</h3> <h3 className="text-sm font-medium text-mineshaft-200">Request Duration Constraints</h3>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<ReviewField <ReviewField label="Minimum Duration" value={constraints.accessDuration.min} />
label="Minimum Duration" <ReviewField label="Maximum Duration" value={constraints.accessDuration.max} />
value={`${constraints.requestDurationSeconds.min} second${constraints.requestDurationSeconds.min !== 1 ? "s" : ""}`}
/>
<ReviewField
label="Maximum Duration"
value={`${constraints.requestDurationSeconds.max} second${constraints.requestDurationSeconds.max !== 1 ? "s" : ""}`}
/>
</div> </div>
</div> </div>
@@ -95,14 +79,6 @@ export const PolicyReviewStep = () => {
Condition {index + 1} Condition {index + 1}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div>
<span className="text-xs text-mineshaft-400">Resource IDs: </span>
<span className="text-xs text-mineshaft-200">
{condition.resourceIds.length > 0
? condition.resourceIds.join(", ")
: "None specified"}
</span>
</div>
<div> <div>
<span className="text-xs text-mineshaft-400">Account Paths: </span> <span className="text-xs text-mineshaft-400">Account Paths: </span>
<span className="text-xs text-mineshaft-200"> <span className="text-xs text-mineshaft-200">
@@ -0,0 +1,3 @@
export const RequestGrantTab = () => {
return <div>Grant list</div>;
};