mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-31 15:32:32 +00:00
Compare commits
3 Commits
doc/add-ca
...
aws-syncs-
Author | SHA1 | Date | |
---|---|---|---|
|
9cdb4dcde9 | ||
|
69fb87bbfc | ||
|
b0cd5bd10d |
@@ -1722,6 +1722,13 @@ export const SecretSyncs = {
|
||||
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: {
|
||||
AWS_PARAMETER_STORE: {
|
||||
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 {
|
||||
CreateAwsConnectionSchema,
|
||||
SanitizedAwsConnectionSchema,
|
||||
UpdateAwsConnectionSchema
|
||||
} 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";
|
||||
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.AWS,
|
||||
server,
|
||||
@@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
createSchema: CreateAwsConnectionSchema,
|
||||
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,
|
||||
TValidateAppConnectionCredentials
|
||||
} 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 { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
|
||||
listAvailableAppConnectionsForUser,
|
||||
github: githubConnectionService(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 {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
@@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
|
||||
| TValidateAzureKeyVaultConnectionCredentials
|
||||
| TValidateAzureAppConfigurationConnectionCredentials
|
||||
| TValidateDatabricksConnectionCredentials;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
region: AWSRegion;
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
@@ -0,0 +1,88 @@
|
||||
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 aliasEntries: AWS.KMS.AliasList = [];
|
||||
let aliasMarker: string | undefined;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise();
|
||||
aliasEntries.push(...(response.Aliases || []));
|
||||
aliasMarker = response.NextMarker;
|
||||
} while (aliasMarker);
|
||||
|
||||
const keyMetadataRecord: Record<string, AWS.KMS.KeyMetadata | undefined> = {};
|
||||
for await (const aliasEntry of aliasEntries) {
|
||||
if (aliasEntry.TargetKeyId) {
|
||||
const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise();
|
||||
|
||||
keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
const validAliasEntries = aliasEntries.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;
|
||||
|
||||
const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId];
|
||||
|
||||
if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const kmsKeys = validAliasEntries.map((aliasEntry) => {
|
||||
return {
|
||||
id: aliasEntry.TargetKeyId!,
|
||||
alias: aliasEntry.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 { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||
|
||||
|
@@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
||||
|
||||
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 BATCH_SIZE = 10;
|
||||
@@ -80,6 +82,129 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
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,
|
||||
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
|
||||
needsTagsPermissions: boolean
|
||||
): Promise<{ shouldManageTags: boolean; awsParameterStoreTagsRecord: 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") {
|
||||
if (!needsTagsPermissions) {
|
||||
return { shouldManageTags: false, awsParameterStoreTagsRecord: {} };
|
||||
}
|
||||
|
||||
throw new SecretSyncError({
|
||||
message:
|
||||
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return { shouldManageTags: true, 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 (
|
||||
ssm: AWS.SSM,
|
||||
params: AWS.SSM.PutParameterRequest,
|
||||
@@ -98,6 +223,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 (
|
||||
ssm: AWS.SSM,
|
||||
parameters: AWS.SSM.Parameter[],
|
||||
@@ -132,35 +293,90 @@ const deleteParametersBatch = async (
|
||||
|
||||
export const AwsParameterStoreSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
// TODO(scott): KMS Key ID, Tags
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||
|
||||
// skip empty values (not allowed by AWS) or secrets that haven't changed
|
||||
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
|
||||
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||
ssm,
|
||||
awsParameterStoreSecretsRecord,
|
||||
Boolean(syncOptions.tags?.length || 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
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await putParameter(ssm, {
|
||||
Name: `${destinationConfig.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: value,
|
||||
Overwrite: true
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
// create parameter or update if changed
|
||||
if (
|
||||
!(key in awsParameterStoreSecretsRecord) ||
|
||||
value !== awsParameterStoreSecretsRecord[key].Value ||
|
||||
(syncOptions.keyId ?? "alias/aws/ssm") !== awsParameterStoreMetadataRecord[key]?.KeyId
|
||||
) {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldManageTags) {
|
||||
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) {
|
||||
try {
|
||||
await addTagsToParameter(ssm, {
|
||||
ResourceId: `${destinationConfig.path}${key}`,
|
||||
Tags: tagsToAdd
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTagsFromParameter(ssm, {
|
||||
ResourceId: `${destinationConfig.path}${key}`,
|
||||
TagKeys: tagKeysToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||
@@ -20,19 +21,68 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
.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,
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Resource tag key required")
|
||||
.max(128, "Resource tag name cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
})
|
||||
.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),
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSParameterStore
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSParameterStore
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.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),
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
@@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
|
||||
}
|
||||
|
||||
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||
secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
|
||||
secretMap[importedSecret.key] = {
|
||||
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||
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 { 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 { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) =>
|
||||
z.object({
|
||||
initialSyncBehavior: (options.canImportSecrets
|
||||
const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
|
||||
destination,
|
||||
syncOptionsConfig: { canImportSecrets },
|
||||
merge,
|
||||
isUpdateSchema
|
||||
}: {
|
||||
destination: SecretSync;
|
||||
syncOptionsConfig: TSyncOptionsConfig;
|
||||
merge?: T;
|
||||
isUpdateSchema?: boolean;
|
||||
}) => {
|
||||
const baseSchema = z.object({
|
||||
initialSyncBehavior: (canImportSecrets
|
||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).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)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
|
||||
});
|
||||
|
||||
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({
|
||||
destination: true,
|
||||
destinationConfig: true,
|
||||
syncOptions: true
|
||||
}).extend({
|
||||
// destination needs to be on the extended object for type differentiation
|
||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig),
|
||||
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }),
|
||||
// join properties
|
||||
projectId: z.string(),
|
||||
connection: z.object({
|
||||
@@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?
|
||||
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({
|
||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
|
||||
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)
|
||||
.describe(SecretSyncs.CREATE(destination).secretPath),
|
||||
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({
|
||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
||||
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
||||
@@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
||||
.optional()
|
||||
.describe(SecretSyncs.UPDATE(destination).secretPath),
|
||||
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
|
||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig)
|
||||
.optional()
|
||||
.describe(SecretSyncs.UPDATE(destination).syncOptions)
|
||||
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true })
|
||||
});
|
||||
|
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
|
||||
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { QueueJobs } from "@app/queue";
|
||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import {
|
||||
TAwsSecretsManagerSync,
|
||||
TAwsSecretsManagerSyncInput,
|
||||
@@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job<
|
||||
|
||||
export type TSecretMap = Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
||||
{
|
||||
value: string;
|
||||
comment?: string;
|
||||
skipMultilineEncoding?: boolean | null | undefined;
|
||||
secretMetadata?: ResourceMetadataDTO;
|
||||
}
|
||||
>;
|
||||
|
@@ -1,87 +0,0 @@
|
||||
---
|
||||
title: "Terraform Cloud"
|
||||
description: "How to authenticate with Infisical from Terraform Cloud using OIDC."
|
||||
---
|
||||
|
||||
This guide will walk you through setting up Terraform Cloud to inject a [workload identity token](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens) and use it for OIDC-based authentication with the Infisical Terraform provider. You'll start by creating a machine identity in Infisical, then configure Terraform Cloud to pass the injected token into your Terraform runs.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Machine Identity in Infisical">
|
||||
Follow the instructions [in this documentation](/documentation/platform/identities/oidc-auth/general) to create a machine identity with OIDC auth. Infisical OIDC configuration values for Terraform Cloud:
|
||||
1. Set the OIDC Discovery URL to https://app.terraform.io.
|
||||
2. Set the Issuer to https://app.terraform.io.
|
||||
3. Configure the Audience to match the value you will use for **TFC_WORKLOAD_IDENTITY_AUDIENCE** in Terraform Cloud for the next step.
|
||||
|
||||
|
||||
To view all possible claims available from Terraform cloud, visit [HashiCorp’s documentation](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#token-structure).
|
||||
|
||||
</Step>
|
||||
<Step title="Enable Workload Identity Token Injection in Terraform Cloud">
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Generate single token">
|
||||
1. **Navigate to your workspace** in Terraform Cloud.
|
||||
2. **Add a workspace variable** named `TFC_WORKLOAD_IDENTITY_AUDIENCE`:
|
||||
- **Key**: `TFC_WORKLOAD_IDENTITY_AUDIENCE`
|
||||
- **Value**: For example, `my-infisical-audience`
|
||||
- **Category**: Environment
|
||||
|
||||
> **Important**:
|
||||
> - The presence of `TFC_WORKLOAD_IDENTITY_AUDIENCE` is required for Terraform Cloud to inject a token.
|
||||
> - If you are self-hosting HCP Terraform agents, ensure they are **v1.7.0 or above**.
|
||||
|
||||
Once set, Terraform Cloud will inject a workload identity token into the run environment as `TFC_WORKLOAD_IDENTITY_TOKEN`.
|
||||
</Tab>
|
||||
<Tab title="(Optional) Generate Multiple Tokens">
|
||||
If you need multiple tokens (each with a different audience), create additional variables:
|
||||
|
||||
```
|
||||
TFC_WORKLOAD_IDENTITY_AUDIENCE_[YOUR_TAG_HERE]
|
||||
```
|
||||
|
||||
For example:
|
||||
- `TFC_WORKLOAD_IDENTITY_AUDIENCE_INFISICAL`
|
||||
- `TFC_WORKLOAD_IDENTITY_AUDIENCE_OTHER_SERVICE`
|
||||
|
||||
Terraform Cloud will then inject:
|
||||
- `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL`
|
||||
- `TFC_WORKLOAD_IDENTITY_TOKEN_OTHER_SERVICE`
|
||||
|
||||
> **Note**:
|
||||
> - The `[YOUR_TAG_HERE]` can only contain letters, numbers, and underscores.
|
||||
> - You **cannot** use the reserved keyword `TYPE`.
|
||||
> - Generating multiple tokens requires **v1.12.0 or later** if you are self-hosting agents.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
If you are running on self-hosted HCP Terraform agents, you must use v1.7.0 or later to enable token injection. If you need to generate multiple tokens, you must use v1.12.0 or later.
|
||||
</Warning>
|
||||
</Step>
|
||||
<Step title="Configure the Infisical Provider">
|
||||
In your Terraform configuration, reference the injected token by name. For example:
|
||||
|
||||
```hcl
|
||||
provider "infisical" {
|
||||
host = "https://app.infisical.com"
|
||||
|
||||
auth = {
|
||||
oidc = {
|
||||
identity_id = "<identity-id>"
|
||||
# This must match the environment variable Terraform injects:
|
||||
token_environment_variable_name = "TFC_WORKLOAD_IDENTITY_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`host`**: Defaults to `https://app.infisical.com`. Override if using a self-hosted Infisical instance.
|
||||
- **`identity_id`**: The OIDC identity ID from Infisical.
|
||||
- **`token_environment_variable_name`**: Must match the injected variable name from Terraform Cloud. If using single token, use `TFC_WORKLOAD_IDENTITY_TOKEN`. If using multiple tokens, choose the one you want to use (e.g., `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL`).
|
||||
</Step>
|
||||
<Step title="Validate Your Setup">
|
||||
1. Run a plan and apply in Terraform Cloud.
|
||||
2. Verify the Infisical provider authenticates successfully without issues. If you run into authentication errors, double-check the Infisical identity has the correct roles/permissions in Infisical.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
@@ -1,36 +0,0 @@
|
||||
---
|
||||
title: "Networking"
|
||||
sidebarTitle: "Networking"
|
||||
description: "Network configuration details for Infisical Cloud"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services.
|
||||
|
||||
## Egress IP Addresses
|
||||
|
||||
Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services.
|
||||
|
||||
### US Region
|
||||
|
||||
To allow connections from Infisical US, add these IP addresses to your ingress rules:
|
||||
|
||||
- `3.213.63.16`
|
||||
- `54.164.68.7`
|
||||
|
||||
### EU Region
|
||||
|
||||
To allow connections from Infisical EU, add these IP addresses to your ingress rules:
|
||||
|
||||
- `3.77.89.19`
|
||||
- `3.125.209.189`
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
You may need to allow Infisical’s egress IPs if your services require inbound connections for:
|
||||
|
||||
- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials
|
||||
- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services
|
||||
- Secret integrations - When syncing secrets with third-party services like Azure Key Vault
|
||||
- Native authentication with machine identities - When using methods like Kubernetes authentication
|
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:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:ListTagsForResource", // 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:ListAliases", // 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:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:ListTagsForResource", // 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:ListAliases", // 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.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
||||
|
@@ -77,6 +77,13 @@ via the UI or API for the third-party service you intend to sync secrets to.
|
||||
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
|
||||
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync.
|
||||
|
||||
<Note>
|
||||
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||
including associated data, not present or imported in Infisical before syncing will be
|
||||
overwritten, and changes directly in the connected service outside of infisical may also
|
||||
be overwritten by future syncs.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
Some third-party services do not support importing secrets.
|
||||
</Info>
|
||||
|
@@ -85,10 +85,6 @@
|
||||
"documentation/guides/microsoft-power-apps",
|
||||
"documentation/guides/organization-structure"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Setup",
|
||||
"pages": ["documentation/setup/networking"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -236,8 +232,7 @@
|
||||
"documentation/platform/identities/oidc-auth/general",
|
||||
"documentation/platform/identities/oidc-auth/github",
|
||||
"documentation/platform/identities/oidc-auth/circleci",
|
||||
"documentation/platform/identities/oidc-auth/gitlab",
|
||||
"documentation/platform/identities/oidc-auth/terraform-cloud"
|
||||
"documentation/platform/identities/oidc-auth/gitlab"
|
||||
]
|
||||
},
|
||||
"documentation/platform/mfa",
|
||||
@@ -638,8 +633,7 @@
|
||||
"api-reference/endpoints/oidc-auth/attach",
|
||||
"api-reference/endpoints/oidc-auth/retrieve",
|
||||
"api-reference/endpoints/oidc-auth/update",
|
||||
"api-reference/endpoints/oidc-auth/revoke",
|
||||
"integrations/frameworks/terraform-cloud"
|
||||
"api-reference/endpoints/oidc-auth/revoke"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@@ -4,6 +4,8 @@ sidebarTitle: "Go"
|
||||
icon: "golang"
|
||||
---
|
||||
|
||||
|
||||
|
||||
If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [Package](https://pkg.go.dev/github.com/infisical/go-sdk)
|
||||
@@ -55,9 +57,7 @@ func main() {
|
||||
This example demonstrates how to use the Infisical Go SDK in a simple Go application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project.
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity
|
||||
Tokens](/platform/identities/overview). Setting it as an environment variable
|
||||
would be best.
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
|
||||
# Installation
|
||||
@@ -95,10 +95,6 @@ client := infisical.NewInfisicalClient(context.Background(), infisical.Config{
|
||||
<ParamField query="SilentMode" type="boolean" default={false} optional>
|
||||
Whether or not to suppress logs such as warnings from the token refreshing process. Defaults to false if not specified.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CacheExpiryInSeconds" type="number" default={0} optional>
|
||||
Defines how long certain responses should be cached in memory, in seconds. When set to a positive value, responses from specific methods (like secret fetching) will be cached for this duration. Set to 0 to disable caching.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
@@ -144,7 +140,6 @@ Call `.Auth().UniversalAuthLogin()` with empty arguments to use the following en
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
|
||||
```go
|
||||
_, err := client.Auth().UniversalAuthLogin("CLIENT_ID", "CLIENT_SECRET")
|
||||
|
||||
@@ -155,12 +150,9 @@ if err != nil {
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running
|
||||
your application on Google Cloud Platform. Please [read
|
||||
more](/documentation/platform/identities/gcp-auth) about this authentication
|
||||
method.
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
@@ -170,7 +162,6 @@ Call `.Auth().GcpIdTokenAuthLogin()` with empty arguments to use the following e
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
|
||||
```go
|
||||
_, err := client.Auth().GcpIdTokenAuthLogin("YOUR_MACHINE_IDENTITY_ID")
|
||||
|
||||
@@ -190,7 +181,6 @@ Call `.Auth().GcpIamAuthLogin()` with empty arguments to use the following envir
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
|
||||
```go
|
||||
_, err = client.Auth().GcpIamAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_KEY_FILE_PATH")
|
||||
|
||||
@@ -201,12 +191,9 @@ if err != nil {
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running
|
||||
your application on AWS. Please [read
|
||||
more](/documentation/platform/identities/aws-auth) about this authentication
|
||||
method.
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
@@ -216,7 +203,6 @@ Call `.Auth().AwsIamAuthLogin()` with empty arguments to use the following envir
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
|
||||
```go
|
||||
_, err = client.Auth().AwsIamAuthLogin("MACHINE_IDENTITY_ID")
|
||||
|
||||
@@ -226,13 +212,11 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
#### Azure Auth
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running
|
||||
your application on Azure. Please [read
|
||||
more](/documentation/platform/identities/azure-auth) about this authentication
|
||||
method.
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
@@ -242,7 +226,6 @@ Call `.Auth().AzureAuthLogin()` with empty arguments to use the following enviro
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
|
||||
```go
|
||||
_, err = client.Auth().AzureAuthLogin("MACHINE_IDENTITY_ID")
|
||||
|
||||
@@ -253,12 +236,9 @@ if err != nil {
|
||||
```
|
||||
|
||||
#### Kubernetes Auth
|
||||
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running
|
||||
your application on Kubernetes. Please [read
|
||||
more](/documentation/platform/identities/kubernetes-auth) about this
|
||||
authentication method.
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
@@ -269,7 +249,6 @@ Call `.Auth().KubernetesAuthLogin()` with empty arguments to use the following e
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
|
||||
```go
|
||||
// Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed
|
||||
_, err = client.Auth().KubernetesAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_TOKEN_PATH")
|
||||
@@ -283,7 +262,6 @@ if err != nil {
|
||||
## Working With Secrets
|
||||
|
||||
### List Secrets
|
||||
|
||||
`client.Secrets().List(options)`
|
||||
|
||||
Retrieve all secrets within the Infisical project and environment that client is connected to.
|
||||
@@ -333,9 +311,7 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
|
||||
</ParamField>
|
||||
|
||||
###
|
||||
|
||||
### Retrieve Secret
|
||||
|
||||
`client.Secrets().Retrieve(options)`
|
||||
|
||||
Retrieve a secret from Infisical. By default `Secrets().Retrieve()` fetches and returns a shared secret.
|
||||
@@ -351,31 +327,27 @@ secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
|
||||
### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string" required>
|
||||
The key of the secret to retrieve.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets
|
||||
should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not
|
||||
specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string" required>
|
||||
The key of the secret to retrieve.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
###
|
||||
|
||||
### Create Secret
|
||||
|
||||
`client.Secrets().Create(options)`
|
||||
|
||||
Create a new secret in Infisical.
|
||||
@@ -391,38 +363,36 @@ secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="SecretValue" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="SecretComment" type="string" optional>
|
||||
A comment for the secret.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets
|
||||
should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not
|
||||
specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="SecretValue" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="SecretComment" type="string" optional>
|
||||
A comment for the secret.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
###
|
||||
|
||||
### Update Secret
|
||||
|
||||
`client.Secrets().Update(options)`
|
||||
@@ -442,42 +412,33 @@ secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
|
||||
### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="NewSecretValue" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField
|
||||
query="NewSkipMultilineEncoding"
|
||||
type="boolean"
|
||||
default="false"
|
||||
optional
|
||||
>
|
||||
Whether or not to skip multiline encoding for the new secret value.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets
|
||||
should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not
|
||||
specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="NewSecretValue" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="NewSkipMultilineEncoding" type="boolean" default="false" optional>
|
||||
Whether or not to skip multiline encoding for the new secret value.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
###
|
||||
|
||||
### Delete Secret
|
||||
|
||||
`client.Secrets().Delete(options)`
|
||||
|
||||
Delete a secret in Infisical.
|
||||
@@ -493,33 +454,30 @@ secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
|
||||
### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets
|
||||
should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not
|
||||
specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretKey" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="SecretPath" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="Type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Working With folders
|
||||
|
||||
|
||||
###
|
||||
|
||||
### List Folders
|
||||
|
||||
`client.Folders().List(options)`
|
||||
|
||||
Retrieve all within the Infisical project and environment that client is connected to.
|
||||
@@ -552,9 +510,7 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{
|
||||
</ParamField>
|
||||
|
||||
###
|
||||
|
||||
### Create Folder
|
||||
|
||||
`client.Folders().Create(options)`
|
||||
|
||||
Create a new folder in Infisical.
|
||||
@@ -571,27 +527,25 @@ folder, err := client.Folders().Create(infisical.CreateFolderOptions{
|
||||
### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder will be created.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment where the folder will be
|
||||
created.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path to create the folder in. The root path is `/`.
|
||||
</ParamField>
|
||||
<ParamField query="Name" type="string" optional>
|
||||
The name of the folder to create.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder will be created.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment where the folder will be created.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path to create the folder in. The root path is `/`.
|
||||
</ParamField>
|
||||
<ParamField query="Name" type="string" optional>
|
||||
The name of the folder to create.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
|
||||
###
|
||||
|
||||
### Update Folder
|
||||
|
||||
`client.Folders().Update(options)`
|
||||
|
||||
Update an existing folder in Infisical.
|
||||
@@ -609,30 +563,27 @@ folder, err := client.Folders().Update(infisical.UpdateFolderOptions{
|
||||
### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder will be updated.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where the folder
|
||||
lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path from where the folder should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="FolderID" type="string" required>
|
||||
The ID of the folder to update.
|
||||
</ParamField>
|
||||
<ParamField query="NewName" type="string" required>
|
||||
The new name of the folder.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder will be updated.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where the folder lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path from where the folder should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="FolderID" type="string" required>
|
||||
The ID of the folder to update.
|
||||
</ParamField>
|
||||
<ParamField query="NewName" type="string" required>
|
||||
The new name of the folder.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
###
|
||||
|
||||
### Delete Folder
|
||||
|
||||
`client.Folders().Delete(options)`
|
||||
|
||||
Delete a folder in Infisical.
|
||||
@@ -669,5 +620,6 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
|
||||
The path from where the folder should be deleted.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Checkbox, FormControl, Switch } from "@app/components/v2";
|
||||
import { Button, FormControl, Modal, ModalContent, Switch } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
@@ -16,10 +18,10 @@ import {
|
||||
useSecretSyncOption
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
||||
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||
|
||||
@@ -32,7 +34,7 @@ type Props = {
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
||||
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
||||
{ 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: "Review", key: "review", fields: [] }
|
||||
];
|
||||
@@ -42,8 +44,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
|
||||
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
|
||||
@@ -77,6 +80,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
onComplete(secretSync);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setShowConfirmation(false);
|
||||
createNotification({
|
||||
title: `Failed to add ${destinationName} Sync`,
|
||||
text: err.message,
|
||||
@@ -94,7 +98,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger, watch, control } = formMethods;
|
||||
const { handleSubmit, trigger, control } = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
@@ -102,7 +106,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
handleSubmit(onSubmit)();
|
||||
setShowConfirmation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,113 +127,146 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
const initialSyncBehavior = watch("syncOptions.initialSyncBehavior");
|
||||
if (showConfirmation)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
Secret Sync Behavior
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-bunker-200">
|
||||
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||
including associated data, not present or imported in Infisical before syncing will be
|
||||
overwritten, and changes directly in the connected service outside of infisical may also
|
||||
be overwritten by future syncs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4">
|
||||
<Button
|
||||
isDisabled={createSecretSync.isPending}
|
||||
isLoading={createSecretSync.isPending}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
I Understand
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isDisabled={createSecretSync.isPending}
|
||||
variant="plain"
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const isEnabled = await isTabEnabled(index);
|
||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
key={tab.key}
|
||||
>
|
||||
{index + 1}. {tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<SecretSyncSourceFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncDestinationFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncOptionsFields />
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAutoSyncEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? "Secrets will automatically be synced when changes occur in the source location."
|
||||
: "Secrets will not automatically be synced when changes occur in the source location. You can still trigger syncs manually."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||
id="auto-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
<>
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const isEnabled = await isTabEnabled(index);
|
||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
key={tab.key}
|
||||
>
|
||||
{index + 1}. {tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<SecretSyncSourceFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncDestinationFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncOptionsFields />
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAutoSyncEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? "Secrets will automatically be synced when changes occur in the source location."
|
||||
: "Secrets will not automatically be synced when changes occur in the source location. You can still trigger syncs manually."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
{isFinalStep &&
|
||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination && (
|
||||
<Checkbox
|
||||
id="confirm-overwrite"
|
||||
isChecked={confirmOverwrite}
|
||||
containerClassName="-mt-5"
|
||||
onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))}
|
||||
>
|
||||
<p
|
||||
className={`mt-5 text-wrap text-xs ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}
|
||||
>
|
||||
I understand all secrets present in the configured {destinationName} destination will
|
||||
be removed if they are not present within Infisical.
|
||||
</p>
|
||||
</Checkbox>
|
||||
)}
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button
|
||||
isDisabled={
|
||||
isFinalStep &&
|
||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
|
||||
!confirmOverwrite
|
||||
}
|
||||
onClick={handleNext}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
{selectedTabIndex > 0 && (
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||
id="auto-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{selectedTabIndex > 0 && (
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<Modal isOpen={showConfirmation} onOpenChange={setShowConfirmation}>
|
||||
<ModalContent
|
||||
title="Import Secrets"
|
||||
subTitle={`Import secrets into Infisical from this ${destinationName} Sync destination.`}
|
||||
>
|
||||
<div className="mt-6 flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
Secret Sync Behavior
|
||||
</div>
|
||||
<p className="mb-2 mt-1 text-sm text-bunker-200">
|
||||
Secret Syncs are the source of truth for connected third-party services. Any secret
|
||||
not present or imported in Infisical before syncing will be overwritten, and changes
|
||||
made directly in the connected service may also be overwritten by future syncs from
|
||||
Infisical.
|
||||
</p>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -8,10 +8,10 @@ import { Button, ModalClose } from "@app/components/v2";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||
|
||||
type Props = {
|
||||
|
@@ -8,13 +8,17 @@ import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsRegionSelect } from "./shared";
|
||||
|
||||
export const AwsParameterStoreSyncFields = () => {
|
||||
const { control } = useFormContext<
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField />
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("syncOptions.keyId", undefined);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||
|
@@ -0,0 +1,209 @@
|
||||
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)
|
||||
}
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
noOptionsMessage={({ inputValue }) =>
|
||||
inputValue ? undefined : (
|
||||
<p>
|
||||
To configure a KMS key, ensure the following permissions are present on the
|
||||
selected IAM role:{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:ListKeys"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:ListAliases"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Encrypt"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Decrypt"
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
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-[20vh] 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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -1,12 +1,14 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
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 = {
|
||||
hideInitialSync?: boolean;
|
||||
@@ -21,6 +23,24 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
@@ -0,0 +1 @@
|
||||
export * from "./SecretSyncOptionsFields";
|
@@ -1,17 +1,71 @@
|
||||
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 { 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 { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AwsParameterStoreSyncReviewFields = () => {
|
||||
export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
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 && tags.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AwsParameterStoreDestinationReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
const [{ region, path }] = watch(["destinationConfig"]);
|
||||
|
||||
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
||||
|
||||
|
@@ -3,15 +3,18 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
||||
import {
|
||||
AwsParameterStoreDestinationReviewFields,
|
||||
AwsParameterStoreSyncOptionsReviewFields
|
||||
} from "./AwsParameterStoreSyncReviewFields";
|
||||
import { AwsSecretsManagerSyncReviewFields } from "./AwsSecretsManagerSyncReviewFields";
|
||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||
|
||||
@@ -19,6 +22,7 @@ export const SecretSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
let DestinationFieldsComponent: ReactNode;
|
||||
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -38,7 +42,8 @@ export const SecretSyncReviewFields = () => {
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
|
||||
DestinationFieldsComponent = <AwsParameterStoreDestinationReviewFields />;
|
||||
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsReviewFields />;
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
||||
@@ -84,7 +89,7 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<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 className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Auto-Sync">
|
||||
@@ -97,6 +102,7 @@ export const SecretSyncReviewFields = () => {
|
||||
</SecretSyncLabel>
|
||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
@@ -1,16 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AwsParameterStoreSyncDestinationSchema = 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")
|
||||
export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
z.object({
|
||||
keyId: z.string().optional(),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"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")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
@@ -1,30 +1,33 @@
|
||||
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 { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
export const AwsSecretsManagerSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
)
|
||||
});
|
||||
export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
|
@@ -1,19 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AzureAppConfigurationSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||
destinationConfig: z.object({
|
||||
configurationUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Azure App Configuration URL is required" })
|
||||
.url()
|
||||
.refine(
|
||||
(val) => val.endsWith(".azconfig.io"),
|
||||
"URL should have the following format: https://resource-name-here.azconfig.io"
|
||||
),
|
||||
label: z.string().optional()
|
||||
export const AzureAppConfigurationSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||
destinationConfig: z.object({
|
||||
configurationUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Azure App Configuration URL is required" })
|
||||
.url()
|
||||
.refine(
|
||||
(val) => val.endsWith(".azconfig.io"),
|
||||
"URL should have the following format: https://resource-name-here.azconfig.io"
|
||||
),
|
||||
label: z.string().optional()
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@@ -1,10 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AzureKeyVaultSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AzureKeyVault),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required")
|
||||
export const AzureKeyVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AzureKeyVault),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z
|
||||
.string()
|
||||
.url("Invalid vault base URL format")
|
||||
.min(1, "Vault base URL required")
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@@ -0,0 +1,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()
|
||||
});
|
||||
};
|
@@ -1,10 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const DatabricksSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.Databricks),
|
||||
destinationConfig: z.object({
|
||||
scope: z.string().trim().min(1, "Databricks scope required")
|
||||
export const DatabricksSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Databricks),
|
||||
destinationConfig: z.object({
|
||||
scope: z.string().trim().min(1, "Databricks scope required")
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@@ -1,12 +1,15 @@
|
||||
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 { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
|
||||
export const GcpSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
destinationConfig: z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID required")
|
||||
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
destinationConfig: z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID required")
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@@ -1,45 +1,48 @@
|
||||
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 {
|
||||
GitHubSyncScope,
|
||||
GitHubSyncVisibility
|
||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
|
||||
export const GitHubSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.GitHub),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Organization),
|
||||
org: z.string().min(1, "Organization name required"),
|
||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||
selectedRepositoryIds: z.number().array().optional()
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Repository),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required")
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||
owner: z.string().min(1, "Repository owner 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) {
|
||||
if (
|
||||
options.visibility === GitHubSyncVisibility.Selected &&
|
||||
!options.selectedRepositoryIds?.length
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Select at least 1 repository",
|
||||
path: ["selectedRepositoryIds"]
|
||||
});
|
||||
export const GitHubSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.GitHub),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Organization),
|
||||
org: z.string().min(1, "Organization name required"),
|
||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||
selectedRepositoryIds: z.number().array().optional()
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Repository),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required")
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||
owner: z.string().min(1, "Repository owner 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) {
|
||||
if (
|
||||
options.visibility === GitHubSyncVisibility.Selected &&
|
||||
!options.selectedRepositoryIds?.length
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
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 { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
|
||||
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
|
||||
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||
|
||||
const BaseSecretSyncSchema = z.object({
|
||||
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", [
|
||||
AwsParameterStoreSyncDestinationSchema,
|
||||
AwsSecretsManagerSyncDestinationSchema,
|
||||
@@ -44,8 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
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>;
|
||||
|
2
frontend/src/hooks/api/appConnections/aws/index.ts
Normal file
2
frontend/src/hooks/api/appConnections/aws/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
42
frontend/src/hooks/api/appConnections/aws/queries.tsx
Normal file
42
frontend/src/hooks/api/appConnections/aws/queries.tsx
Normal file
@@ -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
|
||||
});
|
||||
};
|
16
frontend/src/hooks/api/appConnections/aws/types.ts
Normal file
16
frontend/src/hooks/api/appConnections/aws/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
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 { 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 & {
|
||||
destination: SecretSync.AWSParameterStore;
|
||||
@@ -13,4 +13,9 @@ export type TAwsParameterStoreSync = TRootSecretSync & {
|
||||
name: 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 { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export type RootSyncOptions = {
|
||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||
// prependPrefix?: string;
|
||||
// appendSuffix?: string;
|
||||
};
|
||||
|
||||
export type TRootSecretSync = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -24,11 +30,7 @@ export type TRootSecretSync = {
|
||||
lastRemoveJobId: string | null;
|
||||
lastRemovedAt: Date | null;
|
||||
lastRemoveMessage: string | null;
|
||||
syncOptions: {
|
||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||
// prependPrefix?: string;
|
||||
// appendSuffix?: string;
|
||||
};
|
||||
syncOptions: RootSyncOptions;
|
||||
connection: {
|
||||
app: AppConnection;
|
||||
id: string;
|
||||
|
@@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
|
||||
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
||||
currentWorkspace.id,
|
||||
{
|
||||
refetchInterval: 4000
|
||||
refetchInterval: 30000
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -37,7 +37,7 @@ const PageContent = () => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
|
||||
|
||||
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
|
||||
refetchInterval: 4000
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
@@ -66,7 +66,7 @@ const PageContent = () => {
|
||||
|
||||
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
||||
|
||||
// const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||
const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||
|
||||
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
||||
|
||||
@@ -108,10 +108,7 @@ const PageContent = () => {
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
||||
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
||||
<SecretSyncOptionsSection
|
||||
secretSync={secretSync}
|
||||
// onEditOptions={handleEditOptions}
|
||||
/>
|
||||
<SecretSyncOptionsSection secretSync={secretSync} onEditOptions={handleEditOptions} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<SecretSyncDestinationSection
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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 && tags.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from "./SecretSyncOptionsSection";
|
Reference in New Issue
Block a user