Merge branch 'main' into feat/point-in-time-revamp

This commit is contained in:
carlosmonastyrski
2025-06-05 18:31:27 -03:00
757 changed files with 31611 additions and 4112 deletions
+1
View File
@@ -25,6 +25,7 @@
"@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@lottiefiles/dotlottie-web": "^0.38.2",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
+1
View File
@@ -29,6 +29,7 @@
"@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@lottiefiles/dotlottie-web": "^0.38.2",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -78,11 +78,14 @@ export default function CodeInputStep({
const resendVerificationEmail = async () => {
setIsResendingVerificationEmail(true);
setIsLoading(true);
await mutateAsync({ email });
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 2000);
try {
await mutateAsync({ email });
} finally {
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 1000);
}
};
return (
@@ -41,7 +41,7 @@ export const DeleteProjectTemplateModal = ({ isOpen, onOpenChange, template }: P
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey="confirm"
onDeleteApproved={handleDeleteProjectTemplate}
/>
@@ -112,7 +112,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
/>
<DeleteActionModal
isOpen={popUp.removeTemplate.isOpen}
title={`Are you sure want to delete ${projectTemplate.name}?`}
title={`Are you sure you want to delete ${projectTemplate.name}?`}
deleteKey="confirm"
onChange={(isOpen) => handlePopUpToggle("removeTemplate", isOpen)}
onDeleteApproved={handleRemoveTemplate}
@@ -66,7 +66,7 @@ export const DeleteSecretRotationV2Modal = ({
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteSecretRotation}
>
@@ -62,6 +62,7 @@ const Content = ({ secretRotation }: ContentProps) => {
let Component: ReactNode;
switch (generatedCredentialsResponse.type) {
case SecretRotation.PostgresCredentials:
case SecretRotation.MySqlCredentials:
case SecretRotation.MsSqlCredentials:
Component = (
<ViewSqlCredentialsRotationGeneratedCredentials
@@ -1,11 +1,13 @@
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { ViewRotationGeneratedCredentialsDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/ViewRotationGeneratedCredentialsDisplay";
import { TMsSqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mssql-credentials-rotation";
import { TMySqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mysql-credentials-rotation";
import { TPostgresCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/postgres-credentials-rotation";
type Props = {
generatedCredentialsResponse:
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
| TMySqlCredentialsRotationGeneratedCredentialsResponse
| TPostgresCredentialsRotationGeneratedCredentialsResponse;
};
@@ -12,6 +12,7 @@ import { SqlCredentialsRotationParametersFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
@@ -15,6 +15,7 @@ import { SqlCredentialsRotationReviewFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
@@ -12,6 +12,7 @@ import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
@@ -5,6 +5,7 @@ import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations
import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema";
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
import { MySqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mysql-credentials-rotation-schema";
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
@@ -17,6 +18,7 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
AzureClientSecretRotationSchema,
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
MySqlCredentialsRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]),
@@ -0,0 +1,12 @@
import { z } from "zod";
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const MySqlCredentialsRotationSchema = z
.object({
type: z.literal(SecretRotation.MySqlCredentials)
})
.merge(SqlCredentialsRotationSchema)
.merge(BaseSecretRotationSchema);
@@ -0,0 +1,97 @@
import { useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Modal, ModalContent } from "@app/components/v2";
import {
SecretScanningDataSource,
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceForm } from "./forms";
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
import { SecretScanningDataSourceSelect } from "./SecretScanningDataSourceSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
if (selectedDataSource) {
return (
<SecretScanningDataSourceForm
onCancel={() => setSelectedDataSource(null)}
type={selectedDataSource}
{...props}
/>
);
}
return <SecretScanningDataSourceSelect onSelect={setSelectedDataSource} />;
};
export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...props }: Props) => {
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
null
);
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) setSelectedDataSource(null);
onOpenChange(open);
}}
>
<ModalContent
title={
selectedDataSource ? (
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
) : (
<div className="flex items-center text-mineshaft-300">
Add Data Source
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
)
}
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
}
bodyClassName="overflow-visible"
>
<Content
onComplete={() => {
setSelectedDataSource(null);
onOpenChange(false);
}}
selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource}
{...props}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,66 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
TSecretScanningDataSource,
useDeleteSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
type Props = {
dataSource?: TSecretScanningDataSource;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onComplete?: () => void;
};
export const DeleteSecretScanningDataSourceModal = ({
isOpen,
onOpenChange,
dataSource,
onComplete
}: Props) => {
const deleteDataSource = useDeleteSecretScanningDataSource();
if (!dataSource) return null;
const { id: dataSourceId, name, type, projectId } = dataSource;
const handleDeleteDataSource = async () => {
const dataSourceType = SECRET_SCANNING_DATA_SOURCE_MAP[type].name;
try {
await deleteDataSource.mutateAsync({
dataSourceId,
type,
projectId
});
createNotification({
text: `Successfully deleted ${dataSourceType} Data Source`,
type: "success"
});
if (onComplete) onComplete();
onOpenChange(false);
} catch {
createNotification({
text: `Failed to delete ${dataSourceType} Data Source`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteDataSource}
>
<p className="mt-1 font-inter text-sm text-mineshaft-400">
Findings associated with this data source will be preserved.
</p>
</DeleteActionModal>
);
};
@@ -0,0 +1,36 @@
import { Modal, ModalContent } from "@app/components/v2";
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceForm } from "./forms";
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
dataSource?: TSecretScanningDataSource;
};
export const EditSecretScanningDataSourceModal = ({
dataSource,
onOpenChange,
...props
}: Props) => {
if (!dataSource) return null;
return (
<Modal {...props} onOpenChange={onOpenChange}>
<ModalContent
title={<SecretScanningDataSourceModalHeader isConfigured type={dataSource.type} />}
className="max-w-2xl"
bodyClassName="overflow-visible"
>
<SecretScanningDataSourceForm
onComplete={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)}
dataSource={dataSource}
type={dataSource.type}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,47 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
type Props = {
type: SecretScanningDataSource;
isConfigured: boolean;
};
export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Props) => {
const dataSourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<div className="flex w-full items-start gap-2">
<img
alt={`${dataSourceDetails.name} logo`}
src={`/images/integrations/${dataSourceDetails.image}`}
className="h-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
{dataSourceDetails.name} Data Source
<a
target="_blank"
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
</p>
</div>
</div>
);
};
@@ -0,0 +1,91 @@
import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Spinner, Tooltip } from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
useSecretScanningDataSourceOptions
} from "@app/hooks/api/secretScanningV2";
type Props = {
onSelect: (type: SecretScanningDataSource) => void;
};
export const SecretScanningDataSourceSelect = ({ onSelect }: Props) => {
const { isPending, data: dataSourceOptions } = useSecretScanningDataSourceOptions();
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="grid grid-cols-3 gap-2">
{dataSourceOptions?.map(({ type }) => {
const { image, name, size } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<button
type="button"
key={type}
onClick={() => onSelect(type)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
width={size}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
Coming Soon
</div>
</div>
</Tooltip>
</div>
);
};
@@ -0,0 +1,90 @@
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
type Props = {
status: SecretScanningScanStatus;
statusMessage?: string | null;
className?: string;
scannedAt?: string | null;
};
export const SecretScanningScanStatusBadge = ({
status,
statusMessage,
className,
scannedAt
}: Props) => {
if (status === SecretScanningScanStatus.Failed) {
let errorMessage = statusMessage;
if (statusMessage) {
try {
errorMessage = JSON.stringify(JSON.parse(statusMessage), null, 2);
} catch {
errorMessage = statusMessage;
}
}
return (
<Tooltip
position="left"
className="max-w-sm select-text"
content={
<div className="flex flex-col gap-2 whitespace-normal py-1">
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">Failure Reason</div>
</div>
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">{errorMessage}</div>
{scannedAt && (
<div className="mt-1 text-xs text-mineshaft-400">
Attempted {formatDistance(new Date(scannedAt), new Date(), { addSuffix: true })}
</div>
)}
</div>
</div>
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
Scan Error
</Badge>
</div>
</Tooltip>
);
}
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
return (
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant="primary"
>
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
<span>Scanning</span>
</Badge>
);
}
return (
<Badge
variant="success"
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faCheck} />
<span>Complete</span>
</Badge>
);
};
@@ -0,0 +1,126 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { MultiValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import {
TGitHubRadarConnectionRepository,
useGitHubRadarConnectionListRepositories
} from "@app/hooks/api/appConnections/github-radar";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
enum ScanMethod {
AllRepositories = "all-repositories",
SelectRepositories = "select-repositories"
}
export const GitHubDataSourceConfigFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitHub;
}
>();
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const { data: repositories, isPending: areRepositoriesLoading } =
useGitHubRadarConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) });
const includeRepos = watch("config.includeRepos");
const scanMethod =
!includeRepos || includeRepos[0] === "*"
? ScanMethod.AllRepositories
: ScanMethod.SelectRepositories;
useEffect(() => {
if (!includeRepos) {
setValue("config.includeRepos", ["*"]);
}
}, [includeRepos, setValue]);
return (
<>
<SecretScanningDataSourceConnectionField
isUpdate={isUpdate}
onChange={() => {
if (scanMethod === ScanMethod.SelectRepositories) {
setValue("config.includeRepos", []);
}
}}
/>
<FormControl label="Scan Repositories">
<Select
value={scanMethod}
onValueChange={(val) => {
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!connectionId}
>
{Object.values(ScanMethod).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
{scanMethod === ScanMethod.SelectRepositories && (
<Controller
name="config.includeRepos"
defaultValue={["*"]}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Include Repositories"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the repository you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={areRepositoriesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
isMulti
value={repositories?.filter((repository) => value.includes(repository.name))}
onChange={(newValue) => {
onChange(
newValue
? (newValue as MultiValue<TGitHubRadarConnectionRepository>).map(
(p) => p.name
)
: null
);
}}
options={repositories}
placeholder="Select repositories..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
);
};
@@ -0,0 +1,55 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Switch } from "@app/components/v2";
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
};
export const SecretScanningDataSourceConfigFields = () => {
const { watch, control } = useFormContext<TSecretScanningDataSourceForm>();
const type = watch("type");
const Component = COMPONENT_MAP[type];
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
return (
<>
<p className="mb-4 text-sm text-bunker-300">Connect and configure your Data Source.</p>
<Component />
<Controller
control={control}
name="isAutoScanEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={
value
? `Scans will automatically be triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source.`
: "Manually trigger scans to detect secret leaks."
}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-scan-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p className="w-[9.6rem]">Auto-Scan {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
);
}}
/>
</>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceConfigFields";
@@ -0,0 +1,103 @@
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { TSecretScanningDataSourceForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
isUpdate?: boolean;
};
export const SecretScanningDataSourceConnectionField = ({
onChange: callback,
isUpdate
}: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
const dataSourceType = watch("type");
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
);
return (
<>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="App Connections can be created from the Organization Settings page."
isError={Boolean(error)}
errorText={error?.message}
label={`${connectionName} Connection`}
helperText={
isUpdate ? (
"Cannot be updated"
) : (
<p>
Check out{" "}
<a
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
our docs
</a>{" "}
to ensure your connection has the required permissions for secret scanning.
</p>
)
}
>
<FilterableSelect
value={value}
onChange={(newValue) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {connectionName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${connectionName} Connections. Contact an admin to create one.`
)}
</p>
)}
</>
);
};
@@ -0,0 +1,51 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { TSecretScanningDataSourceForm } from "./schemas";
export const SecretScanningDataSourceDetailsFields = () => {
const { control } = useFormContext<TSecretScanningDataSourceForm>();
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Provide a name and description for this Data Source.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Must be slug-friendly"
isError={Boolean(error)}
errorText={error?.message}
label="Name"
>
<Input autoFocus value={value} onChange={onChange} placeholder="my-data-source" />
</FormControl>
)}
control={control}
name="name"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Description"
>
<TextArea
value={value ?? ""}
onChange={onChange}
placeholder="Provide a description for this data source..."
className="!resize-none"
rows={4}
/>
</FormControl>
)}
control={control}
name="description"
/>
</>
);
};
@@ -0,0 +1,180 @@
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import {
useCreateSecretScanningDataSource,
useUpdateSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2/mutations";
import { SecretScanningDataSourceSchema, TSecretScanningDataSourceForm } from "./schemas";
import { SecretScanningDataSourceConfigFields } from "./SecretScanningDataSourceConfigFields";
import { SecretScanningDataSourceDetailsFields } from "./SecretScanningDataSourceDetailsFields";
import { SecretScanningDataSourceReviewFields } from "./SecretScanningDataSourceReviewFields";
type Props = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
type: SecretScanningDataSource;
onCancel: () => void;
dataSource?: TSecretScanningDataSource;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
[
{ name: "Configuration", key: "config", fields: ["config", "isAutoScanEnabled", "connection"] },
{ name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] }
];
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
const createDataSource = useCreateSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource();
const { currentWorkspace } = useWorkspace();
const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const formMethods = useForm<TSecretScanningDataSourceForm>({
resolver: zodResolver(SecretScanningDataSourceSchema),
defaultValues: dataSource ?? {
type,
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
},
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TSecretScanningDataSourceForm) => {
const mutation = dataSource
? updateDataSource.mutateAsync({
dataSourceId: dataSource.id,
projectId: dataSource.projectId,
...formData
})
: createDataSource.mutateAsync({
...formData,
connectionId: connection?.id,
projectId: currentWorkspace.id
});
try {
const source = await mutation;
createNotification({
text: `Successfully ${source ? "updated" : "created"} ${sourceType} Data Source`,
type: "success"
});
onComplete(source);
} catch (err: any) {
createNotification({
title: `Failed to ${dataSource ? "update" : "create"} ${sourceType} Data Source`,
text: err.message,
type: "error"
});
}
};
const handlePrev = () => {
if (selectedTabIndex === 0) {
onCancel();
return;
}
setSelectedTabIndex((prev) => prev - 1);
};
const {
handleSubmit,
trigger,
formState: { isSubmitting }
} = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const handleNext = async () => {
if (isFinalStep) {
handleSubmit(onSubmit)();
return;
}
const isValid = await isStepValid(selectedTabIndex);
if (!isValid) return;
setSelectedTabIndex((prev) => prev + 1);
};
const isTabEnabled = async (index: number) => {
let isEnabled = true;
for (let i = index - 1; i >= 0; i -= 1) {
// eslint-disable-next-line no-await-in-loop
isEnabled = isEnabled && (await isStepValid(i));
}
return isEnabled;
};
return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
<FormProvider {...formMethods}>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
{FORM_TABS.map((tab, index) => (
<Tab
onClick={async (e) => {
e.preventDefault();
const isEnabled = await isTabEnabled(index);
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
}}
className={({ selected }) =>
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
key={tab.key}
>
{index + 1}. {tab.name}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<SecretScanningDataSourceConfigFields />
</Tab.Panel>
<Tab.Panel>
<SecretScanningDataSourceDetailsFields />
</Tab.Panel>
<Tab.Panel>
<SecretScanningDataSourceReviewFields />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button
onClick={handleNext}
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
{isFinalStep ? `${dataSource ? "Update" : "Create"} Data Source` : "Next"}
</Button>
<Button onClick={handlePrev} colorSchema="secondary">
Back
</Button>
</div>
</form>
);
};
@@ -0,0 +1,27 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
export const GitHubDataSourceReviewFields = () => {
const { watch } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitHub;
}
>();
const [{ includeRepos }, connection] = watch(["config", "connection"]);
const shouldScanAll = includeRepos[0] === "*";
return (
<SecretScanningDataSourceConfigReviewSection>
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
<GenericFieldLabel label="Scan Repositories">
{shouldScanAll ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
</SecretScanningDataSourceConfigReviewSection>
);
};
@@ -0,0 +1,34 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {
const { watch } = useFormContext<TSecretScanningDataSourceForm>();
const { type, name, description } = watch();
const Component = COMPONENT_MAP[type];
return (
<div className="mb-4 flex flex-col gap-6">
<Component />
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Details</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceReviewFields";
@@ -0,0 +1,16 @@
import { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export const SecretScanningDataSourceConfigReviewSection = ({ children }: Props) => {
return (
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Configuration</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">{children}</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceConfigReviewSection";
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceForm";
@@ -0,0 +1,18 @@
import { z } from "zod";
import { slugSchema } from "@app/lib/schemas";
type SchemaOptions = {
isConnectionRequired: boolean;
};
export const BaseSecretScanningDataSourceSchema = ({ isConnectionRequired }: SchemaOptions) =>
z.object({
name: slugSchema({ field: "Name" }),
description: z.string().trim().max(256, "Cannot exceed 256 characters").nullish(),
connection: isConnectionRequired
? z.object({ name: z.string(), id: z.string().uuid() })
: z.null().or(z.undefined()),
isAutoScanEnabled: z.boolean(),
id: z.string().uuid().optional()
});
@@ -0,0 +1,18 @@
import { z } from "zod";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
export const GitHubDataSourceSchema = z
.object({
type: z.literal(SecretScanningDataSource.GitHub),
config: z.object({
includeRepos: z
.string()
.array()
.min(1, "One or more repositories required")
.max(100, "Cannot configure more than 100 repositories")
})
})
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));
@@ -0,0 +1,9 @@
import { z } from "zod";
import { GitHubDataSourceSchema } from "./github-data-source-schema";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema
]);
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
@@ -0,0 +1,3 @@
export * from "./CreateSecretScanningDataSourceModal";
export * from "./EditSecretScanningDataSourceModal";
export * from "./SecretScanningScanStatus";
@@ -52,7 +52,7 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteSecretSync}
>
@@ -131,7 +131,27 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="When a secret is synced, its key will be injected into the key schema before it reaches the destination. This is useful for organization."
tooltipText={
<div className="flex flex-col gap-3">
<span>
When a secret is synced, values will be injected into the key schema before it
reaches the destination. This is useful for organization.
</span>
<div className="flex flex-col">
<span>Available keys:</span>
<ul className="list-disc pl-4 text-sm">
<li>
<code>{"{{secretKey}}"}</code> - The key of the secret
</li>
<li>
<code>{"{{environment}}"}</code> - The environment which the secret is in
(e.g. dev, staging, prod)
</li>
</ul>
</div>
</div>
}
isError={Boolean(error)}
isOptional
errorText={error?.message}
@@ -146,11 +166,15 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Key Schema
</a>{" "}
to ensure that Infisical only manages the specific keys you intend, keeping
everything else untouched.
<br />
<br />
Destination secrets that do not match the schema will not be deleted or updated.
</span>
}
>
@@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
.string()
.optional()
.refine(
(val) =>
!val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val),
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = ["{{environment}}"];
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
.join("|");
const allowedContentRegex = new RegExp(
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
);
const contentIsValid = allowedContentRegex.test(val);
const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length;
return contentIsValid && secretKeyCount === 1;
},
{
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
"Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
});
+6 -2
View File
@@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge";
interface IProps {
children: React.ReactNode;
className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const badgeVariants = cva(
@@ -23,9 +24,12 @@ const badgeVariants = cva(
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
export const Badge = ({ children, className, variant }: BadgeProps) => {
export const Badge = ({ children, className, variant, ...props }: BadgeProps) => {
return (
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
<div
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
{...props}
>
{children}
</div>
);
+3 -1
View File
@@ -18,6 +18,7 @@ type Props = {
isDisabled?: boolean;
icon?: IconProp;
isMulti?: boolean;
iconClassName?: string;
};
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
@@ -33,6 +34,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
dropdownContainerClassName,
position,
containerClassName,
iconClassName,
...props
},
ref
@@ -58,7 +60,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
)}
>
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
{props.icon && <FontAwesomeIcon icon={props.icon} />}
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
<div className="flex-1 truncate">
<SelectPrimitive.Value placeholder={placeholder} />
</div>
+10 -4
View File
@@ -27,10 +27,6 @@ export const ROUTE_PATHS = Object.freeze({
"/organization/sso",
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
),
SecretScanning: setRoute(
"/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
),
SecretSharing: setRoute(
"/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
@@ -334,6 +330,16 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
)
},
SecretScanning: {
DataSourceByIdPage: setRoute(
"/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
),
FindingsPage: setRoute(
"/secret-scanning/$projectId/findings",
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/findings"
)
},
Public: {
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
ViewSecretRequestByIDPage: setRoute(
@@ -2,6 +2,7 @@ export { useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export {
OrgPermissionActions,
OrgPermissionBillingActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
@@ -7,6 +7,11 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionBillingActions {
Read = "read",
ManageBilling = "manage-billing"
}
export enum OrgGatewayPermissionActions {
// is there a better word for this. This mean can an identity be a gateway
CreateGateways = "create-gateways",
@@ -100,7 +105,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
@@ -2,7 +2,6 @@ export { useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types";
export {
ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -11,6 +10,7 @@ export {
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub
} from "./types";
@@ -24,15 +24,6 @@ export enum ProjectPermissionSecretActions {
Delete = "delete"
}
export enum ProjectPermissionApprovalActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential",
@@ -113,6 +104,15 @@ export enum ProjectPermissionPkiSubscriberActions {
ListCerts = "list-certs"
}
export enum ProjectPermissionPkiTemplateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert",
ListCerts = "list-certs"
}
export enum ProjectPermissionSecretRotationActions {
Read = "read",
ReadGeneratedCredentials = "read-generated-credentials",
@@ -122,6 +122,26 @@ export enum ProjectPermissionSecretRotationActions {
RotateSecrets = "rotate-secrets"
}
export enum ProjectPermissionSecretScanningDataSourceActions {
Read = "read-data-sources",
Create = "create-data-sources",
Edit = "edit-data-sources",
Delete = "delete-data-sources",
TriggerScans = "trigger-data-source-scans",
ReadScans = "read-data-source-scans",
ReadResources = "read-data-source-resources"
}
export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings",
Update = "update-findings"
}
export enum ProjectPermissionSecretScanningConfigActions {
Read = "read-configs",
Update = "update-configs"
}
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
@@ -214,7 +234,10 @@ export enum ProjectPermissionSub {
Cmek = "cmek",
SecretSyncs = "secret-syncs",
Kmip = "kmip",
Commits = "commits"
Commits = "commits",
SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs"
}
export type SecretSubjectFields = {
@@ -253,6 +276,11 @@ export type PkiSubscriberSubjectFields = {
name: string;
};
export type PkiTemplateSubjectFields = {
name: string;
// (dangtony98): consider adding [commonName] as a subject field in the future
};
export type ProjectPermissionSet =
| [
ProjectPermissionSecretActions,
@@ -300,7 +328,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [
ProjectPermissionIdentityActions,
(
@@ -310,7 +338,13 @@ export type ProjectPermissionSet =
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [
ProjectPermissionPkiTemplateActions,
(
| ProjectPermissionSub.CertificateTemplates
| (ForcedSubject<ProjectPermissionSub.CertificateTemplates> & PkiTemplateSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
@@ -339,6 +373,12 @@ export type ProjectPermissionSet =
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits];
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits]
| [
ProjectPermissionSecretScanningDataSourceActions,
ProjectPermissionSub.SecretScanningDataSources
]
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
+2 -1
View File
@@ -2,6 +2,7 @@ export { useOrganization } from "./OrganizationContext";
export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgPermissionActions,
OrgPermissionBillingActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects,
@@ -10,7 +11,6 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -19,6 +19,7 @@ export {
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub,
useProjectPermission
+13 -2
View File
@@ -1,5 +1,6 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faGithub, IconDefinition } from "@fortawesome/free-brands-svg-icons";
import {
faBullseye,
faKey,
faLink,
faLock,
@@ -19,10 +20,12 @@ import {
DatabricksConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
GitHubRadarConnectionMethod,
HCVaultConnectionMethod,
HumanitecConnectionMethod,
LdapConnectionMethod,
MsSqlConnectionMethod,
MySqlConnectionMethod,
OnePassConnectionMethod,
PostgresConnectionMethod,
TAppConnection,
@@ -35,10 +38,15 @@ import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-con
export const APP_CONNECTION_MAP: Record<
AppConnection,
{ name: string; image: string; size?: number; enterprise?: boolean }
{ name: string; image: string; size?: number; icon?: IconDefinition; enterprise?: boolean }
> = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" },
[AppConnection.GitHubRadar]: {
name: "GitHub Radar",
image: "GitHub.png",
icon: faBullseye
},
[AppConnection.GCP]: {
name: "GCP",
image: "Google Cloud Platform.png"
@@ -58,6 +66,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Vercel]: { name: "Vercel", image: "Vercel.png" },
[AppConnection.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" },
[AppConnection.MySql]: { name: "MySQL", image: "MySql.png" },
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" },
[AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" },
[AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 },
@@ -71,6 +80,7 @@ export const APP_CONNECTION_MAP: Record<
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
switch (method) {
case GitHubConnectionMethod.App:
case GitHubRadarConnectionMethod.App:
return { name: "GitHub App", icon: faGithub };
case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth:
@@ -95,6 +105,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword:
return { name: "Username & Password", icon: faLock };
case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken:
+9 -4
View File
@@ -61,10 +61,14 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project;
};
export const getProjectHomePage = (workspace: Workspace) => {
if (workspace.type === ProjectType.CertificateManager) {
return `/${workspace.type}/$projectId/subscribers` as const;
switch (workspace.type) {
case ProjectType.CertificateManager:
return `/${workspace.type}/$projectId/subscribers` as const;
case ProjectType.SecretScanning:
return `/${workspace.type}/$projectId/data-sources` as const;
default:
return `/${workspace.type}/$projectId/overview` as const;
}
return `/${workspace.type}/$projectId/overview` as const;
};
export const getProjectTitle = (type: ProjectType) => {
@@ -72,7 +76,8 @@ export const getProjectTitle = (type: ProjectType) => {
[ProjectType.SecretManager]: "Secret Management",
[ProjectType.KMS]: "Key Management",
[ProjectType.CertificateManager]: "Cert Management",
[ProjectType.SSH]: "SSH"
[ProjectType.SSH]: "SSH",
[ProjectType.SecretScanning]: "Secret Scanning"
};
return titleConvert[type];
};
@@ -15,6 +15,11 @@ export const SECRET_ROTATION_MAP: Record<
image: "MsSql.png",
size: 50
},
[SecretRotation.MySqlCredentials]: {
name: "MySQL Credentials",
image: "MySql.png",
size: 50
},
[SecretRotation.Auth0ClientSecret]: {
name: "Auth0 Client Secret",
image: "Auth0.png",
@@ -40,6 +45,7 @@ export const SECRET_ROTATION_MAP: Record<
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.MySqlCredentials]: AppConnection.MySql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
@@ -50,6 +56,7 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[SecretRotation.PostgresCredentials]: true,
[SecretRotation.MsSqlCredentials]: true,
[SecretRotation.MySqlCredentials]: true,
[SecretRotation.Auth0ClientSecret]: false,
[SecretRotation.AzureClientSecret]: true,
[SecretRotation.LdapPassword]: false,
+59
View File
@@ -0,0 +1,59 @@
import {
faBan,
faCheck,
faMagnifyingGlassMinus,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
SecretScanningDataSource,
SecretScanningFindingStatus
} from "@app/hooks/api/secretScanningV2";
export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
SecretScanningDataSource,
{ name: string; image: string; size: number }
> = {
[SecretScanningDataSource.GitHub]: {
name: "GitHub",
image: "GitHub.png",
size: 45
}
};
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
SecretScanningDataSource,
AppConnection
> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar
};
export const RESOURCE_DESCRIPTION_HELPER: Record<
SecretScanningDataSource,
{
verb: string;
pluralNoun: string;
singularNoun: string;
singularTitle: string;
pluralTitle: string;
}
> = {
[SecretScanningDataSource.GitHub]: {
verb: "push",
pluralNoun: "repositories",
singularNoun: "repository",
pluralTitle: "Repositories",
singularTitle: "Repository"
}
};
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = {
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" },
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" },
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" },
[SecretScanningFindingStatus.FalsePositive]: {
icon: faMagnifyingGlassMinus,
className: "text-mineshaft-400"
}
};
@@ -21,6 +21,7 @@ export const useCreateAccessApprovalPolicy = () => {
projectSlug,
approvals,
approvers,
bypassers,
name,
secretPath,
enforcementLevel,
@@ -30,6 +31,7 @@ export const useCreateAccessApprovalPolicy = () => {
environment,
projectSlug,
approvals,
bypassers,
approvers,
secretPath,
name,
@@ -53,6 +55,7 @@ export const useUpdateAccessApprovalPolicy = () => {
mutationFn: async ({
id,
approvers,
bypassers,
approvals,
name,
secretPath,
@@ -62,6 +65,7 @@ export const useUpdateAccessApprovalPolicy = () => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -16,6 +16,7 @@ export type TAccessApprovalPolicy = {
enforcementLevel: EnforcementLevel;
updatedAt: Date;
approvers?: Approver[];
bypassers?: Bypasser[];
allowedSelfApprovals: boolean;
};
@@ -24,11 +25,21 @@ export enum ApproverType {
Group = "group"
}
export enum BypasserType {
User = "user",
Group = "group"
}
export type Approver = {
id: string;
type: ApproverType;
};
export type Bypasser = {
id: string;
type: BypasserType;
};
export type TAccessApprovalRequest = {
id: string;
policyId: string;
@@ -68,6 +79,7 @@ export type TAccessApprovalRequest = {
name: string;
approvals: number;
approvers: string[];
bypassers: string[];
secretPath?: string | null;
envId: string;
enforcementLevel: EnforcementLevel;
@@ -146,6 +158,7 @@ export type TCreateAccessPolicyDTO = {
name?: string;
environment: string;
approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number;
secretPath?: string;
enforcementLevel?: EnforcementLevel;
@@ -156,6 +169,7 @@ export type TUpdateAccessPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string;
environment?: string;
approvals?: number;
+2 -16
View File
@@ -1,16 +1,2 @@
export {
useAdminDeleteUser,
useAdminGrantServerAdminAccess,
useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser,
useInvalidateCache,
useRemoveUserServerAdminAccess,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy
} from "./mutation";
export {
useAdminGetUsers,
useGetAdminIntegrationsConfig,
useGetServerConfig,
useGetServerRootKmsEncryptionDetails
} from "./queries";
export * from "./mutation";
export * from "./queries";
+35
View File
@@ -63,6 +63,41 @@ export const useAdminDeleteUser = () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
queryClient.invalidateQueries({ queryKey: adminStandaloneKeys.getOrganizations });
}
});
};
export const useAdminDeleteOrganizationMembership = () => {
const queryClient = useQueryClient();
return useMutation<object, object, { organizationId: string; membershipId: string }>({
mutationFn: async ({ organizationId, membershipId }) => {
await apiRequest.delete(
`/api/v1/admin/organization-management/organizations/${organizationId}/memberships/${membershipId}`
);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getOrganizations]
});
}
});
};
export const useAdminDeleteOrganization = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (organizationId: string) => {
await apiRequest.delete(
`/api/v1/admin/organization-management/organizations/${organizationId}`
);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getOrganizations]
});
}
});
};
+27
View File
@@ -6,8 +6,10 @@ import { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types";
import {
AdminGetIdentitiesFilters,
AdminGetOrganizationsFilters,
AdminGetUsersFilters,
AdminIntegrationsConfig,
OrganizationWithProjects,
TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails,
TServerConfig
@@ -15,12 +17,15 @@ import {
export const adminStandaloneKeys = {
getUsers: "get-users",
getOrganizations: "get-organizations",
getIdentities: "get-identities"
};
export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getOrganizations: (filters: AdminGetOrganizationsFilters) =>
[adminStandaloneKeys.getOrganizations, { filters }] as const,
getIdentities: (filters: AdminGetIdentitiesFilters) =>
[adminStandaloneKeys.getIdentities, { filters }] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const,
@@ -34,6 +39,28 @@ export const fetchServerConfig = async () => {
return data.config;
};
export const useAdminGetOrganizations = (filters: AdminGetOrganizationsFilters) => {
return useInfiniteQuery({
initialPageParam: 0,
queryKey: adminQueryKeys.getOrganizations(filters),
queryFn: async ({ pageParam }) => {
const { data } = await apiRequest.get<{ organizations: OrganizationWithProjects[] }>(
"/api/v1/admin/organization-management/organizations",
{
params: {
...filters,
offset: pageParam
}
}
);
return data.organizations;
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
});
};
export const useGetServerConfig = ({
options = {}
}: {
+28
View File
@@ -1,3 +1,5 @@
import { Organization } from "../types";
export enum LoginMethod {
EMAIL = "email",
GOOGLE = "google",
@@ -8,6 +10,27 @@ export enum LoginMethod {
OIDC = "oidc"
}
export type OrganizationWithProjects = Organization & {
members: {
user: {
id: string;
email: string | null;
username: string;
firstName: string | null;
lastName: string | null;
};
membershipId: string;
role: string;
roleId: string | null;
}[];
projects: {
name: string;
id: string;
slug: string;
createdAt: string;
}[];
};
export type TServerConfig = {
initialized: boolean;
allowSignUp: boolean;
@@ -51,6 +74,11 @@ export type TCreateAdminUserDTO = {
salt: string;
};
export type AdminGetOrganizationsFilters = {
limit: number;
searchTerm: string;
};
export type AdminGetUsersFilters = {
limit: number;
searchTerm: string;
@@ -1,6 +1,7 @@
export enum AppConnection {
AWS = "aws",
GitHub = "github",
GitHubRadar = "github-radar",
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
@@ -11,6 +12,7 @@ export enum AppConnection {
Vercel = "vercel",
Postgres = "postgres",
MsSql = "mssql",
MySql = "mysql",
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0",
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,40 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import {
TGitHubRadarConnectionListRepositoriesResponse,
TGitHubRadarConnectionRepository
} from "./types";
const githubRadarConnectionKeys = {
all: [...appConnectionKeys.all, "github-radar"] as const,
listRepositories: (connectionId: string) =>
[...githubRadarConnectionKeys.all, "repositories", connectionId] as const
};
export const useGitHubRadarConnectionListRepositories = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TGitHubRadarConnectionRepository[],
unknown,
TGitHubRadarConnectionRepository[],
ReturnType<typeof githubRadarConnectionKeys.listRepositories>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: githubRadarConnectionKeys.listRepositories(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGitHubRadarConnectionListRepositoriesResponse>(
`/api/v1/app-connections/github-radar/${connectionId}/repositories`
);
return data.repositories;
},
...options
});
};
@@ -0,0 +1,14 @@
export type TGitHubRadarConnectionOrganization = {
login: string;
id: number;
};
export type TGitHubRadarConnectionRepository = {
id: number;
name: string;
owner: TGitHubRadarConnectionOrganization;
};
export type TGitHubRadarConnectionListRepositoriesResponse = {
repositories: TGitHubRadarConnectionRepository[];
};
@@ -17,6 +17,11 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & {
appClientSlug?: string;
};
export type TGitHubRadarConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GitHubRadar;
appClientSlug?: string;
};
export type TGcpConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GCP;
};
@@ -60,6 +65,10 @@ export type TMsSqlConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.MsSql;
};
export type TMySqlConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.MySql;
};
export type TCamundaConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Camunda;
};
@@ -105,6 +114,7 @@ export type TAppConnectionOption =
| TVercelConnectionOption
| TPostgresConnectionOption
| TMsSqlConnectionOption
| TMySqlConnectionOption
| TCamundaConnectionOption
| TWindmillConnectionOption
| TAuth0ConnectionOption
@@ -116,6 +126,7 @@ export type TAppConnectionOption =
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
[AppConnection.GitHub]: TGitHubConnectionOption;
[AppConnection.GitHubRadar]: TGitHubRadarConnectionOption;
[AppConnection.GCP]: TGcpConnectionOption;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
@@ -126,6 +137,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.Vercel]: TVercelConnectionOption;
[AppConnection.Postgres]: TPostgresConnectionOption;
[AppConnection.MsSql]: TMsSqlConnectionOption;
[AppConnection.MySql]: TMySqlConnectionOption;
[AppConnection.Camunda]: TCamundaConnectionOption;
[AppConnection.Windmill]: TWindmillConnectionOption;
[AppConnection.Auth0]: TAuth0ConnectionOption;
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum GitHubRadarConnectionMethod {
App = "github-app"
}
export type TGitHubRadarConnection = TRootAppConnection & { app: AppConnection.GitHubRadar } & {
method: GitHubRadarConnectionMethod.App;
credentials: {
code: string;
installationId: string;
};
};
@@ -10,10 +10,12 @@ import { TCamundaConnection } from "./camunda-connection";
import { TDatabricksConnection } from "./databricks-connection";
import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection";
import { TGitHubRadarConnection } from "./github-radar-connection";
import { THCVaultConnection } from "./hc-vault-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TLdapConnection } from "./ldap-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TMySqlConnection } from "./mysql-connection";
import { TOCIConnection } from "./oci-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TTeamCityConnection } from "./teamcity-connection";
@@ -31,10 +33,12 @@ export * from "./camunda-connection";
export * from "./databricks-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export * from "./github-radar-connection";
export * from "./hc-vault-connection";
export * from "./humanitec-connection";
export * from "./ldap-connection";
export * from "./mssql-connection";
export * from "./mysql-connection";
export * from "./oci-connection";
export * from "./postgres-connection";
export * from "./teamcity-connection";
@@ -45,6 +49,7 @@ export * from "./windmill-connection";
export type TAppConnection =
| TAwsConnection
| TGitHubConnection
| TGitHubRadarConnection
| TGcpConnection
| TAzureKeyVaultConnection
| TAzureAppConfigurationConnection
@@ -55,6 +60,7 @@ export type TAppConnection =
| TVercelConnection
| TPostgresConnection
| TMsSqlConnection
| TMySqlConnection
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
@@ -92,6 +98,7 @@ export type TDeleteAppConnectionDTO = {
export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GitHubRadar]: TGitHubRadarConnection;
[AppConnection.GCP]: TGcpConnection;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
@@ -102,6 +109,7 @@ export type TAppConnectionMap = {
[AppConnection.Vercel]: TVercelConnection;
[AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MsSql]: TMsSqlConnection;
[AppConnection.MySql]: TMySqlConnection;
[AppConnection.Camunda]: TCamundaConnection;
[AppConnection.Windmill]: TWindmillConnection;
[AppConnection.Auth0]: TAuth0Connection;
@@ -0,0 +1,13 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
import { TBaseSqlConnectionCredentials } from "./shared";
export enum MySqlConnectionMethod {
UsernameAndPassword = "username-and-password"
}
export type TMySqlConnection = TRootAppConnection & { app: AppConnection.MySql } & {
method: MySqlConnectionMethod.UsernameAndPassword;
credentials: TBaseSqlConnectionCredentials;
};
@@ -191,6 +191,20 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity",
[EventType.SECRET_SCANNING_DATA_SOURCE_LIST]: "List Secret Scanning Data Sources",
[EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE]: "Update Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_DELETE]: "Delete Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_GET]: "Get Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN]: "Trigger Secret Scanning Data Source Scan",
[EventType.SECRET_SCANNING_DATA_SOURCE_SCAN]: "Secret Scanning Data Source Scan",
[EventType.SECRET_SCANNING_RESOURCE_LIST]: "List Secret Scanning Resources",
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
[EventType.SECRET_SCANNING_FINDING_LIST]: "List Secret Scanning Findings",
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status",
[EventType.SECRET_SCANNING_CONFIG_GET]: "Get Secret Scanning Configuration",
[EventType.SECRET_SCANNING_CONFIG_UPDATE]: "Update Secret Scanning Configuration",
[EventType.CREATE_PKI_SUBSCRIBER]: "Create PKI subscriber",
[EventType.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber",
[EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber",
@@ -185,6 +185,20 @@ export enum EventType {
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN = "secret-scanning-data-source-trigger-scan",
SECRET_SCANNING_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update",
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
+5 -1
View File
@@ -31,10 +31,14 @@ export const useUpdateCa = () => {
return data;
},
onSuccess: ({ projectId, type }) => {
onSuccess: ({ projectId, type }, { caName }) => {
caKeys.getCaByNameAndProjectId(caName, projectId);
queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
});
queryClient.invalidateQueries({
queryKey: caKeys.getCaByNameAndProjectId(caName, projectId)
});
}
});
};
@@ -1,8 +1,11 @@
export {
useCreateCertTemplate,
useCreateCertTemplateV2,
useCreateEstConfig,
useDeleteCertTemplate,
useDeleteCertTemplateV2,
useUpdateCertTemplate,
useUpdateCertTemplateV2,
useUpdateEstConfig
} from "./mutations";
export { useGetCertTemplate, useGetEstConfig } from "./queries";
export { useGetCertTemplate, useGetEstConfig, useListCertificateTemplates } from "./queries";
@@ -8,9 +8,12 @@ import { certTemplateKeys } from "./queries";
import {
TCertificateTemplate,
TCreateCertificateTemplateDTO,
TCreateCertificateTemplateV2DTO,
TCreateEstConfigDTO,
TDeleteCertificateTemplateDTO,
TDeleteCertificateTemplateV2DTO,
TUpdateCertificateTemplateDTO,
TUpdateCertificateTemplateV2DTO,
TUpdateEstConfigDTO
} from "./types";
@@ -73,6 +76,58 @@ export const useDeleteCertTemplate = () => {
});
};
export const useCreateCertTemplateV2 = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, object, TCreateCertificateTemplateV2DTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post<{
certificateTemplate: TCertificateTemplate;
}>("/api/v2/pki/certificate-templates", dto);
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
}
});
};
export const useUpdateCertTemplateV2 = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, object, TUpdateCertificateTemplateV2DTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.patch<{ certificateTemplate: TCertificateTemplate }>(
`/api/v2/pki/certificate-templates/${dto.templateName}`,
dto
);
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
}
});
};
export const useDeleteCertTemplateV2 = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, object, TDeleteCertificateTemplateV2DTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete<{ certificateTemplate: TCertificateTemplate }>(
`/api/v2/pki/certificate-templates/${dto.templateName}`,
{
data: {
projectId: dto.projectId
}
}
);
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
}
});
};
export const useCreateEstConfig = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TCreateEstConfigDTO>({
@@ -2,10 +2,20 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificateTemplate, TEstConfig } from "./types";
import {
TCertificateTemplate,
TCertificateTemplateV2,
TEstConfig,
TListCertificateTemplatesDTO
} from "./types";
export const certTemplateKeys = {
getCertTemplateById: (id: string) => [{ id }, "cert-template"],
listTemplates: ({ projectId, ...el }: { limit?: number; offset?: number; projectId: string }) => [
"list-template",
projectId,
el
],
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
};
@@ -22,6 +32,29 @@ export const useGetCertTemplate = (id: string) => {
});
};
export const useListCertificateTemplates = ({
limit = 100,
offset = 0,
projectId
}: TListCertificateTemplatesDTO) => {
return useQuery({
queryKey: certTemplateKeys.listTemplates({ limit, offset, projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificateTemplates: TCertificateTemplateV2[];
totalCount?: number;
}>("/api/v2/pki/certificate-templates", {
params: {
limit,
offset,
projectId
}
});
return data;
}
});
};
export const useGetEstConfig = (certificateTemplateId: string) => {
return useQuery({
queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),
@@ -14,6 +14,26 @@ export type TCertificateTemplate = {
extendedKeyUsages: CertExtendedKeyUsage[];
};
export type TCertificateTemplateV2 = {
id: string;
caId: string;
caName: string;
projectId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
updatedAt: string;
createdAt: string;
ca: {
name: string;
id: string;
};
};
export type TCreateCertificateTemplateDTO = {
caId: string;
pkiCollectionId?: string;
@@ -44,6 +64,34 @@ export type TDeleteCertificateTemplateDTO = {
projectId: string;
};
export type TCreateCertificateTemplateV2DTO = {
caName: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
projectId: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
};
export type TUpdateCertificateTemplateV2DTO = {
templateName: string;
caName?: string;
name?: string;
commonName?: string;
subjectAlternativeName?: string;
ttl?: string;
projectId: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
};
export type TDeleteCertificateTemplateV2DTO = {
templateName: string;
projectId: string;
};
export type TCreateEstConfigDTO = {
certificateTemplateId: string;
caChain?: string;
@@ -67,3 +115,9 @@ export type TEstConfig = {
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
};
export type TListCertificateTemplatesDTO = {
limit?: number;
offset?: number;
projectId: string;
};
+32 -1
View File
@@ -13,6 +13,7 @@ export type TDynamicSecret = {
status?: DynamicSecretStatus;
statusDetails?: string;
maxTTL: string;
usernameTemplate?: string | null;
metadata?: { key: string; value: string }[];
};
@@ -31,7 +32,9 @@ export enum DynamicSecretProviders {
SapHana = "sap-hana",
Snowflake = "snowflake",
Totp = "totp",
SapAse = "sap-ase"
SapAse = "sap-ase",
Kubernetes = "kubernetes",
Vertica = "vertica"
}
export enum SqlProviders {
@@ -261,6 +264,32 @@ export type TDynamicSecretProvider =
algorithm?: string;
digits?: number;
};
}
| {
type: DynamicSecretProviders.Kubernetes;
inputs: {
url: string;
clusterToken: string;
ca?: string;
serviceAccountName: string;
credentialType: "dynamic" | "static";
namespace: string;
gatewayId?: string;
sslEnabled: boolean;
audiences: string[];
};
}
| {
type: DynamicSecretProviders.Vertica;
inputs: {
host: string;
port: number;
database: string;
username: string;
password: string;
creationStatement: string;
revocationStatement: string;
};
};
export type TCreateDynamicSecretDTO = {
@@ -272,6 +301,7 @@ export type TCreateDynamicSecretDTO = {
environmentSlug: string;
name: string;
metadata?: { key: string; value: string }[];
usernameTemplate?: string;
};
export type TUpdateDynamicSecretDTO = {
@@ -285,6 +315,7 @@ export type TUpdateDynamicSecretDTO = {
defaultTTL?: string;
maxTTL?: string | null;
inputs?: unknown;
usernameTemplate?: string | null;
};
};
@@ -843,7 +843,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}) => {
const {
data: { identityKubernetesAuth }
@@ -860,7 +861,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}
);
@@ -950,7 +952,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}) => {
const {
data: { identityKubernetesAuth }
@@ -967,7 +970,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}
);
@@ -379,10 +379,16 @@ export type DeleteIdentityAzureAuthDTO = {
identityId: string;
};
export enum IdentityKubernetesAuthTokenReviewMode {
Api = "api",
Gateway = "gateway"
}
export type IdentityKubernetesAuth = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@@ -399,6 +405,7 @@ export type AddIdentityKubernetesAuthDTO = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt?: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@@ -417,6 +424,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
identityId: string;
kubernetesHost?: string;
tokenReviewerJwt?: string | null;
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
+35 -29
View File
@@ -1,3 +1,4 @@
import { useState } from "react";
import { MutationCache, QueryClient } from "@tanstack/react-query";
import axios from "axios";
@@ -18,6 +19,39 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
function ValidationErrorModal({ serverResponse }: { serverResponse: TApiErrors }) {
const [open, setOpen] = useState(true);
if (serverResponse.error !== ApiErrorTypes.ValidationError) {
return null;
}
return (
<Modal isOpen={open} onOpenChange={setOpen}>
<ModalContent title="Validation Error Details">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Field</Th>
<Th>Issue</Th>
</Tr>
</THead>
<TBody>
{serverResponse.message?.map(({ message, path }) => (
<Tr key={path.join(".")}>
<Td>{path.join(".")}</Td>
<Td>{message.toLowerCase()}</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
}
export const onRequestError = (error: unknown) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as TApiErrors;
@@ -27,35 +61,7 @@ export const onRequestError = (error: unknown) => {
title: "Validation Error",
type: "error",
text: "Please check the input and try again.",
callToAction: (
<Modal>
<ModalTrigger asChild>
<Button variant="outline_bg" size="xs">
Show more
</Button>
</ModalTrigger>
<ModalContent title="Validation Error Details">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Field</Th>
<Th>Issue</Th>
</Tr>
</THead>
<TBody>
{serverResponse.message?.map(({ message, path }) => (
<Tr key={path.join(".")}>
<Td>{path.join(".")}</Td>
<Td>{message.toLowerCase()}</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
),
callToAction: <ValidationErrorModal serverResponse={serverResponse} />,
copyActions: [
{
value: serverResponse.reqId,
+1 -1
View File
@@ -96,7 +96,7 @@ export const useUpdateOrgRole = () => {
data: { role }
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
...dto,
permissions: permissions?.length ? packRules(permissions) : undefined
permissions: permissions ? packRules(permissions) : undefined
});
return role;
@@ -14,6 +14,7 @@ export const useCreateSecretApprovalPolicy = () => {
workspaceId,
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -24,6 +25,7 @@ export const useCreateSecretApprovalPolicy = () => {
workspaceId,
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -46,6 +48,7 @@ export const useUpdateSecretApprovalPolicy = () => {
mutationFn: async ({
id,
approvers,
bypassers,
approvals,
secretPath,
name,
@@ -55,6 +58,7 @@ export const useUpdateSecretApprovalPolicy = () => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
approvers,
bypassers,
secretPath,
name,
enforcementLevel,
@@ -25,6 +25,16 @@ export type Approver = {
type: ApproverType;
};
export enum BypasserType {
User = "user",
Group = "group"
}
export type Bypasser = {
id: string;
type: BypasserType;
};
export type TGetSecretApprovalPoliciesDTO = {
workspaceId: string;
};
@@ -41,6 +51,7 @@ export type TCreateSecretPolicyDTO = {
environment: string;
secretPath?: string | null;
approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number;
enforcementLevel: EnforcementLevel;
allowedSelfApprovals: boolean;
@@ -50,6 +61,7 @@ export type TUpdateSecretPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string | null;
approvals?: number;
allowedSelfApprovals?: boolean;
@@ -58,7 +58,7 @@ export type TSecretApprovalRequest = {
secretPath: string;
hasMerged: boolean;
status: "open" | "close";
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
approvers: {
userId: string;
email: string;
@@ -66,6 +66,13 @@ export type TSecretApprovalRequest = {
lastName: string;
username: string;
}[];
bypassers: {
userId: string;
email: string;
firstName: string;
lastName: string;
username: string;
}[];
};
statusChangedByUserId: string;
statusChangedByUser?: {
@@ -1,6 +1,7 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
MySqlCredentials = "mysql-credentials",
Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-secret",
LdapPassword = "ldap-password",
@@ -31,9 +31,15 @@ import { TSqlCredentialsRotationOption } from "@app/hooks/api/secretRotationsV2/
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { DiscriminativePick } from "@app/types";
import {
TMySqlCredentialsRotation,
TMySqlCredentialsRotationGeneratedCredentialsResponse
} from "./mysql-credentials-rotation";
export type TSecretRotationV2 = (
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TMySqlCredentialsRotation
| TAuth0ClientSecretRotation
| TAzureClientSecretRotation
| TLdapPasswordRotation
@@ -56,6 +62,7 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 };
export type TViewSecretRotationGeneratedCredentialsResponse =
| TPostgresCredentialsRotationGeneratedCredentialsResponse
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
| TMySqlCredentialsRotationGeneratedCredentialsResponse
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
| TAzureClientSecretRotationGeneratedCredentialsResponse
| TLdapPasswordRotationGeneratedCredentialsResponse
@@ -105,6 +112,7 @@ export type TViewSecretRotationV2GeneratedCredentialsDTO = {
export type TSecretRotationOptionMap = {
[SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.MySqlCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption;
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption;
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
@@ -114,6 +122,7 @@ export type TSecretRotationOptionMap = {
export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.MySqlCredentials]: TMySqlCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
@@ -0,0 +1,17 @@
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import {
TSecretRotationV2Base,
TSecretRotationV2GeneratedCredentialsResponseBase,
TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationProperties
} from "@app/hooks/api/secretRotationsV2/types/shared";
export type TMySqlCredentialsRotation = TSecretRotationV2Base & {
type: SecretRotation.MySqlCredentials;
} & TSqlCredentialsRotationProperties;
export type TMySqlCredentialsRotationGeneratedCredentialsResponse =
TSecretRotationV2GeneratedCredentialsResponseBase<
SecretRotation.MySqlCredentials,
TSqlCredentialsRotationGeneratedCredentials
>;
@@ -14,8 +14,11 @@ export type TSqlCredentialsRotationProperties = {
export type TSqlCredentialsRotationOption = {
name: string;
type: SecretRotation.PostgresCredentials | SecretRotation.MsSqlCredentials;
connection: AppConnection.Postgres | AppConnection.MsSql;
type:
| SecretRotation.PostgresCredentials
| SecretRotation.MsSqlCredentials
| SecretRotation.MySqlCredentials;
connection: AppConnection.Postgres | AppConnection.MsSql | AppConnection.MySql;
template: {
secretsMapping: TSqlCredentialsRotationProperties["secretsMapping"];
createUserStatement: string;
@@ -0,0 +1,33 @@
export enum SecretScanningDataSource {
GitHub = "github"
}
export enum SecretScanningScanStatus {
Completed = "completed",
Failed = "failed",
Queued = "queued",
Scanning = "scanning"
}
export enum SecretScanningScanType {
FullScan = "full-scan",
DiffScan = "diff-scan"
}
export enum SecretScanningFindingStatus {
Resolved = "resolved",
Unresolved = "unresolved",
FalsePositive = "false-positive",
Ignore = "ignore"
}
export enum SecretScanningResource {
Repository = "repository",
Project = "project"
}
export enum SecretScanningFindingSeverity {
High = "high",
Medium = "medium",
Low = "low"
}
@@ -0,0 +1,4 @@
export * from "./enums";
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -0,0 +1,160 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretScanningV2Keys } from "./queries";
import {
TCreateSecretScanningDataSourceDTO,
TDeleteSecretScanningDataSourceDTO,
TGetSecretScanningConfigResponse,
TSecretScanningConfig,
TSecretScanningDataSourceResponse,
TSecretScanningFindingResponse,
TTriggerSecretScanningDataSourceDTO,
TUpdateSecretScanningDataSourceDTO,
TUpdateSecretScanningFinding
} from "./types";
export const useCreateSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, ...params }: TCreateSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.post<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}`,
params
);
return data.dataSource;
},
onSuccess: (_, { projectId }) =>
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
})
});
};
export const useUpdateSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, dataSourceId, ...params }: TUpdateSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.patch<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`,
params
);
return data.dataSource;
},
onSuccess: (_, { projectId, dataSourceId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listResources(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listScans(dataSourceId)
});
}
});
};
export const useDeleteSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.delete<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`
);
return data.dataSource;
},
onSuccess: (_, { projectId, dataSourceId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listResources(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listScans(dataSourceId)
});
}
});
};
export const useTriggerSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, dataSourceId, resourceId }: TTriggerSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.post<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}${resourceId ? `/resources/${resourceId}` : ""}/scan`
);
return data.dataSource;
},
onSuccess: (_, { projectId, dataSourceId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listResources(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listScans(dataSourceId)
});
}
});
};
export const useUpdateSecretScanningFinding = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ findingId, ...params }: TUpdateSecretScanningFinding) => {
const { data } = await apiRequest.patch<TSecretScanningFindingResponse>(
`/api/v2/secret-scanning/findings/${findingId}`,
params
);
return data.finding;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listFindings(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.findingCount(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSource()
});
}
});
};
export const useUpdateSecretScanningConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
`/api/v2/secret-scanning/configs?projectId=${projectId}`,
params
);
return data.config;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.configByProjectId(projectId)
});
}
});
};
@@ -0,0 +1,248 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TGetSecretScanningConfigResponse,
TGetSecretScanningDataSource,
TGetSecretScanningUnresolvedFindingsResponse,
TListSecretScanningDataSourceOptions,
TListSecretScanningDataSources,
TListSecretScanningFindingsResponse,
TListSecretScanningResourcesResponse,
TListSecretScanningScansResponse,
TSecretScanningConfig,
TSecretScanningDataSource,
TSecretScanningDataSourceOption,
TSecretScanningDataSourceResponse,
TSecretScanningDataSourceWithDetails,
TSecretScanningFinding,
TSecretScanningResourceWithDetails,
TSecretScanningScanWithDetails
} from "./types";
export const secretScanningV2Keys = {
all: ["secret-scanning-v2"] as const,
dataSource: () => [...secretScanningV2Keys.all, "data-source"] as const,
dataSourceOptions: () => [...secretScanningV2Keys.dataSource(), "options"] as const,
dataSourceById: (dataSourceId: string) => [
...secretScanningV2Keys.dataSource(),
"byId",
dataSourceId
],
listDataSources: (projectId: string) => [...secretScanningV2Keys.dataSource(), "list", projectId],
listResources: (dataSourceId: string) => [
...secretScanningV2Keys.dataSource(),
"resources",
dataSourceId
],
listScans: (dataSourceId: string) => [
...secretScanningV2Keys.dataSource(),
"scans",
dataSourceId
],
finding: () => [...secretScanningV2Keys.all, "finding"] as const,
findingCount: (projectId: string) =>
[...secretScanningV2Keys.finding(), "count", projectId] as const,
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId],
configByProjectId: (projectId: string) =>
[...secretScanningV2Keys.all, "config", projectId] as const
};
export const useSecretScanningDataSourceOptions = (
options?: Omit<
UseQueryOptions<
TSecretScanningDataSourceOption[],
unknown,
TSecretScanningDataSourceOption[],
ReturnType<typeof secretScanningV2Keys.dataSourceOptions>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.dataSourceOptions(),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningDataSourceOptions>(
"/api/v2/secret-scanning/data-sources/options"
);
return data.dataSourceOptions;
},
...options
});
};
export const useListSecretScanningDataSources = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningDataSourceWithDetails[],
unknown,
TSecretScanningDataSourceWithDetails[],
ReturnType<typeof secretScanningV2Keys.listDataSources>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listDataSources(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningDataSources>(
"/api/v2/secret-scanning/data-sources-dashboard",
{ params: { projectId } }
);
return data.dataSources;
},
...options
});
};
export const useGetSecretScanningDataSource = (
{ dataSourceId, type }: TGetSecretScanningDataSource,
options?: Omit<
UseQueryOptions<
TSecretScanningDataSource,
unknown,
TSecretScanningDataSource,
ReturnType<typeof secretScanningV2Keys.dataSourceById>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId),
queryFn: async () => {
const { data } = await apiRequest.get<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`
);
return data.dataSource;
},
...options
});
};
export const useListSecretScanningResources = (
{ dataSourceId, type }: TGetSecretScanningDataSource,
options?: Omit<
UseQueryOptions<
TSecretScanningResourceWithDetails[],
unknown,
TSecretScanningResourceWithDetails[],
ReturnType<typeof secretScanningV2Keys.listResources>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listResources(dataSourceId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningResourcesResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}/resources-dashboard`
);
return data.resources;
},
...options
});
};
export const useListSecretScanningScans = (
{ dataSourceId, type }: TGetSecretScanningDataSource,
options?: Omit<
UseQueryOptions<
TSecretScanningScanWithDetails[],
unknown,
TSecretScanningScanWithDetails[],
ReturnType<typeof secretScanningV2Keys.listScans>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listScans(dataSourceId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningScansResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}/scans-dashboard`
);
return data.scans;
},
...options
});
};
export const useGetSecretScanningUnresolvedFindingCount = (
projectId: string,
options?: Omit<
UseQueryOptions<number, unknown, number, ReturnType<typeof secretScanningV2Keys.findingCount>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.findingCount(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretScanningUnresolvedFindingsResponse>(
"/api/v2/secret-scanning/unresolved-findings-count",
{ params: { projectId } }
);
return data.unresolvedFindings;
},
...options
});
};
export const useListSecretScanningFindings = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningFinding[],
unknown,
TSecretScanningFinding[],
ReturnType<typeof secretScanningV2Keys.listFindings>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listFindings(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningFindingsResponse>(
"/api/v2/secret-scanning/findings",
{ params: { projectId } }
);
return data.findings;
},
...options
});
};
export const useGetSecretScanningConfig = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningConfig,
unknown,
TSecretScanningConfig,
ReturnType<typeof secretScanningV2Keys.configByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.configByProjectId(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
"/api/v2/secret-scanning/configs",
{ params: { projectId } }
);
return data.config;
},
...options
});
};
@@ -0,0 +1,17 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretScanningDataSource } from "../enums";
import { TSecretScanningDataSourceBase } from "./shared";
export type TGitHubDataSource = TSecretScanningDataSourceBase & {
type: SecretScanningDataSource.GitHub;
config: {
includeRepos: string[];
};
};
export type TGitHubDataSourceOption = {
name: string;
type: SecretScanningDataSource.GitHub;
connection: AppConnection.GitHub;
};
@@ -0,0 +1,166 @@
import { DiscriminativePick } from "@app/types";
import {
SecretScanningDataSource,
SecretScanningFindingSeverity,
SecretScanningFindingStatus,
SecretScanningResource,
SecretScanningScanStatus,
SecretScanningScanType
} from "../enums";
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
export type TSecretScanningDataSource = TGitHubDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt: string | null;
lastScanStatus: SecretScanningScanStatus | null;
lastScanStatusMessage: string | null;
unresolvedFindings: number | null;
};
export type TListSecretScanningDataSources = {
dataSources: TSecretScanningDataSourceWithDetails[];
};
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption;
export type TListSecretScanningDataSourceOptions = {
dataSourceOptions: TSecretScanningDataSourceOption[];
};
export type TSecretScanningDataSourceResponse = { dataSource: TSecretScanningDataSource };
export type TCreateSecretScanningDataSourceDTO = DiscriminativePick<
TSecretScanningDataSource,
"name" | "config" | "description" | "connectionId" | "type" | "isAutoScanEnabled" | "projectId"
>;
export type TUpdateSecretScanningDataSourceDTO = Partial<
Omit<TCreateSecretScanningDataSourceDTO, "type" | "connectionId" | "projectId">
> & {
type: SecretScanningDataSource;
dataSourceId: string;
// required for query invalidation
projectId: string;
};
export type TDeleteSecretScanningDataSourceDTO = {
type: SecretScanningDataSource;
dataSourceId: string;
// required for query invalidation
projectId: string;
};
export type TTriggerSecretScanningDataSourceDTO = {
type: SecretScanningDataSource;
dataSourceId: string;
resourceId?: string;
// required for query invalidation
projectId: string;
};
export type TUpdateSecretScanningFinding = {
findingId: string;
status: SecretScanningFindingStatus;
remarks?: string | null;
// required for query invalidation
projectId: string;
};
export type TSecretScanningFindingResponse = {
finding: TSecretScanningFinding;
};
export type TGetSecretScanningDataSource = {
dataSourceId: string;
type: SecretScanningDataSource;
};
export type TSecretScanningResourceWithDetails = {
id: string;
dataSourceId: string;
type: SecretScanningResource;
externalId: string;
name: string;
createdAt: string;
updatedAt: string;
lastScannedAt: string;
lastScanStatus: SecretScanningScanStatus;
lastScanStatusMessage: string | null;
unresolvedFindings: number;
};
export type TListSecretScanningResourcesResponse = {
resources: TSecretScanningResourceWithDetails[];
};
export type TSecretScanningScanWithDetails = {
id: string;
createdAt: string;
resourceId: string;
type: SecretScanningScanType;
status: SecretScanningScanStatus;
statusMessage?: string | null;
unresolvedFindings: number;
resolvedFindings: number;
resourceName: string;
};
export type TListSecretScanningScansResponse = {
scans: TSecretScanningScanWithDetails[];
};
export type TGetSecretScanningUnresolvedFindingsResponse = {
unresolvedFindings: number;
};
export type TSecretScanningConfig = {
projectId: string;
content: string | null;
};
export type TGetSecretScanningConfigResponse = {
config: TSecretScanningConfig;
};
export type TSecretScanningFinding = {
id: string;
dataSourceName: string;
dataSourceType: SecretScanningDataSource;
resourceName: string;
resourceType: SecretScanningResource;
rule: string;
severity: SecretScanningFindingSeverity;
status: SecretScanningFindingStatus;
remarks?: string;
fingerprint: string;
// TODO scott: this will need to be type differentiated once we add other scan types
details: {
description: string;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
file: string;
link: string;
symlinkFile: string;
commit: string;
entropy: string;
author: string;
email: string;
date: string;
message: string;
tags: string[];
ruleID: string;
fingerprint: string;
};
projectId: string;
scanId: string;
createdAt: string;
updatedAt: string;
};
export type TListSecretScanningFindingsResponse = {
findings: TSecretScanningFinding[];
};
@@ -0,0 +1 @@
export * from "./secret-scanning-data-source-base";
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
export type TSecretScanningDataSourceBase = {
id: string;
name: string;
description?: string | null;
connectionId?: string | null;
createdAt: string;
updatedAt: string;
projectId: string;
connection?: {
app: AppConnection;
id: string;
name: string;
} | null;
isAutoScanEnabled: boolean;
isDisconnected: boolean;
};
@@ -50,6 +50,7 @@ export type SubscriptionPlan = {
enforceMfa: boolean;
projectTemplates: boolean;
kmip: boolean;
secretScanning: boolean;
enterpriseSecretSyncs: boolean;
enterpriseAppConnections: boolean;
};
+2 -1
View File
@@ -12,7 +12,8 @@ export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms",
SSH = "ssh"
SSH = "ssh",
SecretScanning = "secret-scanning"
}
export enum ProjectUserMembershipTemporaryMode {
+4 -2
View File
@@ -6,16 +6,18 @@ import { useDebounce } from "@app/hooks/useDebounce";
export const usePagination = <T extends string>(
initialOrderBy: T,
{
initPerPage = 100
initPerPage = 100,
initSearch = ""
}: {
initPerPage?: number;
initSearch?: string;
} = {}
) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(initPerPage);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState("");
const [search, setSearch] = useState(initSearch);
const [debouncedSearch] = useDebounce(search);
const offset = (page - 1) * perPage;
@@ -1,27 +1,23 @@
import { useTranslation } from "react-i18next";
import { faArrowLeft, faInfo, faMobile, faQuestion } from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Outlet, useRouterState } from "@tanstack/react-router";
import { WishForm } from "@app/components/features/WishForm";
import { Banner } from "@app/components/page-frames/Banner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
import { AdminSidebar } from "./Sidebar";
export const AdminLayout = () => {
const { t } = useTranslation();
const { config } = useServerConfig();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
return (
@@ -30,54 +26,11 @@ export const AdminLayout = () => {
<div className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden md:flex`}>
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div className="flex-grow">
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
</div>
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
</aside>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
<AdminSidebar />
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet />
</main>
</div>
@@ -0,0 +1,141 @@
import { faArrowLeft, faInfo, faQuestion } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Menu,
MenuGroup,
MenuItem
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
const generalTabs = [
{
label: "General",
icon: "settings-cog",
link: "/admin/"
},
{
label: "Encryption",
icon: "lock-closed",
link: "/admin/encryption"
},
{
label: "Authentication",
icon: "check",
link: "/admin/authentication"
},
{
label: "Integrations",
icon: "sliding-carousel",
link: "/admin/integrations"
},
{
label: "Caching",
icon: "note",
link: "/admin/caching"
}
];
const resourceTabs = [
{
label: "Organizations",
icon: "groups",
link: "/admin/resources/organizations"
},
{
label: "User Identities",
icon: "user",
link: "/admin/resources/user-identities"
},
{
label: "Machine Identities",
icon: "key-user",
link: "/admin/resources/machine-identities"
}
];
export const AdminSidebar = () => {
const matchRoute = useMatchRoute();
return (
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div className="flex-grow">
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
<Menu>
<MenuGroup title="General">
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
{tab.label}
</MenuItem>
</Link>
);
})}
</MenuGroup>
<MenuGroup title="Resources">
{resourceTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
{tab.label}
</MenuItem>
</Link>
);
})}
</MenuGroup>
</Menu>
</div>
<div className="relative mb-4 mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
</aside>
);
};
@@ -55,16 +55,15 @@ export const OrganizationLayout = () => {
linkOptions({ to: "/organization/kms/overview" }).to,
linkOptions({ to: "/organization/kms/settings" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/ssh/settings" }).to
linkOptions({ to: "/organization/ssh/settings" }).to,
linkOptions({ to: "/organization/secret-scanning/overview" }).to,
linkOptions({ to: "/organization/secret-scanning/settings" }).to
] as string[]
).includes(location.pathname);
const shouldShowOrgSidebar =
location.pathname.startsWith("/organization") &&
(!isSecretSharingPage || shouldShowProductsSidebar) &&
!([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes(
location.pathname
);
(!isSecretSharingPage || shouldShowProductsSidebar);
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";

Some files were not shown because too many files have changed in this diff Show More