diff --git a/.github/workflows/run-cli-tests.yml b/.github/workflows/run-cli-tests.yml index f8e9d7797..da6f507a7 100644 --- a/.github/workflows/run-cli-tests.yml +++ b/.github/workflows/run-cli-tests.yml @@ -50,6 +50,6 @@ jobs: CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} - INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} + # INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} run: go test -v -count=1 ./test diff --git a/Makefile b/Makefile index 11143162e..aec2dad74 100644 --- a/Makefile +++ b/Makefile @@ -15,3 +15,16 @@ up-prod: down: docker compose -f docker-compose.dev.yml down + +reviewable-ui: + cd frontend && \ + npm run lint:fix && \ + npm run type:check + +reviewable-api: + cd backend && \ + npm run lint:fix && \ + npm run type:check + +reviewable: reviewable-ui reviewable-api + diff --git a/backend/package-lock.json b/backend/package-lock.json index ffe9cfcb0..71d994f2e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,6 +25,7 @@ "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", "@node-saml/passport-saml": "^4.0.4", + "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", "@peculiar/asn1-schema": "^2.3.8", @@ -7812,19 +7813,45 @@ } }, "node_modules/@octokit/plugin-retry": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", - "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz", + "integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", + "@octokit/request-error": "^4.0.1", + "@octokit/types": "^10.0.0", "bottleneck": "^2.15.3" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz", + "integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==", + "dependencies": { + "@octokit/types": "^10.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz", + "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" } }, "node_modules/@octokit/plugin-throttling": { @@ -17396,6 +17423,22 @@ "node": ">=18" } }, + "node_modules/probot/node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, "node_modules/probot/node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 1328db4dd..4ea217808 100644 --- a/backend/package.json +++ b/backend/package.json @@ -121,6 +121,7 @@ "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", "@node-saml/passport-saml": "^4.0.4", + "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", "@peculiar/asn1-schema": "^2.3.8", diff --git a/backend/scripts/create-backend-file.ts b/backend/scripts/create-backend-file.ts index fb71994ce..39e9c816c 100644 --- a/backend/scripts/create-backend-file.ts +++ b/backend/scripts/create-backend-file.ts @@ -7,14 +7,33 @@ const prompt = promptSync({ sigint: true }); +type ComponentType = 1 | 2 | 3; + console.log(` Component List -------------- +0. Exit 1. Service component 2. DAL component 3. Router component `); -const componentType = parseInt(prompt("Select a component: "), 10); + +function getComponentType(): ComponentType { + while (true) { + const input = prompt("Select a component (0-3): "); + const componentType = parseInt(input, 10); + + if (componentType === 0) { + console.log("Exiting the program. Goodbye!"); + process.exit(0); + } else if (componentType === 1 || componentType === 2 || componentType === 3) { + return componentType; + } else { + console.log("Invalid input. Please enter 0, 1, 2, or 3."); + } + } +} +const componentType = getComponentType(); if (componentType === 1) { const componentName = prompt("Enter service name: "); diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index bd9e483ae..da1d93bf7 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; +import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -91,6 +92,7 @@ declare module "fastify" { id: string; orgId: string; }; + rateLimits: RateLimitConfiguration; // passport data passportUser: { isUserCompleted: string; diff --git a/backend/src/db/migrations/20240806113425_remove-creation-limit-rate-limit.ts b/backend/src/db/migrations/20240806113425_remove-creation-limit-rate-limit.ts new file mode 100644 index 000000000..d82e4d65d --- /dev/null +++ b/backend/src/db/migrations/20240806113425_remove-creation-limit-rate-limit.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit"); + await knex.schema.alterTable(TableName.RateLimit, (t) => { + if (hasCreationLimitCol) { + t.dropColumn("creationLimit"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit"); + await knex.schema.alterTable(TableName.RateLimit, (t) => { + if (!hasCreationLimitCol) { + t.integer("creationLimit").defaultTo(30).notNullable(); + } + }); +} diff --git a/backend/src/db/migrations/20240806185442_drop-tag-name.ts b/backend/src/db/migrations/20240806185442_drop-tag-name.ts new file mode 100644 index 000000000..db85ab216 --- /dev/null +++ b/backend/src/db/migrations/20240806185442_drop-tag-name.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name"); + if (hasNameField) { + await knex.schema.alterTable(TableName.SecretTag, (t) => { + t.dropColumn("name"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name"); + if (!hasNameField) { + await knex.schema.alterTable(TableName.SecretTag, (t) => { + t.string("name"); + }); + } +} diff --git a/backend/src/db/schemas/rate-limit.ts b/backend/src/db/schemas/rate-limit.ts index 86b8776cc..233f6cdbc 100644 --- a/backend/src/db/schemas/rate-limit.ts +++ b/backend/src/db/schemas/rate-limit.ts @@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({ authRateLimit: z.number().default(60), inviteUserRateLimit: z.number().default(30), mfaRateLimit: z.number().default(20), - creationLimit: z.number().default(30), publicEndpointLimit: z.number().default(30), createdAt: z.date(), updatedAt: z.date() diff --git a/backend/src/db/schemas/secret-tags.ts b/backend/src/db/schemas/secret-tags.ts index 04bb7b752..7ba7204a4 100644 --- a/backend/src/db/schemas/secret-tags.ts +++ b/backend/src/db/schemas/secret-tags.ts @@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models"; export const SecretTagsSchema = z.object({ id: z.string().uuid(), - name: z.string(), slug: z.string(), color: z.string().nullable().optional(), createdAt: z.date(), diff --git a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts index 5ef9f7eeb..c19af4d22 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts @@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide .default("/") .transform(removeTrailingSlash) .describe(DYNAMIC_SECRET_LEASES.RENEW.path), - environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl) + environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.environmentSlug) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/rate-limit-router.ts b/backend/src/ee/routes/v1/rate-limit-router.ts index 2b08a0c32..66ea62ece 100644 --- a/backend/src/ee/routes/v1/rate-limit-router.ts +++ b/backend/src/ee/routes/v1/rate-limit-router.ts @@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => { authRateLimit: z.number(), inviteUserRateLimit: z.number(), mfaRateLimit: z.number(), - creationLimit: z.number(), publicEndpointLimit: z.number() }), response: { diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index 316cf34a5..249e239c5 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => { .del() .returning("id"); numberOfRetryOnFailure = 0; // reset - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - setTimeout(resolve, 100); // time to breathe for db - }); } catch (error) { numberOfRetryOnFailure += 1; logger.error(error, "Failed to delete audit log on pruning"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); } - } while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE); + } while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE); }; return { ...auditLogOrm, pruneAuditLog, find }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 41186756a..ff4689ebc 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -352,6 +352,7 @@ interface DeleteIntegrationEvent { targetServiceId?: string; path?: string; region?: string; + shouldDeleteIntegrationSecrets?: boolean; }; } diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index fc0fb54a7..bd40f75cb 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ secretRotation: true, caCrl: false, instanceUserManagement: false, - externalKms: false + externalKms: false, + rateLimits: { + readLimit: 60, + writeLimit: 200, + secretsLimit: 40 + } }); export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index df85f008f..70db8e4d9 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -58,6 +58,11 @@ export type TFeatureSet = { caCrl: false; instanceUserManagement: false; externalKms: false; + rateLimits: { + readLimit: number; + writeLimit: number; + secretsLimit: number; + }; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/rate-limit/rate-limit-service.ts b/backend/src/ee/services/rate-limit/rate-limit-service.ts index df90ca03f..208fa8428 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-service.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-service.ts @@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger"; import { TLicenseServiceFactory } from "../license/license-service"; import { TRateLimitDALFactory } from "./rate-limit-dal"; -import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; +import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; -let rateLimitMaxConfiguration = { +let rateLimitMaxConfiguration: RateLimitConfiguration = { readLimit: 60, publicEndpointLimit: 30, writeLimit: 200, secretsLimit: 60, authRateLimit: 60, inviteUserRateLimit: 30, - mfaRateLimit: 20, - creationLimit: 30 + mfaRateLimit: 20 }; Object.freeze(rateLimitMaxConfiguration); @@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL secretsLimit: rateLimit.secretsRateLimit, authRateLimit: rateLimit.authRateLimit, inviteUserRateLimit: rateLimit.inviteUserRateLimit, - mfaRateLimit: rateLimit.mfaRateLimit, - creationLimit: rateLimit.creationLimit + mfaRateLimit: rateLimit.mfaRateLimit }; logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration); diff --git a/backend/src/ee/services/rate-limit/rate-limit-types.ts b/backend/src/ee/services/rate-limit/rate-limit-types.ts index 19519aafb..d924dce51 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-types.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-types.ts @@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = { authRateLimit: number; inviteUserRateLimit: number; mfaRateLimit: number; - creationLimit: number; publicEndpointLimit: number; }; @@ -14,3 +13,13 @@ export type TRateLimit = { createdAt: Date; updatedAt: Date; } & TRateLimitUpdateDTO; + +export type RateLimitConfiguration = { + readLimit: number; + publicEndpointLimit: number; + writeLimit: number; + secretsLimit: number; + authRateLimit: number; + inviteUserRateLimit: number; + mfaRateLimit: number; +}; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index b827f6a21..592f6ccd0 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { .select({ secVerTagId: "secVerTag.id", secVerTagColor: "secVerTag.color", - secVerTagSlug: "secVerTag.slug", - secVerTagName: "secVerTag.name" + secVerTagSlug: "secVerTag.slug" }) .select( db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ) .select( db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), @@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { { key: "tagJnId", label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ + mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({ id, - name, + name: slug, slug, color }) @@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { { key: "secVerTagId", label: "tags" as const, - mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ + mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({ // eslint-disable-next-line id, // eslint-disable-next-line - name, + name: slug, // eslint-disable-next-line slug, // eslint-disable-next-line @@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { .select({ secVerTagId: "secVerTag.id", secVerTagColor: "secVerTag.color", - secVerTagSlug: "secVerTag.slug", - secVerTagName: "secVerTag.name" + secVerTagSlug: "secVerTag.slug" }) .select( db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ) .select( db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"), @@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { { key: "tagJnId", label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ + mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({ id, - name, + name: slug, slug, color }) @@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { { key: "secVerTagId", label: "tags" as const, - mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ + mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({ // eslint-disable-next-line id, // eslint-disable-next-line - name, + name: slug, // eslint-disable-next-line slug, // eslint-disable-next-line diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 4ae4d25ea..81ccf35f6 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -224,12 +224,10 @@ export const secretApprovalRequestServiceFactory = ({ secretKey: el.key, id: el.id, version: el.version, - secretValue: el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : undefined, + secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : undefined, + : "", secret: el.secret ? { secretKey: el.secret.key, @@ -237,10 +235,10 @@ export const secretApprovalRequestServiceFactory = ({ version: el.secret.version, secretValue: el.secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() - : undefined, + : "", secretComment: el.secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString() - : undefined + : "" } : undefined, secretVersion: el.secretVersion @@ -250,10 +248,10 @@ export const secretApprovalRequestServiceFactory = ({ version: el.secretVersion.version, secretValue: el.secretVersion.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() - : undefined, + : "", secretComment: el.secretVersion.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() - : undefined + : "" } : undefined })); diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 52d10b690..b77d6cbc2 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({ secretDAL: secretV2BridgeDAL, folderDAL, secretImportDAL, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }); // secrets that gets replicated across imports const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({ @@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({ }); } if (locallyDeletedSecrets.length) { - await secretDAL.delete( + await secretV2BridgeDAL.delete( { $in: { id: locallyDeletedSecrets.map(({ id }) => id) diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index c5d8bd4fa..225339104 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({ secretKey: el.key, secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : undefined, + : "", secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : undefined + : "" })) }; } else { diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index 8e5014fdd..e4d341c1a 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ); return sqlNestRelationships({ data, @@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => { { key: "tagVersionId", label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ + mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ id, - name, + name: slug, slug, color, vId @@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ); return sqlNestRelationships({ data, @@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => { { key: "tagVersionId", label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ + mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ id, - name, + name: slug, slug, color, vId @@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ); const formated = sqlNestRelationships({ @@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => { { key: "tagVersionId", label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ + mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ id, - name, + name: slug, slug, color, vId @@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ); const formated = sqlNestRelationships({ @@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => { { key: "tagVersionId", label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ + mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ id, - name, + name: slug, slug, color, vId diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 90eacb9a7..b3aa089ab 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -596,7 +596,8 @@ export const RAW_SECRETS = { "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", 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." + includeImports: "Weather to include imported secrets or not.", + tagSlugs: "The comma separated tag slugs to filter secrets" }, CREATE: { secretName: "The name of the secret to create.", diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index dcab16218..cbf5db3b0 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -128,6 +128,16 @@ export const ormify = (db: Kne throw new DatabaseError({ error, name: "Create" }); } }, + // This spilit the insert into multiple chunk + batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => { + try { + if (!data.length) return []; + const res = await (tx || db).batchInsert(tableName, data as never).returning("*"); + return res as Tables[Tname]["base"][]; + } catch (error) { + throw new DatabaseError({ error, name: "batchInsert" }); + } + }, upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => { try { if (!data.length) return []; diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 79b709ee6..bdbf80371 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -1,7 +1,6 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import { Redis } from "ioredis"; -import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service"; import { getConfig } from "@app/lib/config/env"; export const globalRateLimiterCfg = (): RateLimitPluginOptions => { @@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => { // GET endpoints export const readLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().readLimit, + hook: "preValidation", + max: (req) => req.rateLimits.readLimit, keyGenerator: (req) => req.realIp }; // POST, PATCH, PUT, DELETE endpoints export const writeLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().writeLimit, + hook: "preValidation", + max: (req) => req.rateLimits.writeLimit, keyGenerator: (req) => req.realIp }; @@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = { export const secretsLimit: RateLimitOptions = { // secrets, folders, secret imports timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().secretsLimit, + hook: "preValidation", + max: (req) => req.rateLimits.secretsLimit, keyGenerator: (req) => req.realIp }; export const authRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().authRateLimit, + hook: "preValidation", + max: (req) => req.rateLimits.authRateLimit, keyGenerator: (req) => req.realIp }; export const inviteUserRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().inviteUserRateLimit, + hook: "preValidation", + max: (req) => req.rateLimits.inviteUserRateLimit, keyGenerator: (req) => req.realIp }; export const mfaRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().mfaRateLimit, + hook: "preValidation", + max: (req) => req.rateLimits.mfaRateLimit, keyGenerator: (req) => { return req.headers.authorization?.split(" ")[1] || req.realIp; } }; -export const creationLimit: RateLimitOptions = { - // identity, project, org - timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().creationLimit, - keyGenerator: (req) => req.realIp -}; - // Public endpoints to avoid brute force attacks export const publicEndpointLimit: RateLimitOptions = { // Read Shared Secrets timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().publicEndpointLimit, + hook: "preValidation", + max: (req) => req.rateLimits.publicEndpointLimit, keyGenerator: (req) => req.realIp }; diff --git a/backend/src/server/plugins/inject-rate-limits.ts b/backend/src/server/plugins/inject-rate-limits.ts new file mode 100644 index 000000000..1674ea542 --- /dev/null +++ b/backend/src/server/plugins/inject-rate-limits.ts @@ -0,0 +1,38 @@ +import fp from "fastify-plugin"; + +import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service"; +import { getConfig } from "@app/lib/config/env"; + +export const injectRateLimits = fp(async (server) => { + server.decorateRequest("rateLimits", null); + server.addHook("onRequest", async (req) => { + const appCfg = getConfig(); + + const instanceRateLimiterConfig = getRateLimiterConfig(); + if (!req.auth?.orgId) { + // for public endpoints, we always use the instance-wide default rate limits + req.rateLimits = instanceRateLimiterConfig; + return; + } + + const { rateLimits, customRateLimits } = await server.services.license.getPlan(req.auth.orgId); + + if (customRateLimits && !appCfg.isCloud) { + // we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration + // note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object + req.rateLimits = instanceRateLimiterConfig; + return; + } + + // we're using the null coalescing operator in order to handle outdated licenses + req.rateLimits = { + readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit, + writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit, + secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit, + publicEndpointLimit: instanceRateLimiterConfig.publicEndpointLimit, + authRateLimit: instanceRateLimiterConfig.authRateLimit, + inviteUserRateLimit: instanceRateLimiterConfig.inviteUserRateLimit, + mfaRateLimit: instanceRateLimiterConfig.mfaRateLimit + }; + }); +}); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 41d5f3249..7b68bbb94 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -189,6 +189,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectPermission } from "../plugins/auth/inject-permission"; +import { injectRateLimits } from "../plugins/inject-rate-limits"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; @@ -924,8 +925,15 @@ export const registerRoutes = async ( folderDAL, integrationDAL, integrationAuthDAL, - secretQueueService + secretQueueService, + integrationAuthService, + projectBotService, + secretV2BridgeDAL, + secretImportDAL, + secretDAL, + kmsService }); + const serviceTokenService = serviceTokenServiceFactory({ projectEnvDAL, serviceTokenDAL, @@ -1058,7 +1066,8 @@ export const registerRoutes = async ( snapshotDAL, identityAccessTokenDAL, secretSharingDAL, - secretVersionV2DAL: secretVersionV2BridgeDAL + secretVersionV2DAL: secretVersionV2BridgeDAL, + identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL }); const oidcService = oidcConfigServiceFactory({ @@ -1173,6 +1182,7 @@ export const registerRoutes = async ( await server.register(injectIdentity, { userDAL, serviceTokenDAL }); await server.register(injectPermission); + await server.register(injectRateLimits); await server.register(injectAuditLogInfo); server.route({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index a10a962cf..02d9e37e0 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -63,8 +63,8 @@ export const secretRawSchema = z.object({ version: z.number(), type: z.string(), secretKey: z.string(), - secretValue: z.string().optional(), - secretComment: z.string().optional(), + secretValue: z.string(), + secretComment: z.string(), secretReminderNote: z.string().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(), diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index b1cd6cd9d..23aca9625 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { IDENTITIES } from "@app/lib/api-docs"; -import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/", config: { - rateLimit: creationLimit + rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 97a7f4d7a..6526dd940 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { params: z.object({ integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId) }), + querystring: z.object({ + shouldDeleteIntegrationSecrets: z + .enum(["true", "false"]) + .optional() + .transform((val) => val === "true") + }), response: { 200: z.object({ integration: IntegrationsSchema @@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - id: req.params.integrationId + id: req.params.integrationId, + shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets }); await server.services.auditLog.createAuditLog({ @@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { targetService: integration.targetService, targetServiceId: integration.targetServiceId, path: integration.path, - region: integration.region + region: integration.region, + shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets // eslint-disable-next-line }) as any } diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index ce92409f6..7d696999e 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -1,3 +1,4 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { SecretTagsSchema } from "@app/db/schemas"; @@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaceTag: SecretTagsSchema + // akhilmhdh: for terraform backward compatiability + workspaceTag: SecretTagsSchema.extend({ name: z.string() }) }) } }, @@ -79,7 +81,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - workspaceTag: SecretTagsSchema + // akhilmhdh: for terraform backward compatiability + workspaceTag: SecretTagsSchema.extend({ name: z.string() }) }) } }, @@ -108,8 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) }), body: z.object({ - name: z.string().trim().describe(SECRET_TAGS.CREATE.name), - slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug), + slug: z + .string() + .toLowerCase() + .trim() + .describe(SECRET_TAGS.CREATE.slug) + .refine((v) => slugify(v) === v, { + message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens." + }), color: z.string().trim().describe(SECRET_TAGS.CREATE.color) }), response: { @@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) }), body: z.object({ - name: z.string().trim().describe(SECRET_TAGS.UPDATE.name), - slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug), + slug: z + .string() + .toLowerCase() + .trim() + .describe(SECRET_TAGS.UPDATE.slug) + .refine((v) => slugify(v) === v, { + message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens." + }), color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) }), response: { diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 49488776b..e6ea094c3 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -9,7 +9,7 @@ import { UsersSchema } from "@app/db/schemas"; import { ORGANIZATIONS } from "@app/lib/api-docs"; -import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/", config: { - rateLimit: creationLimit + rateLimit: writeLimit }, schema: { body: z.object({ diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 85043ecee..a45cb51fa 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -10,7 +10,7 @@ import { } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; -import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -148,7 +148,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/", config: { - rateLimit: creationLimit + rateLimit: writeLimit }, schema: { description: "Create a new project", diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index b0776d150..7a1868fef 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { tags: SecretTagsSchema.pick({ id: true, slug: true, - name: true, color: true - }).array() + }) + .extend({ name: z.string() }) + .array() }) ) }) @@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - name: true, - color: true - }).array() + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true }) - ) + .extend({ name: z.string() }) + .array() + }) }) } }, @@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .enum(["true", "false"]) .default("false") .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.includeImports) + .describe(RAW_SECRETS.LIST.includeImports), + tagSlugs: z + .string() + .describe(RAW_SECRETS.LIST.tagSlugs) + .optional() + // split by comma and trim the strings + .transform((el) => (el ? el.split(",").map((i) => i.trim()) : [])) }), response: { 200: z.object({ @@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { tags: SecretTagsSchema.pick({ id: true, slug: true, - name: true, color: true }) + .extend({ name: z.string() }) .array() .optional() }) @@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId: workspaceId, path: secretPath, includeImports: req.query.include_imports, - recursive: req.query.recursive + recursive: req.query.recursive, + tagSlugs: req.query.tagSlugs }); await server.services.auditLog.createAuditLog({ @@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { tags: SecretTagsSchema.pick({ id: true, slug: true, - name: true, color: true }) + .extend({ name: z.string() }) .array() .optional() }) @@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { tags: SecretTagsSchema.pick({ id: true, slug: true, - name: true, color: true - }).array() + }) + .extend({ name: z.string() }) + .array() }) .array(), imports: z diff --git a/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts b/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts index 6310da97a..ec0fd6fbf 100644 --- a/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts +++ b/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; export type TIdentityUaClientSecretDALFactory = ReturnType; @@ -23,5 +24,55 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => { } }; - return { ...uaClientSecretOrm, incrementUsage }; + const removeExpiredClientSecrets = async (tx?: Knex) => { + const BATCH_SIZE = 10000; + const MAX_RETRY_ON_FAILURE = 3; + + let deletedClientSecret: { id: string }[] = []; + let numberOfRetryOnFailure = 0; + + do { + try { + const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret) + .where({ + isClientSecretRevoked: true + }) + .orWhere((qb) => { + void qb + .where("clientSecretNumUses", ">", 0) + .andWhere( + "clientSecretNumUses", + ">=", + db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret) + ); + }) + .orWhere((qb) => { + void qb + .where("clientSecretTTL", ">", 0) + .andWhereRaw( + `"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()` + ); + }) + .select("id") + .limit(BATCH_SIZE); + + // eslint-disable-next-line no-await-in-loop + deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret) + .whereIn("id", findExpiredClientSecretQuery) + .del() + .returning("id"); + numberOfRetryOnFailure = 0; // reset + } catch (error) { + numberOfRetryOnFailure += 1; + logger.error(error, "Failed to delete client secret on pruning"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); + } + } while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE); + }; + + return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets }; }; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts new file mode 100644 index 000000000..5da48fb6e --- /dev/null +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -0,0 +1,357 @@ +import { retry } from "@octokit/plugin-retry"; +import { Octokit } from "@octokit/rest"; + +import { TIntegrationAuths, TIntegrations } from "@app/db/schemas"; +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { IntegrationMetadataSchema } from "../integration/integration-schema"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; +import { TSecretDALFactory } from "../secret/secret-dal"; +import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; +import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { TIntegrationAuthServiceFactory } from "./integration-auth-service"; +import { Integrations } from "./integration-list"; + +const MAX_SYNC_SECRET_DEPTH = 5; + +/** + * Return the secrets in a given [folderId] including secrets from + * nested imported folders recursively. + */ +const getIntegrationSecretsV2 = async ( + dto: { + projectId: string; + environment: string; + folderId: string; + depth: number; + decryptor: (value: Buffer | null | undefined) => string; + }, + secretV2BridgeDAL: Pick, + folderDAL: Pick, + secretImportDAL: Pick +) => { + const content: Record = {}; + if (dto.depth > MAX_SYNC_SECRET_DEPTH) { + logger.info( + `getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]` + ); + return content; + } + + // process secrets in current folder + const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); + + secrets.forEach((secret) => { + const secretKey = secret.key; + content[secretKey] = true; + }); + + // check if current folder has any imports from other folders + const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false }); + + // if no imports then return secrets in the current folder + if (!secretImports.length) return content; + const importedSecrets = await fnSecretsV2FromImports({ + decryptor: dto.decryptor, + folderDAL, + secretDAL: secretV2BridgeDAL, + secretImportDAL, + allowedImports: secretImports + }); + + for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { + const importedSecret = importedSecrets[i].secrets[j]; + if (!content[importedSecret.key]) { + content[importedSecret.key] = true; + } + } + } + return content; +}; + +/** + * Return the secrets in a given [folderId] including secrets from + * nested imported folders recursively. + */ +const getIntegrationSecretsV1 = async ( + dto: { + projectId: string; + environment: string; + folderId: string; + key: string; + depth: number; + }, + secretDAL: Pick, + folderDAL: Pick, + secretImportDAL: Pick +) => { + let content: Record = {}; + if (dto.depth > MAX_SYNC_SECRET_DEPTH) { + logger.info( + `getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]` + ); + return content; + } + + // process secrets in current folder + const secrets = await secretDAL.findByFolderId(dto.folderId); + secrets.forEach((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: dto.key + }); + + content[secretKey] = true; + }); + + // check if current folder has any imports from other folders + const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false }); + + // if no imports then return secrets in the current folder + if (!secretImport) return content; + + const importedFolders = await folderDAL.findByManySecretPath( + secretImport.map(({ importEnv, importPath }) => ({ + envId: importEnv.id, + secretPath: importPath + })) + ); + + for await (const folder of importedFolders) { + if (folder) { + // get secrets contained in each imported folder by recursively calling + // this function against the imported folder + const importedSecrets = await getIntegrationSecretsV1( + { + environment: dto.environment, + projectId: dto.projectId, + folderId: folder.id, + key: dto.key, + depth: dto.depth + 1 + }, + secretDAL, + folderDAL, + secretImportDAL + ); + + // add the imported secrets to the current folder secrets + content = { ...importedSecrets, ...content }; + } + } + + return content; +}; + +export const deleteGithubSecrets = async ({ + integration, + secrets, + accessToken +}: { + integration: Omit; + secrets: Record; + accessToken: string; +}) => { + interface GitHubSecret { + name: string; + created_at: string; + updated_at: string; + visibility?: "all" | "private" | "selected"; + selected_repositories_url?: string | undefined; + } + + const OctokitWithRetry = Octokit.plugin(retry); + const octokit = new OctokitWithRetry({ + auth: accessToken + }); + + enum GithubScope { + Repo = "github-repo", + Org = "github-org", + Env = "github-env" + } + + let encryptedGithubSecrets: GitHubSecret[]; + + switch (integration.scope) { + case GithubScope.Org: { + encryptedGithubSecrets = ( + await octokit.request("GET /orgs/{org}/actions/secrets", { + org: integration.owner as string + }) + ).data.secrets; + break; + } + case GithubScope.Env: { + encryptedGithubSecrets = ( + await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string + }) + ).data.secrets; + break; + } + default: { + encryptedGithubSecrets = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { + owner: integration.owner as string, + repo: integration.app as string + }) + ).data.secrets; + break; + } + } + + for await (const encryptedSecret of encryptedGithubSecrets) { + if (encryptedSecret.name in secrets) { + switch (integration.scope) { + case GithubScope.Org: { + await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", { + org: integration.owner as string, + secret_name: encryptedSecret.name + }); + break; + } + case GithubScope.Env: { + await octokit.request( + "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", + { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string, + secret_name: encryptedSecret.name + } + ); + break; + } + default: { + await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: encryptedSecret.name + }); + break; + } + } + + // small delay to prevent hitting API rate limits + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + } + } +}; + +export const deleteIntegrationSecrets = async ({ + integration, + integrationAuth, + integrationAuthService, + projectBotService, + secretV2BridgeDAL, + folderDAL, + secretDAL, + secretImportDAL, + kmsService +}: { + integration: Omit & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; + integrationAuth: TIntegrationAuths; + integrationAuthService: Pick; + projectBotService: Pick; + secretV2BridgeDAL: Pick; + folderDAL: Pick; + secretImportDAL: Pick; + secretDAL: Pick; + kmsService: Pick; +}) => { + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: integration.projectId + }); + + const folder = await folderDAL.findBySecretPath( + integration.projectId, + integration.environment.slug, + integration.secretPath + ); + + if (!folder) { + throw new NotFoundError({ + message: "Folder not found." + }); + } + + const { accessToken } = await integrationAuthService.getIntegrationAccessToken( + integrationAuth, + shouldUseSecretV2Bridge, + botKey + ); + + const secrets = shouldUseSecretV2Bridge + ? await getIntegrationSecretsV2( + { + environment: integration.environment.id, + projectId: integration.projectId, + folderId: folder.id, + depth: 1, + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + }, + secretV2BridgeDAL, + folderDAL, + secretImportDAL + ) + : await getIntegrationSecretsV1( + { + environment: integration.environment.id, + projectId: integration.projectId, + folderId: folder.id, + key: botKey as string, + depth: 1 + }, + secretDAL, + folderDAL, + secretImportDAL + ); + + const suffixedSecrets: typeof secrets = {}; + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + + if (metadata) { + Object.keys(secrets).forEach((key) => { + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; + }); + } + + switch (integration.integration) { + case Integrations.GITHUB: { + await deleteGithubSecrets({ + integration, + accessToken, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets + }); + break; + } + default: + throw new BadRequestError({ + message: "Invalid integration" + }); + } +}; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 4fc69b76c..b1f7d4cb8 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({ integration, secrets, accessId, - accessToken + accessToken, + projectId }: { - integration: TIntegrations; + integration: TIntegrations & { secretPath: string; environment: { slug: string } }; secrets: Record; accessId: string | null; accessToken: string; + projectId?: string; }) => { let response: { isSynced: boolean; syncMessage: string } | null = null; if (!accessId) { throw new Error("AWS access ID is required"); } - const config = new AWS.Config({ region: integration.region as string, credentials: { @@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({ const metadata = z.record(z.any()).parse(integration.metadata || {}); const awsParameterStoreSecretsObj: Record = {}; - + logger.info( + `getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]` + ); // now fetch all aws parameter store secrets let hasNext = true; let nextToken: string | undefined; @@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({ nextToken = parameters.NextToken; } + logger.info( + `getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${ + integration.environment.slug + }] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys( + awsParameterStoreSecretsObj + ).join(",")}]` + ); + logger.info( + `getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${ + integration.environment.slug + }] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]` + ); // Identify secrets to create // don't use Promise.all() and promise map here // it will cause rate limit @@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({ // case: secret does not exist in AWS parameter store // -> create secret if (secrets[key].value) { + logger.info( + `getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + ); await ssm .putParameter({ Name: `${integration.path}${key}`, Type: "SecureString", Value: secrets[key].value, ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }), - // Overwrite: true, - Tags: metadata.secretAWSTag - ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ - Key: tag.key, - Value: tag.value - })) - : [] + Overwrite: true }) .promise(); + if (metadata.secretAWSTag?.length) { + try { + await ssm + .addTagsToResource({ + ResourceType: "Parameter", + ResourceId: `${integration.path}${key}`, + Tags: metadata.secretAWSTag + ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ + Key: tag.key, + Value: tag.value + })) + : [] + }) + .promise(); + } catch (err) { + logger.error( + err, + `getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((err as any).code === "AccessDeniedException") { + logger.error( + `AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)` + ); + } + + response = { + isSynced: false, + syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store" + }; + } + } } // case: secret exists in AWS parameter store } else { + logger.info( + `getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + ); // -> update secret if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { await ssm @@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({ }) .promise(); } catch (err) { + logger.error( + err, + `getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((err as any).code === "AccessDeniedException") { logger.error( @@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({ } if (!metadata.shouldDisableDelete) { + logger.info( + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=1]` + ); for (const key in awsParameterStoreSecretsObj) { if (Object.hasOwn(awsParameterStoreSecretsObj, key)) { + logger.info( + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]` + ); if (!(key in secrets)) { + logger.info( + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]` + ); // case: // -> delete secret await ssm @@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({ Name: awsParameterStoreSecretsObj[key].Name as string }) .promise(); + logger.info( + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]` + ); } await new Promise((resolve) => { setTimeout(resolve, 50); @@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({ integration, secrets, accessId, - accessToken + accessToken, + projectId }); break; case Integrations.AWS_SECRET_MANAGER: diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index da9cfc71f..02e520c6e 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; +import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; +import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; +import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TIntegrationDALFactory } from "./integration-dal"; import { TCreateIntegrationDTO, @@ -19,9 +26,15 @@ import { type TIntegrationServiceFactoryDep = { integrationDAL: TIntegrationDALFactory; integrationAuthDAL: TIntegrationAuthDALFactory; - folderDAL: Pick; + integrationAuthService: TIntegrationAuthServiceFactory; + folderDAL: Pick; permissionService: Pick; + projectBotService: TProjectBotServiceFactory; secretQueueService: Pick; + secretV2BridgeDAL: Pick; + secretImportDAL: Pick; + kmsService: Pick; + secretDAL: Pick; }; export type TIntegrationServiceFactory = ReturnType; @@ -31,7 +44,13 @@ export const integrationServiceFactory = ({ integrationAuthDAL, folderDAL, permissionService, - secretQueueService + secretQueueService, + integrationAuthService, + projectBotService, + secretV2BridgeDAL, + secretImportDAL, + kmsService, + secretDAL }: TIntegrationServiceFactoryDep) => { const createIntegration = async ({ app, @@ -161,7 +180,14 @@ export const integrationServiceFactory = ({ return updatedIntegration; }; - const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => { + const deleteIntegration = async ({ + actorId, + id, + actor, + actorAuthMethod, + actorOrgId, + shouldDeleteIntegrationSecrets + }: TDeleteIntegrationDTO) => { const integration = await integrationDAL.findById(id); if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); @@ -174,6 +200,22 @@ export const integrationServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); + const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId); + + if (shouldDeleteIntegrationSecrets) { + await deleteIntegrationSecrets({ + integration, + integrationAuth, + projectBotService, + integrationAuthService, + secretV2BridgeDAL, + folderDAL, + secretImportDAL, + secretDAL, + kmsService + }); + } + const deletedIntegration = await integrationDAL.transaction(async (tx) => { // delete integration const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index abbccbe90..cfb6d70a4 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = { export type TDeleteIntegrationDTO = { id: string; + shouldDeleteIntegrationSecrets?: boolean; } & Omit; export type TSyncIntegrationDTO = { diff --git a/backend/src/services/project-bot/project-bot-dal.ts b/backend/src/services/project-bot/project-bot-dal.ts index 81c177d21..ecb23f78b 100644 --- a/backend/src/services/project-bot/project-bot-dal.ts +++ b/backend/src/services/project-bot/project-bot-dal.ts @@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => { const doc = await db .replicaNode()(TableName.ProjectMembership) .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) + .where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId) .where(`${TableName.Users}.isGhost` as "isGhost", false) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`) diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 83725f536..fffadcb06 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -5,6 +5,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; @@ -13,6 +14,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d type TDailyResourceCleanUpQueueServiceFactoryDep = { auditLogDAL: Pick; identityAccessTokenDAL: Pick; + identityUniversalAuthClientSecretDAL: Pick; secretVersionDAL: Pick; secretVersionV2DAL: Pick; secretFolderVersionDAL: Pick; @@ -33,12 +35,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ secretFolderVersionDAL, identityAccessTokenDAL, secretSharingDAL, - secretVersionV2DAL + secretVersionV2DAL, + identityUniversalAuthClientSecretDAL }: TDailyResourceCleanUpQueueServiceFactoryDep) => { queueService.start(QueueName.DailyResourceCleanUp, async () => { logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); await auditLogDAL.pruneAuditLog(); await identityAccessTokenDAL.removeExpiredTokens(); + await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets(); await snapshotDAL.pruneExcessSnapshots(); await secretVersionDAL.pruneExcessVersions(); diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index 1ccaaefc2..f7c31ac61 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = { secretKey: string; // akhilmhdh: yes i know you can put ?. // But for somereason ts consider ? and undefined explicit as different just ts things - secretValue: string | undefined; - secretComment: string | undefined; + secretValue: string; + secretComment: string; })[]; }; @@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({ secretImportDAL: Pick; depth?: number; cyclicDetector?: Set; - decryptor: (value?: Buffer | null) => string | undefined; + decryptor: (value?: Buffer | null) => string; expandSecretReferences?: ( secrets: Record ) => Promise>; @@ -231,6 +231,7 @@ export const fnSecretsV2FromImports = async ({ _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. })) .concat(folderDeeperImportSecrets); + return { secretPath: importPath, environment: importEnv.slug, @@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({ }; return acc; }, - {} as Record + {} as Record ); // eslint-disable-next-line await expandSecretReferences(secretsGroupByKey); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index c69fec6b6..c3d1a6791 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({ folderDAL, secretDAL: secretV2BridgeDAL, secretImportDAL, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }); return importedSecrets; } diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index d1f149f35..1df64afa2 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => { ...secretTagOrm, saveTagsToSecret: secretJnTagOrm.insertMany, deleteTagsToSecret: secretJnTagOrm.delete, - saveTagsToSecretV2: secretV2JnTagOrm.insertMany, + saveTagsToSecretV2: secretV2JnTagOrm.batchInsert, deleteTagsToSecretV2: secretV2JnTagOrm.delete, findSecretTagsByProjectId, deleteTagsManySecret, diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 76b57dc90..dd595f046 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = { export type TSecretTagServiceFactory = ReturnType; export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => { - const createTag = async ({ - name, - slug, - actor, - color, - actorId, - actorOrgId, - actorAuthMethod, - projectId - }: TCreateTagDTO) => { + const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe const newTag = await secretTagDAL.create({ projectId, - name, slug, color, createdBy: actorId, @@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe return newTag; }; - const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => { + const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => { const tag = await secretTagDAL.findById(id); if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); @@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); - const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug }); + const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug }); return updatedTag; }; @@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); - return tag; + return { ...tag, name: tag.slug }; }; const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => { @@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); - return tag; + return { ...tag, name: tag.slug }; }; const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { diff --git a/backend/src/services/secret-tag/secret-tag-types.ts b/backend/src/services/secret-tag/secret-tag-types.ts index f2ace0901..ede0c55a3 100644 --- a/backend/src/services/secret-tag/secret-tag-types.ts +++ b/backend/src/services/secret-tag/secret-tag-types.ts @@ -1,14 +1,12 @@ import { TProjectPermission } from "@app/lib/types"; export type TCreateTagDTO = { - name: string; color: string; slug: string; } & TProjectPermission; export type TUpdateTagDTO = { id: string; - name?: string; slug?: string; color?: string; } & Omit; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index b41e2c250..fe4d89bac 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) .orderBy("id", "asc"); const data = sqlNestRelationships({ @@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { { key: "tagId", label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ id, color, slug, - name + name: slug }) } ] @@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { .where({ [`${TableName.SecretV2}Id` as const]: secretId }) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); return tags.map((el) => ({ id: el.tagId, color: el.tagColor, slug: el.tagSlug, - name: el.tagName + name: el.tagSlug })); } catch (error) { throw new DatabaseError({ error, name: "get secret tags" }); @@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) .orderBy("id", "asc"); const data = sqlNestRelationships({ @@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { { key: "tagId", label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ id, color, slug, - name + name: slug }) } ] @@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { })) ); if (!newSecretReferences.length) return; - const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences); + const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences); return secretReferences; } catch (error) { throw new DatabaseError({ error, name: "UpsertSecretReference" }); @@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); const docs = sqlNestRelationships({ data: rawDocs, key: "id", @@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { { key: "tagId", label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ id, color, slug, - name + name: slug }) } ] diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 19aef06f3..e58960a4b 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -528,8 +528,8 @@ export const reshapeBridgeSecret = ( environment: string, secretPath: string, secret: Omit & { - value?: string; - comment?: string; + value: string; + comment: string; tags?: { id: string; slug: string; @@ -542,8 +542,8 @@ export const reshapeBridgeSecret = ( secretPath, workspace: workspaceId, environment, - secretValue: secret.value, - secretComment: secret.comment, + secretValue: secret.value || "", + secretComment: secret.comment || "", version: secret.version, type: secret.type, _id: secret.id, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 2cb03cf6d..b594f98ca 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({ return reshapeBridgeSecret(projectId, environment, secretPath, { ...secret[0], value: inputSecret.secretValue, - comment: inputSecret.secretComment + comment: inputSecret.secretComment || "" }); }; @@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({ }); return reshapeBridgeSecret(projectId, environment, secretPath, { ...updatedSecret[0], - value: inputSecret.secretValue, - comment: inputSecret.secretComment + value: inputSecret.secretValue || "", + comment: inputSecret.secretComment || "" }); }; @@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({ throw new BadRequestError({ message: "Must be user to delete personal secret" }); } + const secretToDelete = await secretDAL.findOne({ + key: inputSecret.secretName, + folderId, + ...(inputSecret.type === SecretType.Shared + ? {} + : { + type: SecretType.Personal, + userId: actorId + }) + }); + if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); + const deletedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkDelete({ projectId, @@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({ ...deletedSecret[0], value: deletedSecret[0].encryptedValue ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() - : undefined, + : "", comment: deletedSecret[0].encryptedComment ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() - : undefined + : "" }); }; @@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, includeImports, recursive, + tagSlugs = [], expandSecretReferences: shouldExpandSecretReferences }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission( @@ -496,6 +509,9 @@ export const secretV2BridgeServiceFactory = ({ : "" }) ); + const filteredSecrets = tagSlugs.length + ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) + : decryptedSecrets; const expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, @@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (shouldExpandSecretReferences) { - const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath); + const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath); for (const secretPathKey in secretsGroupByPath) { if (Object.hasOwn(secretsGroupByPath, secretPathKey)) { const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce( @@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({ await expandSecretReferences(secretsGroupByKey); secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => { // eslint-disable-next-line no-param-reassign - decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value; + decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || ""; }); } } @@ -530,7 +546,7 @@ export const secretV2BridgeServiceFactory = ({ if (!includeImports) { return { - secrets: decryptedSecrets + secrets: filteredSecrets }; } @@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({ folderDAL, secretImportDAL, expandSecretReferences, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }); return { - secrets: decryptedSecrets, + secrets: filteredSecrets, imports: importedSecrets }; }; @@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, folderDAL, secretImportDAL, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined }); @@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({ for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { - return reshapeBridgeSecret( - projectId, - importedSecrets[i].environment, - importedSecrets[i].secretPath, - importedSecret - ); + return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }); } } } @@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({ let secretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : undefined; + : ""; if (shouldExpandSecretReferences && secretValue) { const secretReferenceExpandedRecord = { [secret.key]: { value: secretValue } @@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({ value: secretValue, comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : undefined + : "" }); }; @@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({ return newSecrets.map((el) => reshapeBridgeSecret(projectId, environment, secretPath, { ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, - comment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : undefined + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" }) ); }; @@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({ return secrets.map((el) => reshapeBridgeSecret(projectId, environment, secretPath, { ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, - comment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : undefined + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" }) ); }; @@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({ return secretsDeleted.map((el) => reshapeBridgeSecret(projectId, environment, secretPath, { ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, - comment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : undefined + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" }) ); }; @@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({ return secretVersions.map((el) => reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, - comment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : undefined + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" }) ); }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 1964aac05..8c7a32a7f 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -20,6 +20,7 @@ export type TGetSecretsDTO = { environment: string; includeImports?: boolean; recursive?: boolean; + tagSlugs?: string[]; } & TProjectPermission; export type TGetASecretDTO = { diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index aac01d393..290a9597a 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => { .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) .orderBy("id", "asc"); const data = sqlNestRelationships({ data: secs, @@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => { { key: "tagId", label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ id, color, slug, - name + name: slug }) } ] @@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => { .where({ [`${TableName.Secret}Id` as const]: secretId }) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); return tags.map((el) => ({ id: el.tagId, color: el.tagColor, slug: el.tagSlug, - name: el.tagName + name: el.tagSlug })); } catch (error) { throw new DatabaseError({ error, name: "get secret tags" }); @@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => { .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) .orderBy("id", "asc"); const data = sqlNestRelationships({ data: secs, @@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => { { key: "tagId", label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ id, color, slug, - name + name: slug }) } ] @@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.Secret)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) - .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); const docs = sqlNestRelationships({ data: rawDocs, key: "id", @@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => { { key: "tagId", label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ id, color, slug, - name + name: slug }) } ] diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 662067645..d41770846 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -370,7 +370,6 @@ export const decryptSecretRaw = ( id: string; slug: string; color?: string | null; - name: string; }[]; }, key: string @@ -412,7 +411,7 @@ export const decryptSecretRaw = ( _id: secret.id, id: secret.id, user: secret.userId, - tags: secret.tags, + tags: secret.tags?.map((el) => ({ ...el, name: el.slug })), skipMultilineEncoding: secret.skipMultilineEncoding, secretReminderRepeatDays: secret.secretReminderRepeatDays, secretReminderNote: secret.secretReminderNote, diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 28053f4b3..84e49aabe 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -73,12 +73,12 @@ type TSecretQueueFactoryDep = { secretVersionTagDAL: TSecretVersionTagDALFactory; kmsService: Pick; secretV2BridgeDAL: TSecretV2BridgeDALFactory; - secretVersionV2BridgeDAL: Pick; - secretVersionTagV2BridgeDAL: Pick; + secretVersionV2BridgeDAL: Pick; + secretVersionTagV2BridgeDAL: Pick; secretRotationDAL: Pick; secretApprovalRequestDAL: Pick; snapshotDAL: Pick; - snapshotSecretV2BridgeDAL: Pick; + snapshotSecretV2BridgeDAL: Pick; }; export type TGetSecrets = { @@ -728,7 +728,10 @@ export const secretQueueFactory = ({ isSynced: response?.isSynced ?? true }); } catch (err) { - logger.info("Secret integration sync error: %o", err); + logger.error( + err, + `Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]` + ); const message = (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || @@ -828,7 +831,7 @@ export const secretQueueFactory = ({ secretId: string; references: { environment: string; secretPath: string; secretKey: string }[]; }[] = []; - await secretV2BridgeDAL.insertMany( + await secretV2BridgeDAL.batchInsert( projectV1Secrets.map((el) => { const key = decryptSymmetric128BitHexKeyUTF8({ ciphertext: el.secretKeyCiphertext, @@ -1004,14 +1007,14 @@ export const secretQueueFactory = ({ const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById); if (projectV3SecretVersions.length) { - await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx); + await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx); } if (projectV3SecretVersionTags.length) { - await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx); + await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx); } if (projectV3SnapshotSecrets.length) { - await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx); + await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx); } await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx); } diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 3a14f0e30..c62a73f69 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -964,7 +964,8 @@ export const secretServiceFactory = ({ environment, includeImports, expandSecretReferences, - recursive + recursive, + tagSlugs = [] }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); if (shouldUseSecretV2Bridge) { @@ -978,7 +979,8 @@ export const secretServiceFactory = ({ path, recursive, actorAuthMethod, - includeImports + includeImports, + tagSlugs }); return { secrets, imports }; } @@ -998,6 +1000,9 @@ export const secretServiceFactory = ({ }); const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); + const filteredSecrets = tagSlugs.length + ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) + : decryptedSecrets; const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const decryptedImportSecrets = importedSecrets.map((sec) => decryptSecretRaw( @@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({ }; // expand secrets - await batchSecretsExpand(decryptedSecrets); + await batchSecretsExpand(filteredSecrets); // expand imports by batch await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets))); } return { - secrets: decryptedSecrets, + secrets: filteredSecrets, imports: processedImports }; }; @@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({ type, secretName }); + return secret; } @@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({ return { ...updatedSecret[0], - tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color })) + tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color })) }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 99bde05f2..1686ee488 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = { environment: string; includeImports?: boolean; recursive?: boolean; + tagSlugs?: string[]; } & TProjectPermission; export type TGetASecretRawDTO = { diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 9eab5e759..c6ecd8bcd 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques SetQueryParam("environment", request.Environment). SetQueryParam("secretPath", request.SecretPath) + if request.TagSlugs != "" { + req.SetQueryParam("tagSlugs", request.TagSlugs) + } + if request.IncludeImport { req.SetQueryParam("include_imports", "true") } diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index dc577bf9d..f954531e2 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct { SecretPath string `json:"secretPath"` IncludeImport bool `json:"include_imports"` Recursive bool `json:"recursive"` + TagSlugs string `json:"tagSlugs,omitempty"` } type GetRawSecretsV3Response struct { diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index 59e496ec7..0c99377ae 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { - res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false) + res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false, "") if err != nil { return nil, err } diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index c0fea738e..f6b028b7a 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -14,6 +14,7 @@ import ( "github.com/Infisical/infisical-merge/packages/util" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" ) const ( @@ -188,7 +189,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, case FormatCSV: return formatAsCSV(envs), nil case FormatYaml: - return formatAsYaml(envs), nil + return formatAsYaml(envs) default: return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport}) } @@ -224,12 +225,18 @@ func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string { return dotenv } -func formatAsYaml(envs []models.SingleEnvironmentVariable) string { - var dotenv string +func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) { + m := make(map[string]string) for _, env := range envs { - dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value) + m[env.Key] = env.Value } - return dotenv + + yamlBytes, err := yaml.Marshal(m) + if err != nil { + return "", fmt.Errorf("failed to format environment variables as YAML: %w", err) + } + + return string(yamlBytes), nil } // Format environment variables as a JSON file diff --git a/cli/packages/cmd/export_test.go b/cli/packages/cmd/export_test.go new file mode 100644 index 000000000..1be0a7ed2 --- /dev/null +++ b/cli/packages/cmd/export_test.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "testing" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +func TestFormatAsYaml(t *testing.T) { + tests := []struct { + name string + input []models.SingleEnvironmentVariable + expected string + }{ + { + name: "Empty input", + input: []models.SingleEnvironmentVariable{}, + expected: "{}\n", + }, + { + name: "Single environment variable", + input: []models.SingleEnvironmentVariable{ + {Key: "KEY1", Value: "VALUE1"}, + }, + expected: "KEY1: VALUE1\n", + }, + { + name: "Multiple environment variables", + input: []models.SingleEnvironmentVariable{ + {Key: "KEY1", Value: "VALUE1"}, + {Key: "KEY2", Value: "VALUE2"}, + {Key: "KEY3", Value: "VALUE3"}, + }, + expected: "KEY1: VALUE1\nKEY2: VALUE2\nKEY3: VALUE3\n", + }, + { + name: "Overwriting duplicate keys", + input: []models.SingleEnvironmentVariable{ + {Key: "KEY1", Value: "VALUE1"}, + {Key: "KEY1", Value: "VALUE2"}, + }, + expected: "KEY1: VALUE2\n", + }, + { + name: "Special characters in values", + input: []models.SingleEnvironmentVariable{ + {Key: "KEY1", Value: "Value with spaces"}, + {Key: "KEY2", Value: "Value:with:colons"}, + {Key: "KEY3", Value: "Value\nwith\nnewlines"}, + }, + expected: "KEY1: Value with spaces\nKEY2: Value:with:colons\nKEY3: |-\n Value\n with\n newlines\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := formatAsYaml(tt.input) + assert.NoError(t, err) + + // Compare the result with the expected output + assert.Equal(t, tt.expected, result) + + // Additionally, parse the result back into a map to ensure it's valid YAML + var resultMap map[string]string + err = yaml.Unmarshal([]byte(result), &resultMap) + assert.NoError(t, err) + + // Create an expected map from the input + expectedMap := make(map[string]string) + for _, env := range tt.input { + expectedMap[env.Key] = env.Value + } + + assert.Equal(t, expectedMap, resultMap) + }) + } +} diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 2894765ae..c9ac36852 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -155,22 +155,24 @@ var secretsSetCmd = &cobra.Command{ DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - util.RequireLocalWorkspaceFile() - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - token, err := util.GetInfisicalToken(cmd) if err != nil { util.HandleError(err, "Unable to parse flag") } - projectId, err := cmd.Flags().GetString("projectId") + if (token == nil) { + util.RequireLocalWorkspaceFile() + } + + environmentName, _ := cmd.Flags().GetString("env") + if !cmd.Flags().Changed("env") { + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + if environmentFromWorkspace != "" { + environmentName = environmentFromWorkspace + } + } + + projectId, err := cmd.Flags().GetString("projectId") if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -374,6 +376,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } + secretOverriding, err := cmd.Flags().GetBool("secret-overriding") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + request := models.GetAllSecretsParameters{ Environment: environmentName, WorkspaceId: projectId, @@ -394,6 +401,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "To fetch all secrets") } + if secretOverriding { + secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) + } else { + secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) + } + if shouldExpand { authParams := models.ExpandSecretsAuthentication{} if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { @@ -413,11 +426,13 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { if value, ok := secretsMap[secretKeyFromArg]; ok { requestedSecrets = append(requestedSecrets, value) } else { - requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ - Key: secretKeyFromArg, - Type: "*not found*", - Value: "*not found*", - }) + if !(plainOutput || showOnlyValue) { + requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ + Key: secretKeyFromArg, + Type: "*not found*", + Value: "*not found*", + }) + } } } @@ -688,6 +703,7 @@ func init() { secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets") secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") + secretsGetCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsGetCmd) secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index 4720e094e..6a92ef960 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -31,37 +31,52 @@ var AvailableVaults = []VaultBackendType{ } var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set file --passphrase `, - Use: "set [file|auto] [flags]", + Example: `infisical vault set file`, + Use: "set [file|auto]", Short: "Used to configure the vault backends", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - - vaultType := args[0] - - passphrase, err := cmd.Flags().GetString("passphrase") + wantedVaultTypeName := args[0] + currentVaultBackend, err := util.GetCurrentVaultBackend() if err != nil { - util.HandleError(err, "Unable to get passphrase flag") - } - - if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" { - setFileVaultPassphrase(passphrase) + log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) return } - util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n") - selectVaultTypeCmd(cmd, args) - }, -} + if wantedVaultTypeName == string(currentVaultBackend) { + log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) + return + } -var vaultUseCmd = &cobra.Command{ - Example: `infisical vault use [file|auto]`, - Use: "use [file|auto]", - Short: "Used to select the the type of vault backend to store sensitive data securely at rest", - DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), - Run: selectVaultTypeCmd, + if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE { + configFile, err := util.GetConfigFile() + if err != nil { + log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + configFile.VaultBackendType = wantedVaultTypeName + configFile.LoggedInUserEmail = "" + configFile.VaultBackendPassphrase = base64.StdEncoding.EncodeToString([]byte(util.GenerateRandomString(10))) + + err = util.WriteConfigFile(&configFile) + if err != nil { + log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) + return + } + + fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) + + Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) + } else { + var availableVaultsNames []string + for _, vault := range AvailableVaults { + availableVaultsNames = append(availableVaultsNames, vault.Name) + } + log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", ")) + } + }, } // runCmd represents the run command @@ -75,26 +90,6 @@ var vaultCmd = &cobra.Command{ }, } -func setFileVaultPassphrase(passphrase string) { - configFile, err := util.GetConfigFile() - if err != nil { - log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) - return - } - - // encode with base64 - encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) - configFile.VaultBackendPassphrase = encodedPassphrase - - err = util.WriteConfigFile(&configFile) - if err != nil { - log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) - return - } - - util.PrintSuccessMessage("\nSuccessfully, set passphrase for file vault.\n") -} - func printAvailableVaultBackends() { fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:") for _, vaultType := range AvailableVaults { @@ -111,53 +106,8 @@ func printAvailableVaultBackends() { fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend)) } -func selectVaultTypeCmd(cmd *cobra.Command, args []string) { - wantedVaultTypeName := args[0] - currentVaultBackend, err := util.GetCurrentVaultBackend() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - - if wantedVaultTypeName == string(currentVaultBackend) { - log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) - return - } - - if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE { - configFile, err := util.GetConfigFile() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - - configFile.VaultBackendType = wantedVaultTypeName // save selected vault - configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login - - err = util.WriteConfigFile(&configFile) - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) - return - } - - fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) - - Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) - } else { - var availableVaultsNames []string - for _, vault := range AvailableVaults { - availableVaultsNames = append(availableVaultsNames, vault.Name) - } - log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", ")) - } -} - func init() { - - vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault") - vaultCmd.AddCommand(vaultSetCmd) - vaultCmd.AddCommand(vaultUseCmd) rootCmd.AddCommand(vaultCmd) } diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 5cd66f50b..1f9ec0c65 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -38,7 +38,8 @@ const ( SERVICE_TOKEN_IDENTIFIER = "service-token" UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token" - INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" + INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30 + INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key" ) var ( diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index be49e9267..cb5b94080 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -71,7 +71,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { if strings.Contains(err.Error(), "credentials not found in system keyring") { return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again") } else { - return LoggedInUserDetails{}, fmt.Errorf("failed to fetch creditnals from keyring because [err=%s]", err) + return LoggedInUserDetails{}, fmt.Errorf("failed to fetch credentials from keyring because [err=%s]", err) } } diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 9ce8c4a1d..69a310efa 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "math/rand" "os" "os/exec" "path" @@ -25,6 +26,8 @@ type DecodedSymmetricEncryptionDetails = struct { Key []byte } +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { cipherx, err := base64.StdEncoding.DecodeString(cipher) if err != nil { @@ -287,3 +290,11 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) { } return value, nil } + +func GenerateRandomString(length int) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index cadb72ebd..9c8211a3c 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -2,8 +2,9 @@ package util import ( "encoding/base64" + "fmt" - "github.com/manifoldco/promptui" + "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" ) @@ -26,20 +27,13 @@ func SetValueInKeyring(key, value string) error { err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) if err != nil { + log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err)) configFile, _ := GetConfigFile() if configFile.VaultBackendPassphrase == "" { - PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.") - passphrasePrompt := promptui.Prompt{ - Label: "Enter the passphrase to use for keyring encryption", - } - passphrase, err := passphrasePrompt.Run() - if err != nil { - return err - } - - encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) + encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase configFile.VaultBackendPassphrase = encodedPassphrase + configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE err = WriteConfigFile(&configFile) if err != nil { return err @@ -50,6 +44,7 @@ func SetValueInKeyring(key, value string) error { } err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) + log.Debug().Msg(fmt.Sprintf("Error while setting file keyring: %v", err)) } return err @@ -60,13 +55,7 @@ func GetValueInKeyring(key string) (string, error) { if err != nil { PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again") } - - value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) - - if err != nil { - value, err = keyring.Get(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key) - } - return value, err + return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) } @@ -76,11 +65,5 @@ func DeleteValueInKeyring(key string) error { return err } - err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) - - if err != nil { - err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key) - } - - return err + return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) } diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index c51338d61..b6ec7f752 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -1,14 +1,15 @@ package util import ( + "crypto/rand" "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" "os" "path" "regexp" - "slices" "strings" "unicode" @@ -20,7 +21,7 @@ import ( "github.com/zalando/go-keyring" ) -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string) ([]models.SingleEnvironmentVariable, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") @@ -53,6 +54,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str SecretPath: secretPath, IncludeImport: includeImports, Recursive: recursive, + TagSlugs: tagSlugs, }) if err != nil { @@ -76,7 +78,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str } -func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) { +func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string) (models.PlaintextSecretResult, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -86,7 +88,7 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa Environment: environmentName, IncludeImport: includeImports, Recursive: recursive, - // TagSlugs: tagSlugs, + TagSlugs: tagSlugs, } if secretsPath != "" { @@ -281,29 +283,36 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId, - params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) + params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err) if err == nil { - WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, res.Secrets) + backupEncryptionKey, err := GetBackupEncryptionKey() + if err != nil { + return nil, err + } + WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets) } secretsToReturn = res.Secrets errorToReturn = err // only attempt to serve cached secrets if no internet connection and if at least one secret cached if !isConnected { - backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath) - if len(backedSecrets) > 0 { - PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug") - secretsToReturn = backedSecrets - errorToReturn = err + backupEncryptionKey, _ := GetBackupEncryptionKey() + if backupEncryptionKey != nil { + backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey) + if len(backedUpSecrets) > 0 { + PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug") + secretsToReturn = backedUpSecrets + errorToReturn = err + } } } } else { if params.InfisicalToken != "" { log.Debug().Msg("Trying to fetch secrets using service token") - secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) + secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs) } else if params.UniversalAuthAccessToken != "" { if params.WorkspaceId == "" { @@ -311,7 +320,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } log.Debug().Msg("Trying to fetch secrets using universal auth") - res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) + res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs) errorToReturn = err secretsToReturn = res.Secrets @@ -476,71 +485,99 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri return secretsToReturn } -func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error { - var backedUpSecrets []models.BackupSecretKeyRing - secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET) +func GetBackupEncryptionKey() ([]byte, error) { + encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY) if err != nil { if err == keyring.ErrUnsupportedPlatform { - return errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") - } else if err != keyring.ErrNotFound { - return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) + return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") + } else if err == keyring.ErrNotFound { + // generate a new key + randomizedKey := make([]byte, 16) + rand.Read(randomizedKey) + encryptionKey = hex.EncodeToString(randomizedKey) + if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil { + return nil, err + } + return []byte(encryptionKey), nil + } else { + return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) } } - _ = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets) + return []byte(encryptionKey), nil +} - backedUpSecrets = slices.DeleteFunc(backedUpSecrets, func(e models.BackupSecretKeyRing) bool { - return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment - }) - newBackupSecret := models.BackupSecretKeyRing{ - ProjectID: workspace, - Environment: environment, - SecretPath: secretsPath, - Secrets: secrets, - } - backedUpSecrets = append(backedUpSecrets, newBackupSecret) +func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error { + formattedPath := strings.ReplaceAll(secretsPath, "/", "-") + fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath) + secrets_backup_folder_name := "secrets-backup" - listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets) + _, fullConfigFileDirPath, err := GetFullConfigFilePath() if err != nil { - return err + return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err) } - err = SetValueInKeyring(INFISICAL_BACKUP_SECRET, string(listOfSecretsMarshalled)) + // create secrets backup directory + fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) + if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { + err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm) + if err != nil { + return err + } + } + marshaledSecrets, _ := json.Marshal(secrets) + result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey) if err != nil { - return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err) + return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err) + } + listOfSecretsMarshalled, _ := json.Marshal(result) + err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600) + if err != nil { + return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err) } return nil } -func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) { - secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET) +func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) { + formattedPath := strings.ReplaceAll(secretsPath, "/", "-") + fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath) + secrets_backup_folder_name := "secrets-backup" + + _, fullConfigFileDirPath, err := GetFullConfigFilePath() if err != nil { - if err == keyring.ErrUnsupportedPlatform { - return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") - } else if err == keyring.ErrNotFound { - return nil, errors.New("credentials not found in system keyring") - } else { - return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) - } + return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) } - var backedUpSecrets []models.BackupSecretKeyRing - err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets) + fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) + if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { + return nil, nil + } + + encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName) + + encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath) if err != nil { - return nil, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err) + return nil, err } - for _, backupSecret := range backedUpSecrets { - if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath { - return backupSecret.Secrets, nil - } + var encryptedBackUpSecrets models.SymmetricEncryptionResult + err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets) + if err != nil { + return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err) } - return nil, nil + result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce) + if err != nil { + return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err) + } + var plainTextSecrets []models.SingleEnvironmentVariable + _ = json.Unmarshal(result, &plainTextSecrets) + + return plainTextSecrets, nil + } func DeleteBackupSecrets() error { - // keeping this logic for now. Need to remove it later as more users migrate keyring would be used and this folder will be removed completely by then secrets_backup_folder_name := "secrets-backup" _, fullConfigFileDirPath, err := GetFullConfigFilePath() @@ -549,8 +586,8 @@ func DeleteBackupSecrets() error { } fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) - DeleteValueInKeyring(INFISICAL_BACKUP_SECRET) + DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY) return os.RemoveAll(fullPathToSecretsBackupFolder) } diff --git a/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection b/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection index c48627f73..71a189a65 100644 --- a/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection +++ b/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection @@ -1,4 +1,4 @@ -Warning: Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug +Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug ┌───────────────┬──────────────┬─────────────┐ │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ ├───────────────┼──────────────┼─────────────┤ diff --git a/cli/test/secrets_test.go b/cli/test/secrets_test.go index f5d5a7b1f..f11392f52 100644 --- a/cli/test/secrets_test.go +++ b/cli/test/secrets_test.go @@ -7,7 +7,6 @@ import ( "github.com/bradleyjkemp/cupaloy/v2" ) - func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) { output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") @@ -94,7 +93,7 @@ func TestUserAuth_SecretsGetAll(t *testing.T) { } // explicitly called here because it should happen directly after successful secretsGetAll - testUserAuth_SecretsGetAllWithoutConnection(t) + // testUserAuth_SecretsGetAllWithoutConnection(t) } func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) { @@ -107,7 +106,7 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) { // set it to a URL that will always be unreachable newConfigFile.LoggedInUserDomain = "http://localhost:4999" util.WriteConfigFile(&newConfigFile) - + // restore config file defer util.WriteConfigFile(&originalConfigFile) @@ -121,4 +120,4 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) { if err != nil { t.Fatalf("snapshot failed: %v", err) } -} \ No newline at end of file +} diff --git a/company/handbook/spending-money.mdx b/company/handbook/spending-money.mdx index 667ed5ef2..046604399 100644 --- a/company/handbook/spending-money.mdx +++ b/company/handbook/spending-money.mdx @@ -1,14 +1,16 @@ --- -title: "Spenging Money" +title: "Spending Money" sidebarTitle: "Spending Money" description: "The guide to spending money at Infisical." --- Fairly frequently, you might run into situations when you need to spend company money. -**Please spend money in a way that you think is in the best interest of the company.** + +Please spend money in a way that you think is in the best interest of the company. + -## Trivial expenses +# Trivial expenses We don't want you to be slowed down because you're waiting for an approval to purchase some SaaS. For trivial expenses – **Just do it**. @@ -22,6 +24,35 @@ Make sure you keep copies for all receipts. If you expense something on a compan You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually. + +# Equipment + +Infisical is a remote first company so we understand the importance of having a comfortable work setup. To support this, we provide allowances for essential office equipment. + +### Desk & Chair + +Most people already have a comfortable desk and chair, but if you need an upgrade, we offer the following allowances. +While we're not yet able to provide the latest and greatest, we strive to be reasonable given the stage of our company. + +**Desk**: $150 USD + +**Chair**: $150 USD + +### Laptop +Each team member will receive a company-issued Macbook Pro before they start their first day. + +### Notes + +1. All equipment purchased using company allowances remains the property of Infisical. +2. Keep all receipts for equipment purchases and submit them for reimbursement. +3. If you leave Infisical, you may be required to return company-owned equipment. + +Please note that we're unable to offer a split payment option where the Infisical pays half and you pay half for equipment exceeding the allowance. +This is because we don't yet have a formal HR department to handle such logistics. + +For any equipment related questions, please reach out to Maidul. + + ## Brex We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul. \ No newline at end of file diff --git a/docs/api-reference/endpoints/dynamic-secrets/create-lease.mdx b/docs/api-reference/endpoints/dynamic-secrets/create-lease.mdx new file mode 100644 index 000000000..57e235f50 --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/create-lease.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Lease" +openapi: "POST /api/v1/dynamic-secrets/leases" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/create.mdx b/docs/api-reference/endpoints/dynamic-secrets/create.mdx new file mode 100644 index 000000000..1c591bd3d --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/dynamic-secrets" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/delete-lease.mdx b/docs/api-reference/endpoints/dynamic-secrets/delete-lease.mdx new file mode 100644 index 000000000..93e209995 --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/delete-lease.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Lease" +openapi: "DELETE /api/v1/dynamic-secrets/leases/{leaseId}" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/delete.mdx b/docs/api-reference/endpoints/dynamic-secrets/delete.mdx new file mode 100644 index 000000000..e59ac4379 --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/dynamic-secrets/{name}" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/get-lease.mdx b/docs/api-reference/endpoints/dynamic-secrets/get-lease.mdx new file mode 100644 index 000000000..aff12207e --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/get-lease.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Lease" +openapi: "GET /api/v1/dynamic-secrets/leases/{leaseId}" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/get.mdx b/docs/api-reference/endpoints/dynamic-secrets/get.mdx new file mode 100644 index 000000000..bc3267e97 --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/get.mdx @@ -0,0 +1,4 @@ +--- +title: "Get" +openapi: "GET /api/v1/dynamic-secrets/{name}" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/list-leases.mdx b/docs/api-reference/endpoints/dynamic-secrets/list-leases.mdx new file mode 100644 index 000000000..26cad150d --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/list-leases.mdx @@ -0,0 +1,4 @@ +--- +title: "List Leases" +openapi: "GET /api/v1/dynamic-secrets/{name}/leases" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/list.mdx b/docs/api-reference/endpoints/dynamic-secrets/list.mdx new file mode 100644 index 000000000..d76c1167e --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/dynamic-secrets" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/renew-lease.mdx b/docs/api-reference/endpoints/dynamic-secrets/renew-lease.mdx new file mode 100644 index 000000000..ea1777e6b --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/renew-lease.mdx @@ -0,0 +1,4 @@ +--- +title: "Renew Lease" +openapi: "POST /api/v1/dynamic-secrets/leases/{leaseId}/renew" +--- diff --git a/docs/api-reference/endpoints/dynamic-secrets/update.mdx b/docs/api-reference/endpoints/dynamic-secrets/update.mdx new file mode 100644 index 000000000..0ab4aaab4 --- /dev/null +++ b/docs/api-reference/endpoints/dynamic-secrets/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/dynamic-secrets/{name}" +--- diff --git a/docs/cli/commands/vault.mdx b/docs/cli/commands/vault.mdx index 803af127f..b09513c34 100644 --- a/docs/cli/commands/vault.mdx +++ b/docs/cli/commands/vault.mdx @@ -30,8 +30,5 @@ description: "Change the vault type in Infisical" ## Description -To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows. - -To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file --passphrase ` CLI command to specify your password once. - +To safeguard your login details when using the CLI, Infisical attempts to store them in a system keyring. If a system keyring cannot be found on your machine, the data is stored in a config file. diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 1897a651a..da34e076a 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -4,10 +4,10 @@ description: "Learn how to configure Google SAML for Infisical SSO." --- - Google SAML SSO feature is a paid feature. - - If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact sales@infisical.com to purchase an enterprise license to use it. + Google SAML SSO feature is a paid feature. If you're using Infisical Cloud, + then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license + to use it. @@ -15,8 +15,9 @@ description: "Learn how to configure Google SAML for Infisical SSO." In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application. - + ![Google SAML initial configuration](../../../images/sso/google-saml/init-config.png) + 2.1. In your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps and @@ -32,7 +33,7 @@ description: "Learn how to configure Google SAML for Infisical SSO." ![Google SAML custom app details](../../../images/sso/google-saml/custom-saml-app-config.png) - 2.4. Back in Infisical, set **SSO URL**, **IdP Entity ID**, and **Certificate** to the corresponding items from step 2.3. + 2.4. Back in Infisical, set **SSO URL** and **Certificate** to the corresponding items from step 2.3. ![Google SAML Infisical config](../../../images/sso/google-saml/infisical-config.png) @@ -41,7 +42,7 @@ description: "Learn how to configure Google SAML for Infisical SSO." Also, check the **Signed response** checkbox. ![Google SAML app config 2](../../../images/sso/google-saml/custom-saml-app-config-2.png) - + 2.6. In the **Attribute mapping** tab, configure the following map: - **First name** -> **firstName** @@ -49,7 +50,7 @@ description: "Learn how to configure Google SAML for Infisical SSO." - **Primary email** -> **email** ![Google SAML attribute mapping](../../../images/sso/google-saml/attribute-mapping.png) - + Click **Finish**. @@ -57,11 +58,11 @@ description: "Learn how to configure Google SAML for Infisical SSO." and press on **User access**. ![Google SAML user access](../../../images/sso/google-saml/user-access.png) - + To assign everyone in your organization to the application, click **On for everyone** or **Off for everyone** and then click **Save**. - + You can also assign an organizational unit or set of users to an application; you can learn more about that [here](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app). - + ![Google SAML user access assignment](../../../images/sso/google-saml/user-access-assign.png) @@ -75,21 +76,24 @@ description: "Learn how to configure Google SAML for Infisical SSO." To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Google user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. - + We recommend ensuring that your account is provisioned the application in Google prior to enforcing SAML SSO to prevent any unintended issues. + - If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to - set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: - - - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. - - `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) + If you're configuring SAML SSO on a self-hosted instance of Infisical, make + sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to + work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This + can be a random 32-byte base64 string generated with `openssl rand -base64 + 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should + be an absolute URL including the protocol (e.g. https://app.infisical.com) References: -- Google's guide to [set up your own custom SAML app](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app). \ No newline at end of file + +- Google's guide to [set up your own custom SAML app](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app). diff --git a/docs/images/sso/google-saml/infisical-config.png b/docs/images/sso/google-saml/infisical-config.png index 250b4ed37..31943df81 100644 Binary files a/docs/images/sso/google-saml/infisical-config.png and b/docs/images/sso/google-saml/infisical-config.png differ diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index dc76f2584..57329d4fd 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -1,5 +1,5 @@ --- -title: "Kubernetes" +title: "Kubernetes Operator" description: "How to use Infisical to inject secrets into Kubernetes clusters." --- @@ -9,6 +9,10 @@ The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets It uses an `InfisicalSecret` resource to specify authentication and storage methods. The operator continuously updates secrets and can also reload dependent deployments automatically. + + If you are already using the External Secrets operator, you can view the integration documentation for it [here](https://external-secrets.io/latest/provider/infisical/). + + ## Install Operator The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) diff --git a/docs/mint.json b/docs/mint.json index ac61ad5bd..123b33bb5 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -618,6 +618,21 @@ "api-reference/endpoints/secrets/detach-tags" ] }, + { + "group": "Dynamic Secrets", + "pages": [ + "api-reference/endpoints/dynamic-secrets/create", + "api-reference/endpoints/dynamic-secrets/update", + "api-reference/endpoints/dynamic-secrets/delete", + "api-reference/endpoints/dynamic-secrets/get", + "api-reference/endpoints/dynamic-secrets/list", + "api-reference/endpoints/dynamic-secrets/list-leases", + "api-reference/endpoints/dynamic-secrets/create-lease", + "api-reference/endpoints/dynamic-secrets/delete-lease", + "api-reference/endpoints/dynamic-secrets/renew-lease", + "api-reference/endpoints/dynamic-secrets/get-lease" + ] + }, { "group": "Secret Imports", "pages": [ diff --git a/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx b/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx index c65229a86..acbcdcb2d 100644 --- a/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/components/tags/CreateTagModal/CreateTagModal.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; import { faCheck } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -87,7 +88,13 @@ type Props = { }; const createTagSchema = z.object({ - name: z.string().trim(), + slug: z + .string() + .trim() + .toLowerCase() + .refine((v) => slugify(v) === v, { + message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens." + }), color: z.string().trim() }); @@ -110,7 +117,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => { } = useForm({ resolver: zodResolver(createTagSchema) }); - + const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; @@ -123,13 +130,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => { if (!isOpen) reset(); }, [isOpen]); - const onFormSubmit = async ({ name, color }: FormData) => { + const onFormSubmit = async ({ slug, color }: FormData) => { try { await createWsTag({ workspaceID: workspaceId, - tagName: name, tagColor: color, - tagSlug: name.replace(" ", "_") + tagSlug: slug }); onToggle(false); reset(); @@ -155,11 +161,11 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
( - - + + )} /> diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx index 8e4bcbe71..299b23bb5 100644 --- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx +++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { useToggle } from "@app/hooks"; @@ -16,6 +16,7 @@ type Props = { subTitle?: string; onDeleteApproved: () => Promise; buttonText?: string; + children?: ReactNode; }; export const DeleteActionModal = ({ @@ -26,7 +27,8 @@ export const DeleteActionModal = ({ onDeleteApproved, title, subTitle = "This action is irreversible.", - buttonText = "Delete" + buttonText = "Delete", + children }: Props): JSX.Element => { const [inputData, setInputData] = useState(""); const [isLoading, setIsLoading] = useToggle(); @@ -97,6 +99,7 @@ export const DeleteActionModal = ({ placeholder={`Type ${deleteKey} here`} /> + {children} diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 81d0f00ca..de4c1a914 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -110,8 +110,15 @@ export const useCreateIntegration = () => { export const useDeleteIntegration = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, { id: string; workspaceId: string }>({ - mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`), + return useMutation< + {}, + {}, + { id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean } + >({ + mutationFn: ({ id, shouldDeleteIntegrationSecrets }) => + apiRequest.delete( + `/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}` + ), onSuccess: (_, { workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId)); diff --git a/frontend/src/hooks/api/rateLimit/types.ts b/frontend/src/hooks/api/rateLimit/types.ts index 5697fc298..53b075ce4 100644 --- a/frontend/src/hooks/api/rateLimit/types.ts +++ b/frontend/src/hooks/api/rateLimit/types.ts @@ -5,6 +5,5 @@ export type TRateLimit = { authRateLimit: number; inviteUserRateLimit: number; mfaRateLimit: number; - creationLimit: number; publicEndpointLimit: number; }; diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index 311ed0941..d1c4b533d 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -28,11 +28,10 @@ export const useCreateWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => { + mutationFn: async ({ workspaceID, tagColor, tagSlug }) => { const { data } = await apiRequest.post<{ workspaceTag: WsTag }>( `/api/v1/workspace/${workspaceID}/tags`, { - name: tagName, color: tagColor || "", slug: tagSlug } diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 9b4f70587..72d710cfa 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -2,7 +2,6 @@ export type UserWsTags = WsTag[]; export type WsTag = { id: string; - name: string; slug: string; color?: string; projectId: string; @@ -16,7 +15,6 @@ export type WorkspaceTag = { id: string; name: string; slug: string }; export type CreateTagDTO = { workspaceID: string; tagSlug: string; - tagName: string; tagColor: string; }; diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx index 9c46840b5..5af4a86c2 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx @@ -277,18 +277,14 @@ export default function AWSParameterStoreCreateIntegrationPage() {
setShouldDisableDelete(!shouldDisableDelete)} + onCheckedChange={setShouldDisableDelete} isChecked={shouldDisableDelete} > Disable deleting secrets in AWS Parameter Store
- setShouldTag(!shouldTag)} - isChecked={shouldTag} - > + Tag in AWS Parameter Store
diff --git a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx index 5601a7f68..e44fd2539 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx @@ -106,9 +106,13 @@ export const IntegrationsPage = withProjectPermission( handleProviderIntegration(provider); }; - const handleIntegrationDelete = async (integrationId: string, cb: () => void) => { + const handleIntegrationDelete = async ( + integrationId: string, + shouldDeleteIntegrationSecrets: boolean, + cb: () => void + ) => { try { - await deleteIntegration({ id: integrationId, workspaceId }); + await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets }); if (cb) cb(); createNotification({ type: "success", @@ -152,7 +156,7 @@ export const IntegrationsPage = withProjectPermission( isLoading={isIntegrationLoading} integrations={integrations} environments={environments} - onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)} + onIntegrationDelete={handleIntegrationDelete} workspaceId={workspaceId} /> ; integrations?: TIntegration[]; isLoading?: boolean; - onIntegrationDelete: (integration: TIntegration, cb: () => void) => void; + onIntegrationDelete: ( + integrationId: string, + shouldDeleteIntegrationSecrets: boolean, + cb: () => void + ) => Promise; workspaceId: string; }; @@ -37,10 +42,12 @@ export const IntegrationsSection = ({ workspaceId }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "deleteConfirmation" + "deleteConfirmation", + "deleteSecretsConfirmation" ] as const); const { mutate: syncIntegration } = useSyncIntegration(); + const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false); return (
@@ -249,7 +256,10 @@ export const IntegrationsSection = ({
handlePopUpOpen("deleteConfirmation", integration)} + onClick={() => { + setShouldDeleteSecrets.off(); + handlePopUpOpen("deleteConfirmation", integration); + }} ariaLabel="delete" isDisabled={!isAllowed} colorSchema="danger" @@ -281,11 +291,49 @@ export const IntegrationsSection = ({ (popUp?.deleteConfirmation?.data as TIntegration)?.integration || "" } - onDeleteApproved={async () => - onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () => - handlePopUpClose("deleteConfirmation") - ) - } + onDeleteApproved={async () => { + if (shouldDeleteSecrets) { + handlePopUpOpen("deleteSecretsConfirmation"); + return; + } + + await onIntegrationDelete( + (popUp?.deleteConfirmation.data as TIntegration).id, + false, + () => handlePopUpClose("deleteConfirmation") + ); + }} + > + {(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && ( +
+ setShouldDeleteSecrets.toggle()} + > + Delete previously synced secrets from the destination + +
+ )} + + handlePopUpToggle("deleteSecretsConfirmation", isOpen)} + deleteKey="confirm" + onDeleteApproved={async () => { + await onIntegrationDelete( + (popUp?.deleteConfirmation.data as TIntegration).id, + true, + () => { + handlePopUpClose("deleteSecretsConfirmation"); + handlePopUpClose("deleteConfirmation"); + } + ); + }} />
); diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index 3fbe4357b..6adf591e7 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -97,7 +97,7 @@ export const SecretApprovalRequestChangeItem = ({ {secretVersion?.secretComment} - {secretVersion?.tags?.map(({ name, id: tagId, color }) => ( + {secretVersion?.tags?.map(({ slug, id: tagId, color }) => ( -
{name}
+
{slug}
))} @@ -119,7 +119,7 @@ export const SecretApprovalRequestChangeItem = ({ {newVersion?.secretComment} - {newVersion?.tags?.map(({ name, id: tagId, color }) => ( + {newVersion?.tags?.map(({ slug, id: tagId, color }) => ( -
{name}
+
{slug}
))} @@ -157,7 +157,7 @@ export const SecretApprovalRequestChangeItem = ({ {(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map( - ({ name, id: tagId, color }) => ( + ({ slug, id: tagId, color }) => ( -
{name}
+
{slug}
) )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 705234f63..65cb0389f 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -327,7 +327,7 @@ export const ActionBar = ({ Apply tags to filter secrets - {tags.map(({ id, name, color }) => ( + {tags.map(({ id, slug, color }) => ( { evt.preventDefault(); @@ -342,7 +342,7 @@ export const ActionBar = ({ className="mr-2 h-2 w-2 rounded-full" style={{ background: color || "#bec2c8" }} /> - {name} + {slug}
))} diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx index 2e983bda6..e2a1c3b87 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx @@ -254,7 +254,7 @@ export const SecretDetailSidebar = ({ )}
- {fields.map(({ tagColor, id: formId, name, id }) => ( + {fields.map(({ tagColor, id: formId, slug, id }) => ( -
{name}
+
{slug}
))} @@ -296,7 +296,7 @@ export const SecretDetailSidebar = ({ Add tags to this secret {tags.map((tag) => { - const { id: tagId, name, color } = tag; + const { id: tagId, slug, color } = tag; const isSelected = selectedTagsGroupById?.[tagId]; return ( @@ -311,7 +311,7 @@ export const SecretDetailSidebar = ({ className="mr-2 h-2 w-2 rounded-full" style={{ background: color || "#bec2c8" }} /> - {name} + {slug}
); diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx index 22207cb90..5b62dd526 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx @@ -336,7 +336,7 @@ export const SecretItem = memo( Add tags to this secret {tags.map((tag) => { - const { id: tagId, name, color } = tag; + const { id: tagId, slug, color } = tag; const isTagSelected = selectedTagsGroupById?.[tagId]; return ( @@ -358,7 +358,7 @@ export const SecretItem = memo( className="mr-2 h-2 w-2 rounded-full" style={{ background: color || "#bec2c8" }} /> - {name} + {slug} ); diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts index a3040b43c..eb9e4d195 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts @@ -49,7 +49,6 @@ export const formSchema = z.object({ tags: z .object({ id: z.string(), - name: z.string(), slug: z.string(), tagColor: z.string().optional() }) diff --git a/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx index 2a7397ec9..d6680bcef 100644 --- a/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx @@ -151,7 +151,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => { Tags {isModified && ( - {preSecret?.tags?.map(({ name, id: tagId, color }) => ( + {preSecret?.tags?.map(({ slug, id: tagId, color }) => ( { className="h-3 w-3 rounded-full" style={{ backgroundColor: color || "#bec2c8" }} /> -
{name}
+
{slug}
))} )} - {postSecret?.tags?.map(({ name, id: tagId, color }) => ( + {postSecret?.tags?.map(({ slug, id: tagId, color }) => ( { className="h-3 w-3 rounded-full" style={{ backgroundColor: color || "#bec2c8" }} /> -
{name}
+
{slug}
))} diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx index 582205214..6257ffa6f 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx @@ -62,7 +62,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(); const { data } = useGetSSOConfig(currentOrg?.id ?? ""); - const { control, handleSubmit, reset, watch } = useForm({ + const { control, handleSubmit, reset, watch, setValue, getValues } = useForm({ defaultValues: { authProvider: AuthProvider.OKTA_SAML }, @@ -188,8 +188,8 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet entityId: "SP Entity ID", entryPoint: "SSO URL", entryPointPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx", - issuer: "IdP Entity ID", - issuerPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx" + issuer: "Issuer", + issuerPlaceholder: window.origin }; default: return { @@ -204,6 +204,11 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet }; const authProvider = watch("authProvider"); + useEffect(() => { + if (authProvider === AuthProvider.GOOGLE_SAML && getValues("issuer") === "") { + setValue("issuer", window.origin); + } + }, [authProvider]); return ( <> diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx index 7cc1a926e..75f6b69bf 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/AddSecretTagModal.tsx @@ -1,6 +1,7 @@ import { Controller, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; +import { zodResolver } from "@hookform/resolvers/zod"; +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2"; @@ -8,11 +9,13 @@ import { useWorkspace } from "@app/context"; import { useCreateWsTag } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; -const schema = yup.object({ - name: yup.string().required().label("Tag Name") +const schema = z.object({ + slug: z.string().refine((v) => slugify(v) === v, { + message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens." + }) }); -export type FormData = yup.InferType; +export type FormData = z.infer; type Props = { popUp: UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>; @@ -26,7 +29,6 @@ type Props = { }; export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { - const { currentWorkspace } = useWorkspace(); const createWsTag = useCreateWsTag(); const { @@ -35,17 +37,16 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle } handleSubmit, formState: { isSubmitting } } = useForm({ - resolver: yupResolver(schema) + resolver: zodResolver(schema) }); - const onFormSubmit = async ({ name }: FormData) => { + const onFormSubmit = async ({ slug }: FormData) => { try { if (!currentWorkspace?.id) return; await createWsTag.mutateAsync({ workspaceID: currentWorkspace?.id, - tagName: name, - tagSlug: name.replace(/\s+/g, " ").replace(" ", "_"), + tagSlug: slug, tagColor: "" }); @@ -80,11 +81,11 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }
( - - + + )} /> diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx index d0655f984..cc68b0700 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsTable.tsx @@ -40,7 +40,6 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => { - @@ -49,9 +48,8 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => { {isLoading && } {!isLoading && data && - data.map(({ id, name, slug }) => ( - - + data.map(({ id, slug }) => ( +
Tag Slug
{name}
{slug} { handlePopUpOpen("deleteTagConfirmation", { - name, + name: slug, id }) } diff --git a/frontend/src/views/ShareSecretPage/components/ShareSecretsTable.tsx b/frontend/src/views/ShareSecretPage/components/ShareSecretsTable.tsx index 33dfa297a..bdb2b2ffa 100644 --- a/frontend/src/views/ShareSecretPage/components/ShareSecretsTable.tsx +++ b/frontend/src/views/ShareSecretPage/components/ShareSecretsTable.tsx @@ -61,7 +61,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
{!isLoading && data?.secrets && - data.secrets.length >= perPage && + data?.totalCount >= perPage && data?.totalCount !== undefined && ( { authRateLimit: rateLimit?.authRateLimit ?? 60, inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30, mfaRateLimit: rateLimit?.mfaRateLimit ?? 20, - creationLimit: rateLimit?.creationLimit ?? 30, publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30 } }); @@ -60,7 +58,6 @@ export const RateLimitPanel = () => { authRateLimit, inviteUserRateLimit, mfaRateLimit, - creationLimit, publicEndpointLimit } = formData; @@ -71,7 +68,6 @@ export const RateLimitPanel = () => { authRateLimit, inviteUserRateLimit, mfaRateLimit, - creationLimit, publicEndpointLimit }); createNotification({ @@ -210,25 +206,6 @@ export const RateLimitPanel = () => {
)} /> - ( - - field.onChange(Number(e.target.value))} - /> - - )} - />