deconflict merge

This commit is contained in:
Scott Wilson
2025-04-25 17:20:30 -07:00
264 changed files with 4955 additions and 968 deletions
@@ -0,0 +1,42 @@
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { TAwsIamUserSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation";
import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
type Props = {
generatedCredentialsResponse: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
};
export const ViewAwsIamUserSecretRotationGeneratedCredentials = ({
generatedCredentialsResponse: { generatedCredentials, activeIndex }
}: Props) => {
const inactiveIndex = activeIndex === 0 ? 1 : 0;
const activeCredentials = generatedCredentials[activeIndex];
const inactiveCredentials = generatedCredentials[inactiveIndex];
return (
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
<CredentialDisplay label="Access Key ID">
{activeCredentials?.accessKeyId}
</CredentialDisplay>
<CredentialDisplay isSensitive label="Secret Access Key">
{activeCredentials?.secretAccessKey}
</CredentialDisplay>
</>
}
inactiveCredentials={
<>
<CredentialDisplay label="Access Key ID">
{inactiveCredentials?.accessKeyId}
</CredentialDisplay>
<CredentialDisplay isSensitive label="Secret Access Key">
{inactiveCredentials?.secretAccessKey}
</CredentialDisplay>
</>
}
/>
);
};
@@ -20,6 +20,7 @@ import {
} from "@app/hooks/api/secretRotationsV2";
import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials";
type Props = {
secretRotation?: TSecretRotationV2;
@@ -81,6 +82,13 @@ const Content = ({ secretRotation }: ContentProps) => {
/>
);
break;
case SecretRotation.AwsIamUserSecret:
Component = (
<ViewAwsIamUserSecretRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse}
/>
);
break;
default:
throw new Error("Unhandled View Generated Credential Rotation Type");
}
@@ -0,0 +1,84 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { AwsRegionSelect } from "@app/components/secret-syncs/forms/SecretSyncDestinationFields/shared";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { TAwsIamUserSecret, useListAwsConnectionIamUsers } from "@app/hooks/api/appConnections/aws";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const AwsIamUserSecretRotationParametersFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AwsIamUserSecret;
}
>();
const connectionId = watch("connection.id");
const { data: clients, isPending: isClientsPending } = useListAwsConnectionIamUsers({
connectionId
});
return (
<>
<Controller
name="parameters.userName"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="IAM User"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the IAM user you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isClientsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={clients?.find((client) => client.UserName === value) ?? ""}
onChange={(option) => {
onChange((option as SingleValue<TAwsIamUserSecret>)?.UserName ?? "");
}}
options={clients}
placeholder="Select an IAM user..."
getOptionLabel={(option) =>
(option as SingleValue<TAwsIamUserSecret>)?.UserName ?? ""
}
getOptionValue={(option) =>
(option as SingleValue<TAwsIamUserSecret>)?.UserName ?? ""
}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.region"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isOptional
isError={Boolean(error)}
errorText={error?.message}
label="Region"
tooltipText="Required only if no global scope is set."
>
<AwsRegionSelect value={value ?? ""} onChange={onChange} />
</FormControl>
)}
/>
</>
);
};
@@ -4,6 +4,7 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields";
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
@@ -11,7 +12,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields
};
export const SecretRotationV2ParametersFields = () => {
@@ -0,0 +1,30 @@
import { useFormContext } from "react-hook-form";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { SecretRotationReviewSection } from "./shared";
export const AwsIamUserSecretRotationReviewFields = () => {
const { watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AwsIamUserSecret;
}
>();
const [parameters, { accessKeyId, secretAccessKey }] = watch(["parameters", "secretsMapping"]);
return (
<>
<SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="Region">{parameters.region}</GenericFieldLabel>
<GenericFieldLabel label="User Name">{parameters.userName}</GenericFieldLabel>
</SecretRotationReviewSection>
<SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Secret Access Key ID">{accessKeyId}</GenericFieldLabel>
<GenericFieldLabel label="Secret Access Key">{secretAccessKey}</GenericFieldLabel>
</SecretRotationReviewSection>
</>
);
};
@@ -7,6 +7,7 @@ import { getRotateAtLocal } from "@app/helpers/secretRotationsV2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields";
import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields";
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared";
@@ -14,7 +15,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields
};
export const SecretRotationV2ReviewFields = () => {
@@ -0,0 +1,58 @@
import { Controller, useFormContext } from "react-hook-form";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { FormControl, Input } from "@app/components/v2";
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
import { SecretsMappingTable } from "./shared";
export const AwsIamUserSecretRotationSecretsMappingFields = () => {
const { control } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AwsIamUserSecret;
}
>();
const { rotationOption } = useSecretRotationV2Option(SecretRotation.AwsIamUserSecret);
const items = [
{
name: "Access Key ID",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.accessKeyId}
/>
</FormControl>
)}
control={control}
name="secretsMapping.accessKeyId"
/>
)
},
{
name: "Secret Access Key",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.secretAccessKey}
/>
</FormControl>
)}
control={control}
name="secretsMapping.secretAccessKey"
/>
)
}
];
return <SecretsMappingTable items={items} />;
};
@@ -4,6 +4,7 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields";
import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields";
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
@@ -11,7 +12,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields
};
export const SecretRotationV2SecretsMappingFields = () => {
@@ -0,0 +1,18 @@
import { z } from "zod";
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const AwsIamUserSecretRotationSchema = z
.object({
type: z.literal(SecretRotation.AwsIamUserSecret),
parameters: z.object({
userName: z.string().trim().min(1, "User Name required"),
region: z.string().trim().optional()
}),
secretsMapping: z.object({
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
})
})
.merge(BaseSecretRotationSchema);
@@ -1,6 +1,7 @@
import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema";
import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema";
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
@@ -9,7 +10,8 @@ const SecretRotationUnionSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema,
LdapPasswordRotationSchema
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]);
export const SecretRotationV2FormSchema = SecretRotationUnionSchema;
@@ -12,6 +12,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
import { WindmillSyncFields } from "./WindmillSyncFields";
@@ -46,6 +47,8 @@ export const SecretSyncDestinationFields = () => {
return <VercelSyncFields />;
case SecretSync.Windmill:
return <WindmillSyncFields />;
case SecretSync.TeamCity:
return <TeamCitySyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,128 @@
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 {
TTeamCityProjectWithBuildTypes,
useTeamCityConnectionListProjects
} from "@app/hooks/api/appConnections/teamcity";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const TeamCitySyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.TeamCity }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: projects, isLoading: isProjectsLoading } = useTeamCityConnectionListProjects(
connectionId,
{
enabled: Boolean(connectionId)
}
);
// For Build Config dropdown
const selectedProjectId = useWatch({ name: "destinationConfig.project", control });
const selectedProject = projects?.find((proj) => proj.id === selectedProjectId);
const buildTypes = selectedProject?.buildTypes?.buildType || [];
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.project", "");
setValue("destinationConfig.buildConfig", "");
}}
/>
<Controller
name="destinationConfig.project"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists in the connection's TeamCity instance URL."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects?.find((proj) => proj.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TTeamCityProjectWithBuildTypes>)?.id ?? null);
setValue("destinationConfig.buildConfig", "");
}}
options={projects}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.buildConfig"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
isOptional
label="Build Configuration"
helperText={
<Tooltip
className="max-w-md"
content='Ensure the configuration exists in the selected project and that your Access Token has the "View build configuration settings" permission.'
>
<div>
<span>Don&#39;t see the configuration you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId || !selectedProject}
value={buildTypes.find((buildType) => buildType.id === value) ?? null}
onChange={(option) => {
const selectedOption = option as SingleValue<{ id: string; name: string }>;
onChange(selectedOption?.id ?? "");
}}
options={buildTypes}
isClearable
placeholder="Select a build configuration..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<span className="text-sm text-bunker-300">
Not selecting a Build Configuration will sync your secrets to the entire project.
</span>
</>
);
};
@@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
case SecretSync.TeamCity:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -22,6 +22,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields";
@@ -88,6 +89,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Windmill:
DestinationFieldsComponent = <WindmillSyncReviewFields />;
break;
case SecretSync.TeamCity:
DestinationFieldsComponent = <TeamCitySyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,18 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const TeamCitySyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.TeamCity }>();
const project = watch("destinationConfig.project");
const buildConfig = watch("destinationConfig.buildConfig");
return (
<>
<GenericFieldLabel label="Project">{project}</GenericFieldLabel>
<GenericFieldLabel label="Build Configuration">{buildConfig}</GenericFieldLabel>
</>
);
};
@@ -9,6 +9,7 @@ import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-s
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema";
@@ -25,7 +26,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
TerraformCloudSyncDestinationSchema,
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema,
WindmillSyncDestinationSchema
WindmillSyncDestinationSchema,
TeamCitySyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,14 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const TeamCitySyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.TeamCity),
destinationConfig: z.object({
project: z.string().trim().min(1, "Project required"),
buildConfig: z.string().trim().optional()
})
})
);