Merge remote-tracking branch 'origin/main' into feat/kmip-client-management

This commit is contained in:
Sheen Capadngan
2025-02-18 02:50:12 +08:00
507 changed files with 10002 additions and 2517 deletions
+2 -2
View File
@@ -222,10 +222,10 @@
"org-members-description": "Manage members of your organization. These users could afterwards be formed into projects.",
"search-members": "Search members...",
"add-dialog": {
"add-member-to-project": "Add a member to your project",
"add-member-to-project": "Add users to your project",
"already-all-invited": "All the users in your organization are already invited.",
"add-user-org-first": "Add more users to the organization first.",
"user-will-email": "The user will receive an email with the instructions.",
"user-will-email": "Users will receive an email with instructions to gain access.",
"looking-add": "<0>If you are looking to add users to your org,</0><1>click here</1>",
"add-user-to-org": "Add Users to Organization"
}
File diff suppressed because one or more lines are too long
@@ -57,6 +57,10 @@ export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>
const finalChild =
typeof children === "function" ? children(isAllowed, ability as any) : children;
if (!isAllowed && renderGuardBanner) {
return <ProjectPermissionGuardBanner />;
}
if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>;
}
@@ -65,10 +69,6 @@ export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
}
if (!isAllowed && renderGuardBanner) {
return <ProjectPermissionGuardBanner />;
}
if (!isAllowed) return null;
return finalChild;
@@ -18,7 +18,7 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp
if (!secretSync) return null;
const { id: syncId, name, destination } = secretSync;
const { id: syncId, name, destination, projectId } = secretSync;
const handleDeleteSecretSync = async () => {
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -27,7 +27,8 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp
await deleteSync.mutateAsync({
syncId,
destination,
removeSecrets
removeSecrets,
projectId
});
createNotification({
@@ -37,7 +37,7 @@ const FormSchema = z.object({
type TFormData = z.infer<typeof FormSchema>;
const Content = ({ secretSync, onComplete }: ContentProps) => {
const { id: syncId, destination } = secretSync;
const { id: syncId, destination, projectId } = secretSync;
const destinationName = SECRET_SYNC_MAP[destination].name;
const {
@@ -53,7 +53,8 @@ const Content = ({ secretSync, onComplete }: ContentProps) => {
await triggerImportSecrets.mutateAsync({
syncId,
destination,
importBehavior
importBehavior,
projectId
});
createNotification({
@@ -15,7 +15,7 @@ type ContentProps = {
};
const Content = ({ secretSync, onComplete }: ContentProps) => {
const { id: syncId, destination } = secretSync;
const { id: syncId, destination, projectId } = secretSync;
const destinationName = SECRET_SYNC_MAP[destination].name;
const triggerSyncImport = useTriggerSecretSyncRemoveSecrets();
@@ -24,7 +24,8 @@ const Content = ({ secretSync, onComplete }: ContentProps) => {
try {
await triggerSyncImport.mutateAsync({
syncId,
destination
destination,
projectId
});
createNotification({
@@ -10,9 +10,9 @@ type Props = {
};
export const SecretSyncSelect = ({ onSelect }: Props) => {
const { isLoading, data: secretSyncOptions } = useSecretSyncOptions();
const { isPending, data: secretSyncOptions } = useSecretSyncOptions();
if (isLoading) {
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
@@ -41,7 +41,8 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
syncId: secretSync.id,
...formData,
environment: environment?.slug,
connectionId: connection.id
connectionId: connection.id,
projectId: secretSync.projectId
});
createNotification({
@@ -23,7 +23,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[destination];
const { data: options, isLoading } = useListAvailableAppConnections(app);
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
@@ -54,8 +54,8 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isLoading}
options={options}
isLoading={isPending}
options={availableConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
@@ -65,18 +65,14 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
control={control}
name="connection"
/>
{options?.length === 0 && (
{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 {appName} Connections. Create one from the{" "}
<Link
to="/organization/settings"
className="underline"
search={{ selectedTab: "app-connections" }}
>
Organization Settings
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
@@ -1,30 +1,11 @@
import { Controller, useFormContext } from "react-hook-form";
import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { Badge, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { FormControl, Input } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
{props.data.slug}
</Badge>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};
import { AwsRegionSelect } from "./shared";
export const AwsParameterStoreSyncFields = () => {
const { control } = useFormContext<
@@ -37,17 +18,7 @@ export const AwsParameterStoreSyncFields = () => {
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
<FilterableSelect
value={AWS_REGIONS.find((region) => region.slug === value)}
onChange={(option) =>
onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)
}
options={AWS_REGIONS}
placeholder="Select region..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
components={{ Option }}
/>
<AwsRegionSelect value={value} onChange={onChange} />
</FormControl>
)}
control={control}
@@ -0,0 +1,96 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
import { TSecretSyncForm } from "../schemas";
import { AwsRegionSelect } from "./shared";
export const AwsSecretsManagerSyncFields = () => {
const { control, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const mappingBehavior = watch("destinationConfig.mappingBehavior");
return (
<>
<SecretSyncConnectionField />
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
<AwsRegionSelect value={value} onChange={onChange} />
</FormControl>
)}
control={control}
name="destinationConfig.region"
/>
<Controller
name="destinationConfig.mappingBehavior"
control={control}
defaultValue={AwsSecretsManagerSyncMappingBehavior.OneToOne}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<p>Specify how Infisical should map secrets to AWS Secrets Manager:</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">One-To-One</span>: Each
Infisical secret will be mapped to a separate AWS Secrets Manager secret.
</p>
</li>
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">Many-To-One</span>: All
Infisical secrets will be mapped to a single AWS Secrets Manager secret.
</p>
</li>
</ul>
</div>
}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Mapping Behavior"
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select an option..."
dropdownContainerClassName="max-w-none"
>
{Object.values(AwsSecretsManagerSyncMappingBehavior).map((behavior) => {
return (
<SelectItem className="capitalize" value={behavior} key={behavior}>
{behavior}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="AWS Secrets Manager Secret Name"
>
<Input value={value} onChange={onChange} placeholder="Secret name..." />
</FormControl>
)}
control={control}
name="destinationConfig.secretName"
/>
)}
</>
);
};
@@ -0,0 +1,53 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AzureAppConfigurationSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration }
>();
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.configurationUrl", "");
}}
/>
<Controller
name="destinationConfig.configurationUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Configuration URL"
tooltipText="Enter your Azure App Configuration URL. This is the base URL for your Azure App Configuration, e.g. https://resource-name-here.azconfig.io."
>
<Input {...field} placeholder="https://resource-name-here.azconfig.io" />
</FormControl>
)}
/>
<Controller
name="destinationConfig.label"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Label"
isOptional
tooltipText="Enter the label for the secret in Azure App Configuration."
>
<Input {...field} placeholder="infisical-secret" />
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,47 @@
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AzureKeyVaultSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureKeyVault }
>();
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultBaseUrl", "");
}}
/>
<Controller
name="destinationConfig.vaultBaseUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure Key Vault URL. This is the base URL for your Azure Key Vault, e.g. https://example.vault.azure.net."
>
<Input {...field} placeholder="https://example.vault.azure.net" />
</FormControl>
)}
/>
<div className="mt-4 flex items-center gap-2 text-xs text-yellow-400">
<FontAwesomeIcon icon={faInfoCircle} />
<p>
Secret keys with underscores (_) will be converted to hyphens (-) when syncing to Azure
Key Vault.
</p>
</div>
</>
);
};
@@ -0,0 +1,72 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import {
TDatabricksSecretScope,
useDatabricksConnectionListSecretScopes
} from "@app/hooks/api/appConnections/databricks";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const DatabricksSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Databricks }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: secretScopes = [], isPending: isSecretScopesPending } =
useDatabricksConnectionListSecretScopes(connectionId, {
enabled: Boolean(connectionId)
});
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.scope", "");
}}
/>
<Controller
name="destinationConfig.scope"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Secret Scope"
helperText={
<Tooltip
className="max-w-md"
content="Ensure that you've created the secret scope in the selected workspace, the service principal has been assigned to the respective workspace and that your service principal has write permissions for the specified secret scope."
>
<div>
<span>Don&#39;t see the secret scope you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
isLoading={isSecretScopesPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={secretScopes.find((scope) => scope.name === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TDatabricksSecretScope>)?.name ?? null)
}
options={secretScopes}
placeholder="Select a secret scope..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
</>
);
};
@@ -4,6 +4,10 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
@@ -15,10 +19,18 @@ export const SecretSyncDestinationFields = () => {
switch (destination) {
case SecretSync.AWSParameterStore:
return <AwsParameterStoreSyncFields />;
case SecretSync.AWSSecretsManager:
return <AwsSecretsManagerSyncFields />;
case SecretSync.GitHub:
return <GitHubSyncFields />;
case SecretSync.GCPSecretManager:
return <GcpSyncFields />;
case SecretSync.AzureKeyVault:
return <AzureKeyVaultSyncFields />;
case SecretSync.AzureAppConfiguration:
return <AzureAppConfigurationSyncFields />;
case SecretSync.Databricks:
return <DatabricksSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,41 @@
import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, FilterableSelect } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
{props.data.slug}
</Badge>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};
type Props = {
value: string;
onChange: (value: string | undefined) => void;
};
export const AwsRegionSelect = ({ value, onChange }: Props) => {
return (
<FilterableSelect
value={AWS_REGIONS.find((region) => region.slug === value)}
onChange={(option) => onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)}
options={AWS_REGIONS}
placeholder="Select region..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
components={{ Option }}
/>
);
};
@@ -0,0 +1 @@
export * from "./AwsRegionSelect";
@@ -0,0 +1,39 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
export const AwsSecretsManagerSyncReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const [region, mappingBehavior, secretName] = watch([
"destinationConfig.region",
"destinationConfig.mappingBehavior",
"destinationConfig.secretName"
]);
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
return (
<>
<SecretSyncLabel label="Region">
{awsRegion?.name}
<Badge className="ml-1" variant="success">
{awsRegion?.slug}{" "}
</Badge>
</SecretSyncLabel>
<SecretSyncLabel className="capitalize" label="Mapping Behavior">
{mappingBehavior}
</SecretSyncLabel>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
<SecretSyncLabel label="Secret Name">{secretName}</SecretSyncLabel>
)}
</>
);
};
@@ -0,0 +1,20 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureAppConfigurationSyncReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration }
>();
const vaultBaseUrl = watch("destinationConfig.configurationUrl");
const label = watch("destinationConfig.label");
return (
<>
<SecretSyncLabel label="Configuration URL">{vaultBaseUrl}</SecretSyncLabel>
<SecretSyncLabel label="Label">{label}</SecretSyncLabel>
</>
);
};
@@ -0,0 +1,12 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureKeyVaultSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.AzureKeyVault }>();
const vaultBaseUrl = watch("destinationConfig.vaultBaseUrl");
return <SecretSyncLabel label="Vault URL">{vaultBaseUrl}</SecretSyncLabel>;
};
@@ -0,0 +1,12 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const DatabricksSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Databricks }>();
const scope = watch("destinationConfig.scope");
return <SecretSyncLabel label="Secret Scope">{scope}</SecretSyncLabel>;
};
@@ -3,11 +3,15 @@ import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields";
import { Badge } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
@@ -36,12 +40,24 @@ export const SecretSyncReviewFields = () => {
case SecretSync.AWSParameterStore:
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
break;
case SecretSync.AWSSecretsManager:
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
break;
case SecretSync.GitHub:
DestinationFieldsComponent = <GitHubSyncReviewFields />;
break;
case SecretSync.GCPSecretManager:
DestinationFieldsComponent = <GcpSyncReviewFields />;
break;
case SecretSync.AzureKeyVault:
DestinationFieldsComponent = <AzureKeyVaultSyncReviewFields />;
break;
case SecretSync.AzureAppConfiguration:
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
break;
case SecretSync.Databricks:
DestinationFieldsComponent = <DatabricksSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,30 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
export const AwsSecretsManagerSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.AWSSecretsManager),
destinationConfig: z
.discriminatedUnion("mappingBehavior", [
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
}),
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
secretName: z
.string()
.regex(
/^[a-zA-Z0-9/_+=.@-]+$/,
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
)
.min(1, "Secret name is required")
.max(256, "Secret name cannot exceed 256 characters")
})
])
.and(
z.object({
region: z.string().min(1, "Region required")
})
)
});
@@ -0,0 +1,19 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureAppConfigurationSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.AzureAppConfiguration),
destinationConfig: z.object({
configurationUrl: z
.string()
.trim()
.min(1, { message: "Azure App Configuration URL is required" })
.url()
.refine(
(val) => val.endsWith(".azconfig.io"),
"URL should have the following format: https://resource-name-here.azconfig.io"
),
label: z.string().optional()
})
});
@@ -0,0 +1,10 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureKeyVaultSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.AzureKeyVault),
destinationConfig: z.object({
vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required")
})
});
@@ -0,0 +1,10 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const DatabricksSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.Databricks),
destinationConfig: z.object({
scope: z.string().trim().min(1, "Databricks scope required")
})
});
@@ -1,10 +1,14 @@
import { z } from "zod";
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
const BaseSecretSyncSchema = z.object({
@@ -32,8 +36,12 @@ const BaseSecretSyncSchema = z.object({
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
AwsSecretsManagerSyncDestinationSchema,
GitHubSyncDestinationSchema,
GcpSyncDestinationSchema
GcpSyncDestinationSchema,
AzureKeyVaultSyncDestinationSchema,
AzureAppConfigurationSyncDestinationSchema,
DatabricksSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
@@ -126,7 +126,7 @@ export type TBreadcrumbFormat =
};
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
<div className="mx-auto max-w-7xl py-4 text-white">
<Breadcrumb>
<BreadcrumbList>
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
@@ -1,15 +1,17 @@
import { ReactNode } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
className?: string;
};
export const PageHeader = ({ title, description, children }: Props) => (
<div className="mb-4">
export const PageHeader = ({ title, description, children, className }: Props) => (
<div className={twMerge("mb-4", className)}>
<div className="flex w-full justify-between">
<div>
<div className="w-full">
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
</div>
<div>{children}</div>
+3 -3
View File
@@ -46,9 +46,9 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
),
AppConnections: {
GithubOauthCallbackPage: setRoute(
"/organization/app-connections/github/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
OauthCallbackPage: setRoute(
"/organization/app-connections/$appConnection/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
)
}
},
+14 -1
View File
@@ -4,10 +4,13 @@ import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
AwsConnectionMethod,
AzureAppConfigurationConnectionMethod,
AzureKeyVaultConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
TAppConnection
} from "@app/hooks/api/appConnections/types";
import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection";
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
@@ -15,13 +18,21 @@ export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: st
[AppConnection.GCP]: {
name: "GCP",
image: "Google Cloud Platform.png"
}
},
[AppConnection.AzureKeyVault]: { name: "Azure Key Vault", image: "Microsoft Azure.png" },
[AppConnection.AzureAppConfiguration]: {
name: "Azure App Configuration",
image: "Microsoft Azure.png"
},
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
switch (method) {
case GitHubConnectionMethod.App:
return { name: "GitHub App", icon: faGithub };
case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth:
case GitHubConnectionMethod.OAuth:
return { name: "OAuth", icon: faPassport };
case AwsConnectionMethod.AccessKey:
@@ -30,6 +41,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Assume Role", icon: faUser };
case GcpConnectionMethod.ServiceAccountImpersonation:
return { name: "Service Account Impersonation", icon: faUser };
case DatabricksConnectionMethod.ServicePrincipal:
return { name: "Service Principal", icon: faUser };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}
+17 -3
View File
@@ -6,15 +6,29 @@ import {
} from "@app/hooks/api/secretSyncs";
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
[SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" },
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
[SecretSync.AWSSecretsManager]: { name: "AWS Secrets Manager", image: "Amazon Web Services.png" },
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" },
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" },
[SecretSync.AzureKeyVault]: { name: "Azure Key Vault", image: "Microsoft Azure.png" },
[SecretSync.AzureAppConfiguration]: {
name: "Azure App Configuration",
image: "Microsoft Azure.png"
},
[SecretSync.Databricks]: {
name: "Databricks",
image: "Databricks.png"
}
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP
[SecretSync.GCPSecretManager]: AppConnection.GCP,
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
[SecretSync.Databricks]: AppConnection.Databricks
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import { TDatabricksConnectionListSecretScopesResponse, TDatabricksSecretScope } from "./types";
const databricksConnectionKeys = {
all: [...appConnectionKeys.all, "databricks"] as const,
listSecretScopes: (connectionId: string) =>
[...databricksConnectionKeys.all, "workspace-scopes", connectionId] as const
};
export const useDatabricksConnectionListSecretScopes = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TDatabricksSecretScope[],
unknown,
TDatabricksSecretScope[],
ReturnType<typeof databricksConnectionKeys.listSecretScopes>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: databricksConnectionKeys.listSecretScopes(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TDatabricksConnectionListSecretScopesResponse>(
`/api/v1/app-connections/databricks/${connectionId}/secret-scopes`
);
return data.secretScopes;
},
...options
});
};
@@ -0,0 +1,7 @@
export type TDatabricksSecretScope = {
name: string;
};
export type TDatabricksConnectionListSecretScopesResponse = {
secretScopes: TDatabricksSecretScope[];
};
@@ -1,5 +1,8 @@
export enum AppConnection {
AWS = "aws",
GitHub = "github",
GCP = "gcp"
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks"
}
@@ -20,10 +20,33 @@ export type TGcpConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GCP;
};
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.AzureKeyVault;
oauthClientId?: string;
};
export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.AzureKeyVault;
oauthClientId?: string;
};
export type TDatabricksConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Databricks;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
| TGcpConnectionOption
| TAzureAppConfigurationConnectionOption
| TAzureKeyVaultConnectionOption
| TDatabricksConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
[AppConnection.GitHub]: TGitHubConnectionOption;
[AppConnection.GCP]: TGcpConnectionOption;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
[AppConnection.Databricks]: TDatabricksConnectionOption;
};
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum AzureAppConfigurationConnectionMethod {
OAuth = "oauth"
}
export type TAzureAppConfigurationConnection = TRootAppConnection & {
app: AppConnection.AzureAppConfiguration;
} & {
method: AzureAppConfigurationConnectionMethod.OAuth;
credentials: {
code: string;
tenantId?: string;
};
};
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum AzureKeyVaultConnectionMethod {
OAuth = "oauth"
}
export type TAzureKeyVaultConnection = TRootAppConnection & { app: AppConnection.AzureKeyVault } & {
method: AzureKeyVaultConnectionMethod.OAuth;
credentials: {
code: string;
tenantId?: string;
};
};
@@ -0,0 +1,15 @@
import { AppConnection } from "../enums";
import { TRootAppConnection } from "./root-connection";
export enum DatabricksConnectionMethod {
ServicePrincipal = "service-principal"
}
export type TDatabricksConnection = TRootAppConnection & { app: AppConnection.Databricks } & {
method: DatabricksConnectionMethod.ServicePrincipal;
credentials: {
workspaceUrl: string;
clientId: string;
clientSecret: string;
};
};
@@ -1,17 +1,28 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options";
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
import { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection";
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
import { TGcpConnection } from "./gcp-connection";
export * from "./aws-connection";
export * from "./azure-app-configuration-connection";
export * from "./azure-key-vault-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection;
export type TAppConnection =
| TAwsConnection
| TGitHubConnection
| TGcpConnection
| TAzureKeyVaultConnection
| TAzureAppConfigurationConnection
| TDatabricksConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
@@ -40,4 +51,7 @@ export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GCP]: TGcpConnection;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.Databricks]: TDatabricksConnection;
};
@@ -4,6 +4,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_SECRETS]: "List secrets",
[EventType.GET_SECRET]: "Read secret",
[EventType.DELETE_SECRETS]: "Delete secrets",
[EventType.CREATE_SECRETS]: "Create secrets",
[EventType.UPDATE_SECRETS]: "Update secrets",
[EventType.CREATE_SECRET]: "Create secret",
[EventType.UPDATE_SECRET]: "Update secret",
[EventType.DELETE_SECRET]: "Delete secret",
@@ -90,6 +92,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_CMEK]: "Update KMS key",
[EventType.DELETE_CMEK]: "Delete KMS key",
[EventType.GET_CMEKS]: "List KMS keys",
[EventType.GET_CMEK]: "Get KMS key",
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
@@ -20,6 +20,8 @@ export enum UserAgentType {
export enum EventType {
GET_SECRETS = "get-secrets",
DELETE_SECRETS = "delete-secrets",
CREATE_SECRETS = "create-secrets",
UPDATE_SECRETS = "update-secrets",
GET_SECRET = "get-secret",
CREATE_SECRET = "create-secret",
UPDATE_SECRET = "update-secret",
@@ -105,6 +107,7 @@ export enum EventType {
UPDATE_CMEK = "update-cmek",
DELETE_CMEK = "delete-cmek",
GET_CMEKS = "get-cmeks",
GET_CMEK = "get-cmek",
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt",
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
+5 -1
View File
@@ -1,7 +1,11 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store",
AWSSecretsManager = "aws-secrets-manager",
GitHub = "github",
GCPSecretManager = "gcp-secret-manager"
GCPSecretManager = "gcp-secret-manager",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks"
}
export enum SecretSyncStatus {
@@ -23,7 +23,8 @@ export const useCreateSecretSync = () => {
return data.secretSync;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() })
onSuccess: (_, { projectId }) =>
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) })
});
};
@@ -38,8 +39,8 @@ export const useUpdateSecretSync = () => {
return data.secretSync;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
onSuccess: (_, { syncId, destination, projectId }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
@@ -55,8 +56,8 @@ export const useDeleteSecretSync = () => {
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
onSuccess: (_, { syncId, destination, projectId }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
@@ -72,8 +73,8 @@ export const useTriggerSecretSyncSyncSecrets = () => {
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
onSuccess: (_, { syncId, destination, projectId }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
@@ -93,8 +94,8 @@ export const useTriggerSecretSyncImportSecrets = () => {
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
onSuccess: (_, { syncId, destination, projectId }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
@@ -110,8 +111,8 @@ export const useTriggerSecretSyncRemoveSecrets = () => {
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
onSuccess: (_, { syncId, destination, projectId }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
+11 -9
View File
@@ -12,7 +12,7 @@ import {
export const secretSyncKeys = {
all: ["secret-sync"] as const,
options: () => [...secretSyncKeys.all, "options"] as const,
list: () => [...secretSyncKeys.all, "list"] as const,
list: (projectId: string) => [...secretSyncKeys.all, "list", projectId] as const,
byId: (destination: SecretSync, syncId: string) =>
[...secretSyncKeys.all, destination, "by-id", syncId] as const
};
@@ -46,6 +46,14 @@ export const useSecretSyncOption = (destination: SecretSync) => {
return { syncOption, isPending };
};
export const fetchSecretSyncsByProjectId = async (projectId: string) => {
const { data } = await apiRequest.get<TListSecretSyncs>("/api/v1/secret-syncs", {
params: { projectId }
});
return data.secretSyncs;
};
export const useListSecretSyncs = (
projectId: string,
options?: Omit<
@@ -54,14 +62,8 @@ export const useListSecretSyncs = (
>
) => {
return useQuery({
queryKey: secretSyncKeys.list(),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretSyncs>("/api/v1/secret-syncs", {
params: { projectId }
});
return data.secretSyncs;
},
queryKey: secretSyncKeys.list(projectId),
queryFn: () => fetchSecretSyncsByProjectId(projectId),
...options
});
};
@@ -0,0 +1,26 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TAwsSecretsManagerSync = TRootSecretSync & {
destination: SecretSync.AWSSecretsManager;
destinationConfig:
| {
mappingBehavior: AwsSecretsManagerSyncMappingBehavior.OneToOne;
region: string;
}
| {
region: string;
mappingBehavior: AwsSecretsManagerSyncMappingBehavior.ManyToOne;
secretName: string;
};
connection: {
app: AppConnection.AWS;
name: string;
id: string;
};
};
export enum AwsSecretsManagerSyncMappingBehavior {
OneToOne = "one-to-one",
ManyToOne = "many-to-one"
}
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TAzureAppConfigurationSync = TRootSecretSync & {
destination: SecretSync.AzureAppConfiguration;
destinationConfig: {
configurationUrl: string;
label?: string;
};
connection: {
app: AppConnection.AzureAppConfiguration;
name: string;
id: string;
};
};
@@ -0,0 +1,15 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TAzureKeyVaultSync = TRootSecretSync & {
destination: SecretSync.AzureKeyVault;
destinationConfig: {
vaultBaseUrl: string;
};
connection: {
app: AppConnection.AzureKeyVault;
name: string;
id: string;
};
};
@@ -0,0 +1,15 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TDatabricksSync = TRootSecretSync & {
destination: SecretSync.Databricks;
destinationConfig: {
scope: string;
};
connection: {
app: AppConnection.Databricks;
name: string;
id: string;
};
};
@@ -1,8 +1,12 @@
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync";
import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync";
import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
import { DiscriminativePick } from "@app/types";
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
import { TGcpSync } from "./gcp-sync";
export type TSecretSyncOption = {
@@ -11,7 +15,14 @@ export type TSecretSyncOption = {
canImportSecrets: boolean;
};
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
export type TSecretSync =
| TAwsParameterStoreSync
| TAwsSecretsManagerSync
| TGitHubSync
| TGcpSync
| TAzureKeyVaultSync
| TAzureAppConfigurationSync
| TDatabricksSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -34,26 +45,31 @@ export type TUpdateSecretSyncDTO = Partial<
> & {
destination: SecretSync;
syncId: string;
projectId: string;
};
export type TDeleteSecretSyncDTO = {
destination: SecretSync;
syncId: string;
removeSecrets: boolean;
projectId: string;
};
export type TTriggerSecretSyncSyncSecretsDTO = {
destination: SecretSync;
syncId: string;
projectId: string;
};
export type TTriggerSecretSyncImportSecretsDTO = {
destination: SecretSync;
syncId: string;
importBehavior: SecretSyncImportBehavior;
projectId: string;
};
export type TTriggerSecretSyncRemoveSecretsDTO = {
destination: SecretSync;
syncId: string;
projectId: string;
};
+1 -1
View File
@@ -206,7 +206,7 @@ export const useGetWorkspaceAuthorizations = <TData = IntegrationAuth[],>(
select
});
const fetchWorkspaceIntegrations = async (workspaceId: string) => {
export const fetchWorkspaceIntegrations = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ integrations: TIntegration[] }>(
`/api/v1/workspace/${workspaceId}/integrations`
);
@@ -94,6 +94,13 @@ export const OrganizationLayout = () => {
</MenuItem>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
App Connections
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
@@ -10,6 +10,7 @@ import {
faInfo,
faInfoCircle,
faMoneyBill,
faPlug,
faSignOut,
faUser,
faUsers
@@ -100,6 +101,7 @@ export const MinimizedOrgSidebar = () => {
const isMoreSelected = (
[
linkOptions({ to: "/organization/access-management" }).to,
linkOptions({ to: "/organization/app-connections" }).to,
linkOptions({ to: "/organization/settings" }).to,
linkOptions({ to: "/organization/audit-logs" }).to
] as string[]
@@ -311,36 +313,43 @@ export const MinimizedOrgSidebar = () => {
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="three-ellipsis"
icon="settings-cog"
isSelected={isMoreSelected}
>
More
Org Controls
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" className="p-1">
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUsers} />}>
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
Access Control
</DropdownMenuItem>
</Link>
<Link to="/organization/app-connections">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faPlug} />}>
App Connections
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faMoneyBill} />}>
<DropdownMenuItem
icon={<FontAwesomeIcon className="w-3" icon={faMoneyBill} />}
>
Usage & Billing
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/audit-logs">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faBook} />}>
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faBook} />}>
Audit Logs
</DropdownMenuItem>
</Link>
<Link to="/organization/settings">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faCog} />}>
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
Organization Settings
</DropdownMenuItem>
</Link>
@@ -284,7 +284,8 @@ export const CmekTable = () => {
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group/copy duration:0 invisible relative ml-3 group-hover:visible"
size="xs"
className="group/copy duration:0 invisible relative ml-3 rounded-md group-hover:visible"
onClick={() => {
navigator.clipboard.writeText(id);
setCopyCipherText("Copied");
@@ -299,112 +300,116 @@ export const CmekTable = () => {
<Badge variant={variant}>{label}</Badge>
</Td>
<Td>{version}</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
variant="plain"
colorSchema="primary"
className="ml-4 p-0 data-[state=open]:text-primary-400"
ariaLabel="More options"
>
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotEncryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("encryptData", cmek)}
icon={<FontAwesomeIcon icon={faLock} />}
iconPos="left"
isDisabled={cannotEncryptData || isDisabled}
>
Encrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotDecryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("decryptData", cmek)}
icon={<FontAwesomeIcon icon={faLockOpen} />}
iconPos="left"
isDisabled={cannotDecryptData || isDisabled}
>
Decrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("upsertKey", cmek)}
icon={<FontAwesomeIcon icon={faEdit} />}
iconPos="left"
isDisabled={cannotEditKey}
>
Edit Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handleDisableCmek(cmek)}
icon={
<FontAwesomeIcon icon={isDisabled ? faCheckCircle : faCancel} />
}
iconPos="left"
isDisabled={cannotEditKey}
>
{isDisabled ? "Enable" : "Disable"} Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotDeleteKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteKey", cmek)}
icon={<FontAwesomeIcon icon={faTrash} />}
iconPos="left"
isDisabled={cannotDeleteKey}
>
Delete Key
</DropdownMenuItem>
</div>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
<Td>
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
variant="plain"
colorSchema="primary"
className="ml-4 p-0 data-[state=open]:text-primary-400"
ariaLabel="More options"
>
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotEncryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("encryptData", cmek)}
icon={<FontAwesomeIcon icon={faLock} />}
iconPos="left"
isDisabled={cannotEncryptData || isDisabled}
>
Encrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotDecryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("decryptData", cmek)}
icon={<FontAwesomeIcon icon={faLockOpen} />}
iconPos="left"
isDisabled={cannotDecryptData || isDisabled}
>
Decrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("upsertKey", cmek)}
icon={<FontAwesomeIcon icon={faEdit} />}
iconPos="left"
isDisabled={cannotEditKey}
>
Edit Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handleDisableCmek(cmek)}
icon={
<FontAwesomeIcon
icon={isDisabled ? faCheckCircle : faCancel}
/>
}
iconPos="left"
isDisabled={cannotEditKey}
>
{isDisabled ? "Enable" : "Disable"} Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotDeleteKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteKey", cmek)}
icon={<FontAwesomeIcon icon={faTrash} />}
iconPos="left"
isDisabled={cannotDeleteKey}
>
Delete Key
</DropdownMenuItem>
</div>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
@@ -0,0 +1,90 @@
import { Helmet } from "react-helmet";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, PageHeader } from "@app/components/v2";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
AddAppConnectionModal,
AppConnectionsTable
} from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
export const AppConnectionsPage = withPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
return (
<div className="bg-bunker-800">
<Helmet>
<title>Infisical | App Connections</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title={
<div className="flex w-full items-center">
<span>App Connections</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Create}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addConnection");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Connection
</Button>
)}
</OrgPermissionCan>
</div>
}
description="Create and configure connections with third-party apps for re-use across Infisical projects"
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<AppConnectionsTable />
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
/>
</div>
</div>
</div>
</div>
);
},
{
action: OrgPermissionAppConnectionActions.Read,
subject: OrgPermissionSubjects.AppConnections
}
);
@@ -10,6 +10,9 @@ import { DiscriminativePick } from "@app/types";
import { AppConnectionHeader } from "../AppConnectionHeader";
import { AwsConnectionForm } from "./AwsConnectionForm";
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
@@ -53,6 +56,12 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <GitHubConnectionForm />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm />;
case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -92,6 +101,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <GitHubConnectionForm appConnection={appConnection} />;
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm appConnection={appConnection} />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm appConnection={appConnection} />;
case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,178 @@
import crypto from "crypto";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureAppConfigurationConnectionMethod,
TAzureAppConfigurationConnection,
useGetAppConnectionOption
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TAzureAppConfigurationConnection;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureAppConfiguration),
method: z.nativeEnum(AzureAppConfigurationConnectionMethod),
tenantId: z.string().trim().optional()
});
type FormData = z.infer<typeof formSchema>;
export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
const {
option: { oauthClientId },
isLoading
} = useGetAppConnectionOption(AppConnection.AzureAppConfiguration);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection
? {
...appConnection,
tenantId: appConnection.credentials.tenantId
}
: {
app: AppConnection.AzureAppConfiguration,
method: AzureAppConfigurationConnectionMethod.OAuth
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case AzureAppConfigurationConnectionMethod.OAuth:
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration`
);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
};
let isMissingConfig: boolean;
switch (selectedMethod) {
case AzureAppConfigurationConnectionMethod.OAuth:
isMissingConfig = !oauthClientId;
break;
default:
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.AzureAppConfiguration].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See documentation to configure Azure ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(AzureAppConfigurationConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to Azure" : "Connect to Azure"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,178 @@
import crypto from "crypto";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
AzureKeyVaultConnectionMethod,
TAzureKeyVaultConnection
} from "@app/hooks/api/appConnections/types/azure-key-vault-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TAzureKeyVaultConnection;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureKeyVault),
method: z.nativeEnum(AzureKeyVaultConnectionMethod),
tenantId: z.string().trim().optional()
});
type FormData = z.infer<typeof formSchema>;
export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
const {
option: { oauthClientId },
isLoading
} = useGetAppConnectionOption(AppConnection.AzureKeyVault);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection
? {
...appConnection,
tenantId: appConnection.credentials.tenantId
}
: {
app: AppConnection.AzureKeyVault,
method: AzureKeyVaultConnectionMethod.OAuth
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case AzureKeyVaultConnectionMethod.OAuth:
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
};
let isMissingConfig: boolean;
switch (selectedMethod) {
case AzureKeyVaultConnectionMethod.OAuth:
isMissingConfig = !oauthClientId;
break;
default:
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.AzureKeyVault].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See documentation to configure Azure ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(AzureKeyVaultConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to Azure" : "Connect to Azure"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,166 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
DatabricksConnectionMethod,
TDatabricksConnection
} from "@app/hooks/api/appConnections/types/databricks-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TDatabricksConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Databricks)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(DatabricksConnectionMethod.ServicePrincipal),
credentials: z.object({
workspaceUrl: z.string().url().trim().min(1, "Workspace URL required"),
clientId: z.string().trim().min(1, "Client ID required"),
clientSecret: z.string().trim().min(1, "Client secret required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const DatabricksConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Databricks,
method: DatabricksConnectionMethod.ServicePrincipal
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Databricks].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(DatabricksConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.workspaceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Workspace URL"
>
<Input {...field} placeholder="https://xxx-xxxxxxxx-xxx.cloud.databricks.com" />
</FormControl>
)}
/>
<Controller
name="credentials.clientId"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Client ID"
>
<Input {...field} placeholder="05810c8f-c44d-4bd0-a327-aab6dd77719b" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Client Secret"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Databricks"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -11,9 +11,9 @@ type Props = {
};
export const AppConnectionsSelect = ({ onSelect }: Props) => {
const { isLoading, data: appConnectionOptions } = useAppConnectionOptions();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions();
if (isLoading) {
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
@@ -23,7 +23,7 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
}
return (
<div className="grid grid-cols-5 gap-2">
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => (
<button
type="button"
@@ -51,7 +51,7 @@ type AppConnectionFilters = {
};
export const AppConnectionsTable = () => {
const { isLoading, data: appConnections = [] } = useListAppConnections();
const { isPending, data: appConnections = [] } = useListAppConnections();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteConnection",
@@ -159,7 +159,7 @@ export const AppConnectionsTable = () => {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search integrations..."
placeholder="Search connections..."
className="flex-1"
/>
<DropdownMenu>
@@ -262,7 +262,7 @@ export const AppConnectionsTable = () => {
</Tr>
</THead>
<TBody>
{isLoading && (
{isPending && (
<TableSkeleton innerKey="app-connections-table" columns={4} key="app-connections" />
)}
{filteredAppConnections.slice(offset, perPage * page).map((connection) => (
@@ -285,7 +285,7 @@ export const AppConnectionsTable = () => {
onChangePerPage={setPerPage}
/>
)}
{!isLoading && !filteredAppConnections?.length && (
{!isPending && !filteredAppConnections?.length && (
<EmptyState
title={
appConnections.length
@@ -44,7 +44,7 @@ export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection }
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
deleteKey="confirm"
deleteKey={name}
onDeleteApproved={handleDeleteAppConnection}
/>
);
@@ -0,0 +1,23 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AppConnectionsPage } from "./AppConnectionsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/"
)({
component: AppConnectionsPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "App Connections"
}
]
})
});
@@ -1,123 +0,0 @@
import { useEffect } from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ContentLoader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
GitHubConnectionMethod,
TGitHubConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type FormData = Pick<TGitHubConnection, "name" | "method" | "description"> & {
returnUrl?: string;
connectionId?: string;
};
export const GitHubOAuthCallbackPage = () => {
const navigate = useNavigate();
const search = useSearch({
from: ROUTE_PATHS.Organization.AppConnections.GithubOauthCallbackPage.id
});
const updateAppConnection = useUpdateAppConnection();
const createAppConnection = useCreateAppConnection();
const { code, state, installation_id: installationId } = search;
useEffect(() => {
(async () => {
let formData: FormData;
try {
formData = JSON.parse(localStorage.getItem("githubConnectionFormData") ?? "{}") as FormData;
} catch {
createNotification({
type: "error",
text: "Invalid form state, redirecting..."
});
navigate({ to: "/" });
return;
}
// validate state
if (state !== localStorage.getItem("latestCSRFToken")) {
return;
}
localStorage.removeItem("githubConnectionFormData");
localStorage.removeItem("latestCSRFToken");
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.GitHub,
...(installationId
? {
connectionId,
credentials: {
code: code as string,
installationId: installationId as string
}
}
: {
connectionId,
credentials: {
code: code as string
}
})
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
...(installationId
? {
method: GitHubConnectionMethod.App,
credentials: {
code: code as string,
installationId: installationId as string
}
}
: {
method: GitHubConnectionMethod.OAuth,
credentials: {
code: code as string
}
})
});
}
} catch (e: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`,
text: e.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/settings?selectedTab=app-connections"
});
return;
}
createNotification({
text: `Successfully ${connectionId ? "updated" : "added"} GitHub Connection`,
type: "success"
});
navigate({
to: returnUrl ?? "/organization/settings?selectedTab=app-connections"
});
})();
}, []);
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader text="Please wait! Authentication in process." />
</div>
);
};
@@ -0,0 +1,308 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ContentLoader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import {
AzureAppConfigurationConnectionMethod,
AzureKeyVaultConnectionMethod,
GitHubConnectionMethod,
TAzureAppConfigurationConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type BaseFormData = {
returnUrl?: string;
connectionId?: string;
};
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
};
};
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData"
};
export const OAuthCallbackPage = () => {
const navigate = useNavigate();
const [isReady, setIsReady] = useState(false);
const search = useSearch({
from: ROUTE_PATHS.Organization.AppConnections.OauthCallbackPage.id
});
const rawAppConnection = useParams({
strict: false,
select: (el) => el?.appConnection as AppConnection
});
const updateAppConnection = useUpdateAppConnection();
const createAppConnection = useCreateAppConnection();
const { code, state: rawState, installation_id: installationId } = search;
const state = rawState.includes("<:>") ? rawState.split("<:>")[0] : rawState;
const appConnection = rawState.includes("<:>") ? rawState.split("<:>")[1] : rawAppConnection;
const clearState = (app: AppConnection) => {
if (state !== localStorage.getItem("latestCSRFToken")) {
throw new Error("Invalid CSRF token");
}
const dataFieldName = formDataStorageFieldMap[app];
localStorage.removeItem(dataFieldName!);
localStorage.removeItem("latestCSRFToken");
};
const getFormData = <T extends keyof FormDataMap>(app: T): FormDataMap[T] | null => {
const dataFieldName = formDataStorageFieldMap[app];
try {
const rawData = JSON.parse(localStorage.getItem(dataFieldName!) ?? "{}");
return {
...rawData,
app
} as FormDataMap[T];
} catch {
createNotification({
type: "error",
text: `Invalid ${app || ""} form state, redirecting...`
});
navigate({ to: "/" });
return null;
}
};
const handleAzureKeyVault = useCallback(async () => {
const formData = getFormData(AppConnection.AzureKeyVault);
if (formData === null) return null;
clearState(AppConnection.AzureKeyVault);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
connectionId,
credentials: {
code: code as string,
tenantId: formData.tenantId
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
name,
description,
method: AzureKeyVaultConnectionMethod.OAuth,
credentials: {
tenantId: formData.tenantId,
code: code as string
}
});
}
} catch (err: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} Azure Key Vault Connection`,
text: err?.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
const handleAzureAppConfiguration = useCallback(async () => {
const formData = getFormData(AppConnection.AzureAppConfiguration);
if (formData === null) return null;
clearState(AppConnection.AzureAppConfiguration);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
connectionId,
credentials: {
code: code as string,
tenantId: formData.tenantId
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
name,
description,
method: AzureAppConfigurationConnectionMethod.OAuth,
credentials: {
code: code as string,
tenantId: formData.tenantId
}
});
}
} catch (err: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} Azure App Configuration Connection`,
text: err?.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
const handleGithub = useCallback(async () => {
const formData = getFormData(AppConnection.GitHub);
if (formData === null) return null;
clearState(AppConnection.GitHub);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.GitHub,
...(installationId
? {
connectionId,
credentials: {
code: code as string,
installationId: installationId as string
}
}
: {
connectionId,
credentials: {
code: code as string
}
})
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
...(installationId
? {
method: GitHubConnectionMethod.App,
credentials: {
code: code as string,
installationId: installationId as string
}
}
: {
method: GitHubConnectionMethod.OAuth,
credentials: {
code: code as string
}
})
});
}
} catch (e: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`,
text: e.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
useEffect(() => {
if (!isReady) {
setIsReady(!!localStorage.length);
}
}, [localStorage.length]);
useEffect(() => {
if (!isReady) return;
(async () => {
let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null =
null;
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();
} else if (appConnection === AppConnection.AzureAppConfiguration) {
data = await handleAzureAppConfiguration();
}
if (data) {
createNotification({
text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`,
type: "success"
});
} else {
createNotification({
text: "Failed to add connection",
type: "error"
});
}
await navigate({
to: data?.returnUrl ?? "/organization/app-connections"
});
})();
}, [isReady]);
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader text="Please wait! Authentication in process." />
</div>
);
};
@@ -2,7 +2,7 @@ import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { GitHubOAuthCallbackPage } from "./GithubOauthCallbackPage";
import { OAuthCallbackPage } from "./OauthCallbackPage";
const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
code: z.coerce.string().catch(""),
@@ -11,9 +11,9 @@ const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
)({
component: GitHubOAuthCallbackPage,
component: OAuthCallbackPage,
validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema),
search: {
middlewares: [stripSearchParams({ state: "", installation_id: "" })]
@@ -1,77 +0,0 @@
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { AddAppConnectionModal, AppConnectionsTable } from "./components";
export const AppConnectionsTab = withPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
return (
<div>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-semibold text-mineshaft-100">App Connections</p>
<a
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Create and configure connections with third-party apps for re-use across Infisical
projects
</p>
</div>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Create}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addConnection");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Connection
</Button>
)}
</OrgPermissionCan>
</div>
<AppConnectionsTable />
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
/>
</div>
</div>
);
},
{
action: OrgPermissionAppConnectionActions.Read,
subject: OrgPermissionSubjects.AppConnections
}
);
@@ -1 +0,0 @@
export { AppConnectionsTab } from "./AppConnectionsTab";
@@ -4,7 +4,6 @@ import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { AppConnectionsTab } from "../AppConnectionsTab";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { ImportTab } from "../ImportTab";
import { KmipTab } from "../KmipTab/OrgKmipTab";
@@ -27,7 +26,6 @@ export const OrgTabGroup = () => {
key: "workflow-integrations",
component: OrgWorkflowIntegrationTab
},
{ name: "App Connections", key: "app-connections", component: AppConnectionsTab },
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
{ name: "Import", key: "tab-import", component: ImportTab },
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab },
@@ -1,13 +1,19 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission,
useWorkspace
} from "@app/context";
import {
useAddUsersToOrg,
useGetOrgUsers,
@@ -19,7 +25,15 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({
orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
orgMemberships: z
.array(
z.object({
label: z.string().trim(),
value: z.string().trim(),
isNewInvitee: z.boolean().optional()
})
)
.min(1),
projectRoleSlugs: z.array(z.object({ slug: z.string().trim(), name: z.string().trim() })).min(1)
});
@@ -34,6 +48,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { permission } = useOrgPermission();
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
@@ -60,7 +75,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
if (!currentWorkspace) return;
if (!currentOrg?.id) return;
const selectedMembers = orgMemberships.map((orgMembership) =>
const existingMembers = orgMemberships.filter((membership) => !membership.isNewInvitee);
const newInvitees = orgMemberships
.filter((membership) => membership.isNewInvitee)
.map((membership) => membership.value);
const selectedMembers = existingMembers.map((orgMembership) =>
orgUsers?.find((orgUser) => orgUser.id === orgMembership.value)
);
@@ -76,11 +96,11 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const inviteeEmails = selectedMembers
.map((member) => member?.user.username as string)
.filter(Boolean);
if (inviteeEmails.length) {
if (inviteeEmails.length || newInvitees.length) {
await addMembersToProject({
inviteeEmails,
inviteeEmails: [...inviteeEmails, ...newInvitees],
organizationId: orgId,
organizationRoleSlug: ProjectMembershipRole.Member, // ? This doesn't apply in this case, because we know the users being added are already part of the organization
organizationRoleSlug: ProjectMembershipRole.Member, // only applies to new invites
projects: [
{
slug: currentWorkspace.slug,
@@ -125,6 +145,13 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const selectedOrgMemberships = watch("orgMemberships");
const selectedRoleSlugs = watch("projectRoleSlugs");
const { append } = useFieldArray<TAddMemberForm>({ control, name: "orgMemberships" });
const canInviteNewMembers = permission.can(
OrgPermissionActions.Create,
OrgPermissionSubjects.Member
);
return (
<Modal
isOpen={popUp?.addMember?.isOpen}
@@ -135,19 +162,70 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
title={t("section.members.add-dialog.add-member-to-project") as string}
subTitle={t("section.members.add-dialog.user-will-email")}
>
{filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMembers)}>
<div className="flex w-full flex-col items-start gap-2">
<Controller
control={control}
name="orgMemberships"
render={({ field }) => (
<FormControl
className="w-full"
isError={!!errors.orgMemberships?.length}
errorText={errors.orgMemberships?.[0]?.message}
label="Invite users to project"
>
<form onSubmit={handleSubmit(onAddMembers)}>
<div className="flex w-full flex-col items-start gap-2">
<Controller
control={control}
name="orgMemberships"
render={({ field }) => (
<FormControl
className="w-full"
isError={!!errors.orgMemberships?.length}
errorText={errors.orgMemberships?.[0]?.message}
label="Invite users to project"
helperText={
canInviteNewMembers
? "You can invite new users to your organization by typing out their email address"
: undefined
}
>
{canInviteNewMembers ? (
<CreatableSelect
/* eslint-disable-next-line react/no-unstable-nested-components */
noOptionsMessage={() => (
<>
<p>
{!filteredOrgUsers.length && (
<p>All organization members are already assigned to this project.</p>
)}
</p>
<p>
Invite new users to your organization by typing out their email address.
</p>
</>
)}
onCreateOption={(inputValue) =>
append({ label: inputValue, value: inputValue, isNewInvitee: true })
}
formatCreateLabel={(inputValue) => `Invite "${inputValue}"`}
isValidNewOption={(input) =>
Boolean(input) &&
z.string().email().safeParse(input).success &&
!orgUsers
?.flatMap(({ user }) => {
const emails: string[] = [];
if (user.email) {
emails.push(user.email);
}
if (user.username) {
emails.push(user.username);
}
return emails;
})
.includes(input)
}
className="w-full"
placeholder="Add one or more users..."
isMulti
name="members"
options={filteredOrgUsers}
value={field.value}
onChange={field.onChange}
/>
) : (
<FilterableSelect
className="w-full"
placeholder="Add one or more users..."
@@ -157,65 +235,58 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
value={field.value}
onChange={field.onChange}
/>
</FormControl>
)}
/>
)}
</FormControl>
)}
/>
<Controller
control={control}
name="projectRoleSlugs"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
errorText={error?.message}
isError={Boolean(error)}
>
<FilterableSelect
options={roles}
placeholder="Select roles..."
value={value}
onChange={onChange}
isMulti
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={
isSubmitting ||
selectedOrgMemberships.length === 0 ||
selectedRoleSlugs.length === 0
}
>
Add Members
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("addMember", false)}
>
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div>All the users in your organization are already invited.</div>
<Link to={"/organization/access-management" as const}>
<Button variant="outline_bg">Add users to organization</Button>
</Link>
<Controller
control={control}
name="projectRoleSlugs"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
errorText={error?.message}
isError={Boolean(error)}
>
<FilterableSelect
options={roles}
placeholder="Select roles..."
value={value}
onChange={onChange}
isMulti
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</div>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={
isSubmitting ||
selectedOrgMemberships.length === 0 ||
selectedRoleSlugs.length === 0
}
>
Add Members
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("addMember", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
@@ -2,12 +2,15 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useParams } from "@tanstack/react-router";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
Checkbox,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -18,8 +21,10 @@ import {
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetIntegration } from "@app/hooks/api";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { useDeleteIntegration, useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { IntegrationAuditLogsSection } from "./components/IntegrationAuditLogsSection";
import { IntegrationConnectionSection } from "./components/IntegrationConnectionSection";
@@ -38,9 +43,44 @@ export const IntegrationDetailsByIDPage = () => {
refetchInterval: 4000
});
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { mutateAsync: syncIntegration } = useSyncIntegration();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const navigate = useNavigate();
const handleIntegrationDelete = async (shouldDeleteIntegrationSecrets: boolean) => {
try {
await deleteIntegration({
id: integrationId,
workspaceId: currentWorkspace.id,
shouldDeleteIntegrationSecrets
});
createNotification({
type: "success",
text: "Deleted integration"
});
await navigate({
to: `/${ProjectType.SecretManager}/${projectId}/integrations`
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete integration"
});
}
};
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation",
"deleteSecretsConfirmation"
] as const);
return (
<>
@@ -90,7 +130,10 @@ export const IntegrationDetailsByIDPage = () => {
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {}}
onClick={() => {
setShouldDeleteSecrets.off();
handlePopUpOpen("deleteConfirmation", integration);
}}
disabled={!isAllowed}
>
<div className="flex items-center gap-2">
@@ -120,6 +163,57 @@ export const IntegrationDetailsByIDPage = () => {
</div>
)}
</div>
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to remove ${integration?.integration || " "} integration for ${
integration?.app || "this project"
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={
(integration?.integration === "azure-app-configuration" &&
integration?.app?.split("//")[1]?.split(".")[0]) ||
integration?.app ||
integration?.owner ||
integration?.path ||
integration?.integration ||
""
}
onDeleteApproved={async () => {
if (shouldDeleteSecrets) {
handlePopUpOpen("deleteSecretsConfirmation");
return;
}
await handleIntegrationDelete(false);
handlePopUpClose("deleteConfirmation");
}}
>
{integration?.integration === "github" && (
<div className="mt-4">
<Checkbox
id="delete-integration-secrets"
checkIndicatorBg="text-white"
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
>
Delete previously synced secrets from the destination
</Checkbox>
</div>
)}
</DeleteActionModal>
<DeleteActionModal
isOpen={popUp.deleteSecretsConfirmation.isOpen}
title={`Are you sure you also want to delete secrets on ${integration?.integration}?`}
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await handleIntegrationDelete(true);
handlePopUpClose("deleteSecretsConfirmation");
handlePopUpClose("deleteConfirmation");
}}
/>
</>
);
};
@@ -1,5 +1,7 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage";
export const Route = createFileRoute(
@@ -14,7 +16,10 @@ export const Route = createFileRoute(
label: "Integrations",
link: linkOptions({
to: "/secret-manager/$projectId/integrations",
params
params,
search: {
selectedTab: IntegrationsListPageTabs.NativeIntegrations
}
})
},
{
@@ -8,6 +8,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import {
@@ -29,7 +30,7 @@ export const IntegrationsListPage = () => {
const updateSelectedTab = (tab: string) => {
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
search: (prev) => ({ ...prev, selectedTab: tab }),
search: (prev) => ({ ...prev, selectedTab: tab as IntegrationsListPageTabs }),
params: {
projectId: currentWorkspace.id
}
@@ -52,7 +53,7 @@ export const IntegrationsListPage = () => {
/>
<div className="mb-4 mt-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
Integrations Update
</div>
<p className="mb-2 mt-1 text-sm text-bunker-300">
@@ -100,8 +101,7 @@ export const IntegrationsListPage = () => {
<TabPanel value={IntegrationsListPageTabs.SecretSyncs}>
<ProjectPermissionCan
renderGuardBanner
passThrough={false}
I={ProjectPermissionActions.Read}
I={ProjectPermissionSecretSyncActions.Read}
a={ProjectPermissionSub.SecretSyncs}
>
<SecretSyncsTab />
@@ -110,7 +110,6 @@ export const IntegrationsListPage = () => {
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
<ProjectPermissionCan
renderGuardBanner
passThrough={false}
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Integrations}
>
@@ -0,0 +1,43 @@
import {
AwsSecretsManagerSyncMappingBehavior,
TAwsSecretsManagerSync
} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell, SecretSyncTableCellProps } from "../SecretSyncTableCell";
type Props = {
secretSync: TAwsSecretsManagerSync;
};
export const AwsSecretsManagerSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
const { destinationConfig } = secretSync;
let additionalProps: Pick<
SecretSyncTableCellProps,
"additionalTooltipContent" | "infoBadge" | "secondaryClassName"
> = {};
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne) {
additionalProps = {
infoBadge: "secondary",
additionalTooltipContent: (
<div className="mt-4">
<span className="text-xs text-bunker-300">Secret Name:</span>
<p className="text-sm">{destinationConfig.secretName}</p>
</div>
)
};
}
return (
<SecretSyncTableCell
{...additionalProps}
secondaryClassName="capitalize"
primaryText={primaryText}
secondaryText={secondaryText}
/>
);
};
@@ -0,0 +1,14 @@
import { TAzureAppConfigurationSync } from "@app/hooks/api/secretSyncs/types/azure-app-configuration-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TAzureAppConfigurationSync;
};
export const AzureAppConfigurationDestinationSyncCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -0,0 +1,14 @@
import { TAzureKeyVaultSync } from "@app/hooks/api/secretSyncs/types/azure-key-vault-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TAzureKeyVaultSync;
};
export const AzureKeyVaultDestinationSyncCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -0,0 +1,14 @@
import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TDatabricksSync;
};
export const DatabricksSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText="Secret Scope" />;
};
@@ -1,6 +1,10 @@
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol";
import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol";
import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol";
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
@@ -12,10 +16,18 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
case SecretSync.AWSSecretsManager:
return <AwsSecretsManagerSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GitHub:
return <GitHubSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GCPSecretManager:
return <GcpSyncDestinationCol secretSync={secretSync} />;
case SecretSync.AzureKeyVault:
return <AzureKeyVaultDestinationSyncCol secretSync={secretSync} />;
case SecretSync.AzureAppConfiguration:
return <AzureAppConfigurationDestinationSyncCol secretSync={secretSync} />;
case SecretSync.Databricks:
return <DatabricksSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -208,7 +208,9 @@ export const SecretSyncRow = ({
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">Failure Reason</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">
{failureMessage}
</div>
</div>
)}
</div>
@@ -30,7 +30,11 @@ export const SecretSyncTableCell = ({
content={
<>
<p className="text-sm">{primaryText}</p>
{secondaryText && <p className="text-xs leading-3 text-bunker-300">{secondaryText}</p>}
{secondaryText && (
<p className={twMerge("text-xs leading-3 text-bunker-300", secondaryClassName)}>
{secondaryText}
</p>
)}
{additionalTooltipContent}
</>
}
@@ -227,7 +227,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
await updateSync.mutateAsync({
syncId: secretSync.id,
destination: secretSync.destination,
isAutoSyncEnabled
isAutoSyncEnabled,
projectId: secretSync.projectId
});
createNotification({
@@ -248,7 +249,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
try {
await triggerSync.mutateAsync({
syncId: secretSync.id,
destination: secretSync.destination
destination: secretSync.destination,
projectId: secretSync.projectId
});
createNotification({
@@ -17,6 +17,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.path;
secondaryText = destinationConfig.region;
break;
case SecretSync.AWSSecretsManager:
primaryText = destinationConfig.region;
secondaryText = destinationConfig.mappingBehavior;
break;
case SecretSync.GitHub:
switch (destinationConfig.scope) {
case GitHubSyncScope.Organization:
@@ -43,6 +47,18 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.projectId;
secondaryText = "Global";
break;
case SecretSync.AzureKeyVault:
primaryText = destinationConfig.vaultBaseUrl;
break;
case SecretSync.AzureAppConfiguration:
primaryText = destinationConfig.configurationUrl;
if (destinationConfig.label) {
secondaryText = `Label - ${destinationConfig.label}`;
}
break;
case SecretSync.Databricks:
primaryText = destinationConfig.scope;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -1,15 +1,21 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { workspaceKeys } from "@app/hooks/api";
import { TIntegration } from "@app/hooks/api/integrations/types";
import {
fetchSecretSyncsByProjectId,
secretSyncKeys,
TSecretSync
} from "@app/hooks/api/secretSyncs";
import { fetchWorkspaceIntegrations } from "@app/hooks/api/workspace/queries";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { IntegrationsListPage } from "./IntegrationsListPage";
const IntegrationsListPageQuerySchema = z.object({
selectedTab: z
.nativeEnum(IntegrationsListPageTabs)
.catch(IntegrationsListPageTabs.NativeIntegrations)
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional()
});
export const Route = createFileRoute(
@@ -17,7 +23,70 @@ export const Route = createFileRoute(
)({
component: IntegrationsListPage,
validateSearch: zodValidator(IntegrationsListPageQuerySchema),
beforeLoad: ({ context }) => {
beforeLoad: async ({ context, search, params: { projectId } }) => {
if (!search.selectedTab) {
let secretSyncs: TSecretSync[];
try {
secretSyncs = await context.queryClient.ensureQueryData({
queryKey: secretSyncKeys.list(projectId),
queryFn: () => fetchSecretSyncsByProjectId(projectId)
});
} catch {
throw redirect({
to: "/secret-manager/$projectId/integrations",
params: {
projectId
},
search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations }
});
}
if (secretSyncs.length) {
throw redirect({
to: "/secret-manager/$projectId/integrations",
params: {
projectId
},
search: { selectedTab: IntegrationsListPageTabs.SecretSyncs }
});
}
let integrations: TIntegration[];
try {
integrations = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceIntegrations(projectId),
queryFn: () => fetchWorkspaceIntegrations(projectId)
});
} catch {
throw redirect({
to: "/secret-manager/$projectId/integrations",
params: {
projectId
},
search: { selectedTab: IntegrationsListPageTabs.SecretSyncs }
});
}
if (integrations.length) {
throw redirect({
to: "/secret-manager/$projectId/integrations",
params: {
projectId
},
search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations }
});
}
throw redirect({
to: "/secret-manager/$projectId/integrations",
params: {
projectId
},
search: { selectedTab: IntegrationsListPageTabs.SecretSyncs }
});
}
return {
breadcrumbs: [
...context.breadcrumbs,
@@ -92,7 +92,8 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
await updateSync.mutateAsync({
syncId: secretSync.id,
destination: secretSync.destination,
isAutoSyncEnabled
isAutoSyncEnabled,
projectId: secretSync.projectId
});
createNotification({
@@ -111,7 +112,8 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
try {
await triggerSyncSecrets.mutateAsync({
syncId: secretSync.id,
destination: secretSync.destination
destination: secretSync.destination,
projectId: secretSync.projectId
});
createNotification({
@@ -0,0 +1,34 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { Badge } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import {
AwsSecretsManagerSyncMappingBehavior,
TAwsSecretsManagerSync
} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
type Props = {
secretSync: TAwsSecretsManagerSync;
};
export const AwsSecretsManagerSyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
const awsRegion = AWS_REGIONS.find((r) => r.slug === destinationConfig.region);
return (
<>
<SecretSyncLabel label="Region">
{awsRegion?.name}
<Badge className="ml-1" variant="success">
{awsRegion?.slug}{" "}
</Badge>
</SecretSyncLabel>
<SecretSyncLabel label="Mapping Behavior" className="capitalize">
{destinationConfig.mappingBehavior}
</SecretSyncLabel>
{destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
<SecretSyncLabel label="Secret Name">{destinationConfig.secretName}</SecretSyncLabel>
)}
</>
);
};
@@ -0,0 +1,19 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TAzureAppConfigurationSync } from "@app/hooks/api/secretSyncs/types/azure-app-configuration-sync";
type Props = {
secretSync: TAzureAppConfigurationSync;
};
export const AzureAppConfigurationSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { configurationUrl, label }
} = secretSync;
return (
<>
<SecretSyncLabel label="Configuration URL">{configurationUrl}</SecretSyncLabel>
<SecretSyncLabel label="Label">{label}</SecretSyncLabel>
</>
);
};
@@ -0,0 +1,14 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TAzureKeyVaultSync } from "@app/hooks/api/secretSyncs/types/azure-key-vault-sync";
type Props = {
secretSync: TAzureKeyVaultSync;
};
export const AzureKeyVaultSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { vaultBaseUrl }
} = secretSync;
return <SecretSyncLabel label="Vault URL">{vaultBaseUrl}</SecretSyncLabel>;
};
@@ -0,0 +1,14 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync";
type Props = {
secretSync: TDatabricksSync;
};
export const DatabricksSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { scope }
} = secretSync;
return <SecretSyncLabel label="Secret Scope">{scope}</SecretSyncLabel>;
};

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