From 0d3cb2d41a4096ecd9e59e963ee09f72d2d72e0a Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 12 Aug 2025 23:25:31 -0700 Subject: [PATCH 1/3] feature: allow editing of access request by admin reviewers --- ...20250813020335_access-request-edit-cols.ts | 38 ++++ .../db/schemas/access-approval-requests.ts | 4 +- .../v1/access-approval-request-router.ts | 44 +++++ .../access-approval-request-service.ts | 145 ++++++++++++++- .../access-approval-request-types.ts | 23 +++ .../src/lib/workflow-integrations/types.ts | 3 + .../AccessApprovalRequestUpdatedTemplate.tsx | 95 ++++++++++ backend/src/services/smtp/emails/index.ts | 1 + backend/src/services/smtp/smtp-service.ts | 3 + .../src/components/features/TtlFormLabel.tsx | 11 +- .../src/hooks/api/accessApproval/mutation.tsx | 23 ++- .../src/hooks/api/accessApproval/types.ts | 9 + .../AccessApprovalRequest.tsx | 10 ++ .../components/EditAccessRequestModal.tsx | 169 ++++++++++++++++++ .../components/ReviewAccessModal.tsx | 48 ++++- 15 files changed, 610 insertions(+), 16 deletions(-) create mode 100644 backend/src/db/migrations/20250813020335_access-request-edit-cols.ts create mode 100644 backend/src/services/smtp/emails/AccessApprovalRequestUpdatedTemplate.tsx create mode 100644 frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx diff --git a/backend/src/db/migrations/20250813020335_access-request-edit-cols.ts b/backend/src/db/migrations/20250813020335_access-request-edit-cols.ts new file mode 100644 index 000000000..687ee5570 --- /dev/null +++ b/backend/src/db/migrations/20250813020335_access-request-edit-cols.ts @@ -0,0 +1,38 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + const hasEditNoteCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editNote"); + const hasEditedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editedByUserId"); + + if (!hasEditNoteCol || !hasEditedByUserId) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + if (!hasEditedByUserId) { + t.uuid("editedByUserId").nullable(); + t.foreign("editedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + } + + if (!hasEditNoteCol) { + t.string("editNote").nullable(); + } + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEditNoteCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editNote"); + const hasEditedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editedByUserId"); + + if (hasEditNoteCol || hasEditedByUserId) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + if (hasEditedByUserId) { + t.dropColumn("editedByUserId"); + } + + if (hasEditNoteCol) { + t.dropColumn("editNote"); + } + }); + } +} diff --git a/backend/src/db/schemas/access-approval-requests.ts b/backend/src/db/schemas/access-approval-requests.ts index 6a6f09148..14997a974 100644 --- a/backend/src/db/schemas/access-approval-requests.ts +++ b/backend/src/db/schemas/access-approval-requests.ts @@ -20,7 +20,9 @@ export const AccessApprovalRequestsSchema = z.object({ requestedByUserId: z.string().uuid(), note: z.string().nullable().optional(), privilegeDeletedAt: z.date().nullable().optional(), - status: z.string().default("pending") + status: z.string().default("pending"), + editedByUserId: z.string().uuid().nullable().optional(), + editNote: z.string().nullable().optional() }); export type TAccessApprovalRequests = z.infer; diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index c9e7b0d5c..217ae1f13 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { ms } from "@app/lib/ms"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -190,4 +191,47 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv return { review }; } }); + + server.route({ + url: "/:requestId", + method: "PATCH", + schema: { + params: z.object({ + requestId: z.string().trim() + }), + body: z.object({ + temporaryRange: z.string().transform((val, ctx) => { + const parsedMs = ms(val); + + if (typeof parsedMs !== "number" || parsedMs <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')." + }); + return z.NEVER; + } + return val; + }), + editNote: z.string().max(255) + }), + response: { + 200: z.object({ + approval: AccessApprovalRequestsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.accessApprovalRequest.updateAccessApprovalRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + temporaryRange: req.body.temporaryRange, + editNote: req.body.editNote, + requestId: req.params.requestId + }); + return { approval: request }; + } + }); }; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 58f5c57db..607373be4 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -54,7 +54,7 @@ type TSecretApprovalRequestServiceFactoryDep = { accessApprovalPolicyDAL: Pick; accessApprovalRequestReviewerDAL: Pick< TAccessApprovalRequestReviewerDALFactory, - "create" | "find" | "findOne" | "transaction" + "create" | "find" | "findOne" | "transaction" | "delete" >; groupDAL: Pick; projectMembershipDAL: Pick; @@ -301,6 +301,148 @@ export const accessApprovalRequestServiceFactory = ({ return { request: approval }; }; + const updateAccessApprovalRequest: TAccessApprovalRequestServiceFactory["updateAccessApprovalRequest"] = async ({ + temporaryRange, + actorId, + actor, + actorOrgId, + actorAuthMethod, + editNote, + requestId + }) => { + const cfg = getConfig(); + + const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); + if (!accessApprovalRequest) { + throw new NotFoundError({ message: `Access request with ID '${requestId}' not found` }); + } + + const { policy, requestedByUser } = accessApprovalRequest; + if (policy.deletedAt) { + throw new BadRequestError({ + message: "The policy associated with this access request has been deleted." + }); + } + + const { membership, hasRole } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: accessApprovalRequest.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + if (!membership) { + throw new ForbiddenRequestError({ message: "You are not a member of this project" }); + } + + const isApprover = policy.approvers.find((approver) => approver.userId === actorId); + + if (!hasRole(ProjectMembershipRole.Admin) && !isApprover) { + throw new ForbiddenRequestError({ message: "You are not authorized to modify this request" }); + } + + const project = await projectDAL.findById(accessApprovalRequest.projectId); + + if (!project) { + throw new NotFoundError({ message: "The project associated with this access request was not found." }); + } + + if (accessApprovalRequest.status !== ApprovalStatus.PENDING) { + throw new BadRequestError({ message: "The request has been closed" }); + } + + const editedByUser = await userDAL.findById(actorId); + + if (!editedByUser) throw new ForbiddenRequestError({ message: "User not found" }); + + if (accessApprovalRequest.isTemporary && accessApprovalRequest.temporaryRange) { + if (ms(temporaryRange) > ms(accessApprovalRequest.temporaryRange)) { + throw new BadRequestError({ message: "Updated access duration must be less than current access duration" }); + } + } + + const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ + permissions: accessApprovalRequest.permissions + }); + + const approval = await accessApprovalRequestDAL.transaction(async (tx) => { + const approvalRequest = await accessApprovalRequestDAL.updateById( + requestId, + { + temporaryRange, + isTemporary: true, + editNote, + editedByUserId: actorId + }, + tx + ); + + // reset review progress + await accessApprovalRequestReviewerDAL.delete({ + requestId + }); + + const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; + const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`; + const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`; + + await triggerWorkflowIntegrationNotification({ + input: { + notification: { + type: TriggerFeature.ACCESS_REQUEST, + payload: { + projectName: project.name, + requesterFullName, + isTemporary: true, + requesterEmail: requestedByUser.email as string, + secretPath, + environment: envSlug, + permissions: accessTypes, + approvalUrl, + editNote, + editorEmail: editedByUser.email as string, + editorFullName + } + }, + projectId: project.id + }, + dependencies: { + projectDAL, + projectSlackConfigDAL, + kmsService, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL + } + }); + + await smtpService.sendMail({ + recipients: policy.approvers.filter((approver) => approver.email).map((approver) => approver.email!), + subjectLine: "Access Approval Request Updated", + substitutions: { + projectName: project.name, + requesterFullName, + requesterEmail: requestedByUser.email, + isTemporary: true, + expiresIn: msFn(ms(temporaryRange || ""), { long: true }), + secretPath, + environment: envSlug, + permissions: accessTypes, + approvalUrl, + editNote, + editorFullName, + editorEmail: editedByUser.email + }, + template: SmtpTemplates.AccessApprovalRequestUpdated + }); + + return approvalRequest; + }); + + return { request: approval }; + }; + const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({ projectSlug, authorUserId, @@ -650,6 +792,7 @@ export const accessApprovalRequestServiceFactory = ({ return { createAccessApprovalRequest, + updateAccessApprovalRequest, listApprovalRequests, reviewAccessRequest, getCount diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index 88a46192b..ed027835a 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -30,6 +30,12 @@ export type TCreateAccessApprovalRequestDTO = { note?: string; } & Omit; +export type TUpdateAccessApprovalRequestDTO = { + requestId: string; + temporaryRange: string; + editNote: string; +} & Omit; + export type TListApprovalRequestsDTO = { projectSlug: string; authorUserId?: string; @@ -54,6 +60,23 @@ export interface TAccessApprovalRequestServiceFactory { privilegeDeletedAt?: Date | null | undefined; }; }>; + updateAccessApprovalRequest: (arg: TUpdateAccessApprovalRequestDTO) => Promise<{ + request: { + status: string; + id: string; + createdAt: Date; + updatedAt: Date; + policyId: string; + isTemporary: boolean; + requestedByUserId: string; + privilegeId?: string | null | undefined; + requestedBy?: string | null | undefined; + temporaryRange?: string | null | undefined; + permissions?: unknown; + note?: string | null | undefined; + privilegeDeletedAt?: Date | null | undefined; + }; + }>; listApprovalRequests: (arg: TListApprovalRequestsDTO) => Promise<{ requests: { policy: { diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts index c18ecb496..4cb783cd2 100644 --- a/backend/src/lib/workflow-integrations/types.ts +++ b/backend/src/lib/workflow-integrations/types.ts @@ -33,6 +33,9 @@ export type TNotification = permissions: string[]; approvalUrl: string; note?: string; + editNote?: string; + editorFullName?: string; + editorEmail?: string; }; }; diff --git a/backend/src/services/smtp/emails/AccessApprovalRequestUpdatedTemplate.tsx b/backend/src/services/smtp/emails/AccessApprovalRequestUpdatedTemplate.tsx new file mode 100644 index 000000000..2ade57dac --- /dev/null +++ b/backend/src/services/smtp/emails/AccessApprovalRequestUpdatedTemplate.tsx @@ -0,0 +1,95 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; + +interface AccessApprovalRequestUpdatedTemplateProps + extends Omit { + projectName: string; + requesterFullName: string; + requesterEmail: string; + isTemporary: boolean; + secretPath: string; + environment: string; + expiresIn: string; + permissions: string[]; + editNote: string; + editorFullName: string; + editorEmail: string; + approvalUrl: string; +} + +export const AccessApprovalRequestUpdatedTemplate = ({ + projectName, + siteUrl, + requesterFullName, + requesterEmail, + isTemporary, + secretPath, + environment, + expiresIn, + permissions, + editNote, + editorEmail, + editorFullName, + approvalUrl +}: AccessApprovalRequestUpdatedTemplateProps) => { + return ( + + + An access approval request was updated and is pending your review for the project {projectName} + +
+ + {editorFullName} ({editorEmail}) has + updated the access request submitted by {requesterFullName} ( + {requesterEmail}) for {secretPath} in + the {environment} environment. + + + {isTemporary && ( + + This access will expire {expiresIn} after approval. + + )} + + The following permissions are requested: + + {permissions.map((permission) => ( + + - {permission} + + ))} + + Editor Note: "{editNote}" + +
+
+ Review Request +
+
+ ); +}; + +export default AccessApprovalRequestUpdatedTemplate; + +AccessApprovalRequestUpdatedTemplate.PreviewProps = { + requesterFullName: "Abigail Williams", + requesterEmail: "abigail@infisical.com", + isTemporary: true, + secretPath: "/api/secrets", + environment: "Production", + siteUrl: "https://infisical.com", + projectName: "Example Project", + expiresIn: "1 day", + permissions: ["Read Secret", "Delete Project", "Create Dynamic Secret"], + editNote: "Too permissive, they only need 3 days", + editorEmail: "john@infisical.com", + editorFullName: "John Smith" +} as AccessApprovalRequestUpdatedTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 840a98cad..209cd672b 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -1,4 +1,5 @@ export * from "./AccessApprovalRequestTemplate"; +export * from "./AccessApprovalRequestUpdatedTemplate"; export * from "./EmailMfaTemplate"; export * from "./EmailVerificationTemplate"; export * from "./ExternalImportFailedTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index ac56f0ee4..500d0d89c 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger"; import { AccessApprovalRequestTemplate, + AccessApprovalRequestUpdatedTemplate, EmailMfaTemplate, EmailVerificationTemplate, ExternalImportFailedTemplate, @@ -54,6 +55,7 @@ export enum SmtpTemplates { EmailMfa = "emailMfa", UnlockAccount = "unlockAccount", AccessApprovalRequest = "accessApprovalRequest", + AccessApprovalRequestUpdated = "accessApprovalRequestUpdated", AccessSecretRequestBypassed = "accessSecretRequestBypassed", SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview", // HistoricalSecretList = "historicalSecretLeakIncident", not used anymore? @@ -96,6 +98,7 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate, [SmtpTemplates.EmailMfa]: EmailMfaTemplate, [SmtpTemplates.AccessApprovalRequest]: AccessApprovalRequestTemplate, + [SmtpTemplates.AccessApprovalRequestUpdated]: AccessApprovalRequestUpdatedTemplate, [SmtpTemplates.EmailVerification]: EmailVerificationTemplate, [SmtpTemplates.ExternalImportFailed]: ExternalImportFailedTemplate, [SmtpTemplates.ExternalImportStarted]: ExternalImportStartedTemplate, diff --git a/frontend/src/components/features/TtlFormLabel.tsx b/frontend/src/components/features/TtlFormLabel.tsx index fdb9aea5e..83f058c7a 100644 --- a/frontend/src/components/features/TtlFormLabel.tsx +++ b/frontend/src/components/features/TtlFormLabel.tsx @@ -1,4 +1,4 @@ -import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FormLabel, Tooltip } from "../v2"; @@ -10,15 +10,18 @@ export const TtlFormLabel = ({ label }: { label: string }) => ( label={label} icon={ + Examples: 30m, 1h, 3d, etc.{" "} - More + See More Examples{" "} + } @@ -26,7 +29,7 @@ export const TtlFormLabel = ({ label }: { label: string }) => ( } diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 69637243f..539f7dbad 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -6,10 +6,12 @@ import { apiRequest } from "@app/config/request"; import { accessApprovalKeys } from "./queries"; import { TAccessApproval, + TAccessApprovalRequest, TCreateAccessPolicyDTO, TCreateAccessRequestDTO, TDeleteSecretPolicyDTO, - TUpdateAccessPolicyDTO + TUpdateAccessPolicyDTO, + TUpdateAccessRequestDTO } from "./types"; export const useCreateAccessApprovalPolicy = () => { @@ -134,6 +136,25 @@ export const useCreateAccessRequest = () => { }); }; +export const useUpdateAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ requestId, ...payload }) => { + const { data } = await apiRequest.patch<{ approval: TAccessApprovalRequest }>( + `/api/v1/access-approvals/requests/${requestId}`, + payload + ); + + return data.approval; + }, + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries({ + queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug) + }); + } + }); +}; + export const useReviewAccessRequest = () => { const queryClient = useQueryClient(); return useMutation< diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index fc14352f8..fa0b47450 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -103,6 +103,8 @@ export type TAccessApprovalRequest = { }[]; note?: string; + editNote?: string; + editedByUserId: string; }; export type TAccessApproval = { @@ -146,6 +148,13 @@ export type TCreateAccessRequestDTO = { note?: string; } & Omit; +export type TUpdateAccessRequestDTO = { + requestId: string; + editNote: string; + temporaryRange: string; + projectSlug: string; +}; + export type TGetAccessApprovalRequestsDTO = { projectSlug: string; policyId?: string; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index a8cb199d4..c55ff8cb2 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -592,6 +592,16 @@ export const AccessApprovalRequest = ({ setSelectedRequest(null); refetchRequests(); }} + onUpdate={(request) => { + // scott: this isn't ideal but our current use of state makes this complicated... + // we shouldn't be using state like this... + handleSelectRequest({ + ...selectedRequest, + isTemporary: true, + temporaryRange: request.temporaryRange, + reviewers: [] + }); + }} canBypass={generateRequestDetails(selectedRequest).canBypass} /> )} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx new file mode 100644 index 000000000..3cdf8a063 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal.tsx @@ -0,0 +1,169 @@ +import { Controller, useForm } from "react-hook-form"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + TextArea +} from "@app/components/v2"; +import { useUpdateAccessRequest } from "@app/hooks/api/accessApproval/mutation"; +import { accessApprovalKeys } from "@app/hooks/api/accessApproval/queries"; +import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; + +type ContentProps = { + accessRequest: TAccessApprovalRequest; + onComplete: (request: TAccessApprovalRequest) => void; + projectSlug: string; +}; + +const EditSchema = z.object({ + temporaryRange: z.string().nonempty("Required"), + editNote: z.string().nonempty("Required") +}); + +type FormData = z.infer; + +const Content = ({ accessRequest, onComplete, projectSlug }: ContentProps) => { + const update = useUpdateAccessRequest(); + const queryClient = useQueryClient(); + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(EditSchema), + defaultValues: { + temporaryRange: accessRequest.temporaryRange ?? "1h", + editNote: "" + } + }); + + const onSubmit = async (form: FormData) => { + try { + const request = await update.mutateAsync({ + requestId: accessRequest.id, + projectSlug, + ...form + }); + await queryClient.refetchQueries({ + queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug) + }); + + createNotification({ + type: "success", + text: "Access request updated successfully." + }); + onComplete(request); + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to update access request" + }); + } + }; + + return ( +
+
+ + Updating this access request will restart the review process and require all approvers to + re-approve it. +
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + helperText={`Must be less than current access duration: ${accessRequest.isTemporary ? accessRequest.temporaryRange : "Permanent"}`} + > + + + )} + /> + ( + +