Merge remote-tracking branch 'origin' into certificate-alerting

This commit is contained in:
Tuan Dang
2024-08-17 19:46:23 -07:00
109 changed files with 1518 additions and 565 deletions
+1 -1
View File
@@ -50,6 +50,6 @@ jobs:
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} 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 run: go test -v -count=1 ./test
+13
View File
@@ -15,3 +15,16 @@ up-prod:
down: down:
docker compose -f docker-compose.dev.yml 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
+49 -6
View File
@@ -25,6 +25,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
@@ -7812,19 +7813,45 @@
} }
}, },
"node_modules/@octokit/plugin-retry": { "node_modules/@octokit/plugin-retry": {
"version": "6.0.1", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", "integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
"dependencies": { "dependencies": {
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^4.0.1",
"@octokit/types": "^12.0.0", "@octokit/types": "^10.0.0",
"bottleneck": "^2.15.3" "bottleneck": "^2.15.3"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
}, },
"peerDependencies": { "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": { "node_modules/@octokit/plugin-throttling": {
@@ -17396,6 +17423,22 @@
"node": ">=18" "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": { "node_modules/probot/node_modules/commander": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+1
View File
@@ -121,6 +121,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
+20 -1
View File
@@ -7,14 +7,33 @@ const prompt = promptSync({
sigint: true sigint: true
}); });
type ComponentType = 1 | 2 | 3;
console.log(` console.log(`
Component List Component List
-------------- --------------
0. Exit
1. Service component 1. Service component
2. DAL component 2. DAL component
3. Router 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) { if (componentType === 1) {
const componentName = prompt("Enter service name: "); const componentName = prompt("Enter service name: ");
+2
View File
@@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; 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 { 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 { 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 { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -91,6 +92,7 @@ declare module "fastify" {
id: string; id: string;
orgId: string; orgId: string;
}; };
rateLimits: RateLimitConfiguration;
// passport data // passport data
passportUser: { passportUser: {
isUserCompleted: string; isUserCompleted: string;
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (!hasCreationLimitCol) {
t.integer("creationLimit").defaultTo(30).notNullable();
}
});
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (!hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.string("name");
});
}
}
-1
View File
@@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
authRateLimit: z.number().default(60), authRateLimit: z.number().default(60),
inviteUserRateLimit: z.number().default(30), inviteUserRateLimit: z.number().default(30),
mfaRateLimit: z.number().default(20), mfaRateLimit: z.number().default(20),
creationLimit: z.number().default(30),
publicEndpointLimit: z.number().default(30), publicEndpointLimit: z.number().default(30),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()
-1
View File
@@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretTagsSchema = z.object({ export const SecretTagsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(),
slug: z.string(), slug: z.string(),
color: z.string().nullable().optional(), color: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
@@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
.default("/") .default("/")
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.RENEW.path), .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: { response: {
200: z.object({ 200: z.object({
@@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
authRateLimit: z.number(), authRateLimit: z.number(),
inviteUserRateLimit: z.number(), inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(), mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number() publicEndpointLimit: z.number()
}), }),
response: { response: {
@@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => {
.del() .del()
.returning("id"); .returning("id");
numberOfRetryOnFailure = 0; // reset 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) { } catch (error) {
numberOfRetryOnFailure += 1; numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning"); 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 }; return { ...auditLogOrm, pruneAuditLog, find };
@@ -352,6 +352,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
shouldDeleteIntegrationSecrets?: boolean;
}; };
} }
@@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretRotation: true, secretRotation: true,
caCrl: false, caCrl: false,
instanceUserManagement: false, instanceUserManagement: false,
externalKms: false externalKms: false,
rateLimits: {
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
}); });
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -58,6 +58,11 @@ export type TFeatureSet = {
caCrl: false; caCrl: false;
instanceUserManagement: false; instanceUserManagement: false;
externalKms: false; externalKms: false;
rateLimits: {
readLimit: number;
writeLimit: number;
secretsLimit: number;
};
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {
@@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TRateLimitDALFactory } from "./rate-limit-dal"; 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, readLimit: 60,
publicEndpointLimit: 30, publicEndpointLimit: 30,
writeLimit: 200, writeLimit: 200,
secretsLimit: 60, secretsLimit: 60,
authRateLimit: 60, authRateLimit: 60,
inviteUserRateLimit: 30, inviteUserRateLimit: 30,
mfaRateLimit: 20, mfaRateLimit: 20
creationLimit: 30
}; };
Object.freeze(rateLimitMaxConfiguration); Object.freeze(rateLimitMaxConfiguration);
@@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
secretsLimit: rateLimit.secretsRateLimit, secretsLimit: rateLimit.secretsRateLimit,
authRateLimit: rateLimit.authRateLimit, authRateLimit: rateLimit.authRateLimit,
inviteUserRateLimit: rateLimit.inviteUserRateLimit, inviteUserRateLimit: rateLimit.inviteUserRateLimit,
mfaRateLimit: rateLimit.mfaRateLimit, mfaRateLimit: rateLimit.mfaRateLimit
creationLimit: rateLimit.creationLimit
}; };
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration); logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
@@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
authRateLimit: number; authRateLimit: number;
inviteUserRateLimit: number; inviteUserRateLimit: number;
mfaRateLimit: number; mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number; publicEndpointLimit: number;
}; };
@@ -14,3 +13,13 @@ export type TRateLimit = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} & TRateLimitUpdateDTO; } & TRateLimitUpdateDTO;
export type RateLimitConfiguration = {
readLimit: number;
publicEndpointLimit: number;
writeLimit: number;
secretsLimit: number;
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
};
@@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color", secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug", secVerTagSlug: "secVerTag.slug"
secVerTagName: "secVerTag.name"
}) })
.select( .select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
) )
.select( .select(
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
@@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "tagJnId", key: "tagJnId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id, id,
name, name: slug,
slug, slug,
color color
}) })
@@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "secVerTagId", key: "secVerTagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line // eslint-disable-next-line
id, id,
// eslint-disable-next-line // eslint-disable-next-line
name, name: slug,
// eslint-disable-next-line // eslint-disable-next-line
slug, slug,
// eslint-disable-next-line // eslint-disable-next-line
@@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color", secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug", secVerTagSlug: "secVerTag.slug"
secVerTagName: "secVerTag.name"
}) })
.select( .select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
) )
.select( .select(
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"), db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
@@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "tagJnId", key: "tagJnId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id, id,
name, name: slug,
slug, slug,
color color
}) })
@@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "secVerTagId", key: "secVerTagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line // eslint-disable-next-line
id, id,
// eslint-disable-next-line // eslint-disable-next-line
name, name: slug,
// eslint-disable-next-line // eslint-disable-next-line
slug, slug,
// eslint-disable-next-line // eslint-disable-next-line
@@ -224,12 +224,10 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key, secretKey: el.key,
id: el.id, id: el.id,
version: el.version, version: el.version,
secretValue: el.encryptedValue secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined, : "",
secret: el.secret secret: el.secret
? { ? {
secretKey: el.secret.key, secretKey: el.secret.key,
@@ -237,10 +235,10 @@ export const secretApprovalRequestServiceFactory = ({
version: el.secret.version, version: el.secret.version,
secretValue: el.secret.encryptedValue secretValue: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
: undefined, : "",
secretComment: el.secret.encryptedComment secretComment: el.secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
: undefined : ""
} }
: undefined, : undefined,
secretVersion: el.secretVersion secretVersion: el.secretVersion
@@ -250,10 +248,10 @@ export const secretApprovalRequestServiceFactory = ({
version: el.secretVersion.version, version: el.secretVersion.version,
secretValue: el.secretVersion.encryptedValue secretValue: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
: undefined, : "",
secretComment: el.secretVersion.encryptedComment secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: undefined : ""
} }
: undefined : undefined
})); }));
@@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
}); });
// secrets that gets replicated across imports // secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({ const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
@@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
}); });
} }
if (locallyDeletedSecrets.length) { if (locallyDeletedSecrets.length) {
await secretDAL.delete( await secretV2BridgeDAL.delete(
{ {
$in: { $in: {
id: locallyDeletedSecrets.map(({ id }) => id) id: locallyDeletedSecrets.map(({ id }) => id)
@@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({
secretKey: el.key, secretKey: el.key,
secretValue: el.encryptedValue secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined, : "",
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined : ""
})) }))
}; };
} else { } else {
@@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, 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, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, 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, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, 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, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, 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, id,
name, name: slug,
slug, slug,
color, color,
vId vId
+2 -1
View File
@@ -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.", "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.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path 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: { CREATE: {
secretName: "The name of the secret to create.", secretName: "The name of the secret to create.",
+10
View File
@@ -128,6 +128,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Create" }); 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) => { upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
try { try {
if (!data.length) return []; if (!data.length) return [];
+14 -15
View File
@@ -1,7 +1,6 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => { export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints // GET endpoints
export const readLimit: RateLimitOptions = { export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().readLimit, hook: "preValidation",
max: (req) => req.rateLimits.readLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
// POST, PATCH, PUT, DELETE endpoints // POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = { export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().writeLimit, hook: "preValidation",
max: (req) => req.rateLimits.writeLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
@@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = { export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports // secrets, folders, secret imports
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().secretsLimit, hook: "preValidation",
max: (req) => req.rateLimits.secretsLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const authRateLimit: RateLimitOptions = { export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().authRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.authRateLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const inviteUserRateLimit: RateLimitOptions = { export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().inviteUserRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.inviteUserRateLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const mfaRateLimit: RateLimitOptions = { export const mfaRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().mfaRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.mfaRateLimit,
keyGenerator: (req) => { keyGenerator: (req) => {
return req.headers.authorization?.split(" ")[1] || req.realIp; 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 // Public endpoints to avoid brute force attacks
export const publicEndpointLimit: RateLimitOptions = { export const publicEndpointLimit: RateLimitOptions = {
// Read Shared Secrets // Read Shared Secrets
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().publicEndpointLimit, hook: "preValidation",
max: (req) => req.rateLimits.publicEndpointLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
@@ -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
};
});
});
+12 -2
View File
@@ -189,6 +189,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission"; import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
@@ -924,8 +925,15 @@ export const registerRoutes = async (
folderDAL, folderDAL,
integrationDAL, integrationDAL,
integrationAuthDAL, integrationAuthDAL,
secretQueueService secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
secretDAL,
kmsService
}); });
const serviceTokenService = serviceTokenServiceFactory({ const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL, projectEnvDAL,
serviceTokenDAL, serviceTokenDAL,
@@ -1058,7 +1066,8 @@ export const registerRoutes = async (
snapshotDAL, snapshotDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
}); });
const oidcService = oidcConfigServiceFactory({ const oidcService = oidcConfigServiceFactory({
@@ -1173,6 +1182,7 @@ export const registerRoutes = async (
await server.register(injectIdentity, { userDAL, serviceTokenDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectPermission); await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo); await server.register(injectAuditLogInfo);
server.route({ server.route({
@@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
version: z.number(), version: z.number(),
type: z.string(), type: z.string(),
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string(),
secretComment: z.string().optional(), secretComment: z.string(),
secretReminderNote: z.string().nullable().optional(), secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
@@ -3,7 +3,7 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs"; 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 { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
@@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId) integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}), }),
querystring: z.object({
shouldDeleteIntegrationSecrets: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
}),
response: { response: {
200: z.object({ 200: z.object({
integration: IntegrationsSchema integration: IntegrationsSchema
@@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.integrationId id: req.params.integrationId,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: integration.targetService, targetService: integration.targetService,
targetServiceId: integration.targetServiceId, targetServiceId: integration.targetServiceId,
path: integration.path, path: integration.path,
region: integration.region region: integration.region,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
// eslint-disable-next-line // eslint-disable-next-line
}) as any }) as any
} }
@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas"; import { SecretTagsSchema } from "@app/db/schemas";
@@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 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: { response: {
200: z.object({ 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) projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(SECRET_TAGS.CREATE.name), slug: z
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug), .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) color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}), }),
response: { response: {
@@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name), slug: z
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug), .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) color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}), }),
response: { response: {
@@ -9,7 +9,7 @@ import {
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs"; 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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z.object({
@@ -10,7 +10,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs"; 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 { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -148,7 +148,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Create a new project", description: "Create a new project",
+25 -17
View File
@@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() })
.extend({ name: z.string() })
.array()
}) })
) )
}) })
@@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ tags: SecretTagsSchema.pick({
tags: SecretTagsSchema.pick({ id: true,
id: true, slug: true,
slug: true, color: true
name: true,
color: true
}).array()
}) })
) .extend({ name: z.string() })
.array()
})
}) })
} }
}, },
@@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
.transform((value) => value === "true") .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: { response: {
200: z.object({ 200: z.object({
@@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.extend({ name: z.string() })
.array() .array()
.optional() .optional()
}) })
@@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId: workspaceId, projectId: workspaceId,
path: secretPath, path: secretPath,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
recursive: req.query.recursive recursive: req.query.recursive,
tagSlugs: req.query.tagSlugs
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.extend({ name: z.string() })
.array() .array()
.optional() .optional()
}) })
@@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() })
.extend({ name: z.string() })
.array()
}) })
.array(), .array(),
imports: z imports: z
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>; export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@@ -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 };
}; };
@@ -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<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
const content: Record<string, boolean> = {};
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<TSecretDALFactory, "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
let content: Record<string, boolean> = {};
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<TIntegrations, "envId">;
secrets: Record<string, boolean>;
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<TIntegrations, "envId"> & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
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"
});
}
};
@@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({
integration, integration,
secrets, secrets,
accessId, accessId,
accessToken accessToken,
projectId
}: { }: {
integration: TIntegrations; integration: TIntegrations & { secretPath: string; environment: { slug: string } };
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
projectId?: string;
}) => { }) => {
let response: { isSynced: boolean; syncMessage: string } | null = null; let response: { isSynced: boolean; syncMessage: string } | null = null;
if (!accessId) { if (!accessId) {
throw new Error("AWS access ID is required"); throw new Error("AWS access ID is required");
} }
const config = new AWS.Config({ const config = new AWS.Config({
region: integration.region as string, region: integration.region as string,
credentials: { credentials: {
@@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
const metadata = z.record(z.any()).parse(integration.metadata || {}); const metadata = z.record(z.any()).parse(integration.metadata || {});
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {}; const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
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 // now fetch all aws parameter store secrets
let hasNext = true; let hasNext = true;
let nextToken: string | undefined; let nextToken: string | undefined;
@@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
nextToken = parameters.NextToken; 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 // Identify secrets to create
// don't use Promise.all() and promise map here // don't use Promise.all() and promise map here
// it will cause rate limit // it will cause rate limit
@@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
// case: secret does not exist in AWS parameter store // case: secret does not exist in AWS parameter store
// -> create secret // -> create secret
if (secrets[key].value) { 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 await ssm
.putParameter({ .putParameter({
Name: `${integration.path}${key}`, Name: `${integration.path}${key}`,
Type: "SecureString", Type: "SecureString",
Value: secrets[key].value, Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }), ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
// Overwrite: true, Overwrite: true
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
}) })
.promise(); .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 // case: secret exists in AWS parameter store
} else { } else {
logger.info(
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// -> update secret // -> update secret
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
await ssm await ssm
@@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
}) })
.promise(); .promise();
} catch (err) { } 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") { if ((err as any).code === "AccessDeniedException") {
logger.error( logger.error(
@@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
} }
if (!metadata.shouldDisableDelete) { 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) { for (const key in awsParameterStoreSecretsObj) {
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) { 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)) { 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: // case:
// -> delete secret // -> delete secret
await ssm await ssm
@@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
Name: awsParameterStoreSecretsObj[key].Name as string Name: awsParameterStoreSecretsObj[key].Name as string
}) })
.promise(); .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) => { await new Promise((resolve) => {
setTimeout(resolve, 50); setTimeout(resolve, 50);
@@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
integration, integration,
secrets, secrets,
accessId, accessId,
accessToken accessToken,
projectId
}); });
break; break;
case Integrations.AWS_SECRET_MANAGER: case Integrations.AWS_SECRET_MANAGER:
@@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; 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 { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; 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 { TIntegrationDALFactory } from "./integration-dal";
import { import {
TCreateIntegrationDTO, TCreateIntegrationDTO,
@@ -19,9 +26,15 @@ import {
type TIntegrationServiceFactoryDep = { type TIntegrationServiceFactoryDep = {
integrationDAL: TIntegrationDALFactory; integrationDAL: TIntegrationDALFactory;
integrationAuthDAL: TIntegrationAuthDALFactory; integrationAuthDAL: TIntegrationAuthDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; integrationAuthService: TIntegrationAuthServiceFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectBotService: TProjectBotServiceFactory;
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">; secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
}; };
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>; export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
@@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
integrationAuthDAL, integrationAuthDAL,
folderDAL, folderDAL,
permissionService, permissionService,
secretQueueService secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
kmsService,
secretDAL
}: TIntegrationServiceFactoryDep) => { }: TIntegrationServiceFactoryDep) => {
const createIntegration = async ({ const createIntegration = async ({
app, app,
@@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
return updatedIntegration; 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); const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); 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); 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) => { const deletedIntegration = await integrationDAL.transaction(async (tx) => {
// delete integration // delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
@@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
export type TDeleteIntegrationDTO = { export type TDeleteIntegrationDTO = {
id: string; id: string;
shouldDeleteIntegrationSecrets?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSyncIntegrationDTO = { export type TSyncIntegrationDTO = {
@@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
const doc = await db const doc = await db
.replicaNode()(TableName.ProjectMembership) .replicaNode()(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
.where(`${TableName.Users}.isGhost` as "isGhost", false) .where(`${TableName.Users}.isGhost` as "isGhost", false)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`) .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
@@ -5,6 +5,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; 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 { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
@@ -13,6 +14,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
type TDailyResourceCleanUpQueueServiceFactoryDep = { type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">; auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">; secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">; secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
@@ -33,12 +35,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
secretFolderVersionDAL, secretFolderVersionDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL secretVersionV2DAL,
identityUniversalAuthClientSecretDAL
}: TDailyResourceCleanUpQueueServiceFactoryDep) => { }: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await auditLogDAL.pruneAuditLog(); await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens(); await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets();
await snapshotDAL.pruneExcessSnapshots(); await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
@@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
secretKey: string; secretKey: string;
// akhilmhdh: yes i know you can put ?. // akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things // But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string | undefined; secretValue: string;
secretComment: string | undefined; secretComment: string;
})[]; })[];
}; };
@@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number; depth?: number;
cyclicDetector?: Set<string>; cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string | undefined; decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: ( expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }> secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>; ) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
@@ -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. _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); .concat(folderDeeperImportSecrets);
return { return {
secretPath: importPath, secretPath: importPath,
environment: importEnv.slug, environment: importEnv.slug,
@@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
}; };
return acc; return acc;
}, },
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }> {} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
); );
// eslint-disable-next-line // eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey); await expandSecretReferences(secretsGroupByKey);
@@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
folderDAL, folderDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
}); });
return importedSecrets; return importedSecrets;
} }
@@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
...secretTagOrm, ...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany, saveTagsToSecret: secretJnTagOrm.insertMany,
deleteTagsToSecret: secretJnTagOrm.delete, deleteTagsToSecret: secretJnTagOrm.delete,
saveTagsToSecretV2: secretV2JnTagOrm.insertMany, saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
deleteTagsToSecretV2: secretV2JnTagOrm.delete, deleteTagsToSecretV2: secretV2JnTagOrm.delete,
findSecretTagsByProjectId, findSecretTagsByProjectId,
deleteTagsManySecret, deleteTagsManySecret,
@@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>; export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => { export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
const createTag = async ({ const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
name,
slug,
actor,
color,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TCreateTagDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
const newTag = await secretTagDAL.create({ const newTag = await secretTagDAL.create({
projectId, projectId,
name,
slug, slug,
color, color,
createdBy: actorId, createdBy: actorId,
@@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag; 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); const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); 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); 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; return updatedTag;
}; };
@@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); 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) => { 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); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag; return { ...tag, name: tag.slug };
}; };
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
@@ -1,14 +1,12 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TCreateTagDTO = { export type TCreateTagDTO = {
name: string;
color: string; color: string;
slug: string; slug: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateTagDTO = { export type TUpdateTagDTO = {
id: string; id: string;
name?: string;
slug?: string; slug?: string;
color?: string; color?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
@@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where({ [`${TableName.SecretV2}Id` as const]: secretId }) .where({ [`${TableName.SecretV2}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({ return tags.map((el) => ({
id: el.tagId, id: el.tagId,
color: el.tagColor, color: el.tagColor,
slug: el.tagSlug, slug: el.tagSlug,
name: el.tagName name: el.tagSlug
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret tags" }); 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("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
@@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
})) }))
); );
if (!newSecretReferences.length) return; if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences); const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences);
return secretReferences; return secretReferences;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" }); throw new DatabaseError({ error, name: "UpsertSecretReference" });
@@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretV2)) .select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
@@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -528,8 +528,8 @@ export const reshapeBridgeSecret = (
environment: string, environment: string,
secretPath: string, secretPath: string,
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & { secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value?: string; value: string;
comment?: string; comment: string;
tags?: { tags?: {
id: string; id: string;
slug: string; slug: string;
@@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
secretPath, secretPath,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
secretValue: secret.value, secretValue: secret.value || "",
secretComment: secret.comment, secretComment: secret.comment || "",
version: secret.version, version: secret.version,
type: secret.type, type: secret.type,
_id: secret.id, _id: secret.id,
@@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({
return reshapeBridgeSecret(projectId, environment, secretPath, { return reshapeBridgeSecret(projectId, environment, secretPath, {
...secret[0], ...secret[0],
value: inputSecret.secretValue, value: inputSecret.secretValue,
comment: inputSecret.secretComment comment: inputSecret.secretComment || ""
}); });
}; };
@@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({
}); });
return reshapeBridgeSecret(projectId, environment, secretPath, { return reshapeBridgeSecret(projectId, environment, secretPath, {
...updatedSecret[0], ...updatedSecret[0],
value: inputSecret.secretValue, value: inputSecret.secretValue || "",
comment: inputSecret.secretComment comment: inputSecret.secretComment || ""
}); });
}; };
@@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({
throw new BadRequestError({ message: "Must be user to delete personal secret" }); 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) => const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({ fnSecretBulkDelete({
projectId, projectId,
@@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({
...deletedSecret[0], ...deletedSecret[0],
value: deletedSecret[0].encryptedValue value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: undefined, : "",
comment: deletedSecret[0].encryptedComment comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: undefined : ""
}); });
}; };
@@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
includeImports, includeImports,
recursive, recursive,
tagSlugs = [],
expandSecretReferences: shouldExpandSecretReferences expandSecretReferences: shouldExpandSecretReferences
}: TGetSecretsDTO) => { }: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission( 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({ const expandSecretReferences = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
@@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (shouldExpandSecretReferences) { if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath); const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) { for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) { if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce( const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
@@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({
await expandSecretReferences(secretsGroupByKey); await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => { secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign // 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) { if (!includeImports) {
return { return {
secrets: decryptedSecrets secrets: filteredSecrets
}; };
} }
@@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
expandSecretReferences, expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
}); });
return { return {
secrets: decryptedSecrets, secrets: filteredSecrets,
imports: importedSecrets imports: importedSecrets
}; };
}; };
@@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL, secretDAL,
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
}); });
@@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j]; const importedSecret = importedSecrets[i].secrets[j];
if (secretName === importedSecret.key) { if (secretName === importedSecret.key) {
return reshapeBridgeSecret( return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
projectId, ...importedSecret,
importedSecrets[i].environment, value: importedSecret.secretValue || "",
importedSecrets[i].secretPath, comment: importedSecret.secretComment || ""
importedSecret });
);
} }
} }
} }
@@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: undefined; : "";
if (shouldExpandSecretReferences && secretValue) { if (shouldExpandSecretReferences && secretValue) {
const secretReferenceExpandedRecord = { const secretReferenceExpandedRecord = {
[secret.key]: { value: secretValue } [secret.key]: { value: secretValue }
@@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({
value: secretValue, value: secretValue,
comment: secret.encryptedComment comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: undefined : ""
}); });
}; };
@@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({
return newSecrets.map((el) => return newSecrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, { reshapeBridgeSecret(projectId, environment, secretPath, {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({
return secrets.map((el) => return secrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, { reshapeBridgeSecret(projectId, environment, secretPath, {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({
return secretsDeleted.map((el) => return secretsDeleted.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, { reshapeBridgeSecret(projectId, environment, secretPath, {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({
return secretVersions.map((el) => return secretVersions.map((el) =>
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretDTO = { export type TGetASecretDTO = {
+9 -13
View File
@@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
.where({ [`${TableName.Secret}Id` as const]: secretId }) .where({ [`${TableName.Secret}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({ return tags.map((el) => ({
id: el.tagId, id: el.tagId,
color: el.tagColor, color: el.tagColor,
slug: el.tagSlug, slug: el.tagSlug,
name: el.tagName name: el.tagSlug
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret tags" }); 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("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.Secret)) .select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
@@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
+1 -2
View File
@@ -370,7 +370,6 @@ export const decryptSecretRaw = (
id: string; id: string;
slug: string; slug: string;
color?: string | null; color?: string | null;
name: string;
}[]; }[];
}, },
key: string key: string
@@ -412,7 +411,7 @@ export const decryptSecretRaw = (
_id: secret.id, _id: secret.id,
id: secret.id, id: secret.id,
user: secret.userId, user: secret.userId,
tags: secret.tags, tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays, secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote, secretReminderNote: secret.secretReminderNote,
+11 -8
View File
@@ -73,12 +73,12 @@ type TSecretQueueFactoryDep = {
secretVersionTagDAL: TSecretVersionTagDALFactory; secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory; secretV2BridgeDAL: TSecretV2BridgeDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">; secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">; snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">; snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
}; };
export type TGetSecrets = { export type TGetSecrets = {
@@ -728,7 +728,10 @@ export const secretQueueFactory = ({
isSynced: response?.isSynced ?? true isSynced: response?.isSynced ?? true
}); });
} catch (err) { } catch (err) {
logger.info("Secret integration sync error: %o", err); logger.error(
err,
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
);
const message = const message =
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
@@ -828,7 +831,7 @@ export const secretQueueFactory = ({
secretId: string; secretId: string;
references: { environment: string; secretPath: string; secretKey: string }[]; references: { environment: string; secretPath: string; secretKey: string }[];
}[] = []; }[] = [];
await secretV2BridgeDAL.insertMany( await secretV2BridgeDAL.batchInsert(
projectV1Secrets.map((el) => { projectV1Secrets.map((el) => {
const key = decryptSymmetric128BitHexKeyUTF8({ const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext, ciphertext: el.secretKeyCiphertext,
@@ -1004,14 +1007,14 @@ export const secretQueueFactory = ({
const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById); const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById);
if (projectV3SecretVersions.length) { if (projectV3SecretVersions.length) {
await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx); await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx);
} }
if (projectV3SecretVersionTags.length) { if (projectV3SecretVersionTags.length) {
await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx); await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx);
} }
if (projectV3SnapshotSecrets.length) { if (projectV3SnapshotSecrets.length) {
await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx); await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx);
} }
await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx); await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx);
} }
+11 -5
View File
@@ -964,7 +964,8 @@ export const secretServiceFactory = ({
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive recursive,
tagSlugs = []
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -978,7 +979,8 @@ export const secretServiceFactory = ({
path, path,
recursive, recursive,
actorAuthMethod, actorAuthMethod,
includeImports includeImports,
tagSlugs
}); });
return { secrets, imports }; return { secrets, imports };
} }
@@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
}); });
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); 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 processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) => const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw( decryptSecretRaw(
@@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
}; };
// expand secrets // expand secrets
await batchSecretsExpand(decryptedSecrets); await batchSecretsExpand(filteredSecrets);
// expand imports by batch // expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets))); await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
} }
return { return {
secrets: decryptedSecrets, secrets: filteredSecrets,
imports: processedImports imports: processedImports
}; };
}; };
@@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({
type, type,
secretName secretName
}); });
return secret; return secret;
} }
@@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({
return { return {
...updatedSecret[0], ...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 }))
}; };
}; };
@@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretRawDTO = { export type TGetASecretRawDTO = {
+4
View File
@@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("secretPath", request.SecretPath) SetQueryParam("secretPath", request.SecretPath)
if request.TagSlugs != "" {
req.SetQueryParam("tagSlugs", request.TagSlugs)
}
if request.IncludeImport { if request.IncludeImport {
req.SetQueryParam("include_imports", "true") req.SetQueryParam("include_imports", "true")
} }
+1
View File
@@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct {
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"` IncludeImport bool `json:"include_imports"`
Recursive bool `json:"recursive"` Recursive bool `json:"recursive"`
TagSlugs string `json:"tagSlugs,omitempty"`
} }
type GetRawSecretsV3Response struct { type GetRawSecretsV3Response struct {
+1 -1
View File
@@ -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) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath 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 { if err != nil {
return nil, err return nil, err
} }
+12 -5
View File
@@ -14,6 +14,7 @@ import (
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2"
) )
const ( const (
@@ -188,7 +189,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
case FormatCSV: case FormatCSV:
return formatAsCSV(envs), nil return formatAsCSV(envs), nil
case FormatYaml: case FormatYaml:
return formatAsYaml(envs), nil return formatAsYaml(envs)
default: default:
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport}) 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 return dotenv
} }
func formatAsYaml(envs []models.SingleEnvironmentVariable) string { func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) {
var dotenv string m := make(map[string]string)
for _, env := range envs { 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 // Format environment variables as a JSON file
+79
View File
@@ -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)
})
}
}
+32 -16
View File
@@ -155,22 +155,24 @@ var secretsSetCmd = &cobra.Command{
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { 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) token, err := util.GetInfisicalToken(cmd)
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") 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 { if err != nil {
util.HandleError(err, "Unable to parse flag") 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") 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{ request := models.GetAllSecretsParameters{
Environment: environmentName, Environment: environmentName,
WorkspaceId: projectId, WorkspaceId: projectId,
@@ -394,6 +401,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "To fetch all secrets") 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 { if shouldExpand {
authParams := models.ExpandSecretsAuthentication{} authParams := models.ExpandSecretsAuthentication{}
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { 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 { if value, ok := secretsMap[secretKeyFromArg]; ok {
requestedSecrets = append(requestedSecrets, value) requestedSecrets = append(requestedSecrets, value)
} else { } else {
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ if !(plainOutput || showOnlyValue) {
Key: secretKeyFromArg, requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
Type: "*not found*", Key: secretKeyFromArg,
Value: "*not found*", Type: "*not found*",
}) Value: "*not found*",
})
}
} }
} }
@@ -688,6 +703,7 @@ func init() {
secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") 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("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("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.AddCommand(secretsGetCmd)
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsSetCmd)
+37 -87
View File
@@ -31,37 +31,52 @@ var AvailableVaults = []VaultBackendType{
} }
var vaultSetCmd = &cobra.Command{ var vaultSetCmd = &cobra.Command{
Example: `infisical vault set file --passphrase <your-passphrase>`, Example: `infisical vault set file`,
Use: "set [file|auto] [flags]", Use: "set [file|auto]",
Short: "Used to configure the vault backends", Short: "Used to configure the vault backends",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
wantedVaultTypeName := args[0]
vaultType := args[0] currentVaultBackend, err := util.GetCurrentVaultBackend()
passphrase, err := cmd.Flags().GetString("passphrase")
if err != nil { if err != nil {
util.HandleError(err, "Unable to get passphrase flag") log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
}
if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" {
setFileVaultPassphrase(passphrase)
return return
} }
util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n") if wantedVaultTypeName == string(currentVaultBackend) {
selectVaultTypeCmd(cmd, args) log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
}, return
} }
var vaultUseCmd = &cobra.Command{ if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
Example: `infisical vault use [file|auto]`, configFile, err := util.GetConfigFile()
Use: "use [file|auto]", if err != nil {
Short: "Used to select the the type of vault backend to store sensitive data securely at rest", log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
DisableFlagsInUseLine: true, return
Args: cobra.MinimumNArgs(1), }
Run: selectVaultTypeCmd,
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 // 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() { func printAvailableVaultBackends() {
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:") fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
for _, vaultType := range AvailableVaults { 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)) 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() { func init() {
vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault")
vaultCmd.AddCommand(vaultSetCmd) vaultCmd.AddCommand(vaultSetCmd)
vaultCmd.AddCommand(vaultUseCmd)
rootCmd.AddCommand(vaultCmd) rootCmd.AddCommand(vaultCmd)
} }
+2 -1
View File
@@ -38,7 +38,8 @@ const (
SERVICE_TOKEN_IDENTIFIER = "service-token" SERVICE_TOKEN_IDENTIFIER = "service-token"
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-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 ( var (
+1 -1
View File
@@ -71,7 +71,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
if strings.Contains(err.Error(), "credentials not found in system keyring") { 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") return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again")
} else { } 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)
} }
} }
+11
View File
@@ -5,6 +5,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"math/rand"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@@ -25,6 +26,8 @@ type DecodedSymmetricEncryptionDetails = struct {
Key []byte Key []byte
} }
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
cipherx, err := base64.StdEncoding.DecodeString(cipher) cipherx, err := base64.StdEncoding.DecodeString(cipher)
if err != nil { if err != nil {
@@ -287,3 +290,11 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
} }
return value, nil 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)
}
+8 -25
View File
@@ -2,8 +2,9 @@ package util
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"github.com/manifoldco/promptui" "github.com/rs/zerolog/log"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
) )
@@ -26,20 +27,13 @@ func SetValueInKeyring(key, value string) error {
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
if err != nil { if err != nil {
log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err))
configFile, _ := GetConfigFile() configFile, _ := GetConfigFile()
if configFile.VaultBackendPassphrase == "" { if configFile.VaultBackendPassphrase == "" {
PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.") encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase
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))
configFile.VaultBackendPassphrase = encodedPassphrase configFile.VaultBackendPassphrase = encodedPassphrase
configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE
err = WriteConfigFile(&configFile) err = WriteConfigFile(&configFile)
if err != nil { if err != nil {
return err return err
@@ -50,6 +44,7 @@ func SetValueInKeyring(key, value string) error {
} }
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) 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 return err
@@ -60,13 +55,7 @@ func GetValueInKeyring(key string) (string, error) {
if err != nil { if err != nil {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again") PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
} }
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
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
} }
@@ -76,11 +65,5 @@ func DeleteValueInKeyring(key string) error {
return err return err
} }
err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
if err != nil {
err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
}
return err
} }
+90 -53
View File
@@ -1,14 +1,15 @@
package util package util
import ( import (
"crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"regexp" "regexp"
"slices"
"strings" "strings"
"unicode" "unicode"
@@ -20,7 +21,7 @@ import (
"github.com/zalando/go-keyring" "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) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") 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, SecretPath: secretPath,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive, Recursive: recursive,
TagSlugs: tagSlugs,
}) })
if err != nil { 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 := resty.New()
httpClient.SetAuthToken(accessToken). httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -86,7 +88,7 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive, Recursive: recursive,
// TagSlugs: tagSlugs, TagSlugs: tagSlugs,
} }
if secretsPath != "" { if secretsPath != "" {
@@ -281,29 +283,36 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} }
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId, 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) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
if err == nil { 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 secretsToReturn = res.Secrets
errorToReturn = err errorToReturn = err
// only attempt to serve cached secrets if no internet connection and if at least one secret cached // only attempt to serve cached secrets if no internet connection and if at least one secret cached
if !isConnected { if !isConnected {
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath) backupEncryptionKey, _ := GetBackupEncryptionKey()
if len(backedSecrets) > 0 { if backupEncryptionKey != nil {
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug") backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey)
secretsToReturn = backedSecrets if len(backedUpSecrets) > 0 {
errorToReturn = err 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 { } else {
if params.InfisicalToken != "" { if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token") 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 != "" { } else if params.UniversalAuthAccessToken != "" {
if params.WorkspaceId == "" { if params.WorkspaceId == "" {
@@ -311,7 +320,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} }
log.Debug().Msg("Trying to fetch secrets using universal auth") 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 errorToReturn = err
secretsToReturn = res.Secrets secretsToReturn = res.Secrets
@@ -476,71 +485,99 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn return secretsToReturn
} }
func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error { func GetBackupEncryptionKey() ([]byte, error) {
var backedUpSecrets []models.BackupSecretKeyRing encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
if err != nil { if err != nil {
if err == keyring.ErrUnsupportedPlatform { 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") 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 { } else if err == keyring.ErrNotFound {
return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) // 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 { func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
}) fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
newBackupSecret := models.BackupSecretKeyRing{ secrets_backup_folder_name := "secrets-backup"
ProjectID: workspace,
Environment: environment,
SecretPath: secretsPath,
Secrets: secrets,
}
backedUpSecrets = append(backedUpSecrets, newBackupSecret)
listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets) _, fullConfigFileDirPath, err := GetFullConfigFilePath()
if err != nil { 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 { 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 return nil
} }
func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) { func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET) 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 != nil {
if err == keyring.ErrUnsupportedPlatform { return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", 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 {
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)
}
} }
var backedUpSecrets []models.BackupSecretKeyRing fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets) 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 { 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 { var encryptedBackUpSecrets models.SymmetricEncryptionResult
if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath { err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets)
return backupSecret.Secrets, nil 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 { 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" secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath() _, fullConfigFileDirPath, err := GetFullConfigFilePath()
@@ -549,8 +586,8 @@ func DeleteBackupSecrets() error {
} }
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET) DeleteValueInKeyring(INFISICAL_BACKUP_SECRET)
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
return os.RemoveAll(fullPathToSecretsBackupFolder) return os.RemoveAll(fullPathToSecretsBackupFolder)
} }
@@ -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 │ │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├───────────────┼──────────────┼─────────────┤ ├───────────────┼──────────────┼─────────────┤
+3 -4
View File
@@ -7,7 +7,6 @@ import (
"github.com/bradleyjkemp/cupaloy/v2" "github.com/bradleyjkemp/cupaloy/v2"
) )
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) { func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") 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 // explicitly called here because it should happen directly after successful secretsGetAll
testUserAuth_SecretsGetAllWithoutConnection(t) // testUserAuth_SecretsGetAllWithoutConnection(t)
} }
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.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 // set it to a URL that will always be unreachable
newConfigFile.LoggedInUserDomain = "http://localhost:4999" newConfigFile.LoggedInUserDomain = "http://localhost:4999"
util.WriteConfigFile(&newConfigFile) util.WriteConfigFile(&newConfigFile)
// restore config file // restore config file
defer util.WriteConfigFile(&originalConfigFile) defer util.WriteConfigFile(&originalConfigFile)
@@ -121,4 +120,4 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("snapshot failed: %v", err) t.Fatalf("snapshot failed: %v", err)
} }
} }
+34 -3
View File
@@ -1,14 +1,16 @@
--- ---
title: "Spenging Money" title: "Spending Money"
sidebarTitle: "Spending Money" sidebarTitle: "Spending Money"
description: "The guide to spending money at Infisical." description: "The guide to spending money at Infisical."
--- ---
Fairly frequently, you might run into situations when you need to spend company money. 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.** <Note>
Please spend money in a way that you think is in the best interest of the company.
</Note>
## 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**. 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. 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 ## Brex
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul. We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
@@ -0,0 +1,4 @@
---
title: "Create Lease"
openapi: "POST /api/v1/dynamic-secrets/leases"
---
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/dynamic-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Delete Lease"
openapi: "DELETE /api/v1/dynamic-secrets/leases/{leaseId}"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/dynamic-secrets/{name}"
---
@@ -0,0 +1,4 @@
---
title: "Get Lease"
openapi: "GET /api/v1/dynamic-secrets/leases/{leaseId}"
---
@@ -0,0 +1,4 @@
---
title: "Get"
openapi: "GET /api/v1/dynamic-secrets/{name}"
---
@@ -0,0 +1,4 @@
---
title: "List Leases"
openapi: "GET /api/v1/dynamic-secrets/{name}/leases"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/dynamic-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Renew Lease"
openapi: "POST /api/v1/dynamic-secrets/leases/{leaseId}/renew"
---
@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/dynamic-secrets/{name}"
---
+1 -4
View File
@@ -30,8 +30,5 @@ description: "Change the vault type in Infisical"
## Description ## 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 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.
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file --passphrase <your-passphrase>` CLI command to specify your password once.</Tip>
+22 -18
View File
@@ -4,10 +4,10 @@ description: "Learn how to configure Google SAML for Infisical SSO."
--- ---
<Info> <Info>
Google SAML SSO feature is a paid feature. 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,
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact [email protected] to purchase an enterprise license
then you should contact [email protected] to purchase an enterprise license to use it. to use it.
</Info> </Info>
<Steps> <Steps>
@@ -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**. 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. 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) ![Google SAML initial configuration](../../../images/sso/google-saml/init-config.png)
</Step> </Step>
<Step title="Create a SAML application in Google"> <Step title="Create a SAML application in Google">
2.1. In your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps and 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) ![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) ![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. Also, check the **Signed response** checkbox.
![Google SAML app config 2](../../../images/sso/google-saml/custom-saml-app-config-2.png) ![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: 2.6. In the **Attribute mapping** tab, configure the following map:
- **First name** -> **firstName** - **First name** -> **firstName**
@@ -49,7 +50,7 @@ description: "Learn how to configure Google SAML for Infisical SSO."
- **Primary email** -> **email** - **Primary email** -> **email**
![Google SAML attribute mapping](../../../images/sso/google-saml/attribute-mapping.png) ![Google SAML attribute mapping](../../../images/sso/google-saml/attribute-mapping.png)
Click **Finish**. Click **Finish**.
</Step> </Step>
<Step title="Assign users in Google Workspace to the application"> <Step title="Assign users in Google Workspace to the application">
@@ -57,11 +58,11 @@ description: "Learn how to configure Google SAML for Infisical SSO."
and press on **User access**. and press on **User access**.
![Google SAML user access](../../../images/sso/google-saml/user-access.png) ![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**. 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). 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) ![Google SAML user access assignment](../../../images/sso/google-saml/user-access-assign.png)
</Step> </Step>
<Step title="Enable SAML SSO in Infisical"> <Step title="Enable SAML SSO in Infisical">
@@ -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; 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. Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
<Warning> <Warning>
We recommend ensuring that your account is provisioned the application in Google We recommend ensuring that your account is provisioned the application in Google
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
</Step> </Step>
</Steps> </Steps>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to If you're configuring SAML SSO on a self-hosted instance of Infisical, make
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: 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
- `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`. can be a random 32-byte base64 string generated with `openssl rand -base64
- `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) 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)
</Note> </Note>
References: 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).
- 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).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 219 KiB

+5 -1
View File
@@ -1,5 +1,5 @@
--- ---
title: "Kubernetes" title: "Kubernetes Operator"
description: "How to use Infisical to inject secrets into Kubernetes clusters." 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. It uses an `InfisicalSecret` resource to specify authentication and storage methods.
The operator continuously updates secrets and can also reload dependent deployments automatically. The operator continuously updates secrets and can also reload dependent deployments automatically.
<Note>
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/).
</Note>
## Install Operator ## Install Operator
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
+15
View File
@@ -618,6 +618,21 @@
"api-reference/endpoints/secrets/detach-tags" "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", "group": "Secret Imports",
"pages": [ "pages": [
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck } from "@fortawesome/free-solid-svg-icons"; import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -87,7 +88,13 @@ type Props = {
}; };
const createTagSchema = z.object({ 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() color: z.string().trim()
}); });
@@ -110,7 +117,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(createTagSchema) resolver: zodResolver(createTagSchema)
}); });
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
@@ -123,13 +130,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
if (!isOpen) reset(); if (!isOpen) reset();
}, [isOpen]); }, [isOpen]);
const onFormSubmit = async ({ name, color }: FormData) => { const onFormSubmit = async ({ slug, color }: FormData) => {
try { try {
await createWsTag({ await createWsTag({
workspaceID: workspaceId, workspaceID: workspaceId,
tagName: name,
tagColor: color, tagColor: color,
tagSlug: name.replace(" ", "_") tagSlug: slug
}); });
onToggle(false); onToggle(false);
reset(); reset();
@@ -155,11 +161,11 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
name="name" name="slug"
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Tag Slug" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your tag name" /> <Input {...field} placeholder="Type your tag slug" />
</FormControl> </FormControl>
)} )}
/> />
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
@@ -16,6 +16,7 @@ type Props = {
subTitle?: string; subTitle?: string;
onDeleteApproved: () => Promise<void>; onDeleteApproved: () => Promise<void>;
buttonText?: string; buttonText?: string;
children?: ReactNode;
}; };
export const DeleteActionModal = ({ export const DeleteActionModal = ({
@@ -26,7 +27,8 @@ export const DeleteActionModal = ({
onDeleteApproved, onDeleteApproved,
title, title,
subTitle = "This action is irreversible.", subTitle = "This action is irreversible.",
buttonText = "Delete" buttonText = "Delete",
children
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [inputData, setInputData] = useState(""); const [inputData, setInputData] = useState("");
const [isLoading, setIsLoading] = useToggle(); const [isLoading, setIsLoading] = useToggle();
@@ -97,6 +99,7 @@ export const DeleteActionModal = ({
placeholder={`Type ${deleteKey} here`} placeholder={`Type ${deleteKey} here`}
/> />
</FormControl> </FormControl>
{children}
</form> </form>
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -110,8 +110,15 @@ export const useCreateIntegration = () => {
export const useDeleteIntegration = () => { export const useDeleteIntegration = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({ return useMutation<
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`), {},
{},
{ id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean }
>({
mutationFn: ({ id, shouldDeleteIntegrationSecrets }) =>
apiRequest.delete(
`/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}`
),
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
@@ -5,6 +5,5 @@ export type TRateLimit = {
authRateLimit: number; authRateLimit: number;
inviteUserRateLimit: number; inviteUserRateLimit: number;
mfaRateLimit: number; mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number; publicEndpointLimit: number;
}; };
+1 -2
View File
@@ -28,11 +28,10 @@ export const useCreateWsTag = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<WsTag, {}, CreateTagDTO>({ return useMutation<WsTag, {}, CreateTagDTO>({
mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => { mutationFn: async ({ workspaceID, tagColor, tagSlug }) => {
const { data } = await apiRequest.post<{ workspaceTag: WsTag }>( const { data } = await apiRequest.post<{ workspaceTag: WsTag }>(
`/api/v1/workspace/${workspaceID}/tags`, `/api/v1/workspace/${workspaceID}/tags`,
{ {
name: tagName,
color: tagColor || "", color: tagColor || "",
slug: tagSlug slug: tagSlug
} }
-2
View File
@@ -2,7 +2,6 @@ export type UserWsTags = WsTag[];
export type WsTag = { export type WsTag = {
id: string; id: string;
name: string;
slug: string; slug: string;
color?: string; color?: string;
projectId: string; projectId: string;
@@ -16,7 +15,6 @@ export type WorkspaceTag = { id: string; name: string; slug: string };
export type CreateTagDTO = { export type CreateTagDTO = {
workspaceID: string; workspaceID: string;
tagSlug: string; tagSlug: string;
tagName: string;
tagColor: string; tagColor: string;
}; };
@@ -277,18 +277,14 @@ export default function AWSParameterStoreCreateIntegrationPage() {
<div className="mt-2 ml-1"> <div className="mt-2 ml-1">
<Switch <Switch
id="delete-aws" id="delete-aws"
onCheckedChange={() => setShouldDisableDelete(!shouldDisableDelete)} onCheckedChange={setShouldDisableDelete}
isChecked={shouldDisableDelete} isChecked={shouldDisableDelete}
> >
Disable deleting secrets in AWS Parameter Store Disable deleting secrets in AWS Parameter Store
</Switch> </Switch>
</div> </div>
<div className="mt-4 ml-1"> <div className="mt-4 ml-1">
<Switch <Switch id="tag-aws" onCheckedChange={setShouldTag} isChecked={shouldTag}>
id="tag-aws"
onCheckedChange={() => setShouldTag(!shouldTag)}
isChecked={shouldTag}
>
Tag in AWS Parameter Store Tag in AWS Parameter Store
</Switch> </Switch>
</div> </div>
@@ -106,9 +106,13 @@ export const IntegrationsPage = withProjectPermission(
handleProviderIntegration(provider); handleProviderIntegration(provider);
}; };
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => { const handleIntegrationDelete = async (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
cb: () => void
) => {
try { try {
await deleteIntegration({ id: integrationId, workspaceId }); await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
if (cb) cb(); if (cb) cb();
createNotification({ createNotification({
type: "success", type: "success",
@@ -152,7 +156,7 @@ export const IntegrationsPage = withProjectPermission(
isLoading={isIntegrationLoading} isLoading={isIntegrationLoading}
integrations={integrations} integrations={integrations}
environments={environments} environments={environments}
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)} onIntegrationDelete={handleIntegrationDelete}
workspaceId={workspaceId} workspaceId={workspaceId}
/> />
<CloudIntegrationSection <CloudIntegrationSection
@@ -7,6 +7,7 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
Checkbox,
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
FormLabel, FormLabel,
@@ -16,7 +17,7 @@ import {
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { TIntegration } from "@app/hooks/api/types"; import { TIntegration } from "@app/hooks/api/types";
@@ -25,7 +26,11 @@ type Props = {
environments: Array<{ name: string; slug: string; id: string }>; environments: Array<{ name: string; slug: string; id: string }>;
integrations?: TIntegration[]; integrations?: TIntegration[];
isLoading?: boolean; isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void; onIntegrationDelete: (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
cb: () => void
) => Promise<void>;
workspaceId: string; workspaceId: string;
}; };
@@ -37,10 +42,12 @@ export const IntegrationsSection = ({
workspaceId workspaceId
}: Props) => { }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation" "deleteConfirmation",
"deleteSecretsConfirmation"
] as const); ] as const);
const { mutate: syncIntegration } = useSyncIntegration(); const { mutate: syncIntegration } = useSyncIntegration();
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
return ( return (
<div className="mb-8"> <div className="mb-8">
@@ -249,7 +256,10 @@ export const IntegrationsSection = ({
<div className="flex items-end opacity-80 duration-200 hover:opacity-100"> <div className="flex items-end opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration"> <Tooltip content="Remove Integration">
<IconButton <IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)} onClick={() => {
setShouldDeleteSecrets.off();
handlePopUpOpen("deleteConfirmation", integration);
}}
ariaLabel="delete" ariaLabel="delete"
isDisabled={!isAllowed} isDisabled={!isAllowed}
colorSchema="danger" colorSchema="danger"
@@ -281,11 +291,49 @@ export const IntegrationsSection = ({
(popUp?.deleteConfirmation?.data as TIntegration)?.integration || (popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
"" ""
} }
onDeleteApproved={async () => onDeleteApproved={async () => {
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () => if (shouldDeleteSecrets) {
handlePopUpClose("deleteConfirmation") handlePopUpOpen("deleteSecretsConfirmation");
) return;
} }
await onIntegrationDelete(
(popUp?.deleteConfirmation.data as TIntegration).id,
false,
() => handlePopUpClose("deleteConfirmation")
);
}}
>
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
<div className="mt-4">
<Checkbox
id="delete-integration-secrets"
checkIndicatorBg="text-white"
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
>
Delete previously synced secrets from the destination
</Checkbox>
</div>
)}
</DeleteActionModal>
<DeleteActionModal
isOpen={popUp.deleteSecretsConfirmation.isOpen}
title={`Are you sure you also want to delete secrets on ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration
}?`}
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await onIntegrationDelete(
(popUp?.deleteConfirmation.data as TIntegration).id,
true,
() => {
handlePopUpClose("deleteSecretsConfirmation");
handlePopUpClose("deleteConfirmation");
}
);
}}
/> />
</div> </div>
); );
@@ -97,7 +97,7 @@ export const SecretApprovalRequestChangeItem = ({
</Td> </Td>
<Td>{secretVersion?.secretComment}</Td> <Td>{secretVersion?.secretComment}</Td>
<Td> <Td>
{secretVersion?.tags?.map(({ name, id: tagId, color }) => ( {secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${secretVersion.id}-${tagId}`} key={`${secretVersion.id}-${tagId}`}
@@ -106,7 +106,7 @@ export const SecretApprovalRequestChangeItem = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
</Td> </Td>
@@ -119,7 +119,7 @@ export const SecretApprovalRequestChangeItem = ({
</Td> </Td>
<Td>{newVersion?.secretComment}</Td> <Td>{newVersion?.secretComment}</Td>
<Td> <Td>
{newVersion?.tags?.map(({ name, id: tagId, color }) => ( {newVersion?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${newVersion.id}-${tagId}`} key={`${newVersion.id}-${tagId}`}
@@ -128,7 +128,7 @@ export const SecretApprovalRequestChangeItem = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
</Td> </Td>
@@ -157,7 +157,7 @@ export const SecretApprovalRequestChangeItem = ({
</Td> </Td>
<Td> <Td>
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map( {(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
({ name, id: tagId, color }) => ( ({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${ key={`${
@@ -168,7 +168,7 @@ export const SecretApprovalRequestChangeItem = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
) )
)} )}
@@ -327,7 +327,7 @@ export const ActionBar = ({
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="rounded-l-none"> <DropdownSubMenuContent className="rounded-l-none">
<DropdownMenuLabel>Apply tags to filter secrets</DropdownMenuLabel> <DropdownMenuLabel>Apply tags to filter secrets</DropdownMenuLabel>
{tags.map(({ id, name, color }) => ( {tags.map(({ id, slug, color }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={(evt) => { onClick={(evt) => {
evt.preventDefault(); evt.preventDefault();
@@ -342,7 +342,7 @@ export const ActionBar = ({
className="mr-2 h-2 w-2 rounded-full" className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }} style={{ background: color || "#bec2c8" }}
/> />
{name} {slug}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
@@ -254,7 +254,7 @@ export const SecretDetailSidebar = ({
)} )}
<FormControl label="Tags" className=""> <FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2"> <div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, name, id }) => ( {fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={formId} key={formId}
@@ -271,7 +271,7 @@ export const SecretDetailSidebar = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }} style={{ backgroundColor: tagColor || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
<DropdownMenu> <DropdownMenu>
@@ -296,7 +296,7 @@ export const SecretDetailSidebar = ({
<DropdownMenuContent align="end" className="z-[100]"> <DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel> <DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => { {tags.map((tag) => {
const { id: tagId, name, color } = tag; const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId]; const isSelected = selectedTagsGroupById?.[tagId];
return ( return (
@@ -311,7 +311,7 @@ export const SecretDetailSidebar = ({
className="mr-2 h-2 w-2 rounded-full" className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }} style={{ background: color || "#bec2c8" }}
/> />
{name} {slug}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
); );
@@ -336,7 +336,7 @@ export const SecretItem = memo(
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel> <DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => { {tags.map((tag) => {
const { id: tagId, name, color } = tag; const { id: tagId, slug, color } = tag;
const isTagSelected = selectedTagsGroupById?.[tagId]; const isTagSelected = selectedTagsGroupById?.[tagId];
return ( return (
@@ -358,7 +358,7 @@ export const SecretItem = memo(
className="mr-2 h-2 w-2 rounded-full" className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }} style={{ background: color || "#bec2c8" }}
/> />
{name} {slug}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
); );
@@ -49,7 +49,6 @@ export const formSchema = z.object({
tags: z tags: z
.object({ .object({
id: z.string(), id: z.string(),
name: z.string(),
slug: z.string(), slug: z.string(),
tagColor: z.string().optional() tagColor: z.string().optional()
}) })

Some files were not shown because too many files have changed in this diff Show More