Compare commits
31 Commits
daniel/go-
...
doc/add-ab
Author | SHA1 | Date | |
---|---|---|---|
fb030401ab | |||
f4bd48fd1d | |||
177ccf6c9e | |||
9200137d6c | |||
a196028064 | |||
0c0e20f00e | |||
710429c805 | |||
c121bd930b | |||
87d383a9c4 | |||
6e590a78a0 | |||
ab4b6c17b3 | |||
27cd40c8ce | |||
5f089e0b9d | |||
19940522aa | |||
28b18c1cb1 | |||
7ae2cc2db8 | |||
4a51b4d619 | |||
478e0c5ff5 | |||
5c08136fca | |||
cb8528adc4 | |||
d7935d30ce | |||
ac3bab3074 | |||
63b8301065 | |||
babe70e00f | |||
f23ea0991c | |||
f8ab2bcdfd | |||
9cdb4dcde9 | |||
69fb87bbfc | |||
b0cd5bd10d | |||
15119ffda9 | |||
4df409e627 |
@ -1722,6 +1722,18 @@ export const SecretSyncs = {
|
|||||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
ADDITIONAL_SYNC_OPTIONS: {
|
||||||
|
AWS_PARAMETER_STORE: {
|
||||||
|
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||||
|
tags: "Optional resource tags to add to parameters synced by Infisical.",
|
||||||
|
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.`
|
||||||
|
},
|
||||||
|
AWS_SECRETS_MANAGER: {
|
||||||
|
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||||
|
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||||
|
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||||
|
}
|
||||||
|
},
|
||||||
DESTINATION_CONFIG: {
|
DESTINATION_CONFIG: {
|
||||||
AWS_PARAMETER_STORE: {
|
AWS_PARAMETER_STORE: {
|
||||||
region: "The AWS region to sync secrets to.",
|
region: "The AWS region to sync secrets to.",
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
CreateAwsConnectionSchema,
|
CreateAwsConnectionSchema,
|
||||||
SanitizedAwsConnectionSchema,
|
SanitizedAwsConnectionSchema,
|
||||||
UpdateAwsConnectionSchema
|
UpdateAwsConnectionSchema
|
||||||
} from "@app/services/app-connection/aws";
|
} from "@app/services/app-connection/aws";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
registerAppConnectionEndpoints({
|
registerAppConnectionEndpoints({
|
||||||
app: AppConnection.AWS,
|
app: AppConnection.AWS,
|
||||||
server,
|
server,
|
||||||
@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
createSchema: CreateAwsConnectionSchema,
|
createSchema: CreateAwsConnectionSchema,
|
||||||
updateSchema: UpdateAwsConnectionSchema
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/kms-keys`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
region: z.nativeEnum(AWSRegion),
|
||||||
|
destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager])
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
kmsKeys: z.object({ alias: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const kmsKeys = await server.services.appConnection.aws.listKmsKeys(
|
||||||
|
{
|
||||||
|
connectionId,
|
||||||
|
...req.query
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
return { kmsKeys };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -22,18 +22,19 @@ import {
|
|||||||
TUpdateAppConnectionDTO,
|
TUpdateAppConnectionDTO,
|
||||||
TValidateAppConnectionCredentials
|
TValidateAppConnectionCredentials
|
||||||
} from "@app/services/app-connection/app-connection-types";
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
|
|
||||||
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
|
|
||||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
|
||||||
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
|
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||||
|
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||||
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
|
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||||
|
import { githubConnectionService } from "./github/github-connection-service";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
listAvailableAppConnectionsForUser,
|
listAvailableAppConnectionsForUser,
|
||||||
github: githubConnectionService(connectAppConnectionById),
|
github: githubConnectionService(connectAppConnectionById),
|
||||||
gcp: gcpConnectionService(connectAppConnectionById),
|
gcp: gcpConnectionService(connectAppConnectionById),
|
||||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
aws: awsConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
TAwsConnection,
|
TAwsConnection,
|
||||||
TAwsConnectionConfig,
|
TAwsConnectionConfig,
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
TGitHubConnectionInput,
|
TGitHubConnectionInput,
|
||||||
TValidateGitHubConnectionCredentials
|
TValidateGitHubConnectionCredentials
|
||||||
} from "@app/services/app-connection/github";
|
} from "@app/services/app-connection/github";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TAzureAppConfigurationConnection,
|
TAzureAppConfigurationConnection,
|
||||||
@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
|
|||||||
| TValidateAzureKeyVaultConnectionCredentials
|
| TValidateAzureKeyVaultConnectionCredentials
|
||||||
| TValidateAzureAppConfigurationConnectionCredentials
|
| TValidateAzureAppConfigurationConnectionCredentials
|
||||||
| TValidateDatabricksConnectionCredentials;
|
| TValidateDatabricksConnectionCredentials;
|
||||||
|
|
||||||
|
export type TListAwsConnectionKmsKeys = {
|
||||||
|
connectionId: string;
|
||||||
|
region: AWSRegion;
|
||||||
|
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,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
|
||||||
|
};
|
||||||
|
};
|
@ -2257,7 +2257,9 @@ const syncSecretsFlyio = async ({
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await request.post(
|
type TFlyioErrors = { message: string }[];
|
||||||
|
|
||||||
|
const setSecretsResp = await request.post<{ errors?: TFlyioErrors }>(
|
||||||
IntegrationUrls.FLYIO_API_URL,
|
IntegrationUrls.FLYIO_API_URL,
|
||||||
{
|
{
|
||||||
query: SetSecrets,
|
query: SetSecrets,
|
||||||
@ -2279,6 +2281,10 @@ const syncSecretsFlyio = async ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (setSecretsResp.data.errors?.length) {
|
||||||
|
throw new Error(JSON.stringify(setSecretsResp.data.errors));
|
||||||
|
}
|
||||||
|
|
||||||
// get secrets
|
// get secrets
|
||||||
interface FlyioSecret {
|
interface FlyioSecret {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -34,6 +34,25 @@ export const secretSharingServiceFactory = ({
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
|
const $validateSharedSecretExpiry = (expiresAt: string) => {
|
||||||
|
if (new Date(expiresAt) < new Date()) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Expiry Time to 1 month
|
||||||
|
const expiryTime = new Date(expiresAt).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
if (expiryTime - currentTime > thirtyDays) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fiveMins = 5 * 60 * 1000;
|
||||||
|
if (expiryTime - currentTime < fiveMins) {
|
||||||
|
throw new BadRequestError({ message: "Expiration time cannot be less than 5 mins" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createSharedSecret = async ({
|
const createSharedSecret = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -49,18 +68,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
}: TCreateSharedSecretDTO) => {
|
}: TCreateSharedSecretDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||||
|
$validateSharedSecretExpiry(expiresAt);
|
||||||
if (new Date(expiresAt) < new Date()) {
|
|
||||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit Expiry Time to 1 month
|
|
||||||
const expiryTime = new Date(expiresAt).getTime();
|
|
||||||
const currentTime = new Date().getTime();
|
|
||||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
if (expiryTime - currentTime > thirtyDays) {
|
|
||||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secretValue.length > 10_000) {
|
if (secretValue.length > 10_000) {
|
||||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
@ -100,17 +108,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
}: TCreatePublicSharedSecretDTO) => {
|
}: TCreatePublicSharedSecretDTO) => {
|
||||||
if (new Date(expiresAt) < new Date()) {
|
$validateSharedSecretExpiry(expiresAt);
|
||||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit Expiry Time to 1 month
|
|
||||||
const expiryTime = new Date(expiresAt).getTime();
|
|
||||||
const currentTime = new Date().getTime();
|
|
||||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
if (expiryTime - currentTime > thirtyDays) {
|
|
||||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
|
@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
|||||||
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
||||||
|
|
||||||
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
||||||
|
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
||||||
|
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const BATCH_SIZE = 10;
|
const BATCH_SIZE = 10;
|
||||||
@ -80,6 +82,129 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
|||||||
return awsParameterStoreSecretsRecord;
|
return awsParameterStoreSecretsRecord;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||||
|
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
|
||||||
|
let hasNext = true;
|
||||||
|
let nextToken: string | undefined;
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (hasNext) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const parameters = await ssm
|
||||||
|
.describeParameters({
|
||||||
|
MaxResults: 10,
|
||||||
|
NextToken: nextToken,
|
||||||
|
ParameterFilters: [
|
||||||
|
{
|
||||||
|
Key: "Path",
|
||||||
|
Option: "OneLevel",
|
||||||
|
Values: [path]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
attempt = 0;
|
||||||
|
|
||||||
|
if (parameters.Parameters) {
|
||||||
|
parameters.Parameters.forEach((parameter) => {
|
||||||
|
if (parameter.Name) {
|
||||||
|
// no leading slash if path is '/'
|
||||||
|
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||||
|
awsParameterStoreMetadataRecord[secKey] = parameter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = Boolean(parameters.NextToken);
|
||||||
|
nextToken = parameters.NextToken;
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
attempt += 1;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep();
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsParameterStoreMetadataRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParameterStoreTagsRecord = async (
|
||||||
|
ssm: AWS.SSM,
|
||||||
|
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 (
|
const putParameter = async (
|
||||||
ssm: AWS.SSM,
|
ssm: AWS.SSM,
|
||||||
params: AWS.SSM.PutParameterRequest,
|
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 (
|
const deleteParametersBatch = async (
|
||||||
ssm: AWS.SSM,
|
ssm: AWS.SSM,
|
||||||
parameters: AWS.SSM.Parameter[],
|
parameters: AWS.SSM.Parameter[],
|
||||||
@ -132,29 +293,45 @@ const deleteParametersBatch = async (
|
|||||||
|
|
||||||
export const AwsParameterStoreSyncFns = {
|
export const AwsParameterStoreSyncFns = {
|
||||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const { destinationConfig } = secretSync;
|
const { destinationConfig, syncOptions } = secretSync;
|
||||||
|
|
||||||
const ssm = await getSSM(secretSync);
|
const ssm = await getSSM(secretSync);
|
||||||
|
|
||||||
// TODO(scott): KMS Key ID, Tags
|
|
||||||
|
|
||||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||||
|
|
||||||
for await (const entry of Object.entries(secretMap)) {
|
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||||
const [key, { value }] = entry;
|
|
||||||
|
|
||||||
// skip empty values (not allowed by AWS) or secrets that haven't changed
|
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||||
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
|
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
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyId = syncOptions.keyId ?? "alias/aws/ssm";
|
||||||
|
|
||||||
|
// create parameter or update if changed
|
||||||
|
if (
|
||||||
|
!(key in awsParameterStoreSecretsRecord) ||
|
||||||
|
value !== awsParameterStoreSecretsRecord[key].Value ||
|
||||||
|
keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await putParameter(ssm, {
|
await putParameter(ssm, {
|
||||||
Name: `${destinationConfig.path}${key}`,
|
Name: `${destinationConfig.path}${key}`,
|
||||||
Type: "SecureString",
|
Type: "SecureString",
|
||||||
Value: value,
|
Value: value,
|
||||||
Overwrite: true
|
Overwrite: true,
|
||||||
|
KeyId: keyId
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
@ -164,6 +341,47 @@ export const AwsParameterStoreSyncFns = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
||||||
|
|
||||||
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
GenericCreateSecretSyncFieldsSchema,
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
GenericUpdateSecretSyncFieldsSchema
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||||
@ -20,19 +21,68 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
|||||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({
|
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||||
|
keyId: z
|
||||||
|
.string()
|
||||||
|
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||||
|
.min(1, "Invalid KMS Key ID")
|
||||||
|
.max(256, "Invalid KMS Key ID")
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||||
|
tags: z
|
||||||
|
.object({
|
||||||
|
key: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
|
"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 key 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: "Resource tag keys must be unique"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
||||||
|
syncSecretMetadataAsTags: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags)
|
||||||
|
});
|
||||||
|
|
||||||
|
const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(
|
||||||
|
SecretSync.AWSParameterStore,
|
||||||
|
AwsParameterStoreSyncOptionsConfig,
|
||||||
|
AwsParameterStoreSyncOptionsSchema
|
||||||
|
).extend({
|
||||||
destination: z.literal(SecretSync.AWSParameterStore),
|
destination: z.literal(SecretSync.AWSParameterStore),
|
||||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSParameterStore
|
SecretSync.AWSParameterStore,
|
||||||
|
AwsParameterStoreSyncOptionsConfig,
|
||||||
|
AwsParameterStoreSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSParameterStore
|
SecretSync.AWSParameterStore,
|
||||||
|
AwsParameterStoreSyncOptionsConfig,
|
||||||
|
AwsParameterStoreSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
|
import { UntagResourceCommandOutput } from "@aws-sdk/client-kms";
|
||||||
import {
|
import {
|
||||||
BatchGetSecretValueCommand,
|
BatchGetSecretValueCommand,
|
||||||
CreateSecretCommand,
|
CreateSecretCommand,
|
||||||
CreateSecretCommandInput,
|
CreateSecretCommandInput,
|
||||||
DeleteSecretCommand,
|
DeleteSecretCommand,
|
||||||
DeleteSecretResponse,
|
DeleteSecretResponse,
|
||||||
|
DescribeSecretCommand,
|
||||||
|
DescribeSecretCommandInput,
|
||||||
ListSecretsCommand,
|
ListSecretsCommand,
|
||||||
SecretsManagerClient,
|
SecretsManagerClient,
|
||||||
|
TagResourceCommand,
|
||||||
|
TagResourceCommandOutput,
|
||||||
|
UntagResourceCommand,
|
||||||
UpdateSecretCommand,
|
UpdateSecretCommand,
|
||||||
UpdateSecretCommandInput
|
UpdateSecretCommandInput
|
||||||
} from "@aws-sdk/client-secrets-manager";
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
import { AWSError } from "aws-sdk";
|
import { AWSError } from "aws-sdk";
|
||||||
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
import {
|
||||||
|
CreateSecretResponse,
|
||||||
|
DescribeSecretResponse,
|
||||||
|
SecretListEntry,
|
||||||
|
SecretValueEntry,
|
||||||
|
Tag
|
||||||
|
} from "aws-sdk/clients/secretsmanager";
|
||||||
|
|
||||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||||
@ -21,6 +33,7 @@ import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-syn
|
|||||||
|
|
||||||
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||||
|
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const BATCH_SIZE = 20;
|
const BATCH_SIZE = 20;
|
||||||
@ -135,6 +148,46 @@ const getSecretValuesRecord = async (
|
|||||||
return awsSecretValuesRecord;
|
return awsSecretValuesRecord;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const describeSecret = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
input: DescribeSecretCommandInput,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<DescribeSecretResponse> => {
|
||||||
|
try {
|
||||||
|
return await client.send(new DescribeSecretCommand(input));
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return describeSecret(client, input, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretDescriptionsRecord = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
awsSecretsRecord: TAwsSecretsRecord
|
||||||
|
): Promise<TAwsSecretDescriptionsRecord> => {
|
||||||
|
const awsSecretDescriptionsRecord: TAwsSecretValuesRecord = {};
|
||||||
|
|
||||||
|
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||||
|
try {
|
||||||
|
awsSecretDescriptionsRecord[secretKey] = await describeSecret(client, {
|
||||||
|
SecretId: secretKey
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
secretKey,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsSecretDescriptionsRecord;
|
||||||
|
};
|
||||||
|
|
||||||
const createSecret = async (
|
const createSecret = async (
|
||||||
client: SecretsManagerClient,
|
client: SecretsManagerClient,
|
||||||
input: CreateSecretCommandInput,
|
input: CreateSecretCommandInput,
|
||||||
@ -189,9 +242,71 @@ const deleteSecret = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addTags = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
secretKey: string,
|
||||||
|
tags: Tag[],
|
||||||
|
attempt = 0
|
||||||
|
): Promise<TagResourceCommandOutput> => {
|
||||||
|
try {
|
||||||
|
return await client.send(new TagResourceCommand({ SecretId: secretKey, Tags: tags }));
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return addTags(client, secretKey, tags, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTags = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
secretKey: string,
|
||||||
|
tagKeys: string[],
|
||||||
|
attempt = 0
|
||||||
|
): Promise<UntagResourceCommandOutput> => {
|
||||||
|
try {
|
||||||
|
return await client.send(new UntagResourceCommand({ SecretId: secretKey, TagKeys: tagKeys }));
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return removeTags(client, secretKey, tagKeys, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processTags = ({
|
||||||
|
syncTagsRecord,
|
||||||
|
awsTagsRecord
|
||||||
|
}: {
|
||||||
|
syncTagsRecord: Record<string, string>;
|
||||||
|
awsTagsRecord: Record<string, string>;
|
||||||
|
}) => {
|
||||||
|
const tagsToAdd: Tag[] = [];
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncFns = {
|
export const AwsSecretsManagerSyncFns = {
|
||||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const { destinationConfig } = secretSync;
|
const { destinationConfig, syncOptions } = secretSync;
|
||||||
|
|
||||||
const client = await getSecretsManagerClient(secretSync);
|
const client = await getSecretsManagerClient(secretSync);
|
||||||
|
|
||||||
@ -199,9 +314,15 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
|
|
||||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||||
|
|
||||||
|
const awsDescriptionsRecord = await getSecretDescriptionsRecord(client, awsSecretsRecord);
|
||||||
|
|
||||||
|
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||||
|
|
||||||
|
const keyId = syncOptions.keyId ?? "alias/aws/secretsmanager";
|
||||||
|
|
||||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||||
for await (const entry of Object.entries(secretMap)) {
|
for await (const entry of Object.entries(secretMap)) {
|
||||||
const [key, { value }] = entry;
|
const [key, { value, secretMetadata }] = entry;
|
||||||
|
|
||||||
// skip secrets that don't have a value set
|
// skip secrets that don't have a value set
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -211,15 +332,12 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
|
|
||||||
if (awsSecretsRecord[key]) {
|
if (awsSecretsRecord[key]) {
|
||||||
// skip secrets that haven't changed
|
// skip secrets that haven't changed
|
||||||
if (awsValuesRecord[key]?.SecretString === value) {
|
if (awsValuesRecord[key]?.SecretString !== value || keyId !== awsDescriptionsRecord[key]?.KmsKeyId) {
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateSecret(client, {
|
await updateSecret(client, {
|
||||||
SecretId: key,
|
SecretId: key,
|
||||||
SecretString: value
|
SecretString: value,
|
||||||
|
KmsKeyId: keyId
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
@ -227,11 +345,13 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
secretKey: key
|
secretKey: key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await createSecret(client, {
|
await createSecret(client, {
|
||||||
Name: key,
|
Name: key,
|
||||||
SecretString: value
|
SecretString: value,
|
||||||
|
KmsKeyId: keyId
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
@ -240,6 +360,40 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||||
|
syncTagsRecord: {
|
||||||
|
// configured sync tags take preference over secret metadata
|
||||||
|
...(syncOptions.syncSecretMetadataAsTags &&
|
||||||
|
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||||
|
...syncTagsRecord
|
||||||
|
},
|
||||||
|
awsTagsRecord: Object.fromEntries(
|
||||||
|
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tagsToAdd.length) {
|
||||||
|
try {
|
||||||
|
await addTags(client, key, tagsToAdd);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagKeysToRemove.length) {
|
||||||
|
try {
|
||||||
|
await removeTags(client, key, tagKeysToRemove);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||||
@ -261,17 +415,48 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
if (awsSecretsRecord[destinationConfig.secretName]) {
|
||||||
await updateSecret(client, {
|
await updateSecret(client, {
|
||||||
SecretId: destinationConfig.secretName,
|
SecretId: destinationConfig.secretName,
|
||||||
SecretString: secretValue
|
SecretString: secretValue,
|
||||||
|
KmsKeyId: keyId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createSecret(client, {
|
await createSecret(client, {
|
||||||
Name: destinationConfig.secretName,
|
Name: destinationConfig.secretName,
|
||||||
SecretString: secretValue
|
SecretString: secretValue,
|
||||||
|
KmsKeyId: keyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||||
|
syncTagsRecord,
|
||||||
|
awsTagsRecord: Object.fromEntries(
|
||||||
|
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tagsToAdd.length) {
|
||||||
|
try {
|
||||||
|
await addTags(client, destinationConfig.secretName, tagsToAdd);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: destinationConfig.secretName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagKeysToRemove.length) {
|
||||||
|
try {
|
||||||
|
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: destinationConfig.secretName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
GenericCreateSecretSyncFieldsSchema,
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
GenericUpdateSecretSyncFieldsSchema
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||||
.discriminatedUnion("mappingBehavior", [
|
.discriminatedUnion("mappingBehavior", [
|
||||||
@ -38,22 +39,95 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
const AwsSecretsManagerSyncOptionsSchema = 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_SECRETS_MANAGER.keyId),
|
||||||
|
tags: z
|
||||||
|
.object({
|
||||||
|
key: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
|
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
)
|
||||||
|
.min(1, "Tag key required")
|
||||||
|
.max(128, "Tag key cannot exceed 128 characters"),
|
||||||
|
value: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
|
"Invalid tag value: tag 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)
|
||||||
|
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||||
|
message: "Tag keys must be unique"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.tags),
|
||||||
|
syncSecretMetadataAsTags: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.syncSecretMetadataAsTags)
|
||||||
|
});
|
||||||
|
|
||||||
|
const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||||
|
SecretSync.AWSSecretsManager,
|
||||||
|
AwsSecretsManagerSyncOptionsConfig,
|
||||||
|
AwsSecretsManagerSyncOptionsSchema
|
||||||
|
).extend({
|
||||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSSecretsManager
|
SecretSync.AWSSecretsManager,
|
||||||
).extend({
|
AwsSecretsManagerSyncOptionsConfig,
|
||||||
|
AwsSecretsManagerSyncOptionsSchema
|
||||||
|
)
|
||||||
|
.extend({
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
});
|
})
|
||||||
|
.superRefine((sync, ctx) => {
|
||||||
|
if (
|
||||||
|
sync.destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||||
|
sync.syncOptions.syncSecretMetadataAsTags
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSSecretsManager
|
SecretSync.AWSSecretsManager,
|
||||||
).extend({
|
AwsSecretsManagerSyncOptionsConfig,
|
||||||
|
AwsSecretsManagerSyncOptionsSchema
|
||||||
|
)
|
||||||
|
.extend({
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||||
});
|
})
|
||||||
|
.superRefine((sync, ctx) => {
|
||||||
|
if (
|
||||||
|
sync.destinationConfig?.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||||
|
sync.syncOptions.syncSecretMetadataAsTags
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||||
name: z.literal("AWS Secrets Manager"),
|
name: z.literal("AWS Secrets Manager"),
|
||||||
|
@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||||
|
secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretMap[importedSecret.key] = {
|
secretMap[importedSecret.key] = {
|
||||||
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||||
comment: importedSecret.secretComment,
|
comment: importedSecret.secretComment,
|
||||||
value: importedSecret.secretValue || ""
|
value: importedSecret.secretValue || "",
|
||||||
|
secretMetadata: importedSecret.secretMetadata
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { AnyZodObject, z } from "zod";
|
||||||
|
|
||||||
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
|
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
|
||||||
import { SecretSyncs } from "@app/lib/api-docs";
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
@ -8,34 +8,45 @@ import { SecretSync, SecretSyncInitialSyncBehavior } from "@app/services/secret-
|
|||||||
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) =>
|
const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
|
||||||
z.object({
|
destination,
|
||||||
initialSyncBehavior: (options.canImportSecrets
|
syncOptionsConfig: { canImportSecrets },
|
||||||
|
merge,
|
||||||
|
isUpdateSchema
|
||||||
|
}: {
|
||||||
|
destination: SecretSync;
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig;
|
||||||
|
merge?: T;
|
||||||
|
isUpdateSchema?: boolean;
|
||||||
|
}) => {
|
||||||
|
const baseSchema = z.object({
|
||||||
|
initialSyncBehavior: (canImportSecrets
|
||||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior)
|
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
|
||||||
// prependPrefix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional()
|
|
||||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX),
|
|
||||||
// appendSuffix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional()
|
|
||||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
const schema = merge ? baseSchema.merge(merge) : baseSchema;
|
||||||
|
|
||||||
|
return (
|
||||||
|
isUpdateSchema
|
||||||
|
? schema.describe(SecretSyncs.UPDATE(destination).syncOptions).optional()
|
||||||
|
: schema.describe(SecretSyncs.CREATE(destination).syncOptions)
|
||||||
|
) as T extends AnyZodObject ? z.ZodObject<z.objectUtil.MergeShapes<typeof schema.shape, T["shape"]>> : typeof schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
destination: SecretSync,
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig,
|
||||||
|
merge?: T
|
||||||
|
) =>
|
||||||
SecretSyncsSchema.omit({
|
SecretSyncsSchema.omit({
|
||||||
destination: true,
|
destination: true,
|
||||||
destinationConfig: true,
|
destinationConfig: true,
|
||||||
syncOptions: true
|
syncOptions: true
|
||||||
}).extend({
|
}).extend({
|
||||||
// destination needs to be on the extended object for type differentiation
|
// destination needs to be on the extended object for type differentiation
|
||||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig),
|
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }),
|
||||||
// join properties
|
// join properties
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
connection: z.object({
|
connection: z.object({
|
||||||
@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?
|
|||||||
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
export const GenericCreateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
destination: SecretSync,
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig,
|
||||||
|
merge?: T
|
||||||
|
) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
|
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
|
||||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId),
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId),
|
||||||
@ -66,10 +81,14 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
|||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(SecretSyncs.CREATE(destination).secretPath),
|
.describe(SecretSyncs.CREATE(destination).secretPath),
|
||||||
isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
|
isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
|
||||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions)
|
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
export const GenericUpdateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||||
|
destination: SecretSync,
|
||||||
|
syncOptionsConfig: TSyncOptionsConfig,
|
||||||
|
merge?: T
|
||||||
|
) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
||||||
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
||||||
@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(SecretSyncs.UPDATE(destination).secretPath),
|
.describe(SecretSyncs.UPDATE(destination).secretPath),
|
||||||
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
|
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
|
||||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig)
|
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true })
|
||||||
.optional()
|
|
||||||
.describe(SecretSyncs.UPDATE(destination).syncOptions)
|
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { Job } from "bullmq";
|
|||||||
|
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { QueueJobs } from "@app/queue";
|
import { QueueJobs } from "@app/queue";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import {
|
import {
|
||||||
TAwsSecretsManagerSync,
|
TAwsSecretsManagerSync,
|
||||||
TAwsSecretsManagerSyncInput,
|
TAwsSecretsManagerSyncInput,
|
||||||
@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job<
|
|||||||
|
|
||||||
export type TSecretMap = Record<
|
export type TSecretMap = Record<
|
||||||
string,
|
string,
|
||||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
{
|
||||||
|
value: string;
|
||||||
|
comment?: string;
|
||||||
|
skipMultilineEncoding?: boolean | null | undefined;
|
||||||
|
secretMetadata?: ResourceMetadataDTO;
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
|
BIN
docs/images/app-connections/aws/kms-key-user.png
Normal file
After Width: | Height: | Size: 492 KiB |
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 500 KiB |
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 523 KiB |
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 885 KiB |
Before Width: | Height: | Size: 832 KiB After Width: | Height: | Size: 878 KiB |
@ -82,22 +82,26 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"Sid": "AllowSecretsManagerAccess",
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
|
"secretsmanager:ListSecrets",
|
||||||
"secretsmanager:GetSecretValue",
|
"secretsmanager:GetSecretValue",
|
||||||
|
"secretsmanager:BatchGetSecretValue",
|
||||||
"secretsmanager:CreateSecret",
|
"secretsmanager:CreateSecret",
|
||||||
"secretsmanager:UpdateSecret",
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DeleteSecret",
|
||||||
"secretsmanager:DescribeSecret",
|
"secretsmanager:DescribeSecret",
|
||||||
"secretsmanager:TagResource",
|
"secretsmanager:TagResource",
|
||||||
"secretsmanager:UntagResource",
|
"secretsmanager:UntagResource",
|
||||||
"kms:ListKeys",
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:ListAliases",
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"kms:Encrypt",
|
"kms:Decrypt", // if you need to specify the KMS key
|
||||||
"kms:Decrypt"
|
"kms:DescribeKey" // if you need to specify the KMS key
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="AWS Parameter Store">
|
<Accordion title="AWS Parameter Store">
|
||||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||||
@ -113,22 +117,24 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"ssm:PutParameter",
|
"ssm:PutParameter",
|
||||||
"ssm:DeleteParameter",
|
|
||||||
"ssm:GetParameters",
|
"ssm:GetParameters",
|
||||||
"ssm:GetParametersByPath",
|
"ssm:GetParametersByPath",
|
||||||
"ssm:DescribeParameters",
|
"ssm:DescribeParameters",
|
||||||
"ssm:DeleteParameters",
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:ListTagsForResource", // if you need to add tags to secrets
|
||||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
"kms:ListKeys", // if you need to specify the KMS key
|
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
|
||||||
"kms:ListAliases", // if you need to specify the KMS key
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:Encrypt", // if you need to specify the KMS key
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"kms:Decrypt" // if you need to specify the KMS key
|
"kms:Decrypt", // if you need to specify the KMS key
|
||||||
|
"kms:DescribeKey" // if you need to specify the KMS key
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
@ -223,22 +229,26 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"Sid": "AllowSecretsManagerAccess",
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
|
"secretsmanager:ListSecrets",
|
||||||
"secretsmanager:GetSecretValue",
|
"secretsmanager:GetSecretValue",
|
||||||
|
"secretsmanager:BatchGetSecretValue",
|
||||||
"secretsmanager:CreateSecret",
|
"secretsmanager:CreateSecret",
|
||||||
"secretsmanager:UpdateSecret",
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DeleteSecret",
|
||||||
"secretsmanager:DescribeSecret",
|
"secretsmanager:DescribeSecret",
|
||||||
"secretsmanager:TagResource",
|
"secretsmanager:TagResource",
|
||||||
"secretsmanager:UntagResource",
|
"secretsmanager:UntagResource",
|
||||||
"kms:ListKeys",
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:ListAliases",
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"kms:Encrypt",
|
"kms:Decrypt", // if you need to specify the KMS key
|
||||||
"kms:Decrypt"
|
"kms:DescribeKey" // if you need to specify the KMS key
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="AWS Parameter Store">
|
<Accordion title="AWS Parameter Store">
|
||||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||||
@ -254,22 +264,24 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"ssm:PutParameter",
|
"ssm:PutParameter",
|
||||||
"ssm:DeleteParameter",
|
|
||||||
"ssm:GetParameters",
|
"ssm:GetParameters",
|
||||||
"ssm:GetParametersByPath",
|
"ssm:GetParametersByPath",
|
||||||
"ssm:DescribeParameters",
|
"ssm:DescribeParameters",
|
||||||
"ssm:DeleteParameters",
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:ListTagsForResource", // if you need to add tags to secrets
|
||||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
"kms:ListKeys", // if you need to specify the KMS key
|
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
|
||||||
"kms:ListAliases", // if you need to specify the KMS key
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:Encrypt", // if you need to specify the KMS key
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"kms:Decrypt" // if you need to specify the KMS key
|
"kms:Decrypt", // if you need to specify the KMS key
|
||||||
|
"kms:DescribeKey" // if you need to specify the KMS key
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
32
docs/integrations/frameworks/ab-initio.mdx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: "AB Initio"
|
||||||
|
description: "How to use Infisical secrets in AB Initio."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical](https://app.infisical.com).
|
||||||
|
- Install the [Infisical CLI](https://infisical.com/docs/cli/overview) to your server.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Authorize Infisical for AB Initio">
|
||||||
|
Create a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) in Infisical and give it the appropriate read permissions for the desired project and secret paths.
|
||||||
|
</Step>
|
||||||
|
<Step title="Add Infisical CLI to your workflow">
|
||||||
|
Update your AB Initio workflows to use Infisical CLI to inject Infisical secrets as environment variables.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login using the machine identity. Modify this accordingly based on the authentication method used.
|
||||||
|
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain)
|
||||||
|
|
||||||
|
# Fetch secrets from Infisical
|
||||||
|
infisical export --projectId="<>" --env="prod" > infisical.env
|
||||||
|
|
||||||
|
# Inject secrets as environment variables
|
||||||
|
source infisical.env
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
@ -40,6 +40,10 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
|||||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
||||||
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
||||||
|
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
|
||||||
|
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
|
||||||
|
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
|
||||||
|
<Note>Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.</Note>
|
||||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||||
|
|
||||||
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
||||||
|
@ -43,6 +43,9 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
|
|||||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
|
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
|
||||||
- **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
- **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
||||||
|
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
|
||||||
|
- **Tags**: Optional tags to add to secrets synced by Infisical.
|
||||||
|
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.
|
||||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||||
|
|
||||||
6. Configure the **Details** of your Secrets Manager Sync, then click **Next**.
|
6. Configure the **Details** of your Secrets Manager 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>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.
|
- <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 made directly in the connected service outside of infisical may also
|
||||||
|
be overwritten by future syncs.
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
Some third-party services do not support importing secrets.
|
Some third-party services do not support importing secrets.
|
||||||
</Info>
|
</Info>
|
||||||
|
@ -515,7 +515,8 @@
|
|||||||
"integrations/frameworks/laravel",
|
"integrations/frameworks/laravel",
|
||||||
"integrations/frameworks/rails",
|
"integrations/frameworks/rails",
|
||||||
"integrations/frameworks/dotnet",
|
"integrations/frameworks/dotnet",
|
||||||
"integrations/platforms/pm2"
|
"integrations/platforms/pm2",
|
||||||
|
"integrations/frameworks/ab-initio"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
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 { Tab } from "@headlessui/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Checkbox, FormControl, Switch } from "@app/components/v2";
|
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import {
|
import {
|
||||||
@ -16,10 +18,10 @@ import {
|
|||||||
useSecretSyncOption
|
useSecretSyncOption
|
||||||
} from "@app/hooks/api/secretSyncs";
|
} from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||||
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
||||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
|
||||||
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
||||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ type Props = {
|
|||||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
||||||
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
||||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||||
{ name: "Options", key: "options", fields: ["syncOptions"] },
|
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||||
{ name: "Review", key: "review", fields: [] }
|
{ name: "Review", key: "review", fields: [] }
|
||||||
];
|
];
|
||||||
@ -42,8 +44,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||||
|
|
||||||
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
|
|
||||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||||
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
|
|
||||||
|
|
||||||
const { syncOption } = useSecretSyncOption(destination);
|
const { syncOption } = useSecretSyncOption(destination);
|
||||||
|
|
||||||
@ -77,6 +80,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
onComplete(secretSync);
|
onComplete(secretSync);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setShowConfirmation(false);
|
||||||
createNotification({
|
createNotification({
|
||||||
title: `Failed to add ${destinationName} Sync`,
|
title: `Failed to add ${destinationName} Sync`,
|
||||||
text: err.message,
|
text: err.message,
|
||||||
@ -94,7 +98,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
setSelectedTabIndex((prev) => prev - 1);
|
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);
|
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||||
|
|
||||||
@ -102,7 +106,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
if (isFinalStep) {
|
if (isFinalStep) {
|
||||||
handleSubmit(onSubmit)();
|
setShowConfirmation(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +127,42 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
return isEnabled;
|
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 made 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 (
|
return (
|
||||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||||
@ -174,7 +213,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||||
id="auto-sync-enabled"
|
id="auto-sync-enabled"
|
||||||
thumbClassName="bg-mineshaft-800"
|
thumbClassName="bg-mineshaft-800"
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
@ -196,32 +235,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</FormProvider>
|
</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">
|
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||||
<Button
|
<Button onClick={handleNext} colorSchema="secondary">
|
||||||
isDisabled={
|
|
||||||
isFinalStep &&
|
|
||||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
|
|
||||||
!confirmOverwrite
|
|
||||||
}
|
|
||||||
onClick={handleNext}
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
{isFinalStep ? "Create Sync" : "Next"}
|
{isFinalStep ? "Create Sync" : "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
{selectedTabIndex > 0 && (
|
{selectedTabIndex > 0 && (
|
||||||
|
@ -8,10 +8,10 @@ import { Button, ModalClose } from "@app/components/v2";
|
|||||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||||
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
||||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
|
||||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -8,13 +8,17 @@ import { TSecretSyncForm } from "../schemas";
|
|||||||
import { AwsRegionSelect } from "./shared";
|
import { AwsRegionSelect } from "./shared";
|
||||||
|
|
||||||
export const AwsParameterStoreSyncFields = () => {
|
export const AwsParameterStoreSyncFields = () => {
|
||||||
const { control } = useFormContext<
|
const { control, setValue } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SecretSyncConnectionField />
|
<SecretSyncConnectionField
|
||||||
|
onChange={() => {
|
||||||
|
setValue("syncOptions.keyId", undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||||
|
@ -9,7 +9,7 @@ import { TSecretSyncForm } from "../schemas";
|
|||||||
import { AwsRegionSelect } from "./shared";
|
import { AwsRegionSelect } from "./shared";
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncFields = () => {
|
export const AwsSecretsManagerSyncFields = () => {
|
||||||
const { control, watch } = useFormContext<
|
const { control, watch, setValue } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@ -59,7 +59,10 @@ export const AwsSecretsManagerSyncFields = () => {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => onChange(val)}
|
onValueChange={(val) => {
|
||||||
|
onChange(val);
|
||||||
|
setValue("syncOptions.syncSecretMetadataAsTags", false);
|
||||||
|
}}
|
||||||
className="w-full border border-mineshaft-500 capitalize"
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
position="popper"
|
position="popper"
|
||||||
placeholder="Select an option..."
|
placeholder="Select an option..."
|
||||||
|
@ -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:ListAliases"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:DescribeKey"
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,208 @@
|
|||||||
|
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 { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||||
|
|
||||||
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncOptionsFields = () => {
|
||||||
|
const { control, watch } = useFormContext<
|
||||||
|
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const region = watch("destinationConfig.region");
|
||||||
|
const connectionId = useWatch({ name: "connection.id", control });
|
||||||
|
const mappingBehavior = watch("destinationConfig.mappingBehavior");
|
||||||
|
|
||||||
|
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
|
||||||
|
{
|
||||||
|
connectionId,
|
||||||
|
region,
|
||||||
|
destination: SecretSync.AWSSecretsManager
|
||||||
|
},
|
||||||
|
{ 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 secrets 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:ListAliases"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:DescribeKey"
|
||||||
|
</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/secretsmanager"
|
||||||
|
? `${option.alias} (Default)`
|
||||||
|
: option.alias
|
||||||
|
}
|
||||||
|
getOptionValue={(option) => option.alias}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormLabel label="Tags" tooltipText="Add tags to secrets 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="mb-6 mt-2 flex">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => tagFields.append({ key: "", value: "" })}
|
||||||
|
>
|
||||||
|
Add Tag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
|
||||||
|
<Controller
|
||||||
|
name="syncOptions.syncSecretMetadataAsTags"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl 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-[14rem]">
|
||||||
|
Sync Secret Metadata as Tags{" "}
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
If enabled, metadata attached to secrets will be added as tags to secrets
|
||||||
|
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,15 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import { useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { TSecretSyncForm } from "./schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||||
|
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hideInitialSync?: boolean;
|
hideInitialSync?: boolean;
|
||||||
@ -21,6 +24,26 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
|
|
||||||
const { syncOption } = useSecretSyncOption(destination);
|
const { syncOption } = useSecretSyncOption(destination);
|
||||||
|
|
||||||
|
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||||
|
|
||||||
|
switch (destination) {
|
||||||
|
case SecretSync.AWSParameterStore:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
|
||||||
|
break;
|
||||||
|
case SecretSync.AWSSecretsManager:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
|
||||||
|
break;
|
||||||
|
case SecretSync.GitHub:
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
case SecretSync.AzureKeyVault:
|
||||||
|
case SecretSync.AzureAppConfiguration:
|
||||||
|
case SecretSync.Databricks:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled Additional Sync Options Fields: ${destination}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
||||||
@ -91,6 +114,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{AdditionalSyncOptionsFieldsComponent}
|
||||||
{/* <Controller
|
{/* <Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./SecretSyncOptionsFields";
|
@ -1,17 +1,71 @@
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
import { Badge } from "@app/components/v2";
|
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AwsParameterStoreSyncReviewFields = () => {
|
export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||||
const { watch } = useFormContext<
|
const { watch } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const [region, path] = watch(["destinationConfig.region", "destinationConfig.path"]);
|
const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
|
{tags && 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);
|
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
import { Badge } from "@app/components/v2";
|
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||||
@ -37,3 +39,55 @@ export const AwsSecretsManagerSyncReviewFields = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncOptionsReviewFields = () => {
|
||||||
|
const { watch } = useFormContext<
|
||||||
|
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<SecretSyncLabel label="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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -3,15 +3,21 @@ import { useFormContext } from "react-hook-form";
|
|||||||
|
|
||||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
|
|
||||||
import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields";
|
|
||||||
import { Badge } from "@app/components/v2";
|
import { Badge } from "@app/components/v2";
|
||||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
import {
|
||||||
|
AwsParameterStoreDestinationReviewFields,
|
||||||
|
AwsParameterStoreSyncOptionsReviewFields
|
||||||
|
} from "./AwsParameterStoreSyncReviewFields";
|
||||||
|
import {
|
||||||
|
AwsSecretsManagerSyncOptionsReviewFields,
|
||||||
|
AwsSecretsManagerSyncReviewFields
|
||||||
|
} from "./AwsSecretsManagerSyncReviewFields";
|
||||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||||
|
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||||
|
|
||||||
@ -19,6 +25,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
const { watch } = useFormContext<TSecretSyncForm>();
|
const { watch } = useFormContext<TSecretSyncForm>();
|
||||||
|
|
||||||
let DestinationFieldsComponent: ReactNode;
|
let DestinationFieldsComponent: ReactNode;
|
||||||
|
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
@ -38,10 +45,12 @@ export const SecretSyncReviewFields = () => {
|
|||||||
|
|
||||||
switch (destination) {
|
switch (destination) {
|
||||||
case SecretSync.AWSParameterStore:
|
case SecretSync.AWSParameterStore:
|
||||||
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
|
DestinationFieldsComponent = <AwsParameterStoreDestinationReviewFields />;
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsReviewFields />;
|
||||||
break;
|
break;
|
||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsReviewFields />;
|
||||||
break;
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||||
@ -84,7 +93,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="w-full border-b border-mineshaft-600">
|
<div className="w-full border-b border-mineshaft-600">
|
||||||
<span className="text-sm text-mineshaft-300">Options</span>
|
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
<SecretSyncLabel label="Auto-Sync">
|
<SecretSyncLabel label="Auto-Sync">
|
||||||
@ -97,6 +106,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
</SecretSyncLabel>
|
</SecretSyncLabel>
|
||||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||||
|
{AdditionalSyncOptionsFieldsComponent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
@ -1,8 +1,36 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AwsParameterStoreSyncDestinationSchema = z.object({
|
export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
|
||||||
|
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, "Tag key 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),
|
destination: z.literal(SecretSync.AWSParameterStore),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
path: z
|
path: z
|
||||||
@ -13,4 +41,5 @@ export const AwsParameterStoreSyncDestinationSchema = z.object({
|
|||||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
|
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
|
||||||
region: z.string().min(1, "Region required")
|
region: z.string().min(1, "Region required")
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -1,9 +1,37 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncDestinationSchema = z.object({
|
export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema(
|
||||||
|
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, "Tag key 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.AWSSecretsManager),
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
destinationConfig: z
|
destinationConfig: z
|
||||||
.discriminatedUnion("mappingBehavior", [
|
.discriminatedUnion("mappingBehavior", [
|
||||||
@ -27,4 +55,5 @@ export const AwsSecretsManagerSyncDestinationSchema = z.object({
|
|||||||
region: z.string().min(1, "Region required")
|
region: z.string().min(1, "Region required")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AzureAppConfigurationSyncDestinationSchema = z.object({
|
export const AzureAppConfigurationSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
configurationUrl: z
|
configurationUrl: z
|
||||||
@ -16,4 +18,5 @@ export const AzureAppConfigurationSyncDestinationSchema = z.object({
|
|||||||
),
|
),
|
||||||
label: z.string().optional()
|
label: z.string().optional()
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const AzureKeyVaultSyncDestinationSchema = z.object({
|
export const AzureKeyVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
destination: z.literal(SecretSync.AzureKeyVault),
|
destination: z.literal(SecretSync.AzureKeyVault),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required")
|
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 { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export const DatabricksSyncDestinationSchema = z.object({
|
export const DatabricksSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
destination: z.literal(SecretSync.Databricks),
|
destination: z.literal(SecretSync.Databricks),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
scope: z.string().trim().min(1, "Databricks scope required")
|
scope: z.string().trim().min(1, "Databricks scope required")
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
|
|
||||||
export const GcpSyncDestinationSchema = z.object({
|
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
destination: z.literal(SecretSync.GCPSecretManager),
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
scope: z.literal(GcpSyncScope.Global),
|
scope: z.literal(GcpSyncScope.Global),
|
||||||
projectId: z.string().min(1, "Project ID required")
|
projectId: z.string().min(1, "Project ID required")
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import {
|
import {
|
||||||
GitHubSyncScope,
|
GitHubSyncScope,
|
||||||
GitHubSyncVisibility
|
GitHubSyncVisibility
|
||||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||||
|
|
||||||
export const GitHubSyncDestinationSchema = z.object({
|
export const GitHubSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
destination: z.literal(SecretSync.GitHub),
|
destination: z.literal(SecretSync.GitHub),
|
||||||
destinationConfig: z
|
destinationConfig: z
|
||||||
.discriminatedUnion("scope", [
|
.discriminatedUnion("scope", [
|
||||||
@ -42,4 +44,5 @@ export const GitHubSyncDestinationSchema = z.object({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -3,37 +3,12 @@ import { z } from "zod";
|
|||||||
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
|
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
|
||||||
import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
|
import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
|
||||||
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
|
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
|
||||||
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
|
||||||
|
|
||||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||||
|
|
||||||
const BaseSecretSyncSchema = z.object({
|
|
||||||
name: slugSchema({ field: "Name" }),
|
|
||||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
|
|
||||||
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
|
||||||
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
|
||||||
secretPath: z.string().min(1, "Secret path required"),
|
|
||||||
syncOptions: z.object({
|
|
||||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
|
||||||
// scott: removed temporarily for evaluation of template formatting
|
|
||||||
// prependPrefix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional(),
|
|
||||||
// appendSuffix: z
|
|
||||||
// .string()
|
|
||||||
// .trim()
|
|
||||||
// .transform((str) => str.toUpperCase())
|
|
||||||
// .optional()
|
|
||||||
}),
|
|
||||||
isAutoSyncEnabled: z.boolean()
|
|
||||||
});
|
|
||||||
|
|
||||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||||
AwsParameterStoreSyncDestinationSchema,
|
AwsParameterStoreSyncDestinationSchema,
|
||||||
AwsSecretsManagerSyncDestinationSchema,
|
AwsSecretsManagerSyncDestinationSchema,
|
||||||
@ -44,8 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
|||||||
DatabricksSyncDestinationSchema
|
DatabricksSyncDestinationSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
|
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||||
|
|
||||||
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema.partial());
|
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema;
|
||||||
|
|
||||||
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
|
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
|
||||||
|
@ -20,7 +20,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
sideOffset={10}
|
sideOffset={-8}
|
||||||
{...props}
|
{...props}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
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
@ -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
@ -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 { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
export type TAwsParameterStoreSync = TRootSecretSync & {
|
export type TAwsParameterStoreSync = TRootSecretSync & {
|
||||||
destination: SecretSync.AWSParameterStore;
|
destination: SecretSync.AWSParameterStore;
|
||||||
@ -13,4 +13,9 @@ export type TAwsParameterStoreSync = TRootSecretSync & {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
syncOptions: RootSyncOptions & {
|
||||||
|
keyId?: string;
|
||||||
|
tags?: { key: string; value?: string }[];
|
||||||
|
syncSecretMetadataAsTags?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
export type TAwsSecretsManagerSync = TRootSecretSync & {
|
export type TAwsSecretsManagerSync = TRootSecretSync & {
|
||||||
destination: SecretSync.AWSSecretsManager;
|
destination: SecretSync.AWSSecretsManager;
|
||||||
@ -19,6 +19,11 @@ export type TAwsSecretsManagerSync = TRootSecretSync & {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
syncOptions: RootSyncOptions & {
|
||||||
|
keyId?: string;
|
||||||
|
tags?: { key: string; value?: string }[];
|
||||||
|
syncSecretMetadataAsTags?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export enum AwsSecretsManagerSyncMappingBehavior {
|
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||||
OneToOne = "one-to-one",
|
OneToOne = "one-to-one",
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export type RootSyncOptions = {
|
||||||
|
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||||
|
// prependPrefix?: string;
|
||||||
|
// appendSuffix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TRootSecretSync = {
|
export type TRootSecretSync = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,11 +30,7 @@ export type TRootSecretSync = {
|
|||||||
lastRemoveJobId: string | null;
|
lastRemoveJobId: string | null;
|
||||||
lastRemovedAt: Date | null;
|
lastRemovedAt: Date | null;
|
||||||
lastRemoveMessage: string | null;
|
lastRemoveMessage: string | null;
|
||||||
syncOptions: {
|
syncOptions: RootSyncOptions;
|
||||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
|
||||||
// prependPrefix?: string;
|
|
||||||
// appendSuffix?: string;
|
|
||||||
};
|
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection;
|
app: AppConnection;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -20,12 +20,13 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
|||||||
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
|
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
|
||||||
const iconRef = useRef<DotLottie | null>(null);
|
const iconRef = useRef<DotLottie | null>(null);
|
||||||
return (
|
return (
|
||||||
|
<div className={!isSelected ? "hover:px-1" : ""}>
|
||||||
<Item
|
<Item
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded my-1 p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
||||||
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
|
isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none",
|
||||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -37,7 +38,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isSelected ? "opacity-100" : "opacity-0"
|
isSelected ? "opacity-100" : "opacity-0"
|
||||||
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
|
} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
|
||||||
/>
|
/>
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="my-auto mb-2 h-6 w-6">
|
<div className="my-auto mb-2 h-6 w-6">
|
||||||
@ -59,5 +60,6 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</Item>
|
</Item>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -84,6 +84,10 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openSupport, setOpenSupport] = useState(false);
|
||||||
|
const [openUser, setOpenUser] = useState(false);
|
||||||
|
const [openOrg, setOpenOrg] = useState(false);
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { mutateAsync } = useGetOrgTrialUrl();
|
const { mutateAsync } = useGetOrgTrialUrl();
|
||||||
@ -160,23 +164,29 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
>
|
>
|
||||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
|
<div className="flex items-center hover:bg-mineshaft-700">
|
||||||
<DropdownMenu modal>
|
<DropdownMenu open={openOrg} onOpenChange={setOpenOrg} modal>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
|
onMouseEnter={() => setOpenOrg(true)}
|
||||||
|
onMouseLeave={() => setOpenOrg(false)}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pb-5 pt-6 transition-all">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
||||||
{currentOrg?.name.charAt(0)}
|
{currentOrg?.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
onMouseEnter={() => setOpenOrg(true)}
|
||||||
|
onMouseLeave={() => setOpenOrg(false)}
|
||||||
align="start"
|
align="start"
|
||||||
side="right"
|
side="right"
|
||||||
className="p-1 shadow-mineshaft-600 drop-shadow-md"
|
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||||
style={{ minWidth: "320px" }}
|
style={{ minWidth: "220px" }}
|
||||||
>
|
>
|
||||||
<div className="px-2 py-1">
|
<div className="px-0.5 py-1">
|
||||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
|
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800 p-1 transition-all duration-300">
|
||||||
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
|
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
|
||||||
{currentOrg?.name.charAt(0)}
|
{currentOrg?.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
@ -241,7 +251,7 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 px-1">
|
<div className="space-y-1">
|
||||||
<Link to="/organization/secret-manager/overview">
|
<Link to="/organization/secret-manager/overview">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuIconButton
|
<MenuIconButton
|
||||||
@ -251,7 +261,7 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
}
|
}
|
||||||
icon="sliding-carousel"
|
icon="sliding-carousel"
|
||||||
>
|
>
|
||||||
Secret Manager
|
Secrets
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@ -264,7 +274,7 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
}
|
}
|
||||||
icon="note"
|
icon="note"
|
||||||
>
|
>
|
||||||
Cert Manager
|
PKI
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@ -296,31 +306,41 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
<Link to="/organization/secret-scanning">
|
<Link to="/organization/secret-scanning">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuIconButton isSelected={isActive} icon="secret-scan">
|
<MenuIconButton isSelected={isActive} icon="secret-scan">
|
||||||
Secret Scanning
|
Scanner
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/organization/secret-sharing">
|
<Link to="/organization/secret-sharing">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuIconButton isSelected={isActive} icon="lock-closed">
|
<MenuIconButton isSelected={isActive} icon="lock-closed">
|
||||||
Secret Sharing
|
Share
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
|
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
|
||||||
<DropdownMenu>
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
|
onMouseEnter={() => setOpen(true)}
|
||||||
|
onMouseLeave={() => setOpen(false)}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<MenuIconButton
|
<MenuIconButton
|
||||||
lottieIconMode="reverse"
|
lottieIconMode="reverse"
|
||||||
icon="settings-cog"
|
icon="settings-cog"
|
||||||
isSelected={isMoreSelected}
|
isSelected={isMoreSelected}
|
||||||
>
|
>
|
||||||
Org Controls
|
Admin
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" side="right" className="p-1">
|
<DropdownMenuContent
|
||||||
|
onMouseEnter={() => setOpen(true)}
|
||||||
|
onMouseLeave={() => setOpen(false)}
|
||||||
|
align="start"
|
||||||
|
side="right"
|
||||||
|
className="p-1"
|
||||||
|
>
|
||||||
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
|
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
|
||||||
<Link to="/organization/access-management">
|
<Link to="/organization/access-management">
|
||||||
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
|
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
|
||||||
@ -364,14 +384,24 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
: "mb-4"
|
: "mb-4"
|
||||||
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
|
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
|
||||||
>
|
>
|
||||||
<DropdownMenu>
|
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
|
||||||
<DropdownMenuTrigger className="w-full">
|
<DropdownMenuTrigger
|
||||||
|
onMouseEnter={() => setOpenSupport(true)}
|
||||||
|
onMouseLeave={() => setOpenSupport(false)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
<MenuIconButton>
|
<MenuIconButton>
|
||||||
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
|
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
|
||||||
Support
|
Support
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent
|
||||||
|
onMouseEnter={() => setOpenSupport(true)}
|
||||||
|
onMouseLeave={() => setOpenSupport(false)}
|
||||||
|
align="end"
|
||||||
|
side="right"
|
||||||
|
className="p-1"
|
||||||
|
>
|
||||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||||
<DropdownMenuItem key={url as string}>
|
<DropdownMenuItem key={url as string}>
|
||||||
<a
|
<a
|
||||||
@ -419,17 +449,28 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu open={openUser} onOpenChange={setOpenUser}>
|
||||||
<DropdownMenuTrigger className="w-full" asChild>
|
<DropdownMenuTrigger
|
||||||
|
onMouseEnter={() => setOpenUser(true)}
|
||||||
|
onMouseLeave={() => setOpenUser(false)}
|
||||||
|
className="w-full"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<MenuIconButton icon="user">User</MenuIconButton>
|
<MenuIconButton icon="user">User</MenuIconButton>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent
|
||||||
<div className="px-2 py-1">
|
onMouseEnter={() => setOpenUser(true)}
|
||||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
|
onMouseLeave={() => setOpenUser(false)}
|
||||||
<div className="p-2">
|
side="right"
|
||||||
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
|
align="end"
|
||||||
|
className="p-1"
|
||||||
|
>
|
||||||
|
<div className="cursor-default px-1 py-1">
|
||||||
|
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
|
||||||
|
<div className="p-1 pr-3">
|
||||||
|
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow flex-col text-white">
|
<div className="flex flex-grow flex-col text-white">
|
||||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||||
@ -477,11 +518,11 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<div className="mt-1 border-t border-mineshaft-600 pt-1">
|
||||||
<Link to="/organization/admin">
|
<Link to="/organization/admin">
|
||||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
<DropdownMenuItem>Organization Admin Console</DropdownMenuItem>
|
||||||
Organization Admin Console
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
||||||
Log Out
|
Log Out
|
||||||
|
@ -120,7 +120,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="API Key" type="text" />
|
<Input {...field} placeholder="API Key" type="text" autoComplete="off" autoCorrect="off" spellCheck="false" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -155,7 +155,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isOptional
|
isOptional
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="Password" type="password" />
|
<Input {...field} placeholder="Password" type="password" autoComplete="new-password" autoCorrect="off" spellCheck="false" aria-autocomplete="none" data-form-type="other" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
|
|||||||
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
||||||
currentWorkspace.id,
|
currentWorkspace.id,
|
||||||
{
|
{
|
||||||
refetchInterval: 4000
|
refetchInterval: 30000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -59,7 +59,12 @@ export const PitDrawer = ({
|
|||||||
onClick={() => onSelectSnapshot(id)}
|
onClick={() => onSelectSnapshot(id)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<div>{formatDistance(new Date(createdAt), new Date())}</div>
|
<div>
|
||||||
|
{(() => {
|
||||||
|
const distance = formatDistance(new Date(createdAt), new Date());
|
||||||
|
return distance.charAt(0).toUpperCase() + distance.slice(1) + " ago";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
|
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@ -70,7 +75,7 @@ export const PitDrawer = ({
|
|||||||
<Button
|
<Button
|
||||||
className="mt-8 px-4 py-3 text-sm"
|
className="mt-8 px-4 py-3 text-sm"
|
||||||
isFullWidth
|
isFullWidth
|
||||||
variant="star"
|
variant="outline_bg"
|
||||||
isLoading={isFetchingNextPage}
|
isLoading={isFetchingNextPage}
|
||||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||||
onClick={fetchNextPage}
|
onClick={fetchNextPage}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
||||||
import {
|
import {
|
||||||
|
faArrowRotateRight,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faCircle,
|
|
||||||
faCircleDot,
|
|
||||||
faClock,
|
faClock,
|
||||||
|
faEyeSlash,
|
||||||
faPlus,
|
faPlus,
|
||||||
faShare,
|
faShare,
|
||||||
faTag,
|
faTag,
|
||||||
@ -236,12 +236,11 @@ export const SecretDetailSidebar = ({
|
|||||||
}}
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
>
|
>
|
||||||
<DrawerContent title="Secret">
|
<DrawerContent title={`Secret – ${secret?.key}`} className="thin-scrollbar">
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<FormControl label="Key">
|
<div className="flex flex-row">
|
||||||
<Input isDisabled {...register("key")} />
|
<div className="w-full">
|
||||||
</FormControl>
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
@ -273,7 +272,29 @@ export const SecretDetailSidebar = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
</div>
|
||||||
|
<div className="ml-1 mt-1.5 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="w-full px-2 py-[0.43rem] font-normal"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||||
|
onClick={() => {
|
||||||
|
const value = secret?.valueOverride ?? secret?.value;
|
||||||
|
if (value) {
|
||||||
|
handleSecretShare(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
|
||||||
|
<div className="mb-4 px-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="skipMultilineEncoding"
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
@ -284,23 +305,69 @@ export const SecretDetailSidebar = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-max text-sm text-mineshaft-300">
|
||||||
|
Multi-line encoding
|
||||||
|
<Tooltip
|
||||||
|
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
id="skipmultiencoding-option"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="items-center justify-between"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`mb-4 w-full border-t border-mineshaft-600 ${isOverridden ? "block" : "hidden"}`}
|
||||||
|
/>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: secretKey,
|
||||||
|
secretTags: selectTagSlugs
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<div className="flex items-center justify-between px-4 pb-4">
|
||||||
|
<span className="w-max text-sm text-mineshaft-300">
|
||||||
|
Override with a personal value
|
||||||
|
<Tooltip
|
||||||
|
content="Override the secret value with a personal value that does not get shared with other users and machines."
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
id="personal-override"
|
id="personal-override"
|
||||||
onCheckedChange={handleOverrideClick}
|
onCheckedChange={handleOverrideClick}
|
||||||
isChecked={isOverridden}
|
isChecked={isOverridden}
|
||||||
>
|
className="justify-start"
|
||||||
Override with a personal value
|
/>
|
||||||
</Switch>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
|
||||||
{isOverridden && (
|
{isOverridden && (
|
||||||
<Controller
|
<Controller
|
||||||
name="valueOverride"
|
name="valueOverride"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl label="Value Override">
|
<FormControl label="Override Value" className="px-4">
|
||||||
<InfisicalSecretInput
|
<InfisicalSecretInput
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
@ -312,7 +379,129 @@ export const SecretDetailSidebar = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormControl label="Metadata">
|
</div>
|
||||||
|
<div className="mb-4 mt-2 flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
|
||||||
|
<div
|
||||||
|
className={`flex justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`text-sm text-mineshaft-300 ${tagFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormControl>
|
||||||
|
<div
|
||||||
|
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
|
||||||
|
>
|
||||||
|
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||||
|
<Tag
|
||||||
|
className="flex w-min items-center space-x-2"
|
||||||
|
key={formId}
|
||||||
|
onClose={() => {
|
||||||
|
if (cannotEditSecret) {
|
||||||
|
createNotification({ type: "error", text: "Access denied" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||||
|
if (tag) handleTagSelect(tag);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-3 w-3 rounded-full"
|
||||||
|
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{slug}</div>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
<DropdownMenu>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: secretKey,
|
||||||
|
secretTags: selectTagSlugs
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="add"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="rounded-md"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<DropdownMenuContent align="start" side="right" className="z-[100]">
|
||||||
|
<DropdownMenuLabel className="pl-2">
|
||||||
|
Add tags to this secret
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{tags.map((tag) => {
|
||||||
|
const { id: tagId, slug, color } = tag;
|
||||||
|
|
||||||
|
const isSelected = selectedTagsGroupById?.[tagId];
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleTagSelect(tag)}
|
||||||
|
key={tagId}
|
||||||
|
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className="mr-2 h-2 w-2 rounded-full"
|
||||||
|
style={{ background: color || "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
{slug}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Tags}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<div className="p-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
className="w-full"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||||
|
onClick={onCreateTag}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create a tag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`flex justify-between px-4 text-mineshaft-100 ${metadataFormFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`text-sm text-mineshaft-300 ${metadataFormFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
|
||||||
|
>
|
||||||
|
Metadata
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||||
<div key={metadataFieldId} className="flex items-end space-x-2">
|
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||||
@ -364,114 +553,32 @@ export const SecretDetailSidebar = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="mt-2">
|
<div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
|
||||||
>
|
|
||||||
Add Key
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl label="Tags" className="">
|
|
||||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
|
||||||
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
|
||||||
<Tag
|
|
||||||
className="flex w-min items-center space-x-2"
|
|
||||||
key={formId}
|
|
||||||
onClose={() => {
|
|
||||||
if (cannotEditSecret) {
|
|
||||||
createNotification({ type: "error", text: "Access denied" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
|
||||||
if (tag) handleTagSelect(tag);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-3 w-3 rounded-full"
|
|
||||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">{slug}</div>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
<DropdownMenu>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="add"
|
ariaLabel="Add Key"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
size="xs"
|
size="xs"
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
isDisabled={!isAllowed}
|
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<DropdownMenuContent align="end" className="z-[100]">
|
|
||||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
const { id: tagId, slug, color } = tag;
|
|
||||||
|
|
||||||
const isSelected = selectedTagsGroupById?.[tagId];
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleTagSelect(tag)}
|
|
||||||
key={tagId}
|
|
||||||
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
iconPos="right"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="mr-2 h-2 w-2 rounded-full"
|
|
||||||
style={{ background: color || "#bec2c8" }}
|
|
||||||
/>
|
|
||||||
{slug}
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Create}
|
|
||||||
a={ProjectPermissionSub.Tags}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
className="w-full"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
|
||||||
onClick={onCreateTag}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Create a tag
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Reminder">
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl label="Comments & Notes">
|
||||||
|
<TextArea
|
||||||
|
className="border border-mineshaft-600 bg-bunker-800 text-sm"
|
||||||
|
{...register("comment")}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||||
<div className="ml-1 mt-2 flex items-center justify-between">
|
<div className="flex items-center justify-between px-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||||
<span className="text-sm text-bunker-300">
|
<span className="text-sm text-bunker-300">
|
||||||
@ -490,9 +597,9 @@ export const SecretDetailSidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-1 mt-2 flex items-center space-x-2">
|
<div className="ml-1 flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
className="w-full px-2 py-1"
|
className="w-full px-2 py-2 font-normal"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
onClick={() => setCreateReminderFormOpen.on()}
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
@ -503,92 +610,147 @@ export const SecretDetailSidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Comments & Notes">
|
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
||||||
<TextArea
|
<div className="mb-2 pl-1">Version History</div>
|
||||||
className="border border-mineshaft-600 text-sm"
|
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||||
{...register("comment")}
|
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
|
||||||
readOnly={isReadOnly}
|
<div className="flex flex-row">
|
||||||
rows={5}
|
<div key={id} className="flex w-full flex-col space-y-1">
|
||||||
/>
|
<div className="flex items-center">
|
||||||
</FormControl>
|
<div className="w-10">
|
||||||
<div className="my-2 mb-4 border-b border-mineshaft-600 pb-4">
|
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||||
<Controller
|
v{version}
|
||||||
control={control}
|
|
||||||
name="skipMultilineEncoding"
|
|
||||||
render={({ field: { value, onChange, onBlur } }) => (
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Switch
|
|
||||||
id="skipmultiencoding-option"
|
|
||||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
|
||||||
isChecked={value}
|
|
||||||
onBlur={onBlur}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
className="items-center"
|
|
||||||
>
|
|
||||||
Multi line encoding
|
|
||||||
<Tooltip
|
|
||||||
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
|
|
||||||
className="z-[100]"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
</Switch>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-1 flex items-center space-x-4">
|
|
||||||
<Button
|
|
||||||
className="w-full px-2 py-1"
|
|
||||||
variant="outline_bg"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
|
||||||
onClick={() => {
|
|
||||||
const value = secret?.valueOverride ?? secret?.value;
|
|
||||||
if (value) {
|
|
||||||
handleSecretShare(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Share Secret
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
|
|
||||||
<div className="mb-2">Version History</div>
|
|
||||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
|
||||||
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
|
|
||||||
<div key={id} className="flex flex-col space-y-1">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
<div className="flex w-full cursor-default">
|
||||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
<div className="relative w-10">
|
||||||
<div className="break-all font-mono">{secretValue}</div>
|
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||||
|
Value:
|
||||||
|
</div>
|
||||||
|
<div className="group break-all pl-1 font-mono">
|
||||||
|
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="select-none"
|
||||||
|
onClick={(e) => {
|
||||||
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
|
const target = e.currentTarget;
|
||||||
|
target.style.borderBottom = "1px dashed";
|
||||||
|
target.style.paddingBottom = "-1px";
|
||||||
|
|
||||||
|
// Create and insert popup
|
||||||
|
const popup = document.createElement("div");
|
||||||
|
popup.className =
|
||||||
|
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||||
|
popup.textContent = "Copied!";
|
||||||
|
target.parentElement?.appendChild(popup);
|
||||||
|
|
||||||
|
// Remove popup and border after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
popup.remove();
|
||||||
|
target.style.borderBottom = "none";
|
||||||
|
}, 3000);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
|
const target = e.currentTarget;
|
||||||
|
target.style.borderBottom = "1px dashed";
|
||||||
|
target.style.paddingBottom = "-1px";
|
||||||
|
|
||||||
|
// Create and insert popup
|
||||||
|
const popup = document.createElement("div");
|
||||||
|
popup.className =
|
||||||
|
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||||
|
popup.textContent = "Copied!";
|
||||||
|
target.parentElement?.appendChild(popup);
|
||||||
|
|
||||||
|
// Remove popup and border after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
popup.remove();
|
||||||
|
target.style.borderBottom = "none";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{secretValue}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget
|
||||||
|
.closest(".group")
|
||||||
|
?.classList.remove("show-value");
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget
|
||||||
|
.closest(".group")
|
||||||
|
?.classList.remove("show-value");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span className="group-[.show-value]:hidden">
|
||||||
|
{secretValue?.replace(/./g, "*")}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.currentTarget
|
||||||
|
.closest(".group")
|
||||||
|
?.classList.add("show-value");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||||
|
>
|
||||||
|
<Tooltip content="Restore Secret Value">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Restore"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 rounded-md"
|
||||||
|
onClick={() => setValue("value", secretValue)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
||||||
<div className="mb-2">
|
<div className="mb-2 mt-4">
|
||||||
Access List
|
Access List
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
|
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
|
||||||
className="z-[100]"
|
className="z-[100]"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{isPending && (
|
{isPending && (
|
||||||
@ -608,14 +770,22 @@ export const SecretDetailSidebar = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isPending && secretAccessList && (
|
{!isPending && secretAccessList && (
|
||||||
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
<div className="mb-4 flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||||
{secretAccessList.users.length > 0 && (
|
{secretAccessList.users.length > 0 && (
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<div className="mb-2 font-bold">Users</div>
|
<div className="mb-2 font-bold">Users</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{secretAccessList.users.map((user) => (
|
{secretAccessList.users.map((user) => (
|
||||||
<div className="rounded-md bg-bunker-500 px-1">
|
<div className="rounded-md bg-bunker-500">
|
||||||
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]">
|
<Tooltip
|
||||||
|
content={user.allowedActions
|
||||||
|
.map(
|
||||||
|
(action) =>
|
||||||
|
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
|
||||||
|
)
|
||||||
|
.join(", ")}
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
|
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
|
||||||
@ -624,7 +794,7 @@ export const SecretDetailSidebar = ({
|
|||||||
projectId: currentWorkspace.id,
|
projectId: currentWorkspace.id,
|
||||||
membershipId: user.membershipId
|
membershipId: user.membershipId
|
||||||
}}
|
}}
|
||||||
className="text-secondary/80 text-sm hover:text-primary"
|
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
|
||||||
>
|
>
|
||||||
{user.name}
|
{user.name}
|
||||||
</Link>
|
</Link>
|
||||||
@ -639,9 +809,14 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="mb-2 font-bold">Identities</div>
|
<div className="mb-2 font-bold">Identities</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{secretAccessList.identities.map((identity) => (
|
{secretAccessList.identities.map((identity) => (
|
||||||
<div className="rounded-md bg-bunker-500 px-1">
|
<div className="rounded-md bg-bunker-500">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={identity.allowedActions.join(", ")}
|
content={identity.allowedActions
|
||||||
|
.map(
|
||||||
|
(action) =>
|
||||||
|
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
|
||||||
|
)
|
||||||
|
.join(", ")}
|
||||||
className="z-[100]"
|
className="z-[100]"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
@ -652,7 +827,7 @@ export const SecretDetailSidebar = ({
|
|||||||
projectId: currentWorkspace.id,
|
projectId: currentWorkspace.id,
|
||||||
identityId: identity.id
|
identityId: identity.id
|
||||||
}}
|
}}
|
||||||
className="text-secondary/80 text-sm hover:text-primary"
|
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
|
||||||
>
|
>
|
||||||
{identity.name}
|
{identity.name}
|
||||||
</Link>
|
</Link>
|
||||||
@ -667,9 +842,14 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="mb-2 font-bold">Groups</div>
|
<div className="mb-2 font-bold">Groups</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{secretAccessList.groups.map((group) => (
|
{secretAccessList.groups.map((group) => (
|
||||||
<div className="rounded-md bg-bunker-500 px-1">
|
<div className="rounded-md bg-bunker-500">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={group.allowedActions.join(", ")}
|
content={group.allowedActions
|
||||||
|
.map(
|
||||||
|
(action) =>
|
||||||
|
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
|
||||||
|
)
|
||||||
|
.join(", ")}
|
||||||
className="z-[100]"
|
className="z-[100]"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
@ -677,7 +857,7 @@ export const SecretDetailSidebar = ({
|
|||||||
params={{
|
params={{
|
||||||
groupId: group.id
|
groupId: group.id
|
||||||
}}
|
}}
|
||||||
className="text-secondary/80 text-sm hover:text-primary"
|
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
|
||||||
>
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
</Link>
|
</Link>
|
||||||
@ -691,7 +871,7 @@ export const SecretDetailSidebar = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="mb-2 flex items-center space-x-4">
|
<div className="mb-4 flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
@ -705,6 +885,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<Button
|
<Button
|
||||||
isFullWidth
|
isFullWidth
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="outline_bg"
|
||||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
@ -722,9 +903,17 @@ export const SecretDetailSidebar = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
<IconButton
|
||||||
Delete
|
colorSchema="danger"
|
||||||
</Button>
|
ariaLabel="Delete Secret"
|
||||||
|
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={onDeleteSecret}
|
||||||
|
>
|
||||||
|
<Tooltip content="Delete Secret">
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,7 @@ const PageContent = () => {
|
|||||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
|
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
|
||||||
|
|
||||||
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
|
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
|
||||||
refetchInterval: 4000
|
refetchInterval: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
@ -66,7 +66,7 @@ const PageContent = () => {
|
|||||||
|
|
||||||
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
||||||
|
|
||||||
// const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||||
|
|
||||||
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
||||||
|
|
||||||
@ -108,10 +108,7 @@ const PageContent = () => {
|
|||||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||||
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
||||||
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
||||||
<SecretSyncOptionsSection
|
<SecretSyncOptionsSection secretSync={secretSync} onEditOptions={handleEditOptions} />
|
||||||
secretSync={secretSync}
|
|
||||||
// onEditOptions={handleEditOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
<SecretSyncDestinationSection
|
<SecretSyncDestinationSection
|
||||||
|
@ -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,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 { TAwsSecretsManagerSync } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretSync: TAwsSecretsManagerSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncOptionsSection = ({ secretSync }: Props) => {
|
||||||
|
const {
|
||||||
|
syncOptions: { keyId, tags, syncSecretMetadataAsTags }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<SecretSyncLabel label="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 Tags">
|
||||||
|
<Badge variant="success">Enabled</Badge>
|
||||||
|
</SecretSyncLabel>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
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";
|
||||||
|
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
|
||||||
|
|
||||||
|
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:
|
||||||
|
AdditionalSyncOptionsComponent = (
|
||||||
|
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
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";
|