mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 15:27:27 +00:00
feature: add support for kms key, tags, and syncing secret metadata to aws parameter store sync
This commit is contained in:
@@ -1722,6 +1722,13 @@ export const SecretSyncs = {
|
|||||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
ADDITIONAL_SYNC_OPTIONS: {
|
||||||
|
AWS_PARAMETER_STORE: {
|
||||||
|
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||||
|
tags: "Optional resource tags to add to parameters synced by Infisical.",
|
||||||
|
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.`
|
||||||
|
}
|
||||||
|
},
|
||||||
DESTINATION_CONFIG: {
|
DESTINATION_CONFIG: {
|
||||||
AWS_PARAMETER_STORE: {
|
AWS_PARAMETER_STORE: {
|
||||||
region: "The AWS region to sync secrets to.",
|
region: "The AWS region to sync secrets to.",
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
CreateAwsConnectionSchema,
|
CreateAwsConnectionSchema,
|
||||||
SanitizedAwsConnectionSchema,
|
SanitizedAwsConnectionSchema,
|
||||||
UpdateAwsConnectionSchema
|
UpdateAwsConnectionSchema
|
||||||
} from "@app/services/app-connection/aws";
|
} from "@app/services/app-connection/aws";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
registerAppConnectionEndpoints({
|
registerAppConnectionEndpoints({
|
||||||
app: AppConnection.AWS,
|
app: AppConnection.AWS,
|
||||||
server,
|
server,
|
||||||
@@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
createSchema: CreateAwsConnectionSchema,
|
createSchema: CreateAwsConnectionSchema,
|
||||||
updateSchema: UpdateAwsConnectionSchema
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/kms-keys`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
region: z.nativeEnum(AWSRegion),
|
||||||
|
destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager])
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
kmsKeys: z.object({ alias: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const kmsKeys = await server.services.appConnection.aws.listKmsKeys(
|
||||||
|
{
|
||||||
|
connectionId,
|
||||||
|
...req.query
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
return { kmsKeys };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ import {
|
|||||||
TUpdateAppConnectionDTO,
|
TUpdateAppConnectionDTO,
|
||||||
TValidateAppConnectionCredentials
|
TValidateAppConnectionCredentials
|
||||||
} from "@app/services/app-connection/app-connection-types";
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
|
|
||||||
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
|
|
||||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
|
||||||
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
|
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||||
|
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||||
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
|
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||||
|
import { githubConnectionService } from "./github/github-connection-service";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
listAvailableAppConnectionsForUser,
|
listAvailableAppConnectionsForUser,
|
||||||
github: githubConnectionService(connectAppConnectionById),
|
github: githubConnectionService(connectAppConnectionById),
|
||||||
gcp: gcpConnectionService(connectAppConnectionById),
|
gcp: gcpConnectionService(connectAppConnectionById),
|
||||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
aws: awsConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
TAwsConnection,
|
TAwsConnection,
|
||||||
TAwsConnectionConfig,
|
TAwsConnectionConfig,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
TGitHubConnectionInput,
|
TGitHubConnectionInput,
|
||||||
TValidateGitHubConnectionCredentials
|
TValidateGitHubConnectionCredentials
|
||||||
} from "@app/services/app-connection/github";
|
} from "@app/services/app-connection/github";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TAzureAppConfigurationConnection,
|
TAzureAppConfigurationConnection,
|
||||||
@@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
|
|||||||
| TValidateAzureKeyVaultConnectionCredentials
|
| TValidateAzureKeyVaultConnectionCredentials
|
||||||
| TValidateAzureAppConfigurationConnectionCredentials
|
| TValidateAzureAppConfigurationConnectionCredentials
|
||||||
| TValidateDatabricksConnectionCredentials;
|
| TValidateDatabricksConnectionCredentials;
|
||||||
|
|
||||||
|
export type TListAwsConnectionKmsKeys = {
|
||||||
|
connectionId: string;
|
||||||
|
region: AWSRegion;
|
||||||
|
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import AWS from "aws-sdk";
|
||||||
|
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
|
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TAwsConnection>;
|
||||||
|
|
||||||
|
const listAwsKmsKeys = async (
|
||||||
|
appConnection: TAwsConnection,
|
||||||
|
{ region, destination }: Pick<TListAwsConnectionKmsKeys, "region" | "destination">
|
||||||
|
) => {
|
||||||
|
const { credentials } = await getAwsConnectionConfig(appConnection, region);
|
||||||
|
|
||||||
|
const awsKms = new AWS.KMS({
|
||||||
|
credentials,
|
||||||
|
region
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Aliases = [] } = await awsKms.listAliases({ Limit: 100 }).promise();
|
||||||
|
|
||||||
|
const aliasEntries = Aliases.filter((aliasEntry) => {
|
||||||
|
if (!aliasEntry.TargetKeyId) return false;
|
||||||
|
|
||||||
|
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
|
||||||
|
|
||||||
|
if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsKeys = aliasEntries.map((alias) => {
|
||||||
|
return {
|
||||||
|
id: alias.TargetKeyId!,
|
||||||
|
alias: alias.AliasName!
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmsKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listKmsKeys = async (
|
||||||
|
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||||
|
|
||||||
|
const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination });
|
||||||
|
|
||||||
|
return kmsKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listKmsKeys
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
import { Knex } from "knex";
|
|
||||||
|
|
||||||
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||||
|
|
||||||
|
|||||||
+212
-17
@@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
|||||||
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
||||||
|
|
||||||
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
||||||
|
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
||||||
|
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const BATCH_SIZE = 10;
|
const BATCH_SIZE = 10;
|
||||||
@@ -80,6 +82,122 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
|||||||
return awsParameterStoreSecretsRecord;
|
return awsParameterStoreSecretsRecord;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||||
|
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
|
||||||
|
let hasNext = true;
|
||||||
|
let nextToken: string | undefined;
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (hasNext) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const parameters = await ssm
|
||||||
|
.describeParameters({
|
||||||
|
MaxResults: 10,
|
||||||
|
NextToken: nextToken,
|
||||||
|
ParameterFilters: [
|
||||||
|
{
|
||||||
|
Key: "Path",
|
||||||
|
Option: "OneLevel",
|
||||||
|
Values: [path]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
attempt = 0;
|
||||||
|
|
||||||
|
if (parameters.Parameters) {
|
||||||
|
parameters.Parameters.forEach((parameter) => {
|
||||||
|
if (parameter.Name) {
|
||||||
|
// no leading slash if path is '/'
|
||||||
|
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||||
|
awsParameterStoreMetadataRecord[secKey] = parameter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = Boolean(parameters.NextToken);
|
||||||
|
nextToken = parameters.NextToken;
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
attempt += 1;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep();
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsParameterStoreMetadataRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParameterStoreTagsRecord = async (
|
||||||
|
ssm: AWS.SSM,
|
||||||
|
path: string,
|
||||||
|
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
|
||||||
|
areTagsPresent: boolean
|
||||||
|
): Promise<TAWSParameterStoreTagsRecord> => {
|
||||||
|
const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {};
|
||||||
|
|
||||||
|
for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||||
|
const [key, parameter] = entry;
|
||||||
|
|
||||||
|
if (!parameter.Name) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tags = await ssm
|
||||||
|
.listTagsForResource({
|
||||||
|
ResourceType: "Parameter",
|
||||||
|
ResourceId: parameter.Name
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
awsParameterStoreTagsRecord[key] = Object.fromEntries(tags.TagList?.map((tag) => [tag.Key, tag.Value]) ?? []);
|
||||||
|
} catch (e) {
|
||||||
|
// users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized
|
||||||
|
// and they aren't trying to configure tags
|
||||||
|
if ((e as AWSError).code === "AccessDeniedException" && !areTagsPresent) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsParameterStoreTagsRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processParameterTags = ({
|
||||||
|
syncTagsRecord,
|
||||||
|
awsTagsRecord
|
||||||
|
}: {
|
||||||
|
syncTagsRecord: Record<string, string>;
|
||||||
|
awsTagsRecord: Record<string, string>;
|
||||||
|
}) => {
|
||||||
|
const tagsToAdd: AWS.SSM.TagList = [];
|
||||||
|
const tagKeysToRemove: string[] = [];
|
||||||
|
|
||||||
|
for (const syncEntry of Object.entries(syncTagsRecord)) {
|
||||||
|
const [syncKey, syncValue] = syncEntry;
|
||||||
|
|
||||||
|
if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey])
|
||||||
|
tagsToAdd.push({ Key: syncKey, Value: syncValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const awsKey of Object.keys(awsTagsRecord)) {
|
||||||
|
if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tagsToAdd, tagKeysToRemove };
|
||||||
|
};
|
||||||
|
|
||||||
const putParameter = async (
|
const putParameter = async (
|
||||||
ssm: AWS.SSM,
|
ssm: AWS.SSM,
|
||||||
params: AWS.SSM.PutParameterRequest,
|
params: AWS.SSM.PutParameterRequest,
|
||||||
@@ -98,6 +216,42 @@ const putParameter = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addTagsToParameter = async (
|
||||||
|
ssm: AWS.SSM,
|
||||||
|
params: Omit<AWS.SSM.AddTagsToResourceRequest, "ResourceType">,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<AWS.SSM.AddTagsToResourceResult> => {
|
||||||
|
try {
|
||||||
|
return await ssm.addTagsToResource({ ...params, ResourceType: "Parameter" }).promise();
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return addTagsToParameter(ssm, params, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTagsFromParameter = async (
|
||||||
|
ssm: AWS.SSM,
|
||||||
|
params: Omit<AWS.SSM.RemoveTagsFromResourceRequest, "ResourceType">,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<AWS.SSM.RemoveTagsFromResourceResult> => {
|
||||||
|
try {
|
||||||
|
return await ssm.removeTagsFromResource({ ...params, ResourceType: "Parameter" }).promise();
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return removeTagsFromParameter(ssm, params, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteParametersBatch = async (
|
const deleteParametersBatch = async (
|
||||||
ssm: AWS.SSM,
|
ssm: AWS.SSM,
|
||||||
parameters: AWS.SSM.Parameter[],
|
parameters: AWS.SSM.Parameter[],
|
||||||
@@ -132,34 +286,75 @@ const deleteParametersBatch = async (
|
|||||||
|
|
||||||
export const AwsParameterStoreSyncFns = {
|
export const AwsParameterStoreSyncFns = {
|
||||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const { destinationConfig } = secretSync;
|
const { destinationConfig, syncOptions } = secretSync;
|
||||||
|
|
||||||
const ssm = await getSSM(secretSync);
|
const ssm = await getSSM(secretSync);
|
||||||
|
|
||||||
// TODO(scott): KMS Key ID, Tags
|
|
||||||
|
|
||||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||||
|
|
||||||
for await (const entry of Object.entries(secretMap)) {
|
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||||
const [key, { value }] = entry;
|
|
||||||
|
|
||||||
// skip empty values (not allowed by AWS) or secrets that haven't changed
|
const awsParameterStoreTagsRecord = await getParameterStoreTagsRecord(
|
||||||
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
|
ssm,
|
||||||
|
destinationConfig.path,
|
||||||
|
awsParameterStoreSecretsRecord,
|
||||||
|
Boolean(syncOptions.tags || syncOptions.syncSecretMetadataAsTags)
|
||||||
|
);
|
||||||
|
|
||||||
|
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||||
|
|
||||||
|
for await (const entry of Object.entries(secretMap)) {
|
||||||
|
const [key, { value, secretMetadata }] = entry;
|
||||||
|
|
||||||
|
// skip empty values (not allowed by AWS)
|
||||||
|
if (!value) {
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// create parameter or update if changed
|
||||||
await putParameter(ssm, {
|
if (
|
||||||
Name: `${destinationConfig.path}${key}`,
|
!(key in awsParameterStoreSecretsRecord) ||
|
||||||
Type: "SecureString",
|
value !== awsParameterStoreSecretsRecord[key].Value ||
|
||||||
Value: value,
|
syncOptions.keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
|
||||||
Overwrite: true
|
) {
|
||||||
|
try {
|
||||||
|
await putParameter(ssm, {
|
||||||
|
Name: `${destinationConfig.path}${key}`,
|
||||||
|
Type: "SecureString",
|
||||||
|
Value: value,
|
||||||
|
Overwrite: true,
|
||||||
|
KeyId: syncOptions.keyId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
|
||||||
|
syncTagsRecord: {
|
||||||
|
// configured sync tags take preference over secret metadata
|
||||||
|
...(syncOptions.syncSecretMetadataAsTags &&
|
||||||
|
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||||
|
...syncTagsRecord
|
||||||
|
},
|
||||||
|
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tagsToAdd.length) {
|
||||||
|
await addTagsToParameter(ssm, {
|
||||||
|
ResourceId: `${destinationConfig.path}${key}`,
|
||||||
|
Tags: tagsToAdd
|
||||||
});
|
});
|
||||||
} catch (error) {
|
}
|
||||||
throw new SecretSyncError({
|
|
||||||
error,
|
if (tagKeysToRemove.length) {
|
||||||
secretKey: key
|
await removeTagsFromParameter(ssm, {
|
||||||
|
ResourceId: `${destinationConfig.path}${key}`,
|
||||||
|
TagKeys: tagKeysToRemove
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-3
@@ -8,6 +8,7 @@ import {
|
|||||||
GenericCreateSecretSyncFieldsSchema,
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
GenericUpdateSecretSyncFieldsSchema
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||||
@@ -20,19 +21,65 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
|||||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({
|
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||||
|
keyId: z
|
||||||
|
.string()
|
||||||
|
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||||
|
.min(1, "Invalid KMS Key ID")
|
||||||
|
.max(256, "Invalid KMS Key ID")
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||||
|
tags: z
|
||||||
|
.object({
|
||||||
|
key: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
|
"Tag keys can only contain Unicode letters, digits, white space and any of the following: _."
|
||||||
|
)
|
||||||
|
.min(1, "AWS tag key required")
|
||||||
|
.max(128, "AWS tag name cannot exceed 128 characters"),
|
||||||
|
value: z
|
||||||
|
.string()
|
||||||
|
.regex(/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, "Invalid AWS tag value")
|
||||||
|
.max(256, "Tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-")
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.max(50)
|
||||||
|
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||||
|
message: "AWS tag keys must be unique"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
||||||
|
syncSecretMetadataAsTags: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags)
|
||||||
|
});
|
||||||
|
|
||||||
|
const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(
|
||||||
|
SecretSync.AWSParameterStore,
|
||||||
|
AwsParameterStoreSyncOptionsConfig,
|
||||||
|
AwsParameterStoreSyncOptionsSchema
|
||||||
|
).extend({
|
||||||
destination: z.literal(SecretSync.AWSParameterStore),
|
destination: z.literal(SecretSync.AWSParameterStore),
|
||||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSParameterStore
|
SecretSync.AWSParameterStore,
|
||||||
|
AwsParameterStoreSyncOptionsConfig,
|
||||||
|
AwsParameterStoreSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSParameterStore
|
SecretSync.AWSParameterStore,
|
||||||
|
AwsParameterStoreSyncOptionsConfig,
|
||||||
|
AwsParameterStoreSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
+11
-3
@@ -9,6 +9,7 @@ import {
|
|||||||
GenericCreateSecretSyncFieldsSchema,
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
GenericUpdateSecretSyncFieldsSchema
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||||
.discriminatedUnion("mappingBehavior", [
|
.discriminatedUnion("mappingBehavior", [
|
||||||
@@ -38,19 +39,26 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||||
|
SecretSync.AWSSecretsManager,
|
||||||
|
AwsSecretsManagerSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSSecretsManager
|
SecretSync.AWSSecretsManager,
|
||||||
|
AwsSecretsManagerSyncOptionsConfig
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSSecretsManager
|
SecretSync.AWSSecretsManager,
|
||||||
|
AwsSecretsManagerSyncOptionsConfig
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||||
|
secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretMap[importedSecret.key] = {
|
secretMap[importedSecret.key] = {
|
||||||
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||||
comment: importedSecret.secretComment,
|
comment: importedSecret.secretComment,
|
||||||
value: importedSecret.secretValue || ""
|
value: importedSecret.secretValue || "",
|
||||||
|
secretMetadata: importedSecret.secretMetadata
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { AnyZodObject, z } from "zod";
|
||||||
|
|
||||||
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
|
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
|
||||||
import { SecretSyncs } from "@app/lib/api-docs";
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
@@ -8,34 +8,45 @@ import { SecretSync, SecretSyncInitialSyncBehavior } from "@app/services/secret-
|
|||||||
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) =>
|
const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
|
||||||
z.object({
|
destination,
|
||||||
initialSyncBehavior: (options.canImportSecrets
|
syncOptionsConfig: { canImportSecrets },
|
||||||
|
merge,
|
||||||
|
isUpdateSchema
|
||||||
|
}: {
|
||||||
|
destination: SecretSync;
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig;
|
||||||
|
merge?: T;
|
||||||
|
isUpdateSchema?: boolean;
|
||||||
|
}) => {
|
||||||
|
const baseSchema = z.object({
|
||||||
|
initialSyncBehavior: (canImportSecrets
|
||||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior)
|
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
|
||||||
// prependPrefix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional()
|
|
||||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX),
|
|
||||||
// appendSuffix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional()
|
|
||||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
const schema = merge ? baseSchema.merge(merge) : baseSchema;
|
||||||
|
|
||||||
|
return (
|
||||||
|
isUpdateSchema
|
||||||
|
? schema.describe(SecretSyncs.UPDATE(destination).syncOptions).optional()
|
||||||
|
: schema.describe(SecretSyncs.CREATE(destination).syncOptions)
|
||||||
|
) as T extends AnyZodObject ? z.ZodObject<z.objectUtil.MergeShapes<typeof schema.shape, T["shape"]>> : typeof schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
destination: SecretSync,
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig,
|
||||||
|
merge?: T
|
||||||
|
) =>
|
||||||
SecretSyncsSchema.omit({
|
SecretSyncsSchema.omit({
|
||||||
destination: true,
|
destination: true,
|
||||||
destinationConfig: true,
|
destinationConfig: true,
|
||||||
syncOptions: true
|
syncOptions: true
|
||||||
}).extend({
|
}).extend({
|
||||||
// destination needs to be on the extended object for type differentiation
|
// destination needs to be on the extended object for type differentiation
|
||||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig),
|
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }),
|
||||||
// join properties
|
// join properties
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
connection: z.object({
|
connection: z.object({
|
||||||
@@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?
|
|||||||
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
export const GenericCreateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
destination: SecretSync,
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig,
|
||||||
|
merge?: T
|
||||||
|
) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
|
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
|
||||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId),
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId),
|
||||||
@@ -66,10 +81,14 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
|||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(SecretSyncs.CREATE(destination).secretPath),
|
.describe(SecretSyncs.CREATE(destination).secretPath),
|
||||||
isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
|
isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
|
||||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions)
|
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
export const GenericUpdateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
destination: SecretSync,
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig,
|
||||||
|
merge?: T
|
||||||
|
) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
||||||
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
||||||
@@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(SecretSyncs.UPDATE(destination).secretPath),
|
.describe(SecretSyncs.UPDATE(destination).secretPath),
|
||||||
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
|
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
|
||||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig)
|
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true })
|
||||||
.optional()
|
|
||||||
.describe(SecretSyncs.UPDATE(destination).syncOptions)
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
|
|||||||
|
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { QueueJobs } from "@app/queue";
|
import { QueueJobs } from "@app/queue";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import {
|
import {
|
||||||
TAwsSecretsManagerSync,
|
TAwsSecretsManagerSync,
|
||||||
TAwsSecretsManagerSyncInput,
|
TAwsSecretsManagerSyncInput,
|
||||||
@@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job<
|
|||||||
|
|
||||||
export type TSecretMap = Record<
|
export type TSecretMap = Record<
|
||||||
string,
|
string,
|
||||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
{
|
||||||
|
value: string;
|
||||||
|
comment?: string;
|
||||||
|
skipMultilineEncoding?: boolean | null | undefined;
|
||||||
|
secretMetadata?: ResourceMetadataDTO;
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 885 KiB |
@@ -118,7 +118,9 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"ssm:GetParametersByPath",
|
"ssm:GetParametersByPath",
|
||||||
"ssm:DescribeParameters",
|
"ssm:DescribeParameters",
|
||||||
"ssm:DeleteParameters",
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:ListTagsForResource", // if you need to add tags to secrets
|
||||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
|
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
|
||||||
"kms:ListKeys", // if you need to specify the KMS key
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
"kms:ListAliases", // if you need to specify the KMS key
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:Encrypt", // if you need to specify the KMS key
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
@@ -259,7 +261,9 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"ssm:GetParametersByPath",
|
"ssm:GetParametersByPath",
|
||||||
"ssm:DescribeParameters",
|
"ssm:DescribeParameters",
|
||||||
"ssm:DeleteParameters",
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:ListTagsForResource", // if you need to add tags to secrets
|
||||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
|
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
|
||||||
"kms:ListKeys", // if you need to specify the KMS key
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
"kms:ListAliases", // if you need to specify the KMS key
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:Encrypt", // if you need to specify the KMS key
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
|||||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
||||||
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
||||||
|
- **Key ID**: The AWS KMS key ID or alias to encrypt parameters with.
|
||||||
|
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
|
||||||
|
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
|
||||||
|
<Note>Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.</Note>
|
||||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||||
|
|
||||||
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import {
|
|||||||
useSecretSyncOption
|
useSecretSyncOption
|
||||||
} from "@app/hooks/api/secretSyncs";
|
} from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||||
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
||||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
|
||||||
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
||||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ type Props = {
|
|||||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
||||||
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
||||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||||
{ name: "Options", key: "options", fields: ["syncOptions"] },
|
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||||
{ name: "Review", key: "review", fields: [] }
|
{ name: "Review", key: "review", fields: [] }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { Button, ModalClose } from "@app/components/v2";
|
|||||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||||
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
||||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
|
||||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
+6
-2
@@ -8,13 +8,17 @@ import { TSecretSyncForm } from "../schemas";
|
|||||||
import { AwsRegionSelect } from "./shared";
|
import { AwsRegionSelect } from "./shared";
|
||||||
|
|
||||||
export const AwsParameterStoreSyncFields = () => {
|
export const AwsParameterStoreSyncFields = () => {
|
||||||
const { control } = useFormContext<
|
const { control, setValue } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SecretSyncConnectionField />
|
<SecretSyncConnectionField
|
||||||
|
onChange={() => {
|
||||||
|
setValue("syncOptions.keyId", undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||||
|
|||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import { SingleValue } from "react-select";
|
||||||
|
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Switch,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
TAwsConnectionKmsKey,
|
||||||
|
useListAwsConnectionKmsKeys
|
||||||
|
} from "@app/hooks/api/appConnections/aws";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncOptionsFields = () => {
|
||||||
|
const { control, watch } = useFormContext<
|
||||||
|
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const region = watch("destinationConfig.region");
|
||||||
|
const connectionId = useWatch({ name: "connection.id", control });
|
||||||
|
|
||||||
|
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
|
||||||
|
{
|
||||||
|
connectionId,
|
||||||
|
region,
|
||||||
|
destination: SecretSync.AWSParameterStore
|
||||||
|
},
|
||||||
|
{ enabled: Boolean(connectionId && region) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const tagFields = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "syncOptions.tags"
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="syncOptions.keyId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="The AWS KMS key to encrypt parameters with"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="KMS Key"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
|
||||||
|
isDisabled={!connectionId}
|
||||||
|
value={kmsKeys.find((org) => org.alias === value) ?? null}
|
||||||
|
onChange={(option) =>
|
||||||
|
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
|
||||||
|
}
|
||||||
|
options={kmsKeys}
|
||||||
|
placeholder="Leave blank to use default KMS key"
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
|
||||||
|
}
|
||||||
|
getOptionValue={(option) => option.alias}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormLabel
|
||||||
|
label="Resource Tags"
|
||||||
|
tooltipText="Add resource tags to parameters synced by Infisical"
|
||||||
|
/>
|
||||||
|
<div className="mb-3 grid max-h-[40vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
|
||||||
|
{tagFields.fields.map(({ id: tagFieldId }, i) => (
|
||||||
|
<Fragment key={tagFieldId}>
|
||||||
|
<div className="col-span-5">
|
||||||
|
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`syncOptions.tags.${i}.key`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input className="text-xs" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-6">
|
||||||
|
{i === 0 && (
|
||||||
|
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`syncOptions.tags.${i}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input className="text-xs" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tooltip content="Remove tag" position="right">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="Remove tag"
|
||||||
|
className="col-span-1 mb-1.5"
|
||||||
|
colorSchema="danger"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => tagFields.remove(i)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => tagFields.append({ key: "", value: "" })}
|
||||||
|
>
|
||||||
|
Add Tag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="syncOptions.syncSecretMetadataAsTags"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mt-6"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||||
|
id="overwrite-existing-secrets"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
isChecked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
>
|
||||||
|
<p className="w-[18rem]">
|
||||||
|
Sync Secret Metadata as Resource Tags{" "}
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
If enabled, metadata attached to secrets will be added as resource tags to
|
||||||
|
parameters synced by Infisical.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
Manually configured tags from the field above will take precedence over
|
||||||
|
secret metadata when tag keys conflict.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||||
|
</Tooltip>
|
||||||
|
</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
-2
@@ -1,12 +1,14 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import { useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { TSecretSyncForm } from "./schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hideInitialSync?: boolean;
|
hideInitialSync?: boolean;
|
||||||
@@ -21,6 +23,24 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
|
|
||||||
const { syncOption } = useSecretSyncOption(destination);
|
const { syncOption } = useSecretSyncOption(destination);
|
||||||
|
|
||||||
|
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||||
|
|
||||||
|
switch (destination) {
|
||||||
|
case SecretSync.AWSParameterStore:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
|
||||||
|
break;
|
||||||
|
case SecretSync.AWSSecretsManager:
|
||||||
|
case SecretSync.GitHub:
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
case SecretSync.AzureKeyVault:
|
||||||
|
case SecretSync.AzureAppConfiguration:
|
||||||
|
case SecretSync.Databricks:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled Additional Sync Options Fields: ${destination}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
||||||
@@ -91,6 +111,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{AdditionalSyncOptionsFieldsComponent}
|
||||||
{/* <Controller
|
{/* <Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./SecretSyncOptionsFields";
|
||||||
+57
-3
@@ -1,17 +1,71 @@
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
import { Badge } from "@app/components/v2";
|
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AwsParameterStoreSyncReviewFields = () => {
|
export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||||
const { watch } = useFormContext<
|
const { watch } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const [region, path] = watch(["destinationConfig.region", "destinationConfig.path"]);
|
const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
|
{tags?.length && (
|
||||||
|
<SecretSyncLabel label="AWS Tags">
|
||||||
|
<Tooltip
|
||||||
|
side="right"
|
||||||
|
className="max-w-xl p-1"
|
||||||
|
content={
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Th className="whitespace-nowrap p-2">Key</Th>
|
||||||
|
<Th className="p-2">Value</Th>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Tr key={tag.key}>
|
||||||
|
<Td className="p-2">{tag.key}</Td>
|
||||||
|
<Td className="p-2">{tag.value}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-min">
|
||||||
|
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
<span>
|
||||||
|
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</SecretSyncLabel>
|
||||||
|
)}
|
||||||
|
{syncSecretMetadataAsTags && (
|
||||||
|
<SecretSyncLabel label="AWS Tags">
|
||||||
|
<Badge variant="success">Enabled</Badge>
|
||||||
|
</SecretSyncLabel>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsParameterStoreDestinationReviewFields = () => {
|
||||||
|
const { watch } = useFormContext<
|
||||||
|
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [{ region, path }] = watch(["destinationConfig"]);
|
||||||
|
|
||||||
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
||||||
|
|
||||||
|
|||||||
+11
-5
@@ -3,15 +3,18 @@ import { useFormContext } from "react-hook-form";
|
|||||||
|
|
||||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
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 { Badge } from "@app/components/v2";
|
||||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
import {
|
||||||
|
AwsParameterStoreDestinationReviewFields,
|
||||||
|
AwsParameterStoreSyncOptionsReviewFields
|
||||||
|
} from "./AwsParameterStoreSyncReviewFields";
|
||||||
|
import { AwsSecretsManagerSyncReviewFields } from "./AwsSecretsManagerSyncReviewFields";
|
||||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||||
|
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
const { watch } = useFormContext<TSecretSyncForm>();
|
const { watch } = useFormContext<TSecretSyncForm>();
|
||||||
|
|
||||||
let DestinationFieldsComponent: ReactNode;
|
let DestinationFieldsComponent: ReactNode;
|
||||||
|
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
@@ -38,7 +42,8 @@ export const SecretSyncReviewFields = () => {
|
|||||||
|
|
||||||
switch (destination) {
|
switch (destination) {
|
||||||
case SecretSync.AWSParameterStore:
|
case SecretSync.AWSParameterStore:
|
||||||
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
|
DestinationFieldsComponent = <AwsParameterStoreDestinationReviewFields />;
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsReviewFields />;
|
||||||
break;
|
break;
|
||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
||||||
@@ -84,7 +89,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="w-full border-b border-mineshaft-600">
|
<div className="w-full border-b border-mineshaft-600">
|
||||||
<span className="text-sm text-mineshaft-300">Options</span>
|
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
<SecretSyncLabel label="Auto-Sync">
|
<SecretSyncLabel label="Auto-Sync">
|
||||||
@@ -97,6 +102,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
</SecretSyncLabel>
|
</SecretSyncLabel>
|
||||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||||
|
{AdditionalSyncOptionsFieldsComponent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|||||||
+40
-11
@@ -1,16 +1,45 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AwsParameterStoreSyncDestinationSchema = z.object({
|
export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
|
||||||
destination: z.literal(SecretSync.AWSParameterStore),
|
z.object({
|
||||||
destinationConfig: z.object({
|
keyId: z.string().optional(),
|
||||||
path: z
|
tags: z
|
||||||
.string()
|
.object({
|
||||||
.trim()
|
key: z
|
||||||
.min(1, "Parameter Store Path required")
|
.string()
|
||||||
.max(2048, "Cannot exceed 2048 characters")
|
.regex(
|
||||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
region: z.string().min(1, "Region required")
|
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
)
|
||||||
|
.min(1, "Key required")
|
||||||
|
.max(128, "AWS tag name cannot exceed 128 characters"),
|
||||||
|
value: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
|
"Values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
)
|
||||||
|
.max(256, "Tag value cannot exceed 256 characters")
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
syncSecretMetadataAsTags: z.boolean().optional()
|
||||||
})
|
})
|
||||||
});
|
).merge(
|
||||||
|
z.object({
|
||||||
|
destination: z.literal(SecretSync.AWSParameterStore),
|
||||||
|
destinationConfig: z.object({
|
||||||
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Parameter Store Path required")
|
||||||
|
.max(2048, "Cannot exceed 2048 characters")
|
||||||
|
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
|
||||||
|
region: z.string().min(1, "Region required")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
+28
-25
@@ -1,30 +1,33 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncDestinationSchema = z.object({
|
export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
z.object({
|
||||||
destinationConfig: z
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
.discriminatedUnion("mappingBehavior", [
|
destinationConfig: z
|
||||||
z.object({
|
.discriminatedUnion("mappingBehavior", [
|
||||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
z.object({
|
||||||
}),
|
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||||
z.object({
|
}),
|
||||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
z.object({
|
||||||
secretName: z
|
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
||||||
.string()
|
secretName: z
|
||||||
.regex(
|
.string()
|
||||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
.regex(
|
||||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
/^[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")
|
.min(1, "Secret name is required")
|
||||||
})
|
.max(256, "Secret name cannot exceed 256 characters")
|
||||||
])
|
})
|
||||||
.and(
|
])
|
||||||
z.object({
|
.and(
|
||||||
region: z.string().min(1, "Region required")
|
z.object({
|
||||||
})
|
region: z.string().min(1, "Region required")
|
||||||
)
|
})
|
||||||
});
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
+17
-14
@@ -1,19 +1,22 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AzureAppConfigurationSyncDestinationSchema = z.object({
|
export const AzureAppConfigurationSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
z.object({
|
||||||
destinationConfig: z.object({
|
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||||
configurationUrl: z
|
destinationConfig: z.object({
|
||||||
.string()
|
configurationUrl: z
|
||||||
.trim()
|
.string()
|
||||||
.min(1, { message: "Azure App Configuration URL is required" })
|
.trim()
|
||||||
.url()
|
.min(1, { message: "Azure App Configuration URL is required" })
|
||||||
.refine(
|
.url()
|
||||||
(val) => val.endsWith(".azconfig.io"),
|
.refine(
|
||||||
"URL should have the following format: https://resource-name-here.azconfig.io"
|
(val) => val.endsWith(".azconfig.io"),
|
||||||
),
|
"URL should have the following format: https://resource-name-here.azconfig.io"
|
||||||
label: z.string().optional()
|
),
|
||||||
|
label: z.string().optional()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
);
|
||||||
|
|||||||
+11
-5
@@ -1,10 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AzureKeyVaultSyncDestinationSchema = z.object({
|
export const AzureKeyVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
destination: z.literal(SecretSync.AzureKeyVault),
|
z.object({
|
||||||
destinationConfig: z.object({
|
destination: z.literal(SecretSync.AzureKeyVault),
|
||||||
vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required")
|
destinationConfig: z.object({
|
||||||
|
vaultBaseUrl: z
|
||||||
|
.string()
|
||||||
|
.url("Invalid vault base URL format")
|
||||||
|
.min(1, "Vault base URL required")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { AnyZodObject, z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
||||||
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
|
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
additionalSyncOptions?: T
|
||||||
|
) => {
|
||||||
|
const baseSyncOptionsSchema = z.object({
|
||||||
|
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||||
|
// scott: removed temporarily for evaluation of template formatting
|
||||||
|
// prependPrefix: z
|
||||||
|
// .string()
|
||||||
|
// .trim()
|
||||||
|
// .transform((str) => str.toUpperCase())
|
||||||
|
// .optional(),
|
||||||
|
// appendSuffix: z
|
||||||
|
// .string()
|
||||||
|
// .trim()
|
||||||
|
// .transform((str) => str.toUpperCase())
|
||||||
|
// .optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncOptionsSchema = additionalSyncOptions
|
||||||
|
? baseSyncOptionsSchema.merge(additionalSyncOptions)
|
||||||
|
: (baseSyncOptionsSchema as T extends AnyZodObject
|
||||||
|
? z.ZodObject<z.objectUtil.MergeShapes<typeof baseSyncOptionsSchema.shape, T["shape"]>>
|
||||||
|
: typeof baseSyncOptionsSchema);
|
||||||
|
|
||||||
|
return z.object({
|
||||||
|
name: slugSchema({ field: "Name" }),
|
||||||
|
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
|
||||||
|
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
||||||
|
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
||||||
|
secretPath: z.string().min(1, "Secret path required"),
|
||||||
|
syncOptions: syncOptionsSchema,
|
||||||
|
isAutoSyncEnabled: z.boolean()
|
||||||
|
});
|
||||||
|
};
|
||||||
+8
-5
@@ -1,10 +1,13 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const DatabricksSyncDestinationSchema = z.object({
|
export const DatabricksSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
destination: z.literal(SecretSync.Databricks),
|
z.object({
|
||||||
destinationConfig: z.object({
|
destination: z.literal(SecretSync.Databricks),
|
||||||
scope: z.string().trim().min(1, "Databricks scope required")
|
destinationConfig: z.object({
|
||||||
|
scope: z.string().trim().min(1, "Databricks scope required")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
);
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
|
|
||||||
export const GcpSyncDestinationSchema = z.object({
|
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
destination: z.literal(SecretSync.GCPSecretManager),
|
z.object({
|
||||||
destinationConfig: z.object({
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
scope: z.literal(GcpSyncScope.Global),
|
destinationConfig: z.object({
|
||||||
projectId: z.string().min(1, "Project ID required")
|
scope: z.literal(GcpSyncScope.Global),
|
||||||
|
projectId: z.string().min(1, "Project ID required")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
);
|
||||||
|
|||||||
+39
-36
@@ -1,45 +1,48 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import {
|
import {
|
||||||
GitHubSyncScope,
|
GitHubSyncScope,
|
||||||
GitHubSyncVisibility
|
GitHubSyncVisibility
|
||||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||||
|
|
||||||
export const GitHubSyncDestinationSchema = z.object({
|
export const GitHubSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
destination: z.literal(SecretSync.GitHub),
|
z.object({
|
||||||
destinationConfig: z
|
destination: z.literal(SecretSync.GitHub),
|
||||||
.discriminatedUnion("scope", [
|
destinationConfig: z
|
||||||
z.object({
|
.discriminatedUnion("scope", [
|
||||||
scope: z.literal(GitHubSyncScope.Organization),
|
z.object({
|
||||||
org: z.string().min(1, "Organization name required"),
|
scope: z.literal(GitHubSyncScope.Organization),
|
||||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
org: z.string().min(1, "Organization name required"),
|
||||||
selectedRepositoryIds: z.number().array().optional()
|
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||||
}),
|
selectedRepositoryIds: z.number().array().optional()
|
||||||
z.object({
|
}),
|
||||||
scope: z.literal(GitHubSyncScope.Repository),
|
z.object({
|
||||||
owner: z.string().min(1, "Repository owner name required"),
|
scope: z.literal(GitHubSyncScope.Repository),
|
||||||
repo: z.string().min(1, "Repository name required")
|
owner: z.string().min(1, "Repository owner name required"),
|
||||||
}),
|
repo: z.string().min(1, "Repository name required")
|
||||||
z.object({
|
}),
|
||||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
z.object({
|
||||||
owner: z.string().min(1, "Repository owner name required"),
|
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||||
repo: z.string().min(1, "Repository name required"),
|
owner: z.string().min(1, "Repository owner name required"),
|
||||||
env: z.string().min(1, "Environment name required")
|
repo: z.string().min(1, "Repository name required"),
|
||||||
})
|
env: z.string().min(1, "Environment name required")
|
||||||
])
|
})
|
||||||
.superRefine((options, ctx) => {
|
])
|
||||||
if (options.scope === GitHubSyncScope.Organization) {
|
.superRefine((options, ctx) => {
|
||||||
if (
|
if (options.scope === GitHubSyncScope.Organization) {
|
||||||
options.visibility === GitHubSyncVisibility.Selected &&
|
if (
|
||||||
!options.selectedRepositoryIds?.length
|
options.visibility === GitHubSyncVisibility.Selected &&
|
||||||
) {
|
!options.selectedRepositoryIds?.length
|
||||||
ctx.addIssue({
|
) {
|
||||||
code: z.ZodIssueCode.custom,
|
ctx.addIssue({
|
||||||
message: "Select at least 1 repository",
|
code: z.ZodIssueCode.custom,
|
||||||
path: ["selectedRepositoryIds"]
|
message: "Select at least 1 repository",
|
||||||
});
|
path: ["selectedRepositoryIds"]
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
});
|
);
|
||||||
|
|||||||
@@ -3,37 +3,12 @@ import { z } from "zod";
|
|||||||
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
|
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 { 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 { 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 { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||||
|
|
||||||
const BaseSecretSyncSchema = z.object({
|
|
||||||
name: slugSchema({ field: "Name" }),
|
|
||||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
|
|
||||||
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
|
||||||
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
|
||||||
secretPath: z.string().min(1, "Secret path required"),
|
|
||||||
syncOptions: z.object({
|
|
||||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
|
||||||
// scott: removed temporarily for evaluation of template formatting
|
|
||||||
// prependPrefix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional(),
|
|
||||||
// appendSuffix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional()
|
|
||||||
}),
|
|
||||||
isAutoSyncEnabled: z.boolean()
|
|
||||||
});
|
|
||||||
|
|
||||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||||
AwsParameterStoreSyncDestinationSchema,
|
AwsParameterStoreSyncDestinationSchema,
|
||||||
AwsSecretsManagerSyncDestinationSchema,
|
AwsSecretsManagerSyncDestinationSchema,
|
||||||
@@ -44,8 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
|||||||
DatabricksSyncDestinationSchema
|
DatabricksSyncDestinationSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
|
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||||
|
|
||||||
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema.partial());
|
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema;
|
||||||
|
|
||||||
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
|
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./queries";
|
||||||
|
export * from "./types";
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TAwsConnectionKmsKey,
|
||||||
|
TAwsConnectionListKmsKeysResponse,
|
||||||
|
TListAwsConnectionKmsKeys
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const awsConnectionKeys = {
|
||||||
|
all: [...appConnectionKeys.all, "aws"] as const,
|
||||||
|
listKmsKeys: (params: TListAwsConnectionKmsKeys) =>
|
||||||
|
[...awsConnectionKeys.all, "kms-keys", params] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListAwsConnectionKmsKeys = (
|
||||||
|
{ connectionId, ...params }: TListAwsConnectionKmsKeys,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TAwsConnectionKmsKey[],
|
||||||
|
unknown,
|
||||||
|
TAwsConnectionKmsKey[],
|
||||||
|
ReturnType<typeof awsConnectionKeys.listKmsKeys>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: awsConnectionKeys.listKmsKeys({ connectionId, ...params }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TAwsConnectionListKmsKeysResponse>(
|
||||||
|
`/api/v1/app-connections/aws/${connectionId}/kms-keys`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.kmsKeys;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export type TDatabricksSecretScope = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDatabricksConnectionListSecretScopesResponse = {
|
||||||
|
secretScopes: TDatabricksSecretScope[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListAwsConnectionKmsKeys = {
|
||||||
|
connectionId: string;
|
||||||
|
region: string;
|
||||||
|
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAwsConnectionKmsKey = {
|
||||||
|
alias: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAwsConnectionListKmsKeysResponse = {
|
||||||
|
kmsKeys: TAwsConnectionKmsKey[];
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
export type TAwsParameterStoreSync = TRootSecretSync & {
|
export type TAwsParameterStoreSync = TRootSecretSync & {
|
||||||
destination: SecretSync.AWSParameterStore;
|
destination: SecretSync.AWSParameterStore;
|
||||||
@@ -13,4 +13,9 @@ export type TAwsParameterStoreSync = TRootSecretSync & {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
syncOptions: RootSyncOptions & {
|
||||||
|
keyId?: string;
|
||||||
|
tags?: { key: string; value?: string }[];
|
||||||
|
syncSecretMetadataAsTags?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export type RootSyncOptions = {
|
||||||
|
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||||
|
// prependPrefix?: string;
|
||||||
|
// appendSuffix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TRootSecretSync = {
|
export type TRootSecretSync = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,11 +30,7 @@ export type TRootSecretSync = {
|
|||||||
lastRemoveJobId: string | null;
|
lastRemoveJobId: string | null;
|
||||||
lastRemovedAt: Date | null;
|
lastRemovedAt: Date | null;
|
||||||
lastRemoveMessage: string | null;
|
lastRemoveMessage: string | null;
|
||||||
syncOptions: {
|
syncOptions: RootSyncOptions;
|
||||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
|
||||||
// prependPrefix?: string;
|
|
||||||
// appendSuffix?: string;
|
|
||||||
};
|
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection;
|
app: AppConnection;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
+2
-5
@@ -66,7 +66,7 @@ const PageContent = () => {
|
|||||||
|
|
||||||
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
||||||
|
|
||||||
// const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||||
|
|
||||||
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
||||||
|
|
||||||
@@ -108,10 +108,7 @@ const PageContent = () => {
|
|||||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||||
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
||||||
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
||||||
<SecretSyncOptionsSection
|
<SecretSyncOptionsSection secretSync={secretSync} onEditOptions={handleEditOptions} />
|
||||||
secretSync={secretSync}
|
|
||||||
// onEditOptions={handleEditOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
<SecretSyncDestinationSection
|
<SecretSyncDestinationSection
|
||||||
|
|||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
|
||||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
|
|
||||||
import { TSecretSync } from "@app/hooks/api/secretSyncs";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
secretSync: TSecretSync;
|
|
||||||
// onEditOptions: VoidFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SecretSyncOptionsSection = ({
|
|
||||||
secretSync
|
|
||||||
// onEditOptions
|
|
||||||
}: Props) => {
|
|
||||||
const {
|
|
||||||
destination,
|
|
||||||
syncOptions: {
|
|
||||||
// appendSuffix,
|
|
||||||
// prependPrefix,
|
|
||||||
initialSyncBehavior
|
|
||||||
}
|
|
||||||
} = secretSync;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
|
||||||
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
|
||||||
{/* <ProjectPermissionCan
|
|
||||||
I={ProjectPermissionSecretSyncActions.Edit}
|
|
||||||
a={ProjectPermissionSub.SecretSyncs}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="secondary"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
ariaLabel="Edit sync options"
|
|
||||||
onClick={onEditOptions}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan> */}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<SecretSyncLabel label="Initial Sync Behavior">
|
|
||||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
|
|
||||||
</SecretSyncLabel>
|
|
||||||
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
|
||||||
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
|
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretSync: TAwsParameterStoreSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncOptionsSection = ({ secretSync }: Props) => {
|
||||||
|
const {
|
||||||
|
syncOptions: { keyId, tags, syncSecretMetadataAsTags }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
|
{tags?.length && (
|
||||||
|
<SecretSyncLabel label="Resource Tags">
|
||||||
|
<Tooltip
|
||||||
|
side="right"
|
||||||
|
className="max-w-xl p-1"
|
||||||
|
content={
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Th className="whitespace-nowrap p-2">Key</Th>
|
||||||
|
<Th className="p-2">Value</Th>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Tr key={tag.key}>
|
||||||
|
<Td className="p-2">{tag.key}</Td>
|
||||||
|
<Td className="p-2">{tag.value}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-min">
|
||||||
|
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
<span>
|
||||||
|
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</SecretSyncLabel>
|
||||||
|
)}
|
||||||
|
{syncSecretMetadataAsTags && (
|
||||||
|
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||||
|
<Badge variant="success">Enabled</Badge>
|
||||||
|
</SecretSyncLabel>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
|
import { IconButton } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
|
||||||
|
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretSync: TSecretSync;
|
||||||
|
onEditOptions: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
|
||||||
|
const {
|
||||||
|
destination,
|
||||||
|
syncOptions: {
|
||||||
|
// appendSuffix,
|
||||||
|
// prependPrefix,
|
||||||
|
initialSyncBehavior
|
||||||
|
}
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
let AdditionalSyncOptionsComponent: ReactNode;
|
||||||
|
|
||||||
|
switch (destination) {
|
||||||
|
case SecretSync.AWSParameterStore:
|
||||||
|
AdditionalSyncOptionsComponent = (
|
||||||
|
<AwsParameterStoreSyncOptionsSection secretSync={secretSync} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SecretSync.AWSSecretsManager:
|
||||||
|
case SecretSync.GitHub:
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
case SecretSync.AzureKeyVault:
|
||||||
|
case SecretSync.AzureAppConfiguration:
|
||||||
|
case SecretSync.Databricks:
|
||||||
|
AdditionalSyncOptionsComponent = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
||||||
|
{AdditionalSyncOptionsComponent && (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionSecretSyncActions.Edit}
|
||||||
|
a={ProjectPermissionSub.SecretSyncs}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
ariaLabel="Edit sync options"
|
||||||
|
onClick={onEditOptions}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SecretSyncLabel label="Initial Sync Behavior">
|
||||||
|
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
|
||||||
|
</SecretSyncLabel>
|
||||||
|
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
||||||
|
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||||
|
{AdditionalSyncOptionsComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./SecretSyncOptionsSection";
|
||||||
Reference in New Issue
Block a user