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 93bea2cd4..18b1548f1 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", @@ -7814,19 +7815,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": { @@ -17404,6 +17431,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 cc190556f..fbd02365b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -122,6 +122,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 6e4f196cc..e5c2e19e4 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"; @@ -92,6 +93,7 @@ declare module "fastify" { id: string; orgId: string; }; + rateLimits: RateLimitConfiguration; // passport data passportUser: { isUserCompleted: string; diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index b9c572ecd..24eca185d 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise { if (!hasVersionColumn) { await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { t.integer("version").nullable(); - // t.dropUnique(["caId"]); + t.dropUnique(["caId"]); }); await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version"); 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/migrations/20240806173521_cert-alerting.ts b/backend/src/db/migrations/20240818024923_cert-alerting.ts similarity index 97% rename from backend/src/db/migrations/20240806173521_cert-alerting.ts rename to backend/src/db/migrations/20240818024923_cert-alerting.ts index 5ea98cda9..f60ce8c02 100644 --- a/backend/src/db/migrations/20240806173521_cert-alerting.ts +++ b/backend/src/db/migrations/20240818024923_cert-alerting.ts @@ -15,6 +15,8 @@ export async function up(knex: Knex): Promise { }); } + await createOnUpdateTrigger(knex, TableName.PkiCollection); + if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) { await knex.schema.createTable(TableName.PkiCollectionItem, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); @@ -28,6 +30,8 @@ export async function up(knex: Knex): Promise { }); } + await createOnUpdateTrigger(knex, TableName.PkiCollectionItem); + if (!(await knex.schema.hasTable(TableName.PkiAlert))) { await knex.schema.createTable(TableName.PkiAlert, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); @@ -43,7 +47,6 @@ export async function up(knex: Knex): Promise { }); } - await createOnUpdateTrigger(knex, TableName.PkiCollection); await createOnUpdateTrigger(knex, TableName.PkiAlert); } diff --git a/backend/src/db/migrations/20240815045235_add-certificate-template.ts b/backend/src/db/migrations/20240818184238_add-certificate-template.ts similarity index 100% rename from backend/src/db/migrations/20240815045235_add-certificate-template.ts rename to backend/src/db/migrations/20240818184238_add-certificate-template.ts 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-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 1564c6dcb..2916eb412 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { TPermissionServiceFactory } from "../permission/permission-service"; @@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({ }; const createAuditLog = async (data: TCreateAuditLogDTO) => { + const appCfg = getConfig(); + if (appCfg.DISABLE_AUDIT_LOG_GENERATION) { + return; + } // add all cases in which project id or org id cannot be added if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" }); 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 37fb137d9..50549417d 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -356,6 +356,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-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 366ad69ea..8c974b05d 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn"; import { containsGlobPatterns } from "@app/lib/picomatch"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { @@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = { secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; projectEnvDAL: Pick; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; + licenseService: Pick; }; export type TSecretApprovalPolicyServiceFactory = ReturnType; @@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({ secretApprovalPolicyDAL, permissionService, secretApprovalPolicyApproverDAL, - projectEnvDAL + projectEnvDAL, + licenseService }: TSecretApprovalPolicyServiceFactoryDep) => { const createSecretApprovalPolicy = async ({ name, @@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({ ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval ); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to create secret approval policy due to plan restriction. Upgrade plan to create secret approval policy." + }); + } + const env = await projectEnvDAL.findOne({ slug: environment, projectId }); if (!env) throw new BadRequestError({ message: "Environment not found" }); @@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy." + }); + } + const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => { const doc = await secretApprovalPolicyDAL.updateById( secretApprovalPolicy.id, @@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({ ProjectPermissionSub.SecretApproval ); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy." + }); + } + await secretApprovalPolicyDAL.deleteById(secretPolicyId); return sapPolicy; }; 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..f6bb33168 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 @@ -50,6 +50,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; @@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = { >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; + licenseService: Pick; }; export type TSecretApprovalRequestServiceFactory = ReturnType; @@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({ kmsService, secretV2BridgeDAL, secretVersionV2BridgeDAL, - secretVersionTagV2BridgeDAL + secretVersionTagV2BridgeDAL, + licenseService }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); @@ -224,12 +227,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 +238,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 +251,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 })); @@ -297,6 +298,14 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to review secret approval request due to plan restriction. Upgrade plan to review secret approval request." + }); + } + const { policy } = secretApprovalRequest; const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, @@ -347,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to update secret approval request due to plan restriction. Upgrade plan to update secret approval request." + }); + } + const { policy } = secretApprovalRequest; const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, @@ -388,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to merge secret approval request due to plan restriction. Upgrade plan to merge secret approval request." + }); + } + const { policy, folderId, projectId } = secretApprovalRequest; const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, 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/keystore/keystore.ts b/backend/src/keystore/keystore.ts index d7bbb0c79..f6d5d1c6e 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock"; export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict -export enum KeyStorePrefixes { - SecretReplication = "secret-replication-import-lock", - KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock", - KmsProjectKeyCreation = "kms-project-key-creation-lock", - WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-", - WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-", - KmsOrgKeyCreation = "kms-org-key-creation-lock", - KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock", - WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-", - WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-" -} +export const KeyStorePrefixes = { + SecretReplication: "secret-replication-import-lock", + KmsProjectDataKeyCreation: "kms-project-data-key-creation-lock", + KmsProjectKeyCreation: "kms-project-key-creation-lock", + WaitUntilReadyKmsProjectDataKeyCreation: "wait-until-ready-kms-project-data-key-creation-", + WaitUntilReadyKmsProjectKeyCreation: "wait-until-ready-kms-project-key-creation-", + KmsOrgKeyCreation: "kms-org-key-creation-lock", + KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock", + WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-", + WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-", + + SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) => + `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, + SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => + `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const +}; + +export const KeyStoreTtls = { + SetSyncSecretIntegrationLastRunTimestampInSeconds: 10 +}; type TWaitTillReady = { key: string; @@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => { const setItemWithExpiry = async ( key: string, - exp: number | string, + expiryInSeconds: number | string, value: string | number | Buffer, prefix?: string - ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp); + ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds); const deleteItem = async (key: string) => redis.del(key); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e937d4047..f4d645165 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/config/env.ts b/backend/src/lib/config/env.ts index 90f04d952..8a2b961e9 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -140,7 +140,8 @@ const envSchema = z MAINTENANCE_MODE: zodStrBool.default("false"), CAPTCHA_SECRET: zpStr(z.string().optional()), PLAIN_API_KEY: zpStr(z.string().optional()), - PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()) + PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), + DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false") }) .transform((data) => ({ ...data, diff --git a/backend/src/lib/fn/dates.ts b/backend/src/lib/fn/dates.ts index f9ea4db10..cd5ca5c12 100644 --- a/backend/src/lib/fn/dates.ts +++ b/backend/src/lib/fn/dates.ts @@ -1,2 +1,8 @@ export const getLastMidnightDateISO = (last = 1) => `${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`; + +export const getTimeDifferenceInSeconds = (lhsTimestamp: string, rhsTimestamp: string) => { + const lhs = new Date(lhsTimestamp); + const rhs = new Date(rhsTimestamp); + return Math.floor((Number(lhs) - Number(rhs)) / 1000); +}; 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/queue/queue-service.ts b/backend/src/queue/queue-service.ts index ec222f0fa..48330cf04 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -16,6 +16,7 @@ export enum QueueName { // TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue AuditLogPrune = "audit-log-prune", DailyResourceCleanUp = "daily-resource-cleanup", + DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", TelemetryInstanceStats = "telemtry-self-hosted-stats", IntegrationSync = "sync-integrations", SecretWebhook = "secret-webhook", @@ -36,6 +37,7 @@ export enum QueueJobs { // TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue AuditLogPrune = "audit-log-prune-job", DailyResourceCleanUp = "daily-resource-cleanup-job", + DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", SecWebhook = "secret-webhook-trigger", TelemetryInstanceStats = "telemetry-self-hosted-stats", IntegrationSync = "secret-integration-pull", @@ -71,6 +73,10 @@ export type TQueueJobTypes = { name: QueueJobs.DailyResourceCleanUp; payload: undefined; }; + [QueueName.DailyExpiringPkiItemAlert]: { + name: QueueJobs.DailyExpiringPkiItemAlert; + payload: undefined; + }; [QueueName.AuditLogPrune]: { name: QueueJobs.AuditLogPrune; payload: undefined; 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 df405d25d..d49c7aace 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -133,6 +133,7 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; @@ -191,6 +192,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"; @@ -362,7 +364,8 @@ export const registerRoutes = async ( projectEnvDAL, secretApprovalPolicyApproverDAL: sapApproverDAL, permissionService, - secretApprovalPolicyDAL + secretApprovalPolicyDAL, + licenseService }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); @@ -749,6 +752,7 @@ export const registerRoutes = async ( kmsService }); const secretQueueService = secretQueueFactory({ + keyStore, queueService, secretDAL, folderDAL, @@ -834,7 +838,8 @@ export const registerRoutes = async ( secretVersionTagV2BridgeDAL, smtpService, projectEnvDAL, - userDAL + userDAL, + licenseService }); const secretService = secretServiceFactory({ @@ -935,8 +940,15 @@ export const registerRoutes = async ( folderDAL, integrationDAL, integrationAuthDAL, - secretQueueService + secretQueueService, + integrationAuthService, + projectBotService, + secretV2BridgeDAL, + secretImportDAL, + secretDAL, + kmsService }); + const serviceTokenService = serviceTokenServiceFactory({ projectEnvDAL, serviceTokenDAL, @@ -1063,13 +1075,18 @@ export const registerRoutes = async ( const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, queueService, - pkiAlertService, secretVersionDAL, secretFolderVersionDAL: folderVersionDAL, snapshotDAL, identityAccessTokenDAL, secretSharingDAL, - secretVersionV2DAL: secretVersionV2BridgeDAL + secretVersionV2DAL: secretVersionV2BridgeDAL, + identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL + }); + + const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ + queueService, + pkiAlertService }); const oidcService = oidcConfigServiceFactory({ @@ -1096,6 +1113,7 @@ export const registerRoutes = async ( await telemetryQueue.startTelemetryCheck(); await dailyResourceCleanUp.startCleanUp(); + await dailyExpiringPkiItemAlert.startSendingAlerts(); await kmsService.startService(); // inject all services @@ -1185,6 +1203,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/index.ts b/backend/src/server/routes/v1/index.ts index c588187c4..e5f35565f 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -78,8 +78,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); - await server.register(registerPkiAlertRouter, { prefix: "/alerts" }); - await server.register(registerPkiCollectionRouter, { prefix: "/collections" }); + await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); + await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); }, { prefix: "/pki" } ); 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/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index afaa99bdf..f64ec9e47 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -22,7 +22,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId), name: z.string().trim().describe(ALERTS.CREATE.name), alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays), - emails: z.array(z.string().trim().email({ message: "Invalid email address" })).describe(ALERTS.CREATE.emails) + emails: z + .array(z.string().trim().email({ message: "Invalid email address" })) + .min(1, { message: "You must specify at least 1 email" }) + .max(5, { message: "You can specify a maximum of 5 emails" }) + .describe(ALERTS.CREATE.emails) }), response: { 200: PkiAlertsSchema @@ -114,6 +118,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId), emails: z .array(z.string().trim().email({ message: "Invalid email address" })) + .min(1, { message: "You must specify at least 1 email" }) + .max(5, { message: "You can specify a maximum of 5 emails" }) .optional() .describe(ALERTS.UPDATE.emails) }), 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 bc5dfdfbb..307ce3b3b 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -11,7 +11,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"; @@ -149,7 +149,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/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 4e3df1173..b1fd87a26 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -1,7 +1,7 @@ import * as x509 from "@peculiar/x509"; import crypto from "crypto"; -import { BadRequestError } from "@app/lib/errors"; +import { NotFoundError } from "@app/lib/errors"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; @@ -106,10 +106,10 @@ export const getCaCredentials = async ({ kmsService }: TGetCaCredentialsDTO) => { const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca) throw new NotFoundError({ message: "CA not found" }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId }); - if (!caSecret) throw new BadRequestError({ message: "CA secret not found" }); + if (!caSecret) throw new NotFoundError({ message: "CA secret not found" }); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -158,7 +158,7 @@ export const getCaCertChains = async ({ kmsService }: TGetCaCertChainsDTO) => { const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca) throw new NotFoundError({ message: "CA not found" }); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -205,7 +205,7 @@ export const getCaCertChain = async ({ kmsService }: TGetCaCertChainDTO) => { const caCert = await certificateAuthorityCertDAL.findById(caCertId); - if (!caCert) throw new BadRequestError({ message: "CA certificate not found" }); + if (!caCert) throw new NotFoundError({ message: "CA certificate not found" }); const ca = await certificateAuthorityDAL.findById(caCert.caId); const keyId = await getProjectKmsCertificateKeyId({ @@ -249,7 +249,7 @@ export const rebuildCaCrl = async ({ kmsService }: TRebuildCaCrlDTO) => { const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca) throw new NotFoundError({ message: "CA not found" }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); 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/pki-alert/expiring-pki-item-alert-queue.ts b/backend/src/services/pki-alert/expiring-pki-item-alert-queue.ts new file mode 100644 index 000000000..a592e9271 --- /dev/null +++ b/backend/src/services/pki-alert/expiring-pki-item-alert-queue.ts @@ -0,0 +1,48 @@ +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; + +type TDailyExpiringPkiItemAlertQueueServiceFactoryDep = { + queueService: TQueueServiceFactory; + pkiAlertService: Pick; +}; + +export type TDailyExpiringPkiItemAlertQueueServiceFactory = ReturnType< + typeof dailyExpiringPkiItemAlertQueueServiceFactory +>; + +export const dailyExpiringPkiItemAlertQueueServiceFactory = ({ + queueService, + pkiAlertService +}: TDailyExpiringPkiItemAlertQueueServiceFactoryDep) => { + queueService.start(QueueName.DailyExpiringPkiItemAlert, async () => { + logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task started`); + await pkiAlertService.sendPkiItemExpiryNotices(); + logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task completed`); + }); + + // we do a repeat cron job in utc timezone at 12 Midnight each day + const startSendingAlerts = async () => { + // clear previous job + await queueService.stopRepeatableJob( + QueueName.DailyExpiringPkiItemAlert, + QueueJobs.DailyExpiringPkiItemAlert, + { pattern: "0 0 * * *", utc: true }, + QueueName.DailyExpiringPkiItemAlert // just a job id + ); + + await queueService.queue(QueueName.DailyExpiringPkiItemAlert, QueueJobs.DailyExpiringPkiItemAlert, undefined, { + delay: 5000, + jobId: QueueName.DailyExpiringPkiItemAlert, + repeat: { pattern: "0 0 * * *", utc: true } + }); + }; + + queueService.listen(QueueName.DailyExpiringPkiItemAlert, "failed", (_, err) => { + logger.error(err, `${QueueName.DailyExpiringPkiItemAlert}: Expiring PKI item alert failed`); + }); + + return { + startSendingAlerts + }; +}; diff --git a/backend/src/services/pki-alert/pki-alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts index 44f029d42..26ab380fb 100644 --- a/backend/src/services/pki-alert/pki-alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -12,8 +12,11 @@ import { TPkiAlertDALFactory } from "./pki-alert-dal"; import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types"; type TPkiAlertServiceFactoryDep = { - pkiAlertDAL: TPkiAlertDALFactory; - pkiCollectionDAL: TPkiCollectionDALFactory; + pkiAlertDAL: Pick< + TPkiAlertDALFactory, + "create" | "findById" | "updateById" | "deleteById" | "getExpiringPkiCollectionItemsForAlerting" + >; + pkiCollectionDAL: Pick; permissionService: Pick; smtpService: Pick; }; diff --git a/backend/src/services/pki-collection/pki-collection-item-dal.ts b/backend/src/services/pki-collection/pki-collection-item-dal.ts index 403fd928b..de896e15c 100644 --- a/backend/src/services/pki-collection/pki-collection-item-dal.ts +++ b/backend/src/services/pki-collection/pki-collection-item-dal.ts @@ -81,7 +81,7 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => { return parseInt((count as unknown as CountResult).count || "0", 10); } catch (error) { - throw new DatabaseError({ error, name: "Count all project certificates" }); + throw new DatabaseError({ error, name: "Count all PKI collection items" }); } }; diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index f7c7ba399..f2a3fa0c4 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -22,10 +22,13 @@ import { } from "./pki-collection-types"; type TPkiCollectionServiceFactoryDep = { - pkiCollectionDAL: TPkiCollectionDALFactory; // TODO: Pick - pkiCollectionItemDAL: TPkiCollectionItemDALFactory; - certificateAuthorityDAL: TCertificateAuthorityDALFactory; - certificateDAL: TCertificateDALFactory; + pkiCollectionDAL: Pick; + pkiCollectionItemDAL: Pick< + TPkiCollectionItemDALFactory, + "findOne" | "create" | "deleteById" | "findPkiCollectionItems" | "countItemsInPkiCollection" + >; + certificateAuthorityDAL: Pick; + certificateDAL: Pick; permissionService: Pick; }; @@ -139,7 +142,7 @@ export const pkiCollectionServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections ); pkiCollection = await pkiCollectionDAL.deleteById(collectionId); 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..dab70806f 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -2,9 +2,9 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { logger } from "@app/lib/logger"; 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,13 +13,13 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d type TDailyResourceCleanUpQueueServiceFactoryDep = { auditLogDAL: Pick; identityAccessTokenDAL: Pick; + identityUniversalAuthClientSecretDAL: Pick; secretVersionDAL: Pick; secretVersionV2DAL: Pick; secretFolderVersionDAL: Pick; snapshotDAL: Pick; secretSharingDAL: Pick; queueService: TQueueServiceFactory; - pkiAlertService: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -27,24 +27,24 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType { 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(); await secretVersionV2DAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions(); - await pkiAlertService.sendPkiItemExpiryNotices(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); }); 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..36edcf195 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -6,11 +6,12 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; +import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError } from "@app/lib/errors"; -import { groupBy, isSamePath, unique } from "@app/lib/fn"; +import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -73,12 +74,13 @@ 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; + keyStore: Pick; }; export type TGetSecrets = { @@ -122,7 +124,8 @@ export const secretQueueFactory = ({ secretRotationDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL + secretApprovalRequestDAL, + keyStore }: TSecretQueueFactoryDep) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const appCfg = getConfig(); @@ -576,7 +579,6 @@ export const secretQueueFactory = ({ ) ); } - const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, @@ -641,108 +643,157 @@ export const secretQueueFactory = ({ `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` ); - const secrets = shouldUseSecretV2Bridge - ? await getIntegrationSecretsV2({ - environment, - projectId, - folderId: folder.id, - depth: 1, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") - }) - : await getIntegrationSecrets({ - environment, - projectId, - folderId: folder.id, - key: botKey as string, - depth: 1 - }); + const lock = await keyStore.acquireLock( + [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)], + 10000, + { + retryCount: 3, + retryDelay: 2000 + } + ); + const lockAcquiredTime = new Date(); - for (const integration of toBeSyncedIntegrations) { - const integrationAuth = { - ...integration.integrationAuth, - createdAt: new Date(), - updatedAt: new Date(), - projectId: integration.projectId - }; + const lastRunSyncIntegrationTimestamp = await keyStore.getItem( + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) + ); - const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( - integrationAuth, - shouldUseSecretV2Bridge, - botKey + // check whether the integration should wait or not + if (lastRunSyncIntegrationTimestamp) { + const INTEGRATION_INTERVAL = 2000; + const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); + if (isStaleSyncIntegration) { + logger.info( + `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + ); + return; + } + + const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( + lockAcquiredTime.toISOString(), + lastRunSyncIntegrationTimestamp ); - let awsAssumeRoleArn = null; - if (shouldUseSecretV2Bridge) { - if (integrationAuth.encryptedAwsAssumeIamRoleArn) { - awsAssumeRoleArn = secretManagerDecryptor({ - cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) - }).toString(); - } - } else if ( - integrationAuth.awsAssumeIamRoleArnTag && - integrationAuth.awsAssumeIamRoleArnIV && - integrationAuth.awsAssumeIamRoleArnCipherText - ) { - awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, - iv: integrationAuth.awsAssumeIamRoleArnIV, - tag: integrationAuth.awsAssumeIamRoleArnTag, - key: botKey as string + if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0) + await new Promise((resolve) => { + setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); }); - } - - const suffixedSecrets: typeof secrets = {}; - const metadata = integration.metadata as Record; - if (metadata) { - Object.keys(secrets).forEach((key) => { - const prefix = metadata?.secretPrefix || ""; - const suffix = metadata?.secretSuffix || ""; - const newKey = prefix + key + suffix; - suffixedSecrets[newKey] = secrets[key]; - }); - } - - try { - // akhilmhdh: this needs to changed later to be more easier to use - // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places - const response = await syncIntegrationSecrets({ - createManySecretsRawFn, - updateManySecretsRawFn, - integrationDAL, - integration, - integrationAuth, - secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, - accessId: accessId as string, - awsAssumeRoleArn, - accessToken, - projectId, - appendices: { - prefix: metadata?.secretPrefix || "", - suffix: metadata?.secretSuffix || "" - } - }); - - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - lastUsed: new Date(), - syncMessage: response?.syncMessage ?? "", - isSynced: response?.isSynced ?? true - }); - } catch (err) { - logger.info("Secret integration sync error: %o", err); - - const message = - (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || - "Unknown error occurred."; - - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - lastUsed: new Date(), - syncMessage: message, - isSynced: false - }); - } } + // akhilmhdh: this try catch is for lock release + try { + const secrets = shouldUseSecretV2Bridge + ? await getIntegrationSecretsV2({ + environment, + projectId, + folderId: folder.id, + depth: 1, + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + }) + : await getIntegrationSecrets({ + environment, + projectId, + folderId: folder.id, + key: botKey as string, + depth: 1 + }); + + for (const integration of toBeSyncedIntegrations) { + const integrationAuth = { + ...integration.integrationAuth, + createdAt: new Date(), + updatedAt: new Date(), + projectId: integration.projectId + }; + + const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( + integrationAuth, + shouldUseSecretV2Bridge, + botKey + ); + let awsAssumeRoleArn = null; + if (shouldUseSecretV2Bridge) { + if (integrationAuth.encryptedAwsAssumeIamRoleArn) { + awsAssumeRoleArn = secretManagerDecryptor({ + cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) + }).toString(); + } + } else if ( + integrationAuth.awsAssumeIamRoleArnTag && + integrationAuth.awsAssumeIamRoleArnIV && + integrationAuth.awsAssumeIamRoleArnCipherText + ) { + awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, + iv: integrationAuth.awsAssumeIamRoleArnIV, + tag: integrationAuth.awsAssumeIamRoleArnTag, + key: botKey as string + }); + } + + const suffixedSecrets: typeof secrets = {}; + const metadata = integration.metadata as Record; + if (metadata) { + Object.keys(secrets).forEach((key) => { + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; + }); + } + + // akhilmhdh: this try catch is for catching integration error and saving it in db + try { + // akhilmhdh: this needs to changed later to be more easier to use + // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places + const response = await syncIntegrationSecrets({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, + integration, + integrationAuth, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, + accessId: accessId as string, + awsAssumeRoleArn, + accessToken, + projectId, + appendices: { + prefix: metadata?.secretPrefix || "", + suffix: metadata?.secretSuffix || "" + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: response?.syncMessage ?? "", + isSynced: response?.isSynced ?? true + }); + } catch (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) || + "Unknown error occurred."; + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: message, + isSynced: false + }); + } + } + } finally { + await lock.release(); + } + + await keyStore.setItemWithExpiry( + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), + KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, + lockAcquiredTime.toISOString() + ); logger.info("Secret integration sync ended: %s", job.id); }); @@ -828,7 +879,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 +1055,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/guides/local-development.mdx b/docs/documentation/guides/local-development.mdx index 9ffc0fca0..6d606bafe 100644 --- a/docs/documentation/guides/local-development.mdx +++ b/docs/documentation/guides/local-development.mdx @@ -9,7 +9,7 @@ description: "Learn how to manage secrets in local development environments." There is a number of issues that arise with secret management in local development environment: 1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another. 2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time. -3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability. +3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded secret as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability. ## Solution @@ -31,4 +31,4 @@ By default, all the secrets in the Infisical environments are shared among proje ### Secret Scanning -In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub. \ No newline at end of file +In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub. 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 9496ffffe..7b8fcdc42 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -619,6 +619,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/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx index e77dfe2b8..9bb7b89d5 100644 --- a/frontend/src/hooks/api/ca/constants.tsx +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -11,7 +11,7 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = { [CaStatus.PENDING_CERTIFICATE]: "Pending Certificate" }; -export const getStatusBadgeVariant = (status: CaStatus) => { +export const getCaStatusBadgeVariant = (status: CaStatus) => { switch (status) { case CaStatus.ACTIVE: return "success"; diff --git a/frontend/src/hooks/api/certificates/constants.tsx b/frontend/src/hooks/api/certificates/constants.tsx index e5a9b7a43..2f2972bbc 100644 --- a/frontend/src/hooks/api/certificates/constants.tsx +++ b/frontend/src/hooks/api/certificates/constants.tsx @@ -1,10 +1,21 @@ -import { CertKeyAlgorithm, CertStatus,CrlReason } from "./enums"; +import { CertKeyAlgorithm, CertStatus, CrlReason } from "./enums"; export const certStatusToNameMap: { [K in CertStatus]: string } = { [CertStatus.ACTIVE]: "Active", [CertStatus.REVOKED]: "Revoked" }; +export const getCertStatusBadgeVariant = (status: CertStatus) => { + switch (status) { + case CertStatus.ACTIVE: + return "success"; + case CertStatus.REVOKED: + return "danger"; + default: + return "primary"; + } +}; + export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = { [CertKeyAlgorithm.RSA_2048]: "RSA 2048", [CertKeyAlgorithm.RSA_4096]: "RSA 4096", 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/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 6a65e32e8..4e3602686 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -273,8 +273,10 @@ export const AppLayout = ({ children }: LayoutProps) => { const orgUsers = await fetchOrgUsers(currentOrg.id); await addUsersToProject.mutateAsync({ usernames: orgUsers - .map((member) => member.user.username) - .filter((username) => username !== user.username), + .filter( + (member) => member.user.username !== user.username && member.status === "accepted" + ) + .map((member) => member.user.username), projectId: newProjectId, orgId: currentOrg.id }); @@ -482,7 +484,7 @@ export const AppLayout = ({ children }: LayoutProps) => { )} - Organization Admin Console + Organization Admin Console
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/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index babf1a15f..c93649f2c 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -552,8 +552,10 @@ const OrganizationPage = () => { await addUsersToProject.mutateAsync({ usernames: orgUsers - .map((member) => member.user.username) - .filter((username) => username !== user.username), + .filter( + (member) => member.user.username !== user.username && member.status === "accepted" + ) + .map((member) => member.user.username), projectId: newProjectId, orgId: currentOrg.id }); 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/Project/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx index f719365eb..2c0bc8706 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx @@ -39,7 +39,7 @@ import { CaStatus, useListWorkspaceCas } from "@app/hooks/api"; import { caStatusToNameMap, caTypeToNameMap, - getStatusBadgeVariant + getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -92,7 +92,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { > {ca.friendlyName} - + {caStatusToNameMap[ca.status]} @@ -100,9 +100,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {

{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}

- {/* - Expires Soon - */}
diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx index 82c4c8d3a..3341e521c 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx @@ -62,7 +62,7 @@ export const CertificateTemplatesSection = () => { onClick={() => handlePopUpOpen("certificateTemplate")} isDisabled={!isAllowed} > - Create Certificate Template + Create )} diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx index 297b2d817..e504c9521 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx @@ -1,4 +1,4 @@ -import { faEllipsis, faGear, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -74,7 +74,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { id: certificateTemplate.id }) } - icon={} + icon={} > Manage Policies @@ -109,7 +109,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { {!isLoading && !data?.certificateTemplates?.length && ( - + )}
diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx index 58bb8ef4c..962083b22 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx @@ -61,7 +61,7 @@ export const CertificatesSection = () => { onClick={() => handlePopUpOpen("certificate")} isDisabled={!isAllowed} > - Issue Certificate + Issue )} diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx index f23299aa8..a3a6106f3 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx @@ -13,6 +13,7 @@ import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { + Badge, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -31,9 +32,10 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useListWorkspaceCertificates } from "@app/hooks/api"; -import { certStatusToNameMap } from "@app/hooks/api/certificates/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils"; + type Props = { handlePopUpOpen: ( popUpName: keyof UsePopUpState< @@ -60,146 +62,155 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { }); return ( -
- - - - - - - - - - - {isLoading && } - {!isLoading && - data?.certificates.map((certificate) => { - return ( - - - - - - - ); - })} - -
Friendly NameStatusValid Until -
{certificate.friendlyName}{certStatusToNameMap[certificate.status]} - {certificate.notAfter - ? format(new Date(certificate.notAfter), "yyyy-MM-dd") - : "-"} - - - -
- - - -
-
- - - {(isAllowed) => ( - - handlePopUpOpen("certificateCert", { - serialNumber: certificate.serialNumber - }) - } - disabled={!isAllowed} - icon={} - > - Export Certificate - - )} - - - {(isAllowed) => ( - - handlePopUpOpen("certificate", { - serialNumber: certificate.serialNumber - }) - } - disabled={!isAllowed} - icon={} - > - View Details - - )} - - - {(isAllowed) => ( - - handlePopUpOpen("revokeCertificate", { - serialNumber: certificate.serialNumber - }) - } - disabled={!isAllowed} - icon={} - > - Revoke Certificate - - )} - - - {(isAllowed) => ( - - handlePopUpOpen("deleteCertificate", { - serialNumber: certificate.serialNumber, - commonName: certificate.commonName - }) - } - disabled={!isAllowed} - icon={} - > - Delete Certificate - - )} - - -
-
- {!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( - setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} - /> - )} - {!isLoading && !data?.certificates?.length && ( - - )} -
-
+ + + + + + + + + + + + {isLoading && } + {!isLoading && + data?.certificates.map((certificate) => { + const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter); + return ( + + + + + + + + ); + })} + +
Friendly NameStatusNot BeforeNot After +
{certificate.friendlyName} + + {label} + + + {certificate.notBefore + ? format(new Date(certificate.notBefore), "yyyy-MM-dd") + : "-"} + + {certificate.notAfter + ? format(new Date(certificate.notAfter), "yyyy-MM-dd") + : "-"} + + + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen("certificateCert", { + serialNumber: certificate.serialNumber + }) + } + disabled={!isAllowed} + icon={} + > + Export Certificate + + )} + + + {(isAllowed) => ( + + handlePopUpOpen("certificate", { + serialNumber: certificate.serialNumber + }) + } + disabled={!isAllowed} + icon={} + > + View Details + + )} + + + {(isAllowed) => ( + + handlePopUpOpen("revokeCertificate", { + serialNumber: certificate.serialNumber + }) + } + disabled={!isAllowed} + icon={} + > + Revoke Certificate + + )} + + + {(isAllowed) => ( + + handlePopUpOpen("deleteCertificate", { + serialNumber: certificate.serialNumber, + commonName: certificate.commonName + }) + } + disabled={!isAllowed} + icon={} + > + Delete Certificate + + )} + + +
+
+ {!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && !data?.certificates?.length && ( + + )} +
); }; diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.utils.ts b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.utils.ts new file mode 100644 index 000000000..93b408bbe --- /dev/null +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.utils.ts @@ -0,0 +1,28 @@ +import ms from "ms"; + +export const getCertValidUntilBadgeDetails = (notAfter: string) => { + const currentDate = new Date().getTime(); + const notAfterDate = new Date(notAfter).getTime(); + const diffInMs = notAfterDate - currentDate; + + let variant: "success" | "primary" | "danger" = "success"; + let label = "Healthy"; + + if (diffInMs > ms("60d")) { + variant = "success"; + } else if (diffInMs > ms("30d")) { + variant = "primary"; + } else { + variant = "danger"; + } + + if (diffInMs > ms("60d")) { + label = "Healthy"; + } else if (diffInMs > ms("0d")) { + label = `Expires in ${ms(diffInMs)}`; + } else { + label = "Expired"; + } + + return { variant, label }; +}; diff --git a/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx index 974c2aa1b..3a5a04345 100644 --- a/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx @@ -1,6 +1,6 @@ import { motion } from "framer-motion"; -import { PkiAlertsSection, PkiCollectionSection } from "./components"; +import { PkiAlertsSection } from "./components"; export const PkiAlertsTab = () => { return ( @@ -11,7 +11,6 @@ export const PkiAlertsTab = () => { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > - ); diff --git a/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionTable.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionTable.tsx index 4176b907b..a27662449 100644 --- a/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionTable.tsx @@ -63,7 +63,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => { } > {pkiCollection.name} - {pkiCollection.description} + {pkiCollection.description ? pkiCollection.description : "-"} diff --git a/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx index 2334cc81b..d8518e6a2 100644 --- a/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx +++ b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { faBoxesStacked, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faBoxesStacked, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -85,7 +85,7 @@ export const PkiCollectionItemsTable = ({ collectionId, type, handlePopUpOpen }: }); }} > - + )} 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))} - /> - - )} - />