mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 08:27:53 +00:00
Fix merge conflicts
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+49
-6
@@ -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",
|
||||||
@@ -7814,19 +7815,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": {
|
||||||
@@ -17404,6 +17431,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",
|
||||||
|
|||||||
@@ -122,6 +122,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",
|
||||||
|
|||||||
@@ -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: ");
|
||||||
|
|||||||
Vendored
+2
@@ -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";
|
||||||
@@ -92,6 +93,7 @@ declare module "fastify" {
|
|||||||
id: string;
|
id: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
rateLimits: RateLimitConfiguration;
|
||||||
// passport data
|
// passport data
|
||||||
passportUser: {
|
passportUser: {
|
||||||
isUserCompleted: string;
|
isUserCompleted: string;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (!hasVersionColumn) {
|
if (!hasVersionColumn) {
|
||||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||||
t.integer("version").nullable();
|
t.integer("version").nullable();
|
||||||
// t.dropUnique(["caId"]);
|
t.dropUnique(["caId"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
|
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -15,6 +15,8 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.PkiCollection);
|
||||||
|
|
||||||
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
|
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
|
||||||
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
|
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
@@ -28,6 +30,8 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.PkiCollectionItem);
|
||||||
|
|
||||||
if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
|
if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
|
||||||
await knex.schema.createTable(TableName.PkiAlert, (t) => {
|
await knex.schema.createTable(TableName.PkiAlert, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
@@ -43,7 +47,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.PkiCollection);
|
|
||||||
await createOnUpdateTrigger(knex, TableName.PkiAlert);
|
await createOnUpdateTrigger(knex, TableName.PkiAlert);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
@@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createAuditLog = async (data: TCreateAuditLogDTO) => {
|
const createAuditLog = async (data: TCreateAuditLogDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (appCfg.DISABLE_AUDIT_LOG_GENERATION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// add all cases in which project id or org id cannot be added
|
// add all cases in which project id or org id cannot be added
|
||||||
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
||||||
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
||||||
|
|||||||
@@ -356,6 +356,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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn";
|
|||||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
|||||||
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
||||||
@@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
secretApprovalPolicyApproverDAL,
|
secretApprovalPolicyApproverDAL,
|
||||||
projectEnvDAL
|
projectEnvDAL,
|
||||||
|
licenseService
|
||||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||||
const createSecretApprovalPolicy = async ({
|
const createSecretApprovalPolicy = async ({
|
||||||
name,
|
name,
|
||||||
@@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
ProjectPermissionSub.SecretApproval
|
ProjectPermissionSub.SecretApproval
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.secretApproval) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to create secret approval policy due to plan restriction. Upgrade plan to create secret approval policy."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||||
|
|
||||||
@@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.secretApproval) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
const doc = await secretApprovalPolicyDAL.updateById(
|
const doc = await secretApprovalPolicyDAL.updateById(
|
||||||
secretApprovalPolicy.id,
|
secretApprovalPolicy.id,
|
||||||
@@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
ProjectPermissionSub.SecretApproval
|
ProjectPermissionSub.SecretApproval
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.secretApproval) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await secretApprovalPolicyDAL.deleteById(secretPolicyId);
|
await secretApprovalPolicyDAL.deleteById(secretPolicyId);
|
||||||
return sapPolicy;
|
return sapPolicy;
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-16
@@ -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
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
|
|||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||||
@@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||||
@@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
kmsService,
|
kmsService,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL
|
secretVersionTagV2BridgeDAL,
|
||||||
|
licenseService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
@@ -224,12 +227,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 +238,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 +251,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
|
||||||
}));
|
}));
|
||||||
@@ -297,6 +298,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.secretApproval) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to review secret approval request due to plan restriction. Upgrade plan to review secret approval request."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
@@ -347,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.secretApproval) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to update secret approval request due to plan restriction. Upgrade plan to update secret approval request."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
@@ -388,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.secretApproval) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to merge secret approval request due to plan restriction. Upgrade plan to merge secret approval request."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { policy, folderId, projectId } = secretApprovalRequest;
|
const { policy, folderId, projectId } = secretApprovalRequest;
|
||||||
const { hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock";
|
|||||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||||
|
|
||||||
// all the key prefixes used must be set here to avoid conflict
|
// all the key prefixes used must be set here to avoid conflict
|
||||||
export enum KeyStorePrefixes {
|
export const KeyStorePrefixes = {
|
||||||
SecretReplication = "secret-replication-import-lock",
|
SecretReplication: "secret-replication-import-lock",
|
||||||
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock",
|
KmsProjectDataKeyCreation: "kms-project-data-key-creation-lock",
|
||||||
KmsProjectKeyCreation = "kms-project-key-creation-lock",
|
KmsProjectKeyCreation: "kms-project-key-creation-lock",
|
||||||
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-",
|
WaitUntilReadyKmsProjectDataKeyCreation: "wait-until-ready-kms-project-data-key-creation-",
|
||||||
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-",
|
WaitUntilReadyKmsProjectKeyCreation: "wait-until-ready-kms-project-key-creation-",
|
||||||
KmsOrgKeyCreation = "kms-org-key-creation-lock",
|
KmsOrgKeyCreation: "kms-org-key-creation-lock",
|
||||||
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock",
|
KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock",
|
||||||
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-",
|
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
||||||
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-"
|
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
||||||
}
|
|
||||||
|
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
|
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
|
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KeyStoreTtls = {
|
||||||
|
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10
|
||||||
|
};
|
||||||
|
|
||||||
type TWaitTillReady = {
|
type TWaitTillReady = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
|
|
||||||
const setItemWithExpiry = async (
|
const setItemWithExpiry = async (
|
||||||
key: string,
|
key: string,
|
||||||
exp: number | string,
|
expiryInSeconds: number | string,
|
||||||
value: string | number | Buffer,
|
value: string | number | Buffer,
|
||||||
prefix?: string
|
prefix?: string
|
||||||
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp);
|
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds);
|
||||||
|
|
||||||
const deleteItem = async (key: string) => redis.del(key);
|
const deleteItem = async (key: string) => redis.del(key);
|
||||||
|
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ const envSchema = z
|
|||||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional())
|
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||||
|
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
|
||||||
})
|
})
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
export const getLastMidnightDateISO = (last = 1) =>
|
export const getLastMidnightDateISO = (last = 1) =>
|
||||||
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
|
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
|
||||||
|
|
||||||
|
export const getTimeDifferenceInSeconds = (lhsTimestamp: string, rhsTimestamp: string) => {
|
||||||
|
const lhs = new Date(lhsTimestamp);
|
||||||
|
const rhs = new Date(rhsTimestamp);
|
||||||
|
return Math.floor((Number(lhs) - Number(rhs)) / 1000);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 [];
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export enum QueueName {
|
|||||||
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
||||||
AuditLogPrune = "audit-log-prune",
|
AuditLogPrune = "audit-log-prune",
|
||||||
DailyResourceCleanUp = "daily-resource-cleanup",
|
DailyResourceCleanUp = "daily-resource-cleanup",
|
||||||
|
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||||
TelemetryInstanceStats = "telemtry-self-hosted-stats",
|
TelemetryInstanceStats = "telemtry-self-hosted-stats",
|
||||||
IntegrationSync = "sync-integrations",
|
IntegrationSync = "sync-integrations",
|
||||||
SecretWebhook = "secret-webhook",
|
SecretWebhook = "secret-webhook",
|
||||||
@@ -36,6 +37,7 @@ export enum QueueJobs {
|
|||||||
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
||||||
AuditLogPrune = "audit-log-prune-job",
|
AuditLogPrune = "audit-log-prune-job",
|
||||||
DailyResourceCleanUp = "daily-resource-cleanup-job",
|
DailyResourceCleanUp = "daily-resource-cleanup-job",
|
||||||
|
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||||
SecWebhook = "secret-webhook-trigger",
|
SecWebhook = "secret-webhook-trigger",
|
||||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||||
IntegrationSync = "secret-integration-pull",
|
IntegrationSync = "secret-integration-pull",
|
||||||
@@ -71,6 +73,10 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.DailyResourceCleanUp;
|
name: QueueJobs.DailyResourceCleanUp;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
};
|
};
|
||||||
|
[QueueName.DailyExpiringPkiItemAlert]: {
|
||||||
|
name: QueueJobs.DailyExpiringPkiItemAlert;
|
||||||
|
payload: undefined;
|
||||||
|
};
|
||||||
[QueueName.AuditLogPrune]: {
|
[QueueName.AuditLogPrune]: {
|
||||||
name: QueueJobs.AuditLogPrune;
|
name: QueueJobs.AuditLogPrune;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -133,6 +133,7 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
|||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||||
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
|
import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue";
|
||||||
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
|
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
|
||||||
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||||
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||||
@@ -191,6 +192,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
|
|||||||
import { injectAuditLogInfo } from "../plugins/audit-log";
|
import { 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";
|
||||||
@@ -362,7 +364,8 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
secretApprovalPolicyDAL
|
secretApprovalPolicyDAL,
|
||||||
|
licenseService
|
||||||
});
|
});
|
||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||||
|
|
||||||
@@ -749,6 +752,7 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
const secretQueueService = secretQueueFactory({
|
const secretQueueService = secretQueueFactory({
|
||||||
|
keyStore,
|
||||||
queueService,
|
queueService,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
@@ -834,7 +838,8 @@ export const registerRoutes = async (
|
|||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
userDAL
|
userDAL,
|
||||||
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
@@ -935,8 +940,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,
|
||||||
@@ -1063,13 +1075,18 @@ export const registerRoutes = async (
|
|||||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
pkiAlertService,
|
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretFolderVersionDAL: folderVersionDAL,
|
secretFolderVersionDAL: folderVersionDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL: secretVersionV2BridgeDAL
|
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||||
|
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
||||||
|
queueService,
|
||||||
|
pkiAlertService
|
||||||
});
|
});
|
||||||
|
|
||||||
const oidcService = oidcConfigServiceFactory({
|
const oidcService = oidcConfigServiceFactory({
|
||||||
@@ -1096,6 +1113,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
await telemetryQueue.startTelemetryCheck();
|
await telemetryQueue.startTelemetryCheck();
|
||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
await kmsService.startService();
|
await kmsService.startService();
|
||||||
|
|
||||||
// inject all services
|
// inject all services
|
||||||
@@ -1185,6 +1203,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: {
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
await server.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||||
await server.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||||
},
|
},
|
||||||
{ prefix: "/pki" }
|
{ prefix: "/pki" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
|||||||
pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
|
pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
|
||||||
name: z.string().trim().describe(ALERTS.CREATE.name),
|
name: z.string().trim().describe(ALERTS.CREATE.name),
|
||||||
alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
|
alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
|
||||||
emails: z.array(z.string().trim().email({ message: "Invalid email address" })).describe(ALERTS.CREATE.emails)
|
emails: z
|
||||||
|
.array(z.string().trim().email({ message: "Invalid email address" }))
|
||||||
|
.min(1, { message: "You must specify at least 1 email" })
|
||||||
|
.max(5, { message: "You can specify a maximum of 5 emails" })
|
||||||
|
.describe(ALERTS.CREATE.emails)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: PkiAlertsSchema
|
200: PkiAlertsSchema
|
||||||
@@ -114,6 +118,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
|||||||
pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
|
pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
|
||||||
emails: z
|
emails: z
|
||||||
.array(z.string().trim().email({ message: "Invalid email address" }))
|
.array(z.string().trim().email({ message: "Invalid email address" }))
|
||||||
|
.min(1, { message: "You must specify at least 1 email" })
|
||||||
|
.max(5, { message: "You can specify a maximum of 5 emails" })
|
||||||
.optional()
|
.optional()
|
||||||
.describe(ALERTS.UPDATE.emails)
|
.describe(ALERTS.UPDATE.emails)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -11,7 +11,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";
|
||||||
@@ -149,7 +149,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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||||
@@ -106,10 +106,10 @@ export const getCaCredentials = async ({
|
|||||||
kmsService
|
kmsService
|
||||||
}: TGetCaCredentialsDTO) => {
|
}: TGetCaCredentialsDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||||
|
|
||||||
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
|
||||||
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
|
if (!caSecret) throw new NotFoundError({ message: "CA secret not found" });
|
||||||
|
|
||||||
const keyId = await getProjectKmsCertificateKeyId({
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: ca.projectId,
|
projectId: ca.projectId,
|
||||||
@@ -158,7 +158,7 @@ export const getCaCertChains = async ({
|
|||||||
kmsService
|
kmsService
|
||||||
}: TGetCaCertChainsDTO) => {
|
}: TGetCaCertChainsDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||||
|
|
||||||
const keyId = await getProjectKmsCertificateKeyId({
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: ca.projectId,
|
projectId: ca.projectId,
|
||||||
@@ -205,7 +205,7 @@ export const getCaCertChain = async ({
|
|||||||
kmsService
|
kmsService
|
||||||
}: TGetCaCertChainDTO) => {
|
}: TGetCaCertChainDTO) => {
|
||||||
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
|
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
|
||||||
if (!caCert) throw new BadRequestError({ message: "CA certificate not found" });
|
if (!caCert) throw new NotFoundError({ message: "CA certificate not found" });
|
||||||
const ca = await certificateAuthorityDAL.findById(caCert.caId);
|
const ca = await certificateAuthorityDAL.findById(caCert.caId);
|
||||||
|
|
||||||
const keyId = await getProjectKmsCertificateKeyId({
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
@@ -249,7 +249,7 @@ export const rebuildCaCrl = async ({
|
|||||||
kmsService
|
kmsService
|
||||||
}: TRebuildCaCrlDTO) => {
|
}: TRebuildCaCrlDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||||
|
|
||||||
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||||
|
|
||||||
|
type TDailyExpiringPkiItemAlertQueueServiceFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDailyExpiringPkiItemAlertQueueServiceFactory = ReturnType<
|
||||||
|
typeof dailyExpiringPkiItemAlertQueueServiceFactory
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const dailyExpiringPkiItemAlertQueueServiceFactory = ({
|
||||||
|
queueService,
|
||||||
|
pkiAlertService
|
||||||
|
}: TDailyExpiringPkiItemAlertQueueServiceFactoryDep) => {
|
||||||
|
queueService.start(QueueName.DailyExpiringPkiItemAlert, async () => {
|
||||||
|
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task started`);
|
||||||
|
await pkiAlertService.sendPkiItemExpiryNotices();
|
||||||
|
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task completed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// we do a repeat cron job in utc timezone at 12 Midnight each day
|
||||||
|
const startSendingAlerts = async () => {
|
||||||
|
// clear previous job
|
||||||
|
await queueService.stopRepeatableJob(
|
||||||
|
QueueName.DailyExpiringPkiItemAlert,
|
||||||
|
QueueJobs.DailyExpiringPkiItemAlert,
|
||||||
|
{ pattern: "0 0 * * *", utc: true },
|
||||||
|
QueueName.DailyExpiringPkiItemAlert // just a job id
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.queue(QueueName.DailyExpiringPkiItemAlert, QueueJobs.DailyExpiringPkiItemAlert, undefined, {
|
||||||
|
delay: 5000,
|
||||||
|
jobId: QueueName.DailyExpiringPkiItemAlert,
|
||||||
|
repeat: { pattern: "0 0 * * *", utc: true }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.listen(QueueName.DailyExpiringPkiItemAlert, "failed", (_, err) => {
|
||||||
|
logger.error(err, `${QueueName.DailyExpiringPkiItemAlert}: Expiring PKI item alert failed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startSendingAlerts
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -12,8 +12,11 @@ import { TPkiAlertDALFactory } from "./pki-alert-dal";
|
|||||||
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
|
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
|
||||||
|
|
||||||
type TPkiAlertServiceFactoryDep = {
|
type TPkiAlertServiceFactoryDep = {
|
||||||
pkiAlertDAL: TPkiAlertDALFactory;
|
pkiAlertDAL: Pick<
|
||||||
pkiCollectionDAL: TPkiCollectionDALFactory;
|
TPkiAlertDALFactory,
|
||||||
|
"create" | "findById" | "updateById" | "deleteById" | "getExpiringPkiCollectionItemsForAlerting"
|
||||||
|
>;
|
||||||
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return parseInt((count as unknown as CountResult).count || "0", 10);
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Count all project certificates" });
|
throw new DatabaseError({ error, name: "Count all PKI collection items" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ import {
|
|||||||
} from "./pki-collection-types";
|
} from "./pki-collection-types";
|
||||||
|
|
||||||
type TPkiCollectionServiceFactoryDep = {
|
type TPkiCollectionServiceFactoryDep = {
|
||||||
pkiCollectionDAL: TPkiCollectionDALFactory; // TODO: Pick
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "create" | "findById" | "updateById" | "deleteById">;
|
||||||
pkiCollectionItemDAL: TPkiCollectionItemDALFactory;
|
pkiCollectionItemDAL: Pick<
|
||||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
TPkiCollectionItemDALFactory,
|
||||||
certificateDAL: TCertificateDALFactory;
|
"findOne" | "create" | "deleteById" | "findPkiCollectionItems" | "countItemsInPkiCollection"
|
||||||
|
>;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ export const pkiCollectionServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Delete,
|
||||||
ProjectPermissionSub.PkiCollections
|
ProjectPermissionSub.PkiCollections
|
||||||
);
|
);
|
||||||
pkiCollection = await pkiCollectionDAL.deleteById(collectionId);
|
pkiCollection = await pkiCollectionDAL.deleteById(collectionId);
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
|||||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
|
||||||
|
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { 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,13 +13,13 @@ 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">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||||
@@ -27,24 +27,24 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
|
|||||||
export const dailyResourceCleanUpQueueServiceFactory = ({
|
export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
pkiAlertService,
|
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
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();
|
||||||
await secretVersionV2DAL.pruneExcessVersions();
|
await secretVersionV2DAL.pruneExcessVersions();
|
||||||
await secretFolderVersionDAL.pruneExcessVersions();
|
await secretFolderVersionDAL.pruneExcessVersions();
|
||||||
await pkiAlertService.sendPkiItemExpiryNotices();
|
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv
|
|||||||
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
||||||
|
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
@@ -73,12 +74,13 @@ 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">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecrets = {
|
export type TGetSecrets = {
|
||||||
@@ -122,7 +124,8 @@ export const secretQueueFactory = ({
|
|||||||
secretRotationDAL,
|
secretRotationDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretApprovalRequestDAL
|
secretApprovalRequestDAL,
|
||||||
|
keyStore
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -576,7 +579,6 @@ export const secretQueueFactory = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
|
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -641,108 +643,157 @@ export const secretQueueFactory = ({
|
|||||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
const secrets = shouldUseSecretV2Bridge
|
const lock = await keyStore.acquireLock(
|
||||||
? await getIntegrationSecretsV2({
|
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||||
environment,
|
10000,
|
||||||
projectId,
|
{
|
||||||
folderId: folder.id,
|
retryCount: 3,
|
||||||
depth: 1,
|
retryDelay: 2000
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
}
|
||||||
})
|
);
|
||||||
: await getIntegrationSecrets({
|
const lockAcquiredTime = new Date();
|
||||||
environment,
|
|
||||||
projectId,
|
|
||||||
folderId: folder.id,
|
|
||||||
key: botKey as string,
|
|
||||||
depth: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const integration of toBeSyncedIntegrations) {
|
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||||
const integrationAuth = {
|
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||||
...integration.integrationAuth,
|
);
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
projectId: integration.projectId
|
|
||||||
};
|
|
||||||
|
|
||||||
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
|
// check whether the integration should wait or not
|
||||||
integrationAuth,
|
if (lastRunSyncIntegrationTimestamp) {
|
||||||
shouldUseSecretV2Bridge,
|
const INTEGRATION_INTERVAL = 2000;
|
||||||
botKey
|
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
||||||
|
if (isStaleSyncIntegration) {
|
||||||
|
logger.info(
|
||||||
|
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||||
|
lockAcquiredTime.toISOString(),
|
||||||
|
lastRunSyncIntegrationTimestamp
|
||||||
);
|
);
|
||||||
let awsAssumeRoleArn = null;
|
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
|
||||||
if (shouldUseSecretV2Bridge) {
|
await new Promise((resolve) => {
|
||||||
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
|
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
|
||||||
awsAssumeRoleArn = secretManagerDecryptor({
|
|
||||||
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
|
|
||||||
}).toString();
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
integrationAuth.awsAssumeIamRoleArnTag &&
|
|
||||||
integrationAuth.awsAssumeIamRoleArnIV &&
|
|
||||||
integrationAuth.awsAssumeIamRoleArnCipherText
|
|
||||||
) {
|
|
||||||
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
|
|
||||||
iv: integrationAuth.awsAssumeIamRoleArnIV,
|
|
||||||
tag: integrationAuth.awsAssumeIamRoleArnTag,
|
|
||||||
key: botKey as string
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const suffixedSecrets: typeof secrets = {};
|
|
||||||
const metadata = integration.metadata as Record<string, string>;
|
|
||||||
if (metadata) {
|
|
||||||
Object.keys(secrets).forEach((key) => {
|
|
||||||
const prefix = metadata?.secretPrefix || "";
|
|
||||||
const suffix = metadata?.secretSuffix || "";
|
|
||||||
const newKey = prefix + key + suffix;
|
|
||||||
suffixedSecrets[newKey] = secrets[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// akhilmhdh: this needs to changed later to be more easier to use
|
|
||||||
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
|
|
||||||
const response = await syncIntegrationSecrets({
|
|
||||||
createManySecretsRawFn,
|
|
||||||
updateManySecretsRawFn,
|
|
||||||
integrationDAL,
|
|
||||||
integration,
|
|
||||||
integrationAuth,
|
|
||||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
|
||||||
accessId: accessId as string,
|
|
||||||
awsAssumeRoleArn,
|
|
||||||
accessToken,
|
|
||||||
projectId,
|
|
||||||
appendices: {
|
|
||||||
prefix: metadata?.secretPrefix || "",
|
|
||||||
suffix: metadata?.secretSuffix || ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
|
||||||
lastSyncJobId: job.id,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
syncMessage: response?.syncMessage ?? "",
|
|
||||||
isSynced: response?.isSynced ?? true
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.info("Secret integration sync error: %o", err);
|
|
||||||
|
|
||||||
const message =
|
|
||||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
|
||||||
"Unknown error occurred.";
|
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
|
||||||
lastSyncJobId: job.id,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
syncMessage: message,
|
|
||||||
isSynced: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// akhilmhdh: this try catch is for lock release
|
||||||
|
try {
|
||||||
|
const secrets = shouldUseSecretV2Bridge
|
||||||
|
? await getIntegrationSecretsV2({
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
depth: 1,
|
||||||
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||||
|
})
|
||||||
|
: await getIntegrationSecrets({
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
key: botKey as string,
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const integration of toBeSyncedIntegrations) {
|
||||||
|
const integrationAuth = {
|
||||||
|
...integration.integrationAuth,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
projectId: integration.projectId
|
||||||
|
};
|
||||||
|
|
||||||
|
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
|
||||||
|
integrationAuth,
|
||||||
|
shouldUseSecretV2Bridge,
|
||||||
|
botKey
|
||||||
|
);
|
||||||
|
let awsAssumeRoleArn = null;
|
||||||
|
if (shouldUseSecretV2Bridge) {
|
||||||
|
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
|
||||||
|
awsAssumeRoleArn = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
|
||||||
|
}).toString();
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
integrationAuth.awsAssumeIamRoleArnTag &&
|
||||||
|
integrationAuth.awsAssumeIamRoleArnIV &&
|
||||||
|
integrationAuth.awsAssumeIamRoleArnCipherText
|
||||||
|
) {
|
||||||
|
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
|
||||||
|
iv: integrationAuth.awsAssumeIamRoleArnIV,
|
||||||
|
tag: integrationAuth.awsAssumeIamRoleArnTag,
|
||||||
|
key: botKey as string
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixedSecrets: typeof secrets = {};
|
||||||
|
const metadata = integration.metadata as Record<string, string>;
|
||||||
|
if (metadata) {
|
||||||
|
Object.keys(secrets).forEach((key) => {
|
||||||
|
const prefix = metadata?.secretPrefix || "";
|
||||||
|
const suffix = metadata?.secretSuffix || "";
|
||||||
|
const newKey = prefix + key + suffix;
|
||||||
|
suffixedSecrets[newKey] = secrets[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// akhilmhdh: this try catch is for catching integration error and saving it in db
|
||||||
|
try {
|
||||||
|
// akhilmhdh: this needs to changed later to be more easier to use
|
||||||
|
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
|
||||||
|
const response = await syncIntegrationSecrets({
|
||||||
|
createManySecretsRawFn,
|
||||||
|
updateManySecretsRawFn,
|
||||||
|
integrationDAL,
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
||||||
|
accessId: accessId as string,
|
||||||
|
awsAssumeRoleArn,
|
||||||
|
accessToken,
|
||||||
|
projectId,
|
||||||
|
appendices: {
|
||||||
|
prefix: metadata?.secretPrefix || "",
|
||||||
|
suffix: metadata?.secretSuffix || ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await integrationDAL.updateById(integration.id, {
|
||||||
|
lastSyncJobId: job.id,
|
||||||
|
lastUsed: new Date(),
|
||||||
|
syncMessage: response?.syncMessage ?? "",
|
||||||
|
isSynced: response?.isSynced ?? true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
err,
|
||||||
|
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
const message =
|
||||||
|
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||||
|
"Unknown error occurred.";
|
||||||
|
|
||||||
|
await integrationDAL.updateById(integration.id, {
|
||||||
|
lastSyncJobId: job.id,
|
||||||
|
lastUsed: new Date(),
|
||||||
|
syncMessage: message,
|
||||||
|
isSynced: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await lock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath),
|
||||||
|
KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds,
|
||||||
|
lockAcquiredTime.toISOString()
|
||||||
|
);
|
||||||
logger.info("Secret integration sync ended: %s", job.id);
|
logger.info("Secret integration sync ended: %s", job.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -828,7 +879,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 +1055,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 │
|
||||||
├───────────────┼──────────────┼─────────────┤
|
├───────────────┼──────────────┼─────────────┤
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
---
|
||||||
@@ -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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ description: "Learn how to manage secrets in local development environments."
|
|||||||
There is a number of issues that arise with secret management in local development environment:
|
There is a number of issues that arise with secret management in local development environment:
|
||||||
1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another.
|
1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another.
|
||||||
2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time.
|
2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time.
|
||||||
3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability.
|
3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded secret as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability.
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
@@ -31,4 +31,4 @@ By default, all the secrets in the Infisical environments are shared among proje
|
|||||||
|
|
||||||
### Secret Scanning
|
### Secret Scanning
|
||||||
|
|
||||||
In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub.
|
In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</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."
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||

|

|
||||||
</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).
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user