mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' into feature/secret-reference-shortcut
This commit is contained in:
@@ -159,8 +159,8 @@ export type TCreateCertificateDTO = {
|
||||
ttl: string; // string compatible with ms
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
keyUsages: string[];
|
||||
extendedKeyUsages: string[];
|
||||
};
|
||||
|
||||
export type TCreateCertificateResponse = {
|
||||
|
||||
@@ -90,7 +90,12 @@ export const useCreateCertTemplateV2 = () => {
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const [firstKey, queryProjectId] = query.queryKey;
|
||||
return firstKey === "list-template" && queryProjectId === projectId;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -107,7 +112,12 @@ export const useUpdateCertTemplateV2 = () => {
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const [firstKey, queryProjectId] = query.queryKey;
|
||||
return firstKey === "list-template" && queryProjectId === projectId;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -127,7 +137,12 @@ export const useDeleteCertTemplateV2 = () => {
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const [firstKey, queryProjectId] = query.queryKey;
|
||||
return firstKey === "list-template" && queryProjectId === projectId;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -109,6 +109,7 @@ export type SecretVersions = {
|
||||
actorType?: string | null;
|
||||
name?: string | null;
|
||||
membershipId?: string | null;
|
||||
groupId?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -94,9 +94,9 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||
export type FormData = z.infer<ReturnType<typeof createSchema>>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateIssuance"]>;
|
||||
popUp: UsePopUpState<["issueCertificate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateIssuance"]>,
|
||||
popUpName: keyof UsePopUpState<["issueCertificate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
profileId?: string;
|
||||
@@ -115,7 +115,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const inputSerialNumber =
|
||||
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "";
|
||||
(popUp?.issueCertificate?.data as { serialNumber: string })?.serialNumber || "";
|
||||
const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
|
||||
|
||||
const { data: cert } = useGetCert(sanitizedSerialNumber);
|
||||
@@ -181,7 +181,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
} = useCertificateTemplate(
|
||||
templateData,
|
||||
actualSelectedProfile,
|
||||
popUp?.certificateIssuance?.isOpen || false,
|
||||
popUp?.issueCertificate?.isOpen || false,
|
||||
setValue,
|
||||
watch
|
||||
);
|
||||
@@ -227,10 +227,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
}, [cert, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (popUp?.certificateIssuance?.isOpen && profileId && !cert) {
|
||||
if (popUp?.issueCertificate?.isOpen && profileId && !cert) {
|
||||
setValue("profileId", profileId);
|
||||
}
|
||||
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
||||
}, [popUp?.issueCertificate?.isOpen, profileId, cert, setValue]);
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
async ({
|
||||
@@ -332,9 +332,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateIssuance?.isOpen}
|
||||
isOpen={popUp?.issueCertificate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateIssuance", isOpen);
|
||||
handlePopUpToggle("issueCertificate", isOpen);
|
||||
if (!isOpen) {
|
||||
resetAllState();
|
||||
}
|
||||
@@ -503,7 +503,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
handlePopUpToggle("certificateIssuance", false);
|
||||
handlePopUpToggle("issueCertificate", false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -75,6 +75,7 @@ export type FormData = z.infer<typeof schema>;
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificate"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
|
||||
preselectedTemplate?: { id: string; name: string };
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
@@ -86,7 +87,7 @@ type TCertificateDetails = {
|
||||
|
||||
const CERT_TEMPLATE_NONE_VALUE = "none";
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentProject } = useProject();
|
||||
const { data: cert } = useGetCert(
|
||||
@@ -147,13 +148,15 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
(cert.extendedKeyUsages || []).map((name) => [name, true])
|
||||
)
|
||||
});
|
||||
} else {
|
||||
} else if (popUp?.certificate?.isOpen) {
|
||||
const templateId = preselectedTemplate?.id || CERT_TEMPLATE_NONE_VALUE;
|
||||
|
||||
reset({
|
||||
caId: "",
|
||||
commonName: "",
|
||||
subjectAltNames: "",
|
||||
ttl: "",
|
||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
|
||||
certificateTemplateId: templateId,
|
||||
keyUsages: {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||
@@ -161,7 +164,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
extendedKeyUsages: {}
|
||||
});
|
||||
}
|
||||
}, [cert]);
|
||||
}, [cert, preselectedTemplate, popUp?.certificate?.isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cert && selectedCertTemplate) {
|
||||
@@ -198,10 +201,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage),
|
||||
.map(([key]) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage)
|
||||
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
|
||||
});
|
||||
|
||||
reset();
|
||||
@@ -269,15 +276,25 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
isDisabled={Boolean(cert) || Boolean(preselectedTemplate)}
|
||||
>
|
||||
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
|
||||
None
|
||||
</SelectItem>
|
||||
{preselectedTemplate &&
|
||||
!templatesData?.certificateTemplates?.find(
|
||||
(t) => t.id === preselectedTemplate.id
|
||||
) && (
|
||||
<SelectItem
|
||||
value={preselectedTemplate.id}
|
||||
key={`cert-template-preselected-${preselectedTemplate.id}`}
|
||||
>
|
||||
{preselectedTemplate.name}
|
||||
</SelectItem>
|
||||
)}
|
||||
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`cert-template-${id}`}>
|
||||
{name}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRenewalModal } from "./CertificateRenewalModal";
|
||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
@@ -26,12 +25,8 @@ export const CertificatesSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||
|
||||
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
|
||||
const isLegacyTemplatesEnabled = true;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificateIssuance",
|
||||
"certificate",
|
||||
"issueCertificate",
|
||||
"certificateImport",
|
||||
"certificateCert",
|
||||
"deleteCertificate",
|
||||
@@ -76,9 +71,7 @@ export const CertificatesSection = () => {
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() =>
|
||||
handlePopUpOpen(isLegacyTemplatesEnabled ? "certificate" : "certificateIssuance")
|
||||
}
|
||||
onClick={() => handlePopUpOpen("issueCertificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue
|
||||
@@ -88,11 +81,7 @@ export const CertificatesSection = () => {
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
{isLegacyTemplatesEnabled ? (
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
) : (
|
||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
)}
|
||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
|
||||
@@ -64,7 +64,7 @@ type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
[
|
||||
"certificate",
|
||||
"issueCertificate",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"certificateCert",
|
||||
@@ -297,7 +297,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificate", {
|
||||
handlePopUpOpen("issueCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
faCertificate,
|
||||
faCog,
|
||||
faEllipsis,
|
||||
faFileContract,
|
||||
faPencil,
|
||||
faPlus,
|
||||
faTrash
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSub,
|
||||
useProject,
|
||||
@@ -50,6 +52,7 @@ import { useDeleteCertTemplateV2 } from "@app/hooks/api";
|
||||
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { CertificateModal } from "../CertificatesPage/components/CertificateModal";
|
||||
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
|
||||
import { PkiTemplateForm } from "./components/PkiTemplateForm";
|
||||
|
||||
@@ -64,7 +67,8 @@ export const PkiTemplateListPage = () => {
|
||||
"certificateTemplate",
|
||||
"deleteTemplate",
|
||||
"enrollmentOptions",
|
||||
"estUpgradePlan"
|
||||
"estUpgradePlan",
|
||||
"certificateFromTemplate"
|
||||
] as const);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
@@ -160,6 +164,27 @@ export const PkiTemplateListPage = () => {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Create}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("certificateFromTemplate", template);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faFileContract} />}
|
||||
>
|
||||
Issue Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Edit}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
@@ -284,6 +309,16 @@ export const PkiTemplateListPage = () => {
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateModal
|
||||
popUp={{
|
||||
certificate: {
|
||||
isOpen: popUp.certificateFromTemplate.isOpen,
|
||||
data: popUp.certificateFromTemplate.data
|
||||
}
|
||||
}}
|
||||
handlePopUpToggle={(_, state) => handlePopUpToggle("certificateFromTemplate", state)}
|
||||
preselectedTemplate={popUp.certificateFromTemplate.data}
|
||||
/>
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.estUpgradePlan.isOpen}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
|
||||
const { data: caData } = useGetCaById(profile.caId);
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
@@ -147,7 +147,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpToggle("certificateIssuance");
|
||||
handlePopUpToggle("issueCertificate");
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
|
||||
@@ -251,7 +251,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
|
||||
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||
resolver: zodResolver(templateSchema),
|
||||
defaultValues: getDefaultValues()
|
||||
defaultValues: getDefaultValues(),
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
criteriaMode: "all"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { MultiValue, SingleValue } from "react-select";
|
||||
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@@ -11,13 +12,10 @@ import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
Input
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useOrganization } from "@app/context";
|
||||
@@ -132,7 +130,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="mt-4 overflow-visible py-4">
|
||||
<form onSubmit={handleSubmit(setFilter)}>
|
||||
<div className="flex min-w-64 flex-col font-inter">
|
||||
<div className="flex max-w-96 min-w-96 flex-col font-inter">
|
||||
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -173,76 +171,24 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
name="eventType"
|
||||
render={({ field }) => (
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="inline-flex thin-scrollbar w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal whitespace-nowrap text-bunker-200 outline-hidden data-placeholder:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? filteredEventTypes.find(
|
||||
(eventType) => eventType.value === selectedEventTypes[0]
|
||||
)?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={2}
|
||||
className="z-100 max-h-80 thin-scrollbar overflow-hidden"
|
||||
>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{filteredEventTypes.length > 0 ? (
|
||||
filteredEventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
filteredEventTypes.length > 1 && event.preventDefault()
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedEventTypes?.includes(eventType.value as EventType)
|
||||
) {
|
||||
field.onChange(
|
||||
selectedEventTypes?.filter(
|
||||
(e: string) => e !== eventType.value
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([
|
||||
...(selectedEventTypes || []),
|
||||
eventType.value
|
||||
]);
|
||||
}
|
||||
}}
|
||||
key={`event-type-${eventType.value}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{eventType.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<FilterableSelect
|
||||
value={filteredEventTypes.filter((eventType) =>
|
||||
field.value.includes(eventType.value as EventType)
|
||||
)}
|
||||
isMulti
|
||||
isClearable
|
||||
onChange={(options) =>
|
||||
field.onChange(
|
||||
(options as MultiValue<(typeof filteredEventTypes)[number]>).map(
|
||||
(option) => option.value
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder="All events"
|
||||
options={filteredEventTypes}
|
||||
getOptionValue={(option) => option.value}
|
||||
getOptionLabel={(option) => option.label}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -256,31 +202,27 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
name="userAgentType"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
value={value === undefined ? "all" : value}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
|
||||
}}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||
<SelectItem value={userAgent} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<FilterableSelect
|
||||
value={
|
||||
userAgentTypes.find(
|
||||
(userAgentType) => value === (userAgentType.value as UserAgentType)
|
||||
) ?? null
|
||||
}
|
||||
isClearable
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<(typeof userAgentTypes)[number]>)?.value)
|
||||
}
|
||||
placeholder="All sources"
|
||||
options={userAgentTypes}
|
||||
getOptionValue={(option) => option.value}
|
||||
getOptionLabel={(option) => option.label}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,6 @@ import { RelayOption } from "./RelayOption";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: slugSchema({ field: "name" }),
|
||||
instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")),
|
||||
relay: z
|
||||
.object(
|
||||
{
|
||||
@@ -78,7 +77,6 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
const [autogenerateToken, setAutogenerateToken] = useState(true);
|
||||
const [step, setStep] = useState<"form" | "command">("form");
|
||||
const [name, setName] = useState("");
|
||||
const [instanceDomain, setInstanceDomain] = useState(siteURL);
|
||||
const [relay, setRelay] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -131,8 +129,7 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
const validation = formSchemaWithIdentity.safeParse({
|
||||
name,
|
||||
relay,
|
||||
identity,
|
||||
instanceDomain
|
||||
identity
|
||||
});
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
@@ -175,8 +172,7 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
const validation = formSchemaWithToken.safeParse({
|
||||
name,
|
||||
relay,
|
||||
identityToken,
|
||||
instanceDomain
|
||||
identityToken
|
||||
});
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
@@ -187,11 +183,10 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
};
|
||||
|
||||
const command = useMemo(() => {
|
||||
const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : "";
|
||||
return `infisical gateway start --name=${name} --relay=${
|
||||
relay?.name || ""
|
||||
}${domainFlag} --token=${identityToken}`;
|
||||
}, [name, relay, identityToken, instanceDomain]);
|
||||
} --domain=${siteURL} --token=${identityToken}`;
|
||||
}, [name, relay, identityToken, siteURL]);
|
||||
|
||||
if (step === "command") {
|
||||
return (
|
||||
@@ -274,19 +269,6 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
/>
|
||||
{errors.relay && <p className="mt-1 text-sm text-red">{errors.relay}</p>}
|
||||
|
||||
<FormLabel
|
||||
label="Infisical Instance Host Address"
|
||||
tooltipText="The host address of the infisical instance that's accessible by the gateway."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={instanceDomain}
|
||||
onChange={(e) => setInstanceDomain(e.target.value)}
|
||||
placeholder="https://app.infisical.com"
|
||||
isError={Boolean(errors.instanceDomain)}
|
||||
/>
|
||||
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
|
||||
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
|
||||
@@ -31,8 +31,7 @@ import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: slugSchema({ field: "name" }),
|
||||
host: z.string().min(1, "Host is required"),
|
||||
instanceDomain: z.string().url("Must be a valid URL").or(z.literal(""))
|
||||
host: z.string().min(1, "Host is required")
|
||||
});
|
||||
|
||||
const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
@@ -62,7 +61,6 @@ export const RelayCliDeploymentMethod = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [host, setHost] = useState("");
|
||||
|
||||
const [instanceDomain, setInstanceDomain] = useState(siteURL);
|
||||
const [identity, setIdentity] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -106,7 +104,7 @@ export const RelayCliDeploymentMethod = () => {
|
||||
setFormErrors([]);
|
||||
|
||||
if (canCreateToken && autogenerateToken) {
|
||||
const validation = formSchemaWithIdentity.safeParse({ name, host, instanceDomain, identity });
|
||||
const validation = formSchemaWithIdentity.safeParse({ name, host, identity });
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
@@ -148,7 +146,6 @@ export const RelayCliDeploymentMethod = () => {
|
||||
const validation = formSchemaWithToken.safeParse({
|
||||
name,
|
||||
host,
|
||||
instanceDomain,
|
||||
identityToken
|
||||
});
|
||||
if (!validation.success) {
|
||||
@@ -169,9 +166,8 @@ export const RelayCliDeploymentMethod = () => {
|
||||
};
|
||||
|
||||
const command = useMemo(() => {
|
||||
const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : "";
|
||||
return `infisical relay start --name=${name}${domainFlag} --host=${host} --token=${identityToken}`;
|
||||
}, [name, instanceDomain, host, identityToken]);
|
||||
return `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`;
|
||||
}, [name, siteURL, host, identityToken]);
|
||||
|
||||
if (step === "command") {
|
||||
return (
|
||||
@@ -239,19 +235,6 @@ export const RelayCliDeploymentMethod = () => {
|
||||
/>
|
||||
{errors.host && <p className="mt-1 text-sm text-red">{errors.host}</p>}
|
||||
|
||||
<FormLabel
|
||||
label="Infisical Instance Host Address"
|
||||
tooltipText="The host address of the infisical instance that's accessible by the relay."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={instanceDomain}
|
||||
onChange={(e) => setInstanceDomain(e.target.value)}
|
||||
placeholder="https://app.infisical.com"
|
||||
isError={Boolean(errors.instanceDomain)}
|
||||
/>
|
||||
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
|
||||
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect";
|
||||
|
||||
import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod";
|
||||
import { RelayTerraformDeploymentMethod } from "./RelayTerraformDeploymentMethod";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -11,7 +12,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RelayDeploymentInfoMap = {
|
||||
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod }
|
||||
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod },
|
||||
terraform: {
|
||||
name: "Terraform",
|
||||
image: "Terraform.png",
|
||||
component: RelayTerraformDeploymentMethod
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type RelayDeploymentMethod = keyof typeof RelayDeploymentInfoMap;
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faCopy, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FilterableSelect,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
ModalClose,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission
|
||||
} from "@app/context";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import {
|
||||
useAddIdentityTokenAuth,
|
||||
useCreateTokenIdentityTokenAuth,
|
||||
useGetIdentityMembershipOrgs,
|
||||
useGetIdentityTokenAuth
|
||||
} from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: slugSchema({ field: "name" })
|
||||
});
|
||||
|
||||
const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
identity: z
|
||||
.object(
|
||||
{
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
identityToken: z.string().min(1, "Token is required")
|
||||
});
|
||||
|
||||
const ec2FormSchema = z.object({
|
||||
awsRegion: z.string().min(1, "AWS Region is required"),
|
||||
vpcId: z.string().min(1, "VPC ID is required"),
|
||||
ami: z.string().min(1, "AMI ID is required"),
|
||||
subnetId: z.string().min(1, "Subnet ID is required")
|
||||
});
|
||||
|
||||
export const RelayTerraformDeploymentMethod = () => {
|
||||
const { protocol, hostname, port } = window.location;
|
||||
const portSuffix = port && port !== "80" ? `:${port}` : "";
|
||||
const siteURL = `${protocol}//${hostname}${portSuffix}`;
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const [autogenerateToken, setAutogenerateToken] = useState(true);
|
||||
const [step, setStep] = useState<"form" | "command">("form");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const [identity, setIdentity] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(null);
|
||||
const [identityToken, setIdentityToken] = useState("");
|
||||
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
|
||||
|
||||
const [awsRegion, setAwsRegion] = useState("us-east-1");
|
||||
const [vpcId, setVpcId] = useState("");
|
||||
const [ami, setAmi] = useState("ami-01b2110eef525172b");
|
||||
const [subnetId, setSubnetId] = useState("");
|
||||
|
||||
const errors = useMemo(() => {
|
||||
const errorMap: Record<string, string | undefined> = {};
|
||||
formErrors.forEach((issue) => {
|
||||
if (issue.path.length > 0) {
|
||||
errorMap[String(issue.path[0])] = issue.message;
|
||||
}
|
||||
});
|
||||
return errorMap;
|
||||
}, [formErrors]);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { permission } = useOrgPermission();
|
||||
const canCreateToken = permission.can(
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
|
||||
useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000
|
||||
});
|
||||
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
|
||||
|
||||
const { mutateAsync: createToken, isPending: isCreatingToken } =
|
||||
useCreateTokenIdentityTokenAuth();
|
||||
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
|
||||
useAddIdentityTokenAuth();
|
||||
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
|
||||
|
||||
const handleGenerateCommand = async () => {
|
||||
setFormErrors([]);
|
||||
|
||||
if (canCreateToken && autogenerateToken) {
|
||||
const validation = formSchemaWithIdentity.safeParse({ name, identity });
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedTabIndex === 0) {
|
||||
const ec2Validation = ec2FormSchema.safeParse({ awsRegion, vpcId, ami, subnetId });
|
||||
if (!ec2Validation.success) {
|
||||
setFormErrors(ec2Validation.error.issues);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const validatedIdentity = validation.data.identity;
|
||||
|
||||
try {
|
||||
const { data: identityTokenAuth } = await refetch();
|
||||
if (!identityTokenAuth) {
|
||||
await addIdentityTokenAuth({
|
||||
identityId: validatedIdentity.id,
|
||||
organizationId,
|
||||
accessTokenTTL: 2592000,
|
||||
accessTokenMaxTTL: 2592000,
|
||||
accessTokenNumUsesLimit: 0,
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
createNotification({
|
||||
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
|
||||
type: "warning"
|
||||
});
|
||||
}
|
||||
|
||||
const token = await createToken({
|
||||
identityId: validatedIdentity.id,
|
||||
name: `relay token for ${name} (autogenerated)`
|
||||
});
|
||||
setIdentityToken(token.accessToken);
|
||||
createNotification({
|
||||
text: "Automatically generated a token for the selected identity.",
|
||||
type: "info"
|
||||
});
|
||||
setStep("command");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to generate token for the selected identity",
|
||||
type: "error"
|
||||
});
|
||||
setIdentityToken("");
|
||||
}
|
||||
} else {
|
||||
const validation = formSchemaWithToken.safeParse({
|
||||
name,
|
||||
identityToken
|
||||
});
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedTabIndex === 0) {
|
||||
const ec2Validation = ec2FormSchema.safeParse({ awsRegion, vpcId, ami, subnetId });
|
||||
if (!ec2Validation.success) {
|
||||
setFormErrors(ec2Validation.error.issues);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setStep("command");
|
||||
}
|
||||
};
|
||||
|
||||
const handleIdentityChange = (
|
||||
selectedIdentity: SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>
|
||||
) => {
|
||||
setIdentity(selectedIdentity);
|
||||
};
|
||||
|
||||
const terraformCommand = useMemo(() => {
|
||||
return `terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "${awsRegion}"
|
||||
}
|
||||
|
||||
# Security Group for the Infisical Relay instance
|
||||
resource "aws_security_group" "infisical_relay_sg" {
|
||||
name = "${name}-relay-sg"
|
||||
description = "Allows inbound traffic for Infisical Relay and SSH"
|
||||
vpc_id = "${vpcId}"
|
||||
|
||||
# Inbound: Allows the Infisical platform to securely communicate with the Relay server.
|
||||
ingress {
|
||||
from_port = 8443
|
||||
to_port = 8443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
# Inbound: Allows Infisical Gateway to securely communicate via the Relay.
|
||||
ingress {
|
||||
from_port = 2222
|
||||
to_port = 2222
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
# Inbound: Allows secure shell (SSH) access for administration.
|
||||
ingress {
|
||||
from_port = 22
|
||||
to_port = 22
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"] # Restrict this to your IP in production
|
||||
}
|
||||
|
||||
# Outbound: Allows the Relay server to make necessary outbound connections to the Infisical platform.
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${name}-relay-sg"
|
||||
}
|
||||
}
|
||||
|
||||
# Elastic IP for a static public IP address
|
||||
resource "aws_eip" "infisical_relay_eip" {
|
||||
tags = {
|
||||
Name = "${name}-relay-eip"
|
||||
}
|
||||
}
|
||||
|
||||
# EC2 instance to run Infisical Relay
|
||||
module "infisical_relay_instance" {
|
||||
source = "terraform-aws-modules/ec2-instance/aws"
|
||||
version = "~> 5.6"
|
||||
|
||||
name = "${name}-relay-instance"
|
||||
ami = "${ami}"
|
||||
instance_type = "t3.micro"
|
||||
subnet_id = "${subnetId}"
|
||||
|
||||
vpc_security_group_ids = [aws_security_group.infisical_relay_sg.id]
|
||||
associate_public_ip_address = false # We are using an Elastic IP instead
|
||||
|
||||
user_data = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# Install Infisical CLI
|
||||
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash
|
||||
apt-get update && apt-get install -y infisical
|
||||
|
||||
# Install the relay as a systemd service.
|
||||
# This example uses a Machine Identity token for authentication via the INFISICAL_TOKEN environment variable.
|
||||
#
|
||||
# Note: For production environments, you might consider fetching the token from AWS Parameter Store or AWS Secrets Manager.
|
||||
export INFISICAL_TOKEN="${identityToken}"
|
||||
sudo -E infisical relay systemd install \\
|
||||
--name "${name}" \\
|
||||
--domain "${siteURL}" \\
|
||||
--host "\${aws_eip.infisical_relay_eip.public_ip}"
|
||||
|
||||
# Start and enable the service to run on boot
|
||||
sudo systemctl start infisical-relay
|
||||
sudo systemctl enable infisical-relay
|
||||
EOT
|
||||
}
|
||||
|
||||
# Associate the Elastic IP with the EC2 instance
|
||||
resource "aws_eip_association" "eip_assoc" {
|
||||
instance_id = module.infisical_relay_instance.id
|
||||
allocation_id = aws_eip.infisical_relay_eip.id
|
||||
}
|
||||
`;
|
||||
}, [name, siteURL, identityToken, awsRegion, vpcId, ami, subnetId]);
|
||||
|
||||
if (step === "command") {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span>Terraform Configuration</span>
|
||||
<IconButton
|
||||
ariaLabel="copy"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(terraformCommand);
|
||||
createNotification({
|
||||
text: "Terraform configuration copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="h-80 overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 font-mono text-sm text-bunker-300">
|
||||
<pre>
|
||||
<code>{terraformCommand}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center">
|
||||
<ModalClose asChild>
|
||||
<Button className="mr-4" size="sm" colorSchema="secondary">
|
||||
Done
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormLabel label="Name" tooltipText="The name for your relay." />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter relay name..."
|
||||
isError={Boolean(errors.name)}
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
|
||||
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
handleIdentityChange(
|
||||
e as SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canCreateToken && (
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
isChecked={autogenerateToken}
|
||||
onCheckedChange={(e) => {
|
||||
setAutogenerateToken(Boolean(e));
|
||||
}}
|
||||
id="autogenerate-token"
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mt-4 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${
|
||||
selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
EC2
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
|
||||
<Tab.Panel>
|
||||
<FormLabel label="AWS Region" />
|
||||
<FilterableSelect
|
||||
value={AWS_REGIONS.find((r) => r.slug === awsRegion)}
|
||||
onChange={(selected) => {
|
||||
if (selected) {
|
||||
setAwsRegion((selected as SingleValue<{ slug: string; name: string }>)!.slug);
|
||||
}
|
||||
}}
|
||||
options={AWS_REGIONS}
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
{errors.awsRegion && <p className="mt-1 text-sm text-red">{errors.awsRegion}</p>}
|
||||
<FormLabel label="VPC ID" className="mt-4" />
|
||||
<Input
|
||||
value={vpcId}
|
||||
onChange={(e) => setVpcId(e.target.value)}
|
||||
placeholder="vpc-..."
|
||||
isError={Boolean(errors.vpcId)}
|
||||
/>
|
||||
{errors.vpcId && <p className="mt-1 text-sm text-red">{errors.vpcId}</p>}
|
||||
<FormLabel
|
||||
label="AMI ID"
|
||||
tooltipText="The ID of the Amazon Machine Image (AMI) for the EC2 linux instance."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={ami}
|
||||
onChange={(e) => setAmi(e.target.value)}
|
||||
placeholder="ami-..."
|
||||
isError={Boolean(errors.ami)}
|
||||
/>
|
||||
{errors.ami && <p className="mt-1 text-sm text-red">{errors.ami}</p>}
|
||||
<FormLabel label="Subnet ID" className="mt-4" />
|
||||
<Input
|
||||
value={subnetId}
|
||||
onChange={(e) => setSubnetId(e.target.value)}
|
||||
placeholder="subnet-..."
|
||||
isError={Boolean(errors.subnetId)}
|
||||
/>
|
||||
{errors.subnetId && <p className="mt-1 text-sm text-red">{errors.subnetId}</p>}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
colorSchema="secondary"
|
||||
onClick={handleGenerateCommand}
|
||||
isLoading={isCreatingToken || isAddingTokenAuth}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { PamResourceType } from "@app/hooks/api/pam";
|
||||
|
||||
type TableLog = {
|
||||
command?: string;
|
||||
data_rows: Record<string, string | number | null>[];
|
||||
total_rows?: number;
|
||||
};
|
||||
|
||||
export const PamSessionLogOutput = ({
|
||||
content,
|
||||
resourceType,
|
||||
search
|
||||
}: {
|
||||
content: string;
|
||||
resourceType: PamResourceType;
|
||||
search: string;
|
||||
}) => {
|
||||
const [isRawView, setIsRawView] = useState(false);
|
||||
|
||||
let parsedContent: TableLog | null = null;
|
||||
|
||||
if (resourceType === PamResourceType.Postgres || resourceType === PamResourceType.MySQL) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
!Array.isArray(parsed) &&
|
||||
parsed.data_rows &&
|
||||
Array.isArray(parsed.data_rows) &&
|
||||
parsed.data_rows.length > 0 &&
|
||||
typeof parsed.data_rows[0] === "object" &&
|
||||
parsed.data_rows[0] !== null
|
||||
) {
|
||||
parsedContent = parsed;
|
||||
}
|
||||
} catch {
|
||||
// Not a valid JSON or doesn't match structure, will render as plain text
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedContent) {
|
||||
const headers = Object.keys(parsedContent.data_rows[0]);
|
||||
return (
|
||||
<div className="font-sans">
|
||||
{isRawView ? (
|
||||
<div className="font-mono break-all whitespace-pre-wrap">
|
||||
<HighlightText text={content} highlight={search} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{parsedContent.command && (
|
||||
<div className="mb-2 font-mono">{`> ${parsedContent.command}`}</div>
|
||||
)}
|
||||
<div className="overflow-x-auto rounded-md border border-mineshaft-600">
|
||||
<table className="w-full min-w-max text-left">
|
||||
<thead className="bg-mineshaft-800">
|
||||
<tr className="border-b border-mineshaft-600">
|
||||
{headers.map((header) => (
|
||||
<th key={header} className="p-2 font-semibold text-mineshaft-200 capitalize">
|
||||
<HighlightText text={header.replace(/_/g, " ")} highlight={search} />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parsedContent.data_rows.map((row, rowIndex) => (
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`row-${rowIndex}`}
|
||||
className="border-b border-mineshaft-700 bg-mineshaft-900 last:border-b-0 hover:bg-mineshaft-800/50"
|
||||
>
|
||||
{headers.map((header) => (
|
||||
<td key={header} className="p-2">
|
||||
<HighlightText text={String(row[header] ?? "")} highlight={search} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-2 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-sm text-bunker-400 underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsRawView((v) => !v);
|
||||
}}
|
||||
>
|
||||
{isRawView ? "View Formatted" : "View Raw"}
|
||||
</button>
|
||||
|
||||
{parsedContent.total_rows !== undefined && (
|
||||
<div className="ml-auto text-right text-sm text-bunker-400">
|
||||
Total rows: {parsedContent.total_rows}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="font-mono break-all whitespace-pre-wrap">
|
||||
<HighlightText text={content} highlight={search} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import { Input } from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { TPamSession } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamSessionLogOutput } from "./PamSessionLogOutput";
|
||||
import { formatLogContent } from "./PamSessionLogsSection.utils";
|
||||
|
||||
type Props = {
|
||||
@@ -104,20 +103,9 @@ export const PamSessionLogsSection = ({ session }: Props) => {
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
{log.output && (
|
||||
<>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="h-px w-full bg-mineshaft-400" />
|
||||
<span className="text-xs text-mineshaft-400">OUTPUT</span>
|
||||
<div className="h-px w-full bg-mineshaft-400" />
|
||||
</div>
|
||||
<div className="pt-2 text-bunker-300">
|
||||
<PamSessionLogOutput
|
||||
content={log.output}
|
||||
resourceType={session.resourceType}
|
||||
search={search}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div className="pt-2 text-bunker-300">
|
||||
<HighlightText text={log.output} highlight={search} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,14 +25,12 @@ import {
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import {
|
||||
useAddUsersToOrg,
|
||||
useAddUserToWsNonE2EE,
|
||||
useGetOrgUsers,
|
||||
useGetProjectRoles,
|
||||
useGetWorkspaceUsers
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
@@ -86,7 +84,6 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
defaultValues: { orgMemberships: [], projectRoleSlugs: [] }
|
||||
});
|
||||
|
||||
const { mutateAsync: addMemberToOrg } = useAddUsersToOrg();
|
||||
const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -140,13 +137,6 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newInvitees.length) {
|
||||
await addMemberToOrg({
|
||||
inviteeEmails: newInvitees,
|
||||
organizationId: orgId,
|
||||
organizationRoleSlug: ProjectMembershipRole.Member // only applies to new invites
|
||||
});
|
||||
}
|
||||
if (newInvitees.length || inviteeEmails.length) {
|
||||
await addUserToProject({
|
||||
usernames: [...inviteeEmails, ...newInvitees],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
@@ -48,6 +49,7 @@ enum GroupMembersOrderBy {
|
||||
}
|
||||
|
||||
export const GroupMembersTable = ({ groupMembership }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
@@ -62,6 +64,21 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
||||
initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
// this handles links from secret versions when the actor is in a group membership
|
||||
const { username, ...restSearch } = useSearch({
|
||||
strict: false
|
||||
});
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
setSearch(username);
|
||||
navigate({
|
||||
to: ".",
|
||||
replace: true,
|
||||
search: restSearch
|
||||
});
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const);
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
@@ -8,6 +10,11 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId"
|
||||
)({
|
||||
component: GroupDetailsByIDPage,
|
||||
validateSearch: zodValidator(
|
||||
z.object({
|
||||
username: z.string().optional().catch(undefined)
|
||||
})
|
||||
),
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination } from "@app/hooks";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useGetSecretApprovalRequestCount,
|
||||
useGetSecretApprovalRequests,
|
||||
@@ -99,6 +99,12 @@ export const SecretApprovalRequest = () => {
|
||||
const totalApprovalCount = data?.totalCount ?? 0;
|
||||
const secretApprovalRequests = data?.approvals ?? [];
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: totalApprovalCount,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||
useGetSecretApprovalRequestCount({ projectId });
|
||||
const { user: userSession } = useUser();
|
||||
@@ -107,7 +113,7 @@ export const SecretApprovalRequest = () => {
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { data: members } = useGetWorkspaceUsers(projectId);
|
||||
const { data: members } = useGetWorkspaceUsers(projectId, true);
|
||||
const isSecretApprovalScreen = Boolean(selectedApprovalId);
|
||||
const { requestId } = search;
|
||||
|
||||
|
||||
@@ -221,9 +221,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<div className="mb-2">
|
||||
<div className="text-sm font-medium text-mineshaft-300">Multi-line Encoding</div>
|
||||
<div className="text-sm">
|
||||
{secretVersion?.skipMultilineEncoding?.toString() || (
|
||||
<span className="text-sm text-mineshaft-300">-</span>
|
||||
)}{" "}
|
||||
{secretVersion?.skipMultilineEncoding?.toString() || "false"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,9 +364,8 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<div className="text-sm font-medium text-mineshaft-300">Multi-line Encoding</div>
|
||||
<div className="text-sm">
|
||||
{newVersion?.skipMultilineEncoding?.toString() ??
|
||||
secretVersion?.skipMultilineEncoding?.toString() ?? (
|
||||
<span className="text-sm text-mineshaft-300">-</span>
|
||||
)}{" "}
|
||||
secretVersion?.skipMultilineEncoding?.toString() ??
|
||||
"false"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateRight,
|
||||
faBan,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faServer,
|
||||
@@ -68,10 +69,14 @@ export const SecretVersionItem = ({
|
||||
const getLinkToModifyHistoryEntity = (
|
||||
actorId: string,
|
||||
actorType: string,
|
||||
membershipId: string | null = ""
|
||||
membershipId: string | null = "",
|
||||
groupId: string | null = "",
|
||||
actorName: string | null = ""
|
||||
) => {
|
||||
switch (actorType) {
|
||||
case ActorType.USER:
|
||||
if (groupId)
|
||||
return `/projects/secret-management/${currentProject.id}/groups/${groupId}?username=${actorName}`;
|
||||
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
|
||||
case ActorType.IDENTITY:
|
||||
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
|
||||
@@ -83,10 +88,26 @@ export const SecretVersionItem = ({
|
||||
const onModifyHistoryClick = (
|
||||
actorId: string | undefined | null,
|
||||
actorType: string | undefined | null,
|
||||
membershipId: string | undefined | null
|
||||
membershipId: string | undefined | null,
|
||||
groupId: string | undefined | null,
|
||||
actorName: string | undefined | null
|
||||
) => {
|
||||
if (!membershipId) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: `This ${actorType === ActorType.USER ? "user" : "identity"} is no longer a member of this project.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||
const redirectLink = getLinkToModifyHistoryEntity(
|
||||
actorId,
|
||||
actorType,
|
||||
membershipId,
|
||||
groupId,
|
||||
actorName
|
||||
);
|
||||
if (redirectLink) {
|
||||
navigate({ to: redirectLink });
|
||||
}
|
||||
@@ -157,15 +178,35 @@ export const SecretVersionItem = ({
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||
<Tooltip
|
||||
className="z-[100] max-w-sm"
|
||||
content={
|
||||
getModifiedByName(actor.actorType, actor.name) +
|
||||
(!actor.membershipId && actor.actorId ? " (Removed from project)" : "")
|
||||
}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
onClick={() =>
|
||||
onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId)
|
||||
onClick={
|
||||
actor.membershipId
|
||||
? () =>
|
||||
onModifyHistoryClick(
|
||||
actor.actorId,
|
||||
actor.actorType,
|
||||
actor.membershipId,
|
||||
actor.groupId,
|
||||
actor.name
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
className="cursor-pointer"
|
||||
className={actor.membershipId ? "cursor-pointer" : undefined}
|
||||
>
|
||||
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2" />
|
||||
{!actor.membershipId &&
|
||||
actor.actorType &&
|
||||
[ActorType.USER, ActorType.IDENTITY].includes(
|
||||
actor.actorType as ActorType
|
||||
) && <FontAwesomeIcon className="ml-1 text-mineshaft-400" icon={faBan} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user