Compare commits
40 Commits
app-connec
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
9d66659f72 | |||
70c9761abe | |||
c9d7559983 | |||
66251403bf | |||
b9c4407507 | |||
624be80768 | |||
8d7b5968d3 | |||
7154b19703 | |||
9ce465b3e2 | |||
598e5c0be5 | |||
72f08a6b89 | |||
55d8762351 | |||
3c92ec4dc3 | |||
f2224262a4 | |||
23eac40740 | |||
4ae88c0447 | |||
7aecaad050 | |||
cf61390e52 | |||
3f02481e78 | |||
7adc103ed2 | |||
4f874734ab | |||
eb6fd8259b | |||
1766a44dd0 | |||
624c9ef8da | |||
dfd4b13574 | |||
a903537441 | |||
92c4d83714 | |||
a6414104ad | |||
071f37666e | |||
cd5078d8b7 | |||
407fd8eda7 | |||
9d976de19b | |||
be99e40050 | |||
800d2c0454 | |||
6d0534b165 | |||
0968893d4b | |||
d24a5d96e3 | |||
55b0dc7f81 | |||
ba03fc256b | |||
ea28c374a7 |
@ -1,4 +1,4 @@
|
||||
name: Release Helm Charts
|
||||
name: Release Infisical Core Helm chart
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
@ -17,6 +17,6 @@ jobs:
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
- name: Build and push helm package to Cloudsmith
|
||||
run: cd helm-charts && sh upload-to-cloudsmith.sh
|
||||
run: cd helm-charts && sh upload-infisical-core-helm-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
@ -1,4 +1,4 @@
|
||||
name: Release Docker image for K8 operator
|
||||
name: Release image + Helm chart K8s Operator
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@ -35,3 +35,18 @@ jobs:
|
||||
tags: |
|
||||
infisical/kubernetes-operator:latest
|
||||
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
- name: Build and push helm package to Cloudsmith
|
||||
run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth);
|
||||
const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects");
|
||||
const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn(
|
||||
TableName.IdentityGcpAuth,
|
||||
"allowedServiceAccounts"
|
||||
);
|
||||
const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones");
|
||||
if (hasTable) {
|
||||
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||
if (hasAllowedProjectsColumn) t.string("allowedProjects", 2500).alter();
|
||||
if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts", 5000).alter();
|
||||
if (hasAllowedZones) t.string("allowedZones", 2500).alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth);
|
||||
const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects");
|
||||
const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn(
|
||||
TableName.IdentityGcpAuth,
|
||||
"allowedServiceAccounts"
|
||||
);
|
||||
const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones");
|
||||
if (hasTable) {
|
||||
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||
if (hasAllowedProjectsColumn) t.string("allowedProjects").alter();
|
||||
if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts").alter();
|
||||
if (hasAllowedZones) t.string("allowedZones").alter();
|
||||
});
|
||||
}
|
||||
}
|
@ -17,9 +17,9 @@ export const IdentityGcpAuthsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
allowedServiceAccounts: z.string(),
|
||||
allowedProjects: z.string(),
|
||||
allowedZones: z.string()
|
||||
allowedServiceAccounts: z.string().nullable().optional(),
|
||||
allowedProjects: z.string().nullable().optional(),
|
||||
allowedZones: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
|
||||
|
@ -317,6 +317,8 @@ interface GetSecretsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
type TSecretMetadata = { key: string; value: string }[];
|
||||
|
||||
interface GetSecretEvent {
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
@ -325,6 +327,7 @@ interface GetSecretEvent {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
@ -336,6 +339,7 @@ interface CreateSecretEvent {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
@ -344,7 +348,12 @@ interface CreateSecretBatchEvent {
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
secrets: Array<{
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -356,6 +365,7 @@ interface UpdateSecretEvent {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
@ -364,7 +374,7 @@ interface UpdateSecretBatchEvent {
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -762,9 +772,9 @@ interface AddIdentityGcpAuthEvent {
|
||||
metadata: {
|
||||
identityId: string;
|
||||
type: string;
|
||||
allowedServiceAccounts: string;
|
||||
allowedProjects: string;
|
||||
allowedZones: string;
|
||||
allowedServiceAccounts?: string | null;
|
||||
allowedProjects?: string | null;
|
||||
allowedZones?: string | null;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
@ -784,9 +794,9 @@ interface UpdateIdentityGcpAuthEvent {
|
||||
metadata: {
|
||||
identityId: string;
|
||||
type?: string;
|
||||
allowedServiceAccounts?: string;
|
||||
allowedProjects?: string;
|
||||
allowedZones?: string;
|
||||
allowedServiceAccounts?: string | null;
|
||||
allowedProjects?: string | null;
|
||||
allowedZones?: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
|
@ -688,7 +688,9 @@ export const RAW_SECRETS = {
|
||||
environment: "The slug of the environment to list secrets from.",
|
||||
secretPath: "The secret path to list secrets from.",
|
||||
includeImports: "Weather to include imported secrets or not.",
|
||||
tagSlugs: "The comma separated tag slugs to filter secrets."
|
||||
tagSlugs: "The comma separated tag slugs to filter secrets.",
|
||||
metadataFilter:
|
||||
"The secret metadata key-value pairs to filter secrets by. When querying for multiple metadata pairs, the query is treated as an AND operation. Secret metadata format is key=value1,value=value2|key=value3,value=value4."
|
||||
},
|
||||
CREATE: {
|
||||
secretName: "The name of the secret to create.",
|
||||
@ -1719,6 +1721,12 @@ export const SecretSyncs = {
|
||||
REGION: "The AWS region to sync secrets to.",
|
||||
PATH: "The Parameter Store path to sync secrets to."
|
||||
},
|
||||
AWS_SECRETS_MANAGER: {
|
||||
REGION: "The AWS region to sync secrets to.",
|
||||
MAPPING_BEHAVIOR:
|
||||
"How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.",
|
||||
SECRET_NAME: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one."
|
||||
},
|
||||
GITHUB: {
|
||||
ORG: "The name of the GitHub organization.",
|
||||
OWNER: "The name of the GitHub account owner of the repository.",
|
||||
|
@ -110,7 +110,6 @@ export const secretRawSchema = z.object({
|
||||
secretReminderNote: z.string().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
@ -0,0 +1,17 @@
|
||||
import {
|
||||
AwsSecretsManagerSyncSchema,
|
||||
CreateAwsSecretsManagerSyncSchema,
|
||||
UpdateAwsSecretsManagerSyncSchema
|
||||
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerAwsSecretsManagerSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.AWSSecretsManager,
|
||||
server,
|
||||
responseSchema: AwsSecretsManagerSyncSchema,
|
||||
createSchema: CreateAwsSecretsManagerSyncSchema,
|
||||
updateSchema: UpdateAwsSecretsManagerSyncSchema
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
|
||||
@ -8,6 +9,7 @@ export * from "./secret-sync-router";
|
||||
|
||||
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
|
||||
[SecretSync.AWSSecretsManager]: registerAwsSecretsManagerSyncRouter,
|
||||
[SecretSync.GitHub]: registerGitHubSyncRouter,
|
||||
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
|
||||
};
|
||||
|
@ -9,17 +9,23 @@ import {
|
||||
AwsParameterStoreSyncListItemSchema,
|
||||
AwsParameterStoreSyncSchema
|
||||
} from "@app/services/secret-sync/aws-parameter-store";
|
||||
import {
|
||||
AwsSecretsManagerSyncListItemSchema,
|
||||
AwsSecretsManagerSyncSchema
|
||||
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
AwsSecretsManagerSyncSchema,
|
||||
GitHubSyncSchema,
|
||||
GcpSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncListItemSchema,
|
||||
AwsSecretsManagerSyncListItemSchema,
|
||||
GitHubSyncListItemSchema,
|
||||
GcpSyncListItemSchema
|
||||
]);
|
||||
|
@ -181,6 +181,66 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
metadataFilter: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) return undefined;
|
||||
|
||||
const result: { key?: string; value?: string }[] = [];
|
||||
const pairs = val.split("|");
|
||||
|
||||
for (const pair of pairs) {
|
||||
const keyValuePair: { key?: string; value?: string } = {};
|
||||
const parts = pair.split(/[,=]/);
|
||||
|
||||
for (let i = 0; i < parts.length; i += 2) {
|
||||
const identifier = parts[i].trim().toLowerCase();
|
||||
const value = parts[i + 1]?.trim();
|
||||
|
||||
if (identifier === "key" && value) {
|
||||
keyValuePair.key = value;
|
||||
} else if (identifier === "value" && value) {
|
||||
keyValuePair.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyValuePair.key && keyValuePair.value) {
|
||||
result.push(keyValuePair);
|
||||
}
|
||||
}
|
||||
|
||||
return result.length ? result : undefined;
|
||||
})
|
||||
.superRefine((metadata, ctx) => {
|
||||
if (metadata && !Array.isArray(metadata)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"Invalid secretMetadata format. Correct format is key=value1,value=value2|key=value3,value=value4."
|
||||
});
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
if (metadata.length > 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "You can only filter by up to 10 metadata fields"
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of metadata) {
|
||||
if (!item.key && !item.value) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"Invalid secretMetadata format, key or value must be provided. Correct format is key=value1,value=value2|key=value3,value=value4."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.describe(RAW_SECRETS.LIST.metadataFilter),
|
||||
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId),
|
||||
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
||||
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
||||
@ -281,6 +341,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
metadataFilter: req.query.metadataFilter,
|
||||
includeImports: req.query.include_imports,
|
||||
recursive: req.query.recursive,
|
||||
tagSlugs: req.query.tagSlugs
|
||||
@ -411,7 +472,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretPath: req.query.secretPath,
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secret.secretMetadata
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -519,7 +581,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretPath: req.body.secretPath,
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -631,7 +694,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretPath: req.body.secretPath,
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1904,6 +1968,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
|
||||
const secretMetadataMap = new Map(
|
||||
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: secrets[0].workspace,
|
||||
...req.auditLogInfo,
|
||||
@ -1915,7 +1983,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: secrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -2010,6 +2079,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
|
||||
const secretMetadataMap = new Map(
|
||||
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: secrets[0].workspace,
|
||||
...req.auditLogInfo,
|
||||
@ -2021,7 +2094,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: secrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -493,6 +493,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
db.ref("parents.environment")
|
||||
)
|
||||
.from(TableName.SecretFolder)
|
||||
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
|
||||
})
|
||||
)
|
||||
|
@ -69,6 +69,8 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
attempt += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleep();
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const AWS_SECRETS_MANAGER_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "AWS Secrets Manager",
|
||||
destination: SecretSync.AWSSecretsManager,
|
||||
connection: AppConnection.AWS,
|
||||
canImportSecrets: true
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||
OneToOne = "one-to-one",
|
||||
ManyToOne = "many-to-one"
|
||||
}
|
@ -0,0 +1,352 @@
|
||||
import {
|
||||
BatchGetSecretValueCommand,
|
||||
CreateSecretCommand,
|
||||
CreateSecretCommandInput,
|
||||
DeleteSecretCommand,
|
||||
DeleteSecretResponse,
|
||||
ListSecretsCommand,
|
||||
SecretsManagerClient,
|
||||
UpdateSecretCommand,
|
||||
UpdateSecretCommandInput
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AWSError } from "aws-sdk";
|
||||
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
||||
|
||||
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 { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types";
|
||||
|
||||
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 20;
|
||||
|
||||
const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => {
|
||||
const { destinationConfig, connection } = secretSync;
|
||||
|
||||
const config = await getAwsConnectionConfig(connection, destinationConfig.region);
|
||||
|
||||
const secretsManagerClient = new SecretsManagerClient({
|
||||
region: config.region,
|
||||
credentials: config.credentials!
|
||||
});
|
||||
|
||||
return secretsManagerClient;
|
||||
};
|
||||
|
||||
const sleep = async () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
const getSecretsRecord = async (client: SecretsManagerClient): Promise<TAwsSecretsRecord> => {
|
||||
const awsSecretsRecord: TAwsSecretsRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const output = await client.send(new ListSecretsCommand({ NextToken: nextToken }));
|
||||
|
||||
attempt = 0;
|
||||
|
||||
if (output.SecretList) {
|
||||
output.SecretList.forEach((secretEntry) => {
|
||||
if (secretEntry.Name) {
|
||||
awsSecretsRecord[secretEntry.Name] = secretEntry;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasNext = Boolean(output.NextToken);
|
||||
nextToken = output.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 awsSecretsRecord;
|
||||
};
|
||||
|
||||
const getSecretValuesRecord = async (
|
||||
client: SecretsManagerClient,
|
||||
awsSecretsRecord: TAwsSecretsRecord
|
||||
): Promise<TAwsSecretValuesRecord> => {
|
||||
const awsSecretValuesRecord: TAwsSecretValuesRecord = {};
|
||||
let attempt = 0;
|
||||
|
||||
const secretIdList = Object.keys(awsSecretsRecord);
|
||||
|
||||
for (let i = 0; i < secretIdList.length; i += BATCH_SIZE) {
|
||||
const batchSecretIds = secretIdList.slice(i, i + BATCH_SIZE);
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const output = await client.send(
|
||||
new BatchGetSecretValueCommand({
|
||||
SecretIdList: batchSecretIds,
|
||||
NextToken: nextToken
|
||||
})
|
||||
);
|
||||
|
||||
attempt = 0;
|
||||
|
||||
if (output.SecretValues) {
|
||||
output.SecretValues.forEach((secretValueEntry) => {
|
||||
if (secretValueEntry.Name) {
|
||||
awsSecretValuesRecord[secretValueEntry.Name] = secretValueEntry;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasNext = Boolean(output.NextToken);
|
||||
nextToken = output.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 awsSecretValuesRecord;
|
||||
};
|
||||
|
||||
const createSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: CreateSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<CreateSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new CreateSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return createSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: UpdateSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<CreateSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new UpdateSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return updateSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
attempt = 0
|
||||
): Promise<DeleteSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new DeleteSecretCommand({ SecretId: secretKey, ForceDeleteWithoutRecovery: true }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return deleteSecret(client, secretKey, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client);
|
||||
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (awsSecretsRecord[key]) {
|
||||
// skip secrets that haven't changed
|
||||
if (awsValuesRecord[key]?.SecretString === value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Many-To-One Mapping
|
||||
|
||||
const secretValue = JSON.stringify(
|
||||
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
||||
);
|
||||
|
||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||
await updateSecret(client, {
|
||||
SecretId: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
});
|
||||
} else {
|
||||
await createSecret(client, {
|
||||
Name: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
});
|
||||
}
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (secretKey === destinationConfig.secretName) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client);
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
return Object.fromEntries(
|
||||
Object.keys(awsSecretsRecord).map((key) => [key, { value: awsValuesRecord[key].SecretString ?? "" }])
|
||||
);
|
||||
}
|
||||
|
||||
// Many-To-One Mapping
|
||||
|
||||
const secretValueEntry = awsValuesRecord[destinationConfig.secretName];
|
||||
|
||||
if (!secretValueEntry) return {};
|
||||
|
||||
try {
|
||||
const parsedValue = (secretValueEntry.SecretString ? JSON.parse(secretValueEntry.SecretString) : {}) as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
|
||||
return Object.fromEntries(Object.entries(parsedValue).map(([key, value]) => [key, { value }]));
|
||||
} catch {
|
||||
throw new SecretSyncError({
|
||||
message:
|
||||
"Failed to import secrets. Invalid format for Many-To-One mapping behavior: requires key/value configuration.",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client);
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (secretKey in secretMap) {
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await deleteSecret(client, destinationConfig.secretName);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
|
||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z
|
||||
.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z
|
||||
.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.SECRET_NAME)
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.REGION)
|
||||
})
|
||||
);
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||
name: z.literal("AWS Secrets Manager"),
|
||||
connection: z.literal(AppConnection.AWS),
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||
|
||||
import {
|
||||
AwsSecretsManagerSyncListItemSchema,
|
||||
AwsSecretsManagerSyncSchema,
|
||||
CreateAwsSecretsManagerSyncSchema
|
||||
} from "./aws-secrets-manager-sync-schemas";
|
||||
|
||||
export type TAwsSecretsManagerSync = z.infer<typeof AwsSecretsManagerSyncSchema>;
|
||||
|
||||
export type TAwsSecretsManagerSyncInput = z.infer<typeof CreateAwsSecretsManagerSyncSchema>;
|
||||
|
||||
export type TAwsSecretsManagerSyncListItem = z.infer<typeof AwsSecretsManagerSyncListItemSchema>;
|
||||
|
||||
export type TAwsSecretsManagerSyncWithCredentials = TAwsSecretsManagerSync & {
|
||||
connection: TAwsConnection;
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export * from "./aws-secrets-manager-sync-constants";
|
||||
export * from "./aws-secrets-manager-sync-fns";
|
||||
export * from "./aws-secrets-manager-sync-schemas";
|
||||
export * from "./aws-secrets-manager-sync-types";
|
@ -1,5 +1,6 @@
|
||||
export enum SecretSync {
|
||||
AWSParameterStore = "aws-parameter-store",
|
||||
AWSSecretsManager = "aws-secrets-manager",
|
||||
GitHub = "github",
|
||||
GCPSecretManager = "gcp-secret-manager"
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ import {
|
||||
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
AwsParameterStoreSyncFns
|
||||
} from "@app/services/secret-sync/aws-parameter-store";
|
||||
import {
|
||||
AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||
AwsSecretsManagerSyncFns
|
||||
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||
import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
@ -18,6 +22,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
|
||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
[SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
||||
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION
|
||||
};
|
||||
@ -73,6 +78,8 @@ export const SecretSyncFns = {
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.AWSSecretsManager:
|
||||
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GCPSecretManager:
|
||||
@ -89,6 +96,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.AWSParameterStore:
|
||||
secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
@ -110,6 +120,8 @@ export const SecretSyncFns = {
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.AWSSecretsManager:
|
||||
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GCPSecretManager:
|
||||
|
@ -3,12 +3,14 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
|
||||
[SecretSync.AWSSecretsManager]: "AWS Secrets Manager",
|
||||
[SecretSync.GitHub]: "GitHub",
|
||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
|
||||
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||
};
|
||||
|
@ -2,6 +2,12 @@ import { Job } from "bullmq";
|
||||
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { QueueJobs } from "@app/queue";
|
||||
import {
|
||||
TAwsSecretsManagerSync,
|
||||
TAwsSecretsManagerSyncInput,
|
||||
TAwsSecretsManagerSyncListItem,
|
||||
TAwsSecretsManagerSyncWithCredentials
|
||||
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||
import {
|
||||
TGitHubSync,
|
||||
TGitHubSyncInput,
|
||||
@ -19,16 +25,25 @@ import {
|
||||
} from "./aws-parameter-store";
|
||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
|
||||
export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
| TAwsSecretsManagerSyncWithCredentials
|
||||
| TGitHubSyncWithCredentials
|
||||
| TGcpSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput;
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
| TAwsSecretsManagerSyncInput
|
||||
| TGitHubSyncInput
|
||||
| TGcpSyncInput;
|
||||
|
||||
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem;
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
| TAwsSecretsManagerSyncListItem
|
||||
| TGitHubSyncListItem
|
||||
| TGcpSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
@ -414,6 +414,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.where((qb) => {
|
||||
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
||||
filters.metadataFilter.forEach((meta) => {
|
||||
void qb.whereExists((subQuery) => {
|
||||
void subQuery
|
||||
.select("secretId")
|
||||
.from(TableName.ResourceMetadata)
|
||||
.whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`)
|
||||
.where(`${TableName.ResourceMetadata}.key`, meta.key)
|
||||
.where(`${TableName.ResourceMetadata}.value`, meta.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretV2),
|
||||
db.raw(
|
||||
@ -481,6 +495,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get all secret" });
|
||||
|
@ -1291,8 +1291,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
]
|
||||
}
|
||||
});
|
||||
if (secretsToUpdate.length !== inputSecrets.length)
|
||||
throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` });
|
||||
if (secretsToUpdate.length !== inputSecrets.length) {
|
||||
const secretsToUpdateNames = secretsToUpdate.map((secret) => secret.key);
|
||||
const invalidSecrets = inputSecrets.filter((secret) => !secretsToUpdateNames.includes(secret.secretKey));
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${invalidSecrets.map((el) => el.secretKey).join(",")}`
|
||||
});
|
||||
}
|
||||
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key);
|
||||
|
||||
secretsToUpdate.forEach((el) => {
|
||||
|
@ -30,6 +30,10 @@ export type TGetSecretsDTO = {
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
metadataFilter?: {
|
||||
key?: string;
|
||||
value?: string;
|
||||
}[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
offset?: number;
|
||||
@ -310,6 +314,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
tagSlugs?: string[];
|
||||
metadataFilter?: { key?: string; value?: string }[];
|
||||
includeTagsInSearch?: boolean;
|
||||
keys?: string[];
|
||||
};
|
||||
|
@ -1263,6 +1263,13 @@ export const secretServiceFactory = ({
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
if (paramsV2.metadataFilter) {
|
||||
throw new BadRequestError({
|
||||
message: "Please upgrade your project to filter secrets by metadata",
|
||||
name: "SecretMetadataNotSupported"
|
||||
});
|
||||
}
|
||||
|
||||
const { secrets, imports } = await getSecrets({
|
||||
actorId,
|
||||
projectId,
|
||||
@ -1444,7 +1451,7 @@ export const secretServiceFactory = ({
|
||||
decryptedSecret.secretValue = expandedSecretValue || "";
|
||||
}
|
||||
|
||||
return decryptedSecret;
|
||||
return { secretMetadata: undefined, ...decryptedSecret };
|
||||
};
|
||||
|
||||
const createSecretRaw = async ({
|
||||
|
@ -182,6 +182,10 @@ export type TGetSecretsRawDTO = {
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
metadataFilter?: {
|
||||
key?: string;
|
||||
value?: string;
|
||||
}[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
offset?: number;
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/aws-secrets-manager"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/aws-secrets-manager/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/sync-name/{syncName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Import Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/import-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/aws-secrets-manager"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/remove-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/sync-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/aws-secrets-manager/{syncId}"
|
||||
---
|
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 805 KiB |
After Width: | Height: | Size: 797 KiB |
After Width: | Height: | Size: 832 KiB |
After Width: | Height: | Size: 848 KiB |
After Width: | Height: | Size: 782 KiB |
After Width: | Height: | Size: 773 KiB |
@ -30,13 +30,13 @@ description: "How to sync secrets from Infisical to Azure App Configuration"
|
||||
|
||||
Press create integration to start syncing secrets to Azure App Configuration.
|
||||
|
||||
<Note>
|
||||
<Warning>
|
||||
The Azure App Configuration integration requires the following permissions to be set on the user / service principal
|
||||
for Infisical to sync secrets to Azure App Configuration: `Read Key-Value`, `Write Key-Value`, `Delete Key-Value`.
|
||||
|
||||
Any role with these permissions would work such as the **App Configuration Data Owner** role. Alternatively, you can use the
|
||||
**App Configuration Data Reader** role for read-only access or **App Configuration Data Contributor** role for read/write access.
|
||||
</Note>
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
<Step title="Additional Configuration">
|
||||
|
@ -39,7 +39,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||
- **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 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.
|
||||
- **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**.
|
||||
|
142
docs/integrations/secret-syncs/aws-secrets-manager.mdx
Normal file
@ -0,0 +1,142 @@
|
||||
---
|
||||
title: "AWS Secrets Manager Sync"
|
||||
description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
|
||||
---
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||

|
||||
|
||||
2. Select the **AWS Secrets Manager** option.
|
||||

|
||||
|
||||
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||

|
||||
|
||||
- **Environment**: The project environment to retrieve secrets from.
|
||||
- **Secret Path**: The folder path to retrieve secrets from.
|
||||
|
||||
<Tip>
|
||||
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
|
||||
</Tip>
|
||||
|
||||
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
|
||||

|
||||
|
||||
- **AWS Connection**: The AWS Connection to authenticate with.
|
||||
- **Region**: The AWS region to deploy secrets to.
|
||||
- **Mapping Behavior**: Specify how Infisical should map secrets to AWS Secrets Manager:
|
||||
- **One-To-One**: Each Infisical secret will be mapped to a separate AWS Secrets Manager secret.
|
||||
- **Many-To-One**: All Infisical secrets will be mapped to a single AWS Secrets Manager secret.
|
||||
- **Secret Name**: Specifies the name of the AWS Secret to map secrets to if **Many-To-One** mapping behavior is selected.
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||

|
||||
|
||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||
- **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 AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
||||
- **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**.
|
||||

|
||||
|
||||
- **Name**: The name of your sync. Must be slug-friendly.
|
||||
- **Description**: An optional description for your sync.
|
||||
|
||||
7. Review your Secrets Manager Sync configuration, then click **Create Sync**.
|
||||

|
||||
|
||||
8. If enabled, your Secrets Manager Sync will begin syncing your secrets to the destination endpoint.
|
||||

|
||||
|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create an **AWS Secrets Manager Sync**, make an API request to the [Create AWS
|
||||
Secrets Manager Sync](/api-reference/endpoints/secret-syncs/aws-secrets-manager/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/secret-syncs/aws-secrets-manager \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-secrets-manager-sync",
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"description": "an example sync",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"environment": "dev",
|
||||
"secretPath": "/my-secrets",
|
||||
"isEnabled": true,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"destinationConfig": {
|
||||
"region": "us-east-1",
|
||||
"mappingBehavior": "one-to-one"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"secretSync": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-secrets-manager-sync",
|
||||
"description": "an example sync",
|
||||
"isEnabled": true,
|
||||
"version": 1,
|
||||
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-11-07T05:31:56Z",
|
||||
"updatedAt": "2023-11-07T05:31:56Z",
|
||||
"syncStatus": "succeeded",
|
||||
"lastSyncJobId": "123",
|
||||
"lastSyncMessage": null,
|
||||
"lastSyncedAt": "2023-11-07T05:31:56Z",
|
||||
"importStatus": null,
|
||||
"lastImportJobId": null,
|
||||
"lastImportMessage": null,
|
||||
"lastImportedAt": null,
|
||||
"removeStatus": null,
|
||||
"lastRemoveJobId": null,
|
||||
"lastRemoveMessage": null,
|
||||
"lastRemovedAt": null,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
"app": "aws",
|
||||
"name": "my-aws-connection",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"environment": {
|
||||
"slug": "dev",
|
||||
"name": "Development",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"folder": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"path": "/my-secrets"
|
||||
},
|
||||
"destination": "aws-secrets-manager",
|
||||
"destinationConfig": {
|
||||
"region": "us-east-1",
|
||||
"mappingBehavior": "one-to-one"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
@ -406,6 +406,7 @@
|
||||
"group": "Syncs",
|
||||
"pages": [
|
||||
"integrations/secret-syncs/aws-parameter-store",
|
||||
"integrations/secret-syncs/aws-secrets-manager",
|
||||
"integrations/secret-syncs/github",
|
||||
"integrations/secret-syncs/gcp-secret-manager"
|
||||
]
|
||||
@ -864,6 +865,20 @@
|
||||
"api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "AWS Secrets Manager",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/list",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/create",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/update",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/delete",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets",
|
||||
"api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GitHub",
|
||||
"pages": [
|
||||
|
@ -1,30 +1,11 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { components, OptionProps, SingleValue } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { Badge, FilterableSelect, FormControl, Input } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="truncate">{children}</p>
|
||||
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
|
||||
{props.data.slug}
|
||||
</Badge>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
import { AwsRegionSelect } from "./shared";
|
||||
|
||||
export const AwsParameterStoreSyncFields = () => {
|
||||
const { control } = useFormContext<
|
||||
@ -37,17 +18,7 @@ export const AwsParameterStoreSyncFields = () => {
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||
<FilterableSelect
|
||||
value={AWS_REGIONS.find((region) => region.slug === value)}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)
|
||||
}
|
||||
options={AWS_REGIONS}
|
||||
placeholder="Select region..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
components={{ Option }}
|
||||
/>
|
||||
<AwsRegionSelect value={value} onChange={onChange} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
|
@ -0,0 +1,96 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsRegionSelect } from "./shared";
|
||||
|
||||
export const AwsSecretsManagerSyncFields = () => {
|
||||
const { control, watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||
>();
|
||||
|
||||
const mappingBehavior = watch("destinationConfig.mappingBehavior");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField />
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||
<AwsRegionSelect value={value} onChange={onChange} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.region"
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.mappingBehavior"
|
||||
control={control}
|
||||
defaultValue={AwsSecretsManagerSyncMappingBehavior.OneToOne}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-lg py-3"
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>Specify how Infisical should map secrets to AWS Secrets Manager:</p>
|
||||
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||
<li>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">One-To-One</span>: Each
|
||||
Infisical secret will be mapped to a separate AWS Secrets Manager secret.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">Many-To-One</span>: All
|
||||
Infisical secrets will be mapped to a single AWS Secrets Manager secret.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Mapping Behavior"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select an option..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(AwsSecretsManagerSyncMappingBehavior).map((behavior) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={behavior} key={behavior}>
|
||||
{behavior}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="AWS Secrets Manager Secret Name"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="Secret name..." />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.secretName"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
||||
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
|
||||
import { GcpSyncFields } from "./GcpSyncFields";
|
||||
import { GitHubSyncFields } from "./GitHubSyncFields";
|
||||
|
||||
@ -15,6 +16,8 @@ export const SecretSyncDestinationFields = () => {
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return <AwsParameterStoreSyncFields />;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
return <AwsSecretsManagerSyncFields />;
|
||||
case SecretSync.GitHub:
|
||||
return <GitHubSyncFields />;
|
||||
case SecretSync.GCPSecretManager:
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { components, OptionProps, SingleValue } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, FilterableSelect } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
|
||||
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="truncate">{children}</p>
|
||||
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
|
||||
{props.data.slug}
|
||||
</Badge>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string | undefined) => void;
|
||||
};
|
||||
|
||||
export const AwsRegionSelect = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<FilterableSelect
|
||||
value={AWS_REGIONS.find((region) => region.slug === value)}
|
||||
onChange={(option) => onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)}
|
||||
options={AWS_REGIONS}
|
||||
placeholder="Select region..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
components={{ Option }}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./AwsRegionSelect";
|
@ -0,0 +1,39 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
export const AwsSecretsManagerSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||
>();
|
||||
|
||||
const [region, mappingBehavior, secretName] = watch([
|
||||
"destinationConfig.region",
|
||||
"destinationConfig.mappingBehavior",
|
||||
"destinationConfig.secretName"
|
||||
]);
|
||||
|
||||
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Region">
|
||||
{awsRegion?.name}
|
||||
<Badge className="ml-1" variant="success">
|
||||
{awsRegion?.slug}{" "}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel className="capitalize" label="Mapping Behavior">
|
||||
{mappingBehavior}
|
||||
</SecretSyncLabel>
|
||||
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
|
||||
<SecretSyncLabel label="Secret Name">{secretName}</SecretSyncLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,6 +3,7 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
@ -36,6 +37,9 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||
break;
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
export const AwsSecretsManagerSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
)
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-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";
|
||||
@ -32,6 +33,7 @@ const BaseSecretSyncSchema = z.object({
|
||||
|
||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncDestinationSchema,
|
||||
AwsSecretsManagerSyncDestinationSchema,
|
||||
GitHubSyncDestinationSchema,
|
||||
GcpSyncDestinationSchema
|
||||
]);
|
||||
|
@ -6,13 +6,15 @@ import {
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||
[SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" },
|
||||
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
|
||||
[SecretSync.AWSSecretsManager]: { name: "AWS Secrets Manager", image: "Amazon Web Services.png" },
|
||||
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" },
|
||||
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
|
||||
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||
};
|
||||
|
@ -4,6 +4,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_SECRETS]: "List secrets",
|
||||
[EventType.GET_SECRET]: "Read secret",
|
||||
[EventType.DELETE_SECRETS]: "Delete secrets",
|
||||
[EventType.CREATE_SECRETS]: "Create secrets",
|
||||
[EventType.UPDATE_SECRETS]: "Update secrets",
|
||||
[EventType.CREATE_SECRET]: "Create secret",
|
||||
[EventType.UPDATE_SECRET]: "Update secret",
|
||||
[EventType.DELETE_SECRET]: "Delete secret",
|
||||
|
@ -19,6 +19,8 @@ export enum UserAgentType {
|
||||
export enum EventType {
|
||||
GET_SECRETS = "get-secrets",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
|
@ -75,7 +75,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
|
||||
};
|
||||
|
||||
export type TDashboardProjectSecretsQuickSearchResponse = {
|
||||
folders: (TSecretFolder & { environment: string; path: string })[];
|
||||
folders: (TSecretFolder & { envId: string; path: string })[];
|
||||
dynamicSecrets: (TDynamicSecret & { environment: string; path: string })[];
|
||||
secrets: SecretV3Raw[];
|
||||
};
|
||||
@ -83,7 +83,7 @@ export type TDashboardProjectSecretsQuickSearchResponse = {
|
||||
export type TDashboardProjectSecretsQuickSearch = {
|
||||
folders: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
|
||||
secrets: Record<string, SecretV3RawSanitized[]>;
|
||||
dynamicSecrets: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
|
||||
dynamicSecrets: Record<string, TDashboardProjectSecretsQuickSearchResponse["dynamicSecrets"]>;
|
||||
};
|
||||
|
||||
export type TGetDashboardProjectSecretsQuickSearchDTO = {
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum SecretSync {
|
||||
AWSParameterStore = "aws-parameter-store",
|
||||
AWSSecretsManager = "aws-secrets-manager",
|
||||
GitHub = "github",
|
||||
GCPSecretManager = "gcp-secret-manager"
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TAwsSecretsManagerSync = TRootSecretSync & {
|
||||
destination: SecretSync.AWSSecretsManager;
|
||||
destinationConfig:
|
||||
| {
|
||||
mappingBehavior: AwsSecretsManagerSyncMappingBehavior.OneToOne;
|
||||
region: string;
|
||||
}
|
||||
| {
|
||||
region: string;
|
||||
mappingBehavior: AwsSecretsManagerSyncMappingBehavior.ManyToOne;
|
||||
secretName: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.AWS;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||
OneToOne = "one-to-one",
|
||||
ManyToOne = "many-to-one"
|
||||
}
|
@ -3,6 +3,7 @@ import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-par
|
||||
import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
|
||||
import { TGcpSync } from "./gcp-sync";
|
||||
|
||||
export type TSecretSyncOption = {
|
||||
@ -11,7 +12,7 @@ export type TSecretSyncOption = {
|
||||
canImportSecrets: boolean;
|
||||
};
|
||||
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
|
||||
export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync;
|
||||
|
||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||
|
||||
|
@ -52,7 +52,7 @@ export const IntegrationsListPage = () => {
|
||||
/>
|
||||
<div className="mb-4 mt-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
Integrations Update
|
||||
</div>
|
||||
<p className="mb-2 mt-1 text-sm text-bunker-300">
|
||||
|
@ -0,0 +1,43 @@
|
||||
import {
|
||||
AwsSecretsManagerSyncMappingBehavior,
|
||||
TAwsSecretsManagerSync
|
||||
} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell, SecretSyncTableCellProps } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TAwsSecretsManagerSync;
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
let additionalProps: Pick<
|
||||
SecretSyncTableCellProps,
|
||||
"additionalTooltipContent" | "infoBadge" | "secondaryClassName"
|
||||
> = {};
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne) {
|
||||
additionalProps = {
|
||||
infoBadge: "secondary",
|
||||
additionalTooltipContent: (
|
||||
<div className="mt-4">
|
||||
<span className="text-xs text-bunker-300">Secret Name:</span>
|
||||
<p className="text-sm">{destinationConfig.secretName}</p>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<SecretSyncTableCell
|
||||
{...additionalProps}
|
||||
secondaryClassName="capitalize"
|
||||
primaryText={primaryText}
|
||||
secondaryText={secondaryText}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
||||
import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol";
|
||||
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
|
||||
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
|
||||
|
||||
@ -12,6 +13,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
return <AwsSecretsManagerSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.GitHub:
|
||||
return <GitHubSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.GCPSecretManager:
|
||||
|
@ -30,7 +30,11 @@ export const SecretSyncTableCell = ({
|
||||
content={
|
||||
<>
|
||||
<p className="text-sm">{primaryText}</p>
|
||||
{secondaryText && <p className="text-xs leading-3 text-bunker-300">{secondaryText}</p>}
|
||||
{secondaryText && (
|
||||
<p className={twMerge("text-xs leading-3 text-bunker-300", secondaryClassName)}>
|
||||
{secondaryText}
|
||||
</p>
|
||||
)}
|
||||
{additionalTooltipContent}
|
||||
</>
|
||||
}
|
||||
|
@ -17,6 +17,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.path;
|
||||
secondaryText = destinationConfig.region;
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
primaryText = destinationConfig.region;
|
||||
secondaryText = destinationConfig.mappingBehavior;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
switch (destinationConfig.scope) {
|
||||
case GitHubSyncScope.Organization:
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
TDeleteSecretBatchDTO,
|
||||
TSecretFolder
|
||||
} from "@app/hooks/api/types";
|
||||
import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components";
|
||||
|
||||
export enum EntryType {
|
||||
FOLDER = "folder",
|
||||
@ -38,7 +39,8 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"bulkDeleteEntries"
|
||||
"bulkDeleteEntries",
|
||||
"bulkMoveSecrets"
|
||||
] as const);
|
||||
|
||||
const selectedFolderCount = Object.keys(selectedEntries.folder).length;
|
||||
@ -165,6 +167,8 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
}
|
||||
};
|
||||
|
||||
const areFoldersSelected = Boolean(Object.keys(selectedEntries[EntryType.FOLDER]).length);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -181,19 +185,46 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
</Tooltip>
|
||||
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
|
||||
{shouldShowDelete && (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
colorSchema="danger"
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpOpen("bulkDeleteEntries")}
|
||||
size="xs"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<>
|
||||
<Tooltip content={areFoldersSelected ? "Moving folders is not supported" : undefined}>
|
||||
<div>
|
||||
<Button
|
||||
isDisabled={areFoldersSelected}
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faAnglesRight} />}
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpOpen("bulkMoveSecrets")}
|
||||
size="xs"
|
||||
>
|
||||
Move
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
colorSchema="danger"
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpOpen("bulkDeleteEntries")}
|
||||
size="xs"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<MoveSecretsModal
|
||||
isOpen={popUp.bulkMoveSecrets.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("bulkMoveSecrets", isOpen)}
|
||||
environments={userAvailableEnvs}
|
||||
projectId={workspaceId}
|
||||
projectSlug={currentWorkspace.slug}
|
||||
sourceSecretPath={secretPath}
|
||||
secrets={selectedEntries[EntryType.SECRET]}
|
||||
onComplete={resetSelectedEntries}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.bulkDeleteEntries.isOpen}
|
||||
deleteKey="delete"
|
||||
|
@ -0,0 +1,395 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SingleValue } from "react-select";
|
||||
import { subject } from "@casl/ability";
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faBan,
|
||||
faCheckCircle,
|
||||
faExclamationCircle,
|
||||
faEyeSlash,
|
||||
faInfoCircle,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import axios from "axios";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Spinner,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useMoveSecrets } from "@app/hooks/api";
|
||||
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
environments: WorkspaceEnv[];
|
||||
projectId: string;
|
||||
projectSlug: string;
|
||||
sourceSecretPath: string;
|
||||
secrets: Record<string, Record<string, SecretV3RawSanitized>>;
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
type ContentProps = Omit<Props, "isOpen" | "onOpenChange">;
|
||||
|
||||
type OptionValue = { secretPath: string };
|
||||
|
||||
enum MoveResult {
|
||||
Success = "success",
|
||||
Info = "info",
|
||||
Error = "error"
|
||||
}
|
||||
|
||||
type MoveResults = {
|
||||
status: MoveResult;
|
||||
name: string;
|
||||
id: string;
|
||||
message: string;
|
||||
}[];
|
||||
|
||||
const Content = ({
|
||||
onComplete,
|
||||
secrets,
|
||||
projectSlug,
|
||||
environments,
|
||||
projectId,
|
||||
sourceSecretPath
|
||||
}: ContentProps) => {
|
||||
const [search, setSearch] = useState(sourceSecretPath);
|
||||
const [debouncedSearch] = useDebounce(search);
|
||||
const [value, setValue] = useState<OptionValue | null>({ secretPath: sourceSecretPath });
|
||||
const [previousValue, setPreviousValue] = useState<OptionValue | null>(value);
|
||||
const moveSecrets = useMoveSecrets();
|
||||
const [shouldOverwrite, setShouldOverwrite] = useState(false);
|
||||
const { permission } = useProjectPermission();
|
||||
const [moveResults, setMoveResults] = useState<MoveResults | null>(null);
|
||||
|
||||
const { data, isPending, isLoading, isFetching } = useGetProjectSecretsQuickSearch({
|
||||
secretPath: "/",
|
||||
environments: environments.map((env) => env.slug),
|
||||
projectId,
|
||||
search: debouncedSearch,
|
||||
tags: {}
|
||||
});
|
||||
|
||||
const { folders = {} } = data ?? {};
|
||||
|
||||
const folderEnvironments = value && folders[value.secretPath]?.map((folder) => folder.envId);
|
||||
|
||||
const moveSecretsEligibility = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
environments.map((env) => [
|
||||
env.slug,
|
||||
{
|
||||
missingPermissions: permission.cannot(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env.slug,
|
||||
secretPath: sourceSecretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
),
|
||||
missingPath: folderEnvironments && !folderEnvironments?.includes(env.id)
|
||||
}
|
||||
])
|
||||
);
|
||||
}, [permission, folderEnvironments]);
|
||||
|
||||
const destinationSelected = Boolean(value?.secretPath) && sourceSecretPath !== value?.secretPath;
|
||||
|
||||
const environmentsToBeSkipped = useMemo(() => {
|
||||
if (!destinationSelected) return [];
|
||||
|
||||
const environmentWarnings: { type: "permission" | "missing"; message: string; id: string }[] =
|
||||
[];
|
||||
|
||||
environments.forEach((env) => {
|
||||
if (moveSecretsEligibility[env.slug].missingPermissions) {
|
||||
environmentWarnings.push({
|
||||
id: env.id,
|
||||
type: "permission",
|
||||
message: `${env.name}: You do not have permission to remove secrets from this environment`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (moveSecretsEligibility[env.slug].missingPath) {
|
||||
environmentWarnings.push({
|
||||
id: env.id,
|
||||
type: "missing",
|
||||
message: `${env.name}: Secret path does not exist in environment`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return environmentWarnings;
|
||||
}, [moveSecretsEligibility]);
|
||||
|
||||
const handleMoveSecrets = async () => {
|
||||
if (!value) {
|
||||
createNotification({
|
||||
text: "error",
|
||||
title: "You must specify a secret path to move the selected secrets to"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const results: MoveResults = [];
|
||||
|
||||
const secretsByEnv: Record<string, SecretV3RawSanitized[]> = Object.fromEntries(
|
||||
environments.map((env) => [env.slug, []])
|
||||
);
|
||||
|
||||
Object.values(secrets).forEach((secretRecord) =>
|
||||
Object.entries(secretRecord).map(([env, secret]) => secretsByEnv[env].push(secret))
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const environment of environments) {
|
||||
const envSlug = environment.slug;
|
||||
|
||||
const secretsToMove = secretsByEnv[envSlug];
|
||||
|
||||
if (
|
||||
moveSecretsEligibility[envSlug].missingPermissions ||
|
||||
moveSecretsEligibility[envSlug].missingPath
|
||||
) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!secretsToMove.length) {
|
||||
results.push({
|
||||
name: environment.name,
|
||||
message: "No secrets selected in environment",
|
||||
status: MoveResult.Info,
|
||||
id: environment.id
|
||||
});
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const { isDestinationUpdated, isSourceUpdated } = await moveSecrets.mutateAsync({
|
||||
projectSlug,
|
||||
shouldOverwrite,
|
||||
sourceEnvironment: environment.slug,
|
||||
sourceSecretPath,
|
||||
destinationEnvironment: environment.slug,
|
||||
destinationSecretPath: value.secretPath,
|
||||
projectId,
|
||||
secretIds: secretsToMove.map((sec) => sec.id)
|
||||
});
|
||||
|
||||
let message = "";
|
||||
let status: MoveResult = MoveResult.Info;
|
||||
|
||||
if (isDestinationUpdated && isSourceUpdated) {
|
||||
message = "Successfully moved selected secrets";
|
||||
status = MoveResult.Success;
|
||||
} else if (isDestinationUpdated) {
|
||||
message =
|
||||
"Successfully created secrets in destination. A secret approval request has been generated for the source.";
|
||||
} else if (isSourceUpdated) {
|
||||
message = "A secret approval request has been generated in the destination";
|
||||
} else {
|
||||
message =
|
||||
"A secret approval request has been generated in both the source and the destination.";
|
||||
}
|
||||
|
||||
results.push({
|
||||
name: environment.name,
|
||||
message,
|
||||
status,
|
||||
id: environment.id
|
||||
});
|
||||
} catch (error) {
|
||||
let errorMessage = (error as Error)?.message ?? "Failed to move secrets";
|
||||
if (axios.isAxiosError(error)) {
|
||||
const { message } = error?.response?.data as { message: string };
|
||||
if (message) errorMessage = message;
|
||||
}
|
||||
|
||||
results.push({
|
||||
name: environment.name,
|
||||
message: errorMessage,
|
||||
status: MoveResult.Error,
|
||||
id: environment.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setMoveResults(results);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (moveResults) onComplete();
|
||||
};
|
||||
}, [moveResults]);
|
||||
|
||||
if (moveResults) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-2">Results</div>
|
||||
<div className="mb-4 flex flex-col divide-y divide-mineshaft-600 rounded bg-mineshaft-900 px-3 py-2">
|
||||
{moveResults.map(({ id, name, status, message }) => {
|
||||
let className: string;
|
||||
let icon: IconDefinition;
|
||||
|
||||
switch (status) {
|
||||
case MoveResult.Success:
|
||||
icon = faCheckCircle;
|
||||
className = "text-green";
|
||||
break;
|
||||
case MoveResult.Info:
|
||||
icon = faInfoCircle;
|
||||
className = "text-blue-500";
|
||||
break;
|
||||
case MoveResult.Error:
|
||||
default:
|
||||
icon = faExclamationCircle;
|
||||
className = "text-red";
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id} className="p-2 text-sm">
|
||||
<FontAwesomeIcon className={twMerge(className, "mr-1")} icon={icon} /> {name}:{" "}
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ModalClose asChild>
|
||||
<Button size="sm" colorSchema="secondary" onClick={() => onComplete()}>
|
||||
Dismiss
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (moveSecrets.isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Moving secrets...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
label="Select New Location"
|
||||
helperText="Nested folders will be displayed as secret path is typed"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isPending || isLoading || isFetching || search !== debouncedSearch}
|
||||
options={Object.keys(folders).map((secretPath) => ({
|
||||
secretPath
|
||||
}))}
|
||||
onMenuOpen={() => {
|
||||
setPreviousValue(value);
|
||||
setSearch(value?.secretPath ?? "/");
|
||||
setValue(null);
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
if (!value) setValue(previousValue);
|
||||
}}
|
||||
inputValue={search}
|
||||
onInputChange={setSearch}
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
setPreviousValue(value);
|
||||
setValue(newValue as SingleValue<OptionValue>);
|
||||
}}
|
||||
getOptionLabel={(option) => option.secretPath}
|
||||
getOptionValue={(option) => option.secretPath}
|
||||
/>
|
||||
</FormControl>
|
||||
{Boolean(environmentsToBeSkipped.length) && (
|
||||
<div className="rounded bg-mineshaft-900 px-3 py-2">
|
||||
<span className="text-sm text-yellow">
|
||||
<FontAwesomeIcon icon={faWarning} className="mr-0.5" /> The following environments will
|
||||
not be affected
|
||||
</span>
|
||||
{environmentsToBeSkipped.map((env) => (
|
||||
<div
|
||||
key={env.id}
|
||||
className={`${env.type === "permission" ? "text-red" : "text-mineshaft-300"} mb-0.5 flex items-start gap-2 text-sm`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="mt-1"
|
||||
icon={env.type === "permission" ? faBan : faEyeSlash}
|
||||
/>
|
||||
<span>{env.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<FormControl
|
||||
className="my-4"
|
||||
helperText={
|
||||
shouldOverwrite
|
||||
? "Secrets with conflicting keys at the destination will be overwritten"
|
||||
: "Secrets with conflicting keys at the destination will not be overwritten"
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-yellow/80"
|
||||
id="overwrite-existing-secrets"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={setShouldOverwrite}
|
||||
isChecked={shouldOverwrite}
|
||||
>
|
||||
<p className="w-[11rem]">Overwrite Existing Secrets</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
isDisabled={!destinationSelected}
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
colorSchema="secondary"
|
||||
onClick={handleMoveSecrets}
|
||||
>
|
||||
Move Secrets
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoveSecretsModal = ({ isOpen, onOpenChange, ...props }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Move Secrets Folder Location"
|
||||
subTitle="Move the selected secrets across all environments to a new folder location"
|
||||
>
|
||||
<Content {...props} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./MoveSecretsDialog";
|
@ -0,0 +1 @@
|
||||
export * from "./MoveSecretsDialog";
|
@ -0,0 +1,34 @@
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import {
|
||||
AwsSecretsManagerSyncMappingBehavior,
|
||||
TAwsSecretsManagerSync
|
||||
} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TAwsSecretsManagerSync;
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const awsRegion = AWS_REGIONS.find((r) => r.slug === destinationConfig.region);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Region">
|
||||
{awsRegion?.name}
|
||||
<Badge className="ml-1" variant="success">
|
||||
{awsRegion?.slug}{" "}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Mapping Behavior" className="capitalize">
|
||||
{destinationConfig.mappingBehavior}
|
||||
</SecretSyncLabel>
|
||||
{destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
|
||||
<SecretSyncLabel label="Secret Name">{destinationConfig.secretName}</SecretSyncLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -10,6 +10,7 @@ import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissi
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
|
||||
import { AwsSecretsManagerSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection";
|
||||
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
|
||||
|
||||
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
|
||||
@ -29,6 +30,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationComponents = <AwsParameterStoreSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
DestinationComponents = <AwsSecretsManagerSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
|
@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.8.9
|
||||
version: v0.8.11
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.8.9"
|
||||
appVersion: "v0.8.11"
|
||||
|
@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.8.8
|
||||
tag: v0.8.11
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
8
helm-charts/upload-infisical-core-helm-cloudsmith.sh
Executable file
@ -0,0 +1,8 @@
|
||||
cd "infisical-standalone-postgres"
|
||||
helm dependency update
|
||||
helm package .
|
||||
for i in *.tgz; do
|
||||
[ -f "$i" ] || break
|
||||
cloudsmith push helm --republish infisical/helm-charts "$i"
|
||||
done
|
||||
cd ..
|
8
helm-charts/upload-k8s-operator-cloudsmith.sh
Executable file
@ -0,0 +1,8 @@
|
||||
cd secrets-operator
|
||||
helm dependency update
|
||||
helm package .
|
||||
for i in *.tgz; do
|
||||
[ -f "$i" ] || break
|
||||
cloudsmith push helm --republish infisical/helm-charts "$i"
|
||||
done
|
||||
cd ..
|
@ -104,7 +104,7 @@ spec:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
SSH_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
|
||||
BINARY_KEY: "{{ toBase64DecodedString .BINARY_KEY_BASE64.Value }}"
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
|
||||
|
@ -156,12 +156,12 @@ func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKub
|
||||
}
|
||||
|
||||
var infisicalSecretTemplateFunctions = template.FuncMap{
|
||||
"decodeBase64ToBytes": func(encodedString string) []byte {
|
||||
"decodeBase64ToBytes": func(encodedString string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return decoded
|
||||
return string(decoded)
|
||||
},
|
||||
}
|
||||
|
||||
@ -222,7 +222,6 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
|
||||
}
|
||||
|
||||
annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
|
||||
|
||||
// create a new secret as specified by the managed secret spec of CRD
|
||||
newKubeSecretInstance := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|