mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: resolved changes made in backend
This commit is contained in:
@@ -366,10 +366,6 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
PamSessionByIDPage: setRoute(
|
||||
"/organizations/$orgId/projects/pam/$projectId/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: {
|
||||
|
||||
10
frontend/src/hooks/api/approvalGrants/index.tsx
Normal file
10
frontend/src/hooks/api/approvalGrants/index.tsx
Normal file
@@ -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";
|
||||
23
frontend/src/hooks/api/approvalGrants/mutations.tsx
Normal file
23
frontend/src/hooks/api/approvalGrants/mutations.tsx
Normal file
@@ -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() });
|
||||
}
|
||||
});
|
||||
};
|
||||
37
frontend/src/hooks/api/approvalGrants/queries.tsx
Normal file
37
frontend/src/hooks/api/approvalGrants/queries.tsx
Normal file
@@ -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;
|
||||
}
|
||||
})
|
||||
};
|
||||
46
frontend/src/hooks/api/approvalGrants/types.ts
Normal file
46
frontend/src/hooks/api/approvalGrants/types.ts
Normal file
@@ -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 = {
|
||||
resourceIds: string[];
|
||||
accountPaths: string[];
|
||||
}[];
|
||||
|
||||
export type PamAccessPolicyConstraints = {
|
||||
requestDurationSeconds: {
|
||||
min: number;
|
||||
max: number;
|
||||
accessDuration: {
|
||||
min: string;
|
||||
max: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,7 +32,7 @@ export type TApprovalPolicy = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
maxRequestTtlSeconds?: number | null;
|
||||
maxRequestTtl?: string | null;
|
||||
type: ApprovalPolicyType;
|
||||
conditions: {
|
||||
version: number;
|
||||
@@ -52,7 +51,7 @@ export type TCreateApprovalPolicyDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
name: string;
|
||||
maxRequestTtlSeconds?: number | null;
|
||||
maxRequestTtl?: string | null;
|
||||
conditions: PamAccessPolicyConditions;
|
||||
constraints: PamAccessPolicyConstraints;
|
||||
steps: ApprovalPolicyStep[];
|
||||
@@ -62,7 +61,7 @@ export type TUpdateApprovalPolicyDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
policyId: string;
|
||||
name?: string;
|
||||
maxRequestTtlSeconds?: number | null;
|
||||
maxRequestTtl?: string | null;
|
||||
conditions?: PamAccessPolicyConditions;
|
||||
constraints?: PamAccessPolicyConstraints;
|
||||
steps?: ApprovalPolicyStep[];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
useApproveApprovalRequest,
|
||||
useCancelApprovalRequest,
|
||||
useCreateApprovalRequest,
|
||||
useRejectApprovalRequest
|
||||
} from "./mutations";
|
||||
|
||||
@@ -6,6 +6,7 @@ import { approvalRequestQuery } from "./queries";
|
||||
import {
|
||||
TApprovalRequest,
|
||||
TApproveApprovalRequestDTO,
|
||||
TCancelApprovalRequestDTO,
|
||||
TCreateApprovalRequestDTO,
|
||||
TRejectApprovalRequestDTO
|
||||
} 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 = {
|
||||
resourceId: string;
|
||||
accountPath: string;
|
||||
requestDurationSeconds: number;
|
||||
accessDuration: number;
|
||||
};
|
||||
|
||||
export type TApprovalRequest = {
|
||||
@@ -78,7 +77,7 @@ export type TCreateApprovalRequestDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
justification?: string | null;
|
||||
expiresAt?: Date | null;
|
||||
requestDuration?: string | null;
|
||||
requestData: PamAccessRequestData;
|
||||
};
|
||||
|
||||
@@ -103,3 +102,8 @@ export type TRejectApprovalRequestDTO = {
|
||||
requestId: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export type TCancelApprovalRequestDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export * from "./accessApproval";
|
||||
export * from "./admin";
|
||||
export * from "./apiKeys";
|
||||
export * from "./approvalGrants";
|
||||
export * from "./approvalPolicies";
|
||||
export * from "./approvalRequests";
|
||||
export * from "./assumePrivileges";
|
||||
export * from "./auditLogs";
|
||||
export * from "./auditLogStreams";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
|
||||
import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 { ROUTE_PATHS } from "@app/const/routes";
|
||||
@@ -61,7 +61,7 @@ const PageContent = () => {
|
||||
<PageHeader
|
||||
scope={ProjectType.PAM}
|
||||
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 w-96 flex-col gap-4">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Popover,
|
||||
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) => {
|
||||
const { accountPath, requestDurationSeconds } = request.requestData.requestData;
|
||||
const { accountPath, accessDuration } = request.requestData.requestData;
|
||||
|
||||
return (
|
||||
<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 label="Requester Email">{request.requesterEmail}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Duration">
|
||||
{formatDuration(requestDurationSeconds)}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Duration">{accessDuration}</GenericFieldLabel>
|
||||
{request.justification && (
|
||||
<GenericFieldLabel label="Justification">
|
||||
<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 = (
|
||||
request: TApprovalRequest,
|
||||
userId: string,
|
||||
@@ -131,7 +121,6 @@ export const RequestsTable = () => {
|
||||
request.requesterName?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.requesterEmail?.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())
|
||||
);
|
||||
}
|
||||
@@ -272,7 +261,7 @@ export const RequestsTable = () => {
|
||||
{!isRequestsLoading &&
|
||||
paginatedRequests.map((request) => {
|
||||
const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups);
|
||||
const { accountPath, requestDurationSeconds } = request.requestData.requestData;
|
||||
const { accountPath, accessDuration } = request.requestData.requestData;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
@@ -297,14 +286,12 @@ export const RequestsTable = () => {
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-200">
|
||||
{formatDuration(requestDurationSeconds)}
|
||||
</span>
|
||||
<span className="text-sm text-mineshaft-200">{accessDuration}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getStatusBadgeColor(request.status)}>
|
||||
{request.status}
|
||||
<Badge className="capitalize" variant={getStatusBadgeColor(request.status)}>
|
||||
{request.status.split("-").join(" ")}
|
||||
</Badge>
|
||||
{needsApproval && (
|
||||
<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
|
||||
} from "@app/hooks/api/approvalPolicies";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { User, Users } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
@@ -113,9 +115,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
||||
{!isPoliciesLoading &&
|
||||
policies.map((policy) => {
|
||||
const isExpanded = expandedRows.has(policy.id);
|
||||
const maxTtl = policy.maxRequestTtlSeconds
|
||||
? `${Math.floor(policy.maxRequestTtlSeconds / 3600)}h`
|
||||
: "No limit";
|
||||
const maxTtl = policy.maxRequestTtl ? policy.maxRequestTtl : "No limit";
|
||||
const conditionsCount = policy.conditions.conditions.length;
|
||||
|
||||
return (
|
||||
@@ -198,8 +198,8 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
||||
{isExpanded && (
|
||||
<Tr className="bg-mineshaft-800">
|
||||
<Td colSpan={5} className="p-0">
|
||||
<div className="max-h-80 overflow-auto overflow-x-hidden">
|
||||
<div className="p-4">
|
||||
<div className="flex max-h-80 w-full gap-2 overflow-auto overflow-x-hidden">
|
||||
<div className="flex-1 p-4">
|
||||
<div className="mb-2 text-sm font-medium text-mineshaft-300">
|
||||
Approval Contraints
|
||||
</div>
|
||||
@@ -211,27 +211,6 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
<span className="text-sm font-medium text-mineshaft-300">
|
||||
Account Paths:
|
||||
@@ -256,7 +235,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex-2 p-4">
|
||||
<div className="mb-2 text-sm font-medium text-mineshaft-300">
|
||||
Approval Sequence
|
||||
</div>
|
||||
@@ -264,11 +243,11 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<div
|
||||
key={`${policy.id}-step-${index + 1}`}
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
Step {index + 1}
|
||||
{step.name && (
|
||||
@@ -282,21 +261,19 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
||||
{step.requiredApprovals !== 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-2 p-3">
|
||||
{step.approvers.map((approver, approverIndex) => (
|
||||
<div
|
||||
<Badge
|
||||
variant="neutral"
|
||||
key={`${policy.id}-step-${index + 1}-approver-${approverIndex + 1}`}
|
||||
className="rounded bg-mineshaft-700 px-2 py-1 text-xs text-mineshaft-300"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faUsers}
|
||||
className="mr-1.5 text-mineshaft-400"
|
||||
/>
|
||||
<span className="capitalize">{approver.type}:</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{getApproverLabel(approver.id, approver.type)}
|
||||
</span>
|
||||
</div>
|
||||
{approver.type === ApproverType.Group ? (
|
||||
<User />
|
||||
) : (
|
||||
<Users />
|
||||
)}
|
||||
{getApproverLabel(approver.id, approver.type)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ type Props = {
|
||||
};
|
||||
|
||||
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: "Approvals", key: "approvals", fields: ["steps"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
@@ -45,12 +45,12 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
resolver: zodResolver(PolicyFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
maxRequestTtlSeconds: null,
|
||||
conditions: [{ resourceIds: [], accountPaths: [] }],
|
||||
maxRequestTtl: null,
|
||||
conditions: [{ accountPaths: [] }],
|
||||
constraints: {
|
||||
requestDurationSeconds: {
|
||||
min: 30,
|
||||
max: 604800
|
||||
accessDuration: {
|
||||
min: "30s",
|
||||
max: "7d"
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
@@ -74,7 +74,7 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (policyData?.policy) {
|
||||
reset({
|
||||
name: policyData.policy.name,
|
||||
maxRequestTtlSeconds: policyData.policy.maxRequestTtlSeconds,
|
||||
maxRequestTtl: policyData.policy.maxRequestTtl,
|
||||
conditions: policyData.policy.conditions.conditions,
|
||||
constraints: policyData.policy.constraints.constraints,
|
||||
steps: policyData.policy.steps.map((step) => ({
|
||||
@@ -85,12 +85,12 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
maxRequestTtlSeconds: null,
|
||||
conditions: [{ resourceIds: [], accountPaths: [] }],
|
||||
maxRequestTtl: null,
|
||||
conditions: [{ accountPaths: [] }],
|
||||
constraints: {
|
||||
requestDurationSeconds: {
|
||||
min: 30,
|
||||
max: 604800
|
||||
accessDuration: {
|
||||
min: "30s",
|
||||
max: "7d"
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
|
||||
@@ -1,21 +1,41 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
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({
|
||||
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
|
||||
.object({
|
||||
resourceIds: z.array(z.string().uuid()),
|
||||
accountPaths: z.array(z.string().min(1))
|
||||
})
|
||||
.array()
|
||||
.min(1, "At least one condition is required"),
|
||||
constraints: z.object({
|
||||
requestDurationSeconds: z.object({
|
||||
min: z.number().min(0).max(604800),
|
||||
max: z.number().min(1).max(604800)
|
||||
accessDuration: z.object({
|
||||
min: DurationSchema(),
|
||||
max: DurationSchema()
|
||||
})
|
||||
}),
|
||||
steps: z
|
||||
|
||||
@@ -33,7 +33,7 @@ export const PolicyConstraintsStep = () => {
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => appendCondition({ resourceIds: [], accountPaths: [] })}
|
||||
onClick={() => appendCondition({ accountPaths: [] })}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
@@ -58,35 +58,6 @@ export const PolicyConstraintsStep = () => {
|
||||
)}
|
||||
</div>
|
||||
<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
|
||||
control={control}
|
||||
name={`conditions.${index}.accountPaths`}
|
||||
@@ -106,7 +77,7 @@ export const PolicyConstraintsStep = () => {
|
||||
.filter(Boolean);
|
||||
pathField.onChange(paths);
|
||||
}}
|
||||
placeholder="e.g., /admin/*, /users/john"
|
||||
placeholder="e.g., /admin/**, /users/john, /**"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
|
||||
import { TPolicyForm } from "../PolicySchema";
|
||||
@@ -25,24 +26,15 @@ export const PolicyDetailsStep = () => {
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxRequestTtlSeconds"
|
||||
name="maxRequestTtl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Max Request TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
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
|
||||
{...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)"
|
||||
/>
|
||||
<Input {...field} value={field.value ?? ""} placeholder="1h" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -58,41 +50,29 @@ export const PolicyDetailsStep = () => {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="constraints.requestDurationSeconds.min"
|
||||
name="constraints.accessDuration.min"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Minimum Hours"
|
||||
label={<TtlFormLabel label="Minimum TTL" />}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="30-604800 hours"
|
||||
helperText="Must be between 30s and 7 days"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={168}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="constraints.requestDurationSeconds.max"
|
||||
name="constraints.accessDuration.max"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Maximum Hours"
|
||||
label={<TtlFormLabel label="Maximum TTL" />}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="30-604800 hours (7 days)"
|
||||
helperText="Must be between 30s and 7 days"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
max={168}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -24,17 +24,7 @@ export const PolicyReviewStep = () => {
|
||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
||||
|
||||
const { name, maxRequestTtlSeconds, 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 { name, maxRequestTtl, conditions, constraints, steps } = watch();
|
||||
|
||||
const getApproverLabel = (approverId: string, approverType: ApproverType) => {
|
||||
if (approverType === ApproverType.User) {
|
||||
@@ -59,7 +49,7 @@ export const PolicyReviewStep = () => {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
|
||||
@@ -68,14 +58,8 @@ export const PolicyReviewStep = () => {
|
||||
<h3 className="text-sm font-medium text-mineshaft-200">Request Duration Constraints</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<ReviewField
|
||||
label="Minimum Duration"
|
||||
value={`${constraints.requestDurationSeconds.min} second${constraints.requestDurationSeconds.min !== 1 ? "s" : ""}`}
|
||||
/>
|
||||
<ReviewField
|
||||
label="Maximum Duration"
|
||||
value={`${constraints.requestDurationSeconds.max} second${constraints.requestDurationSeconds.max !== 1 ? "s" : ""}`}
|
||||
/>
|
||||
<ReviewField label="Minimum Duration" value={constraints.accessDuration.min} />
|
||||
<ReviewField label="Maximum Duration" value={constraints.accessDuration.max} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,14 +79,6 @@ export const PolicyReviewStep = () => {
|
||||
Condition {index + 1}
|
||||
</div>
|
||||
<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>
|
||||
<span className="text-xs text-mineshaft-400">Account Paths: </span>
|
||||
<span className="text-xs text-mineshaft-200">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const RequestGrantTab = () => {
|
||||
return <div>Grant list</div>;
|
||||
};
|
||||
Reference in New Issue
Block a user