mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +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(
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+2
-14
@@ -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">
|
||||||
|
|||||||
+4
-17
@@ -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">
|
||||||
|
|||||||
+18
-41
@@ -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>
|
||||||
|
|||||||
+12
-12
@@ -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: [
|
||||||
|
|||||||
+25
-5
@@ -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
|
||||||
|
|||||||
+2
-31
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+13
-33
@@ -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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+4
-28
@@ -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>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user