mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): completed root folder level secret, secret import and folder
This commit is contained in:
40
backend-pg/package-lock.json
generated
40
backend-pg/package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^1.10.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
"argon2": "^0.31.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
@@ -38,6 +39,8 @@
|
|||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-to-json-schema": "^3.22.0"
|
"zod-to-json-schema": "^3.22.0"
|
||||||
},
|
},
|
||||||
@@ -940,6 +943,14 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@phc/format": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -2269,6 +2280,25 @@
|
|||||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/argon2": {
|
||||||
|
"version": "0.31.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.2.tgz",
|
||||||
|
"integrity": "sha512-QSnJ8By5Mth60IEte45w9Y7v6bWcQw3YhRtJKKN8oNCxnTLDiv/AXXkDPf2srTMfxFVn3QJdVv2nhXESsUa+Yg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||||
|
"@phc/format": "^1.0.0",
|
||||||
|
"node-addon-api": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/argon2/node_modules/node-addon-api": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA=="
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@@ -8571,6 +8601,16 @@
|
|||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||||
|
},
|
||||||
|
"node_modules/tweetnacl-util": {
|
||||||
|
"version": "0.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
||||||
|
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^1.10.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
"argon2": "^0.31.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
@@ -91,6 +92,8 @@
|
|||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-to-json-schema": "^3.22.0"
|
"zod-to-json-schema": "^3.22.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { execSync } from "child_process";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import promptSync from "prompt-sync";
|
import promptSync from "prompt-sync";
|
||||||
|
|
||||||
const prompt = promptSync();
|
const prompt = promptSync({ sigint: true });
|
||||||
|
|
||||||
const migrationName = prompt("Enter name for migration: ");
|
const migrationName = prompt("Enter name for migration: ");
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { readdirSync } from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import promptSync from "prompt-sync";
|
import promptSync from "prompt-sync";
|
||||||
|
|
||||||
const prompt = promptSync();
|
const prompt = promptSync({ sigint: true });
|
||||||
|
|
||||||
const migrationName = prompt("Enter name for seedfile: ");
|
const migrationName = prompt("Enter name for seedfile: ");
|
||||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
|
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import knex from "knex";
|
|||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from "fs";
|
||||||
import promptSync from "prompt-sync";
|
import promptSync from "prompt-sync";
|
||||||
|
|
||||||
const prompt = promptSync();
|
const prompt = promptSync({ sigint: true });
|
||||||
|
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.join(__dirname, "../.env"),
|
path: path.join(__dirname, "../.env"),
|
||||||
|
|||||||
6
backend-pg/src/@types/fastify.d.ts
vendored
6
backend-pg/src/@types/fastify.d.ts
vendored
@@ -15,6 +15,9 @@ import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env
|
|||||||
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
|
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/token/token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/token/token-service";
|
||||||
import { TUserDalFactory } from "@app/services/user/user-dal";
|
import { TUserDalFactory } from "@app/services/user/user-dal";
|
||||||
@@ -64,6 +67,9 @@ declare module "fastify" {
|
|||||||
projectEnv: TProjectEnvServiceFactory;
|
projectEnv: TProjectEnvServiceFactory;
|
||||||
projectKey: TProjectKeyServiceFactory;
|
projectKey: TProjectKeyServiceFactory;
|
||||||
projectRole: TProjectRoleServiceFactory;
|
projectRole: TProjectRoleServiceFactory;
|
||||||
|
secret: TSecretServiceFactory;
|
||||||
|
secretImport: TSecretImportServiceFactory;
|
||||||
|
folder: TSecretFolderServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
|
|||||||
52
backend-pg/src/@types/knex.d.ts
vendored
52
backend-pg/src/@types/knex.d.ts
vendored
@@ -40,6 +40,27 @@ import {
|
|||||||
TProjects,
|
TProjects,
|
||||||
TProjectsInsert,
|
TProjectsInsert,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
|
TSecretBlindIndexes,
|
||||||
|
TSecretBlindIndexesInsert,
|
||||||
|
TSecretBlindIndexesUpdate,
|
||||||
|
TSecretFolders,
|
||||||
|
TSecretFoldersInsert,
|
||||||
|
TSecretFoldersUpdate,
|
||||||
|
TSecretImports,
|
||||||
|
TSecretImportsInsert,
|
||||||
|
TSecretImportsUpdate,
|
||||||
|
TSecrets,
|
||||||
|
TSecretsInsert,
|
||||||
|
TSecretsUpdate,
|
||||||
|
TSecretTagJunction,
|
||||||
|
TSecretTagJunctionInsert,
|
||||||
|
TSecretTagJunctionUpdate,
|
||||||
|
TSecretTags,
|
||||||
|
TSecretTagsInsert,
|
||||||
|
TSecretTagsUpdate,
|
||||||
|
TSecretVersions,
|
||||||
|
TSecretVersionsInsert,
|
||||||
|
TSecretVersionsUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@@ -125,5 +146,36 @@ declare module "knex/types/tables" {
|
|||||||
TProjectKeysInsert,
|
TProjectKeysInsert,
|
||||||
TProjectKeysUpdate
|
TProjectKeysUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
|
||||||
|
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
|
||||||
|
TSecretBlindIndexes,
|
||||||
|
TSecretBlindIndexesInsert,
|
||||||
|
TSecretBlindIndexesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SecretVersion]: Knex.CompositeTableType<
|
||||||
|
TSecretVersions,
|
||||||
|
TSecretVersionsInsert,
|
||||||
|
TSecretVersionsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SecretFolder]: Knex.CompositeTableType<
|
||||||
|
TSecretFolders,
|
||||||
|
TSecretFoldersInsert,
|
||||||
|
TSecretFoldersUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SecretTag]: Knex.CompositeTableType<
|
||||||
|
TSecretTags,
|
||||||
|
TSecretTagsInsert,
|
||||||
|
TSecretTagsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SecretImport]: Knex.CompositeTableType<
|
||||||
|
TSecretImports,
|
||||||
|
TSecretImportsInsert,
|
||||||
|
TSecretImportsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
||||||
|
TSecretTagJunction,
|
||||||
|
TSecretTagJunctionInsert,
|
||||||
|
TSecretTagJunctionUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,14 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.string("slug").notNullable();
|
t.string("slug").notNullable();
|
||||||
|
t.integer("position").notNullable();
|
||||||
t.uuid("projectId").notNullable();
|
t.uuid("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
// this will ensure ever env has its position
|
||||||
|
t.unique(["projectId", "position"], {
|
||||||
|
indexName: "env_pos_composite_uniqe",
|
||||||
|
deferrable: "deferred"
|
||||||
|
});
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -34,9 +40,9 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.text("nonce").notNullable();
|
t.text("nonce").notNullable();
|
||||||
t.uuid("receiverId").notNullable();
|
t.uuid("receiverId").notNullable();
|
||||||
t.foreign("receiverId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
t.foreign("receiverId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
t.uuid("senderId").notNullable();
|
t.uuid("senderId");
|
||||||
// if sender is deleted just don't do anything to this record
|
// if sender is deleted just don't do anything to this record
|
||||||
t.foreign("senderId").references("id").inTable(TableName.Users).onDelete("NO ACTION");
|
t.foreign("senderId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
t.uuid("projectId").notNullable();
|
t.uuid("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
|||||||
25
backend-pg/src/db/migrations/20231218092441_secret-folder.ts
Normal file
25
backend-pg/src/db/migrations/20231218092441_secret-folder.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretFolder))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.integer("version").defaultTo(1);
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("envId").notNullable();
|
||||||
|
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||||
|
t.uuid("parentId");
|
||||||
|
t.foreign("parentId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretFolder);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretFolder);
|
||||||
|
}
|
||||||
30
backend-pg/src/db/migrations/20231218092508_secret-import.ts
Normal file
30
backend-pg/src/db/migrations/20231218092508_secret-import.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretImport))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretImport, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.integer("version").defaultTo(1);
|
||||||
|
t.string("importPath").notNullable();
|
||||||
|
t.uuid("importEnv").notNullable();
|
||||||
|
t.foreign("importEnv").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||||
|
t.integer("position").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
|
t.unique(["folderId", "position"], {
|
||||||
|
indexName: "import_pos_composite_uniqe",
|
||||||
|
deferrable: "deferred"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretImport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretImport);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretImport);
|
||||||
|
}
|
||||||
26
backend-pg/src/db/migrations/20231218092517_secret-tag.ts
Normal file
26
backend-pg/src/db/migrations/20231218092517_secret-tag.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretTag))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretTag, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("slug").notNullable();
|
||||||
|
t.string("tagColor").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("createdBy").notNullable();
|
||||||
|
t.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("NO ACTION");
|
||||||
|
t.uuid("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretTag);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretTag);
|
||||||
|
}
|
||||||
65
backend-pg/src/db/migrations/20231218103423_secret.ts
Normal file
65
backend-pg/src/db/migrations/20231218103423_secret.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas";
|
||||||
|
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretBlindIndex))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretBlindIndex, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.text("encryptedSaltCipherText").notNullable();
|
||||||
|
t.text("saltIV").notNullable();
|
||||||
|
t.text("saltTag").notNullable();
|
||||||
|
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
||||||
|
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
||||||
|
t.uuid("projectId").notNullable().unique();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretBlindIndex);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Secret))) {
|
||||||
|
await knex.schema.createTable(TableName.Secret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.integer("version").defaultTo(1);
|
||||||
|
t.string("type").notNullable().defaultTo(SecretType.Shared);
|
||||||
|
// t.text("secretKeyHash").notNullable();
|
||||||
|
// t.text("secretValueHash");
|
||||||
|
// t.text("secretCommentHash");
|
||||||
|
t.text("secretBlindIndex").notNullable();
|
||||||
|
t.text("secretKeyCiphertext").notNullable();
|
||||||
|
t.text("secretKeyIV").notNullable();
|
||||||
|
t.text("secretKeyTag").notNullable();
|
||||||
|
t.text("secretValueCiphertext").notNullable();
|
||||||
|
t.text("secretValueIV").notNullable(); // symmetric encryption
|
||||||
|
t.text("secretValueTag").notNullable();
|
||||||
|
t.text("secretCommentCiphertext");
|
||||||
|
t.text("secretCommentIV");
|
||||||
|
t.text("secretCommentTag");
|
||||||
|
t.string("secretReminderNotice");
|
||||||
|
t.integer("secretReminderRepeatDays");
|
||||||
|
t.boolean("skipMultilineEncoding").defaultTo(false);
|
||||||
|
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
||||||
|
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
||||||
|
t.jsonb("metadata");
|
||||||
|
t.uuid("userId");
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Secret);
|
||||||
|
// many to many relation between tags
|
||||||
|
await createJunctionTable(knex, TableName.JnSecretTag, TableName.Secret, TableName.SecretTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretBlindIndex);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretBlindIndex);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.JnSecretTag);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Secret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Secret);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas";
|
||||||
|
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretVersion))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretVersion, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.integer("version").defaultTo(1);
|
||||||
|
t.string("type").notNullable().defaultTo(SecretType.Shared);
|
||||||
|
// t.text("secretKeyHash").notNullable();
|
||||||
|
// t.text("secretValueHash");
|
||||||
|
// t.text("secretCommentHash");
|
||||||
|
t.text("secretBlindIndex").notNullable();
|
||||||
|
t.text("secretKeyCiphertext").notNullable();
|
||||||
|
t.text("secretKeyIV").notNullable();
|
||||||
|
t.text("secretKeyTag").notNullable();
|
||||||
|
t.text("secretValueCiphertext").notNullable();
|
||||||
|
t.text("secretValueIV").notNullable(); // symmetric encryption
|
||||||
|
t.text("secretValueTag").notNullable();
|
||||||
|
t.text("secretCommentCiphertext");
|
||||||
|
t.text("secretCommentIV");
|
||||||
|
t.text("secretCommentTag");
|
||||||
|
t.string("secretReminderNotice");
|
||||||
|
t.integer("secretReminderRepeatDays");
|
||||||
|
t.boolean("skipMultilineEncoding").defaultTo(false);
|
||||||
|
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
||||||
|
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
||||||
|
t.jsonb("metadata");
|
||||||
|
t.uuid("secretId");
|
||||||
|
t.foreign("secretId").references("id").inTable(TableName.Secret).onDelete("SET NULL");
|
||||||
|
t.uuid("userId");
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretVersion);
|
||||||
|
// many to many relation between tags
|
||||||
|
await createJunctionTable(
|
||||||
|
knex,
|
||||||
|
TableName.JnSecretVersionTag,
|
||||||
|
TableName.SecretVersion,
|
||||||
|
TableName.SecretTag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.JnSecretVersionTag);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretVersion);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretVersion);
|
||||||
|
}
|
||||||
@@ -12,6 +12,13 @@ export * from "./project-keys";
|
|||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
|
export * from "./secret-blind-indexes";
|
||||||
|
export * from "./secret-folders";
|
||||||
|
export * from "./secret-imports";
|
||||||
|
export * from "./secret-tag-junction";
|
||||||
|
export * from "./secret-tags";
|
||||||
|
export * from "./secret-versions";
|
||||||
|
export * from "./secrets";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./user-actions";
|
export * from "./user-actions";
|
||||||
export * from "./user-encryption-keys";
|
export * from "./user-encryption-keys";
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ export enum TableName {
|
|||||||
Environment = "project_environments",
|
Environment = "project_environments",
|
||||||
ProjectMembership = "project_memberships",
|
ProjectMembership = "project_memberships",
|
||||||
ProjectRoles = "project_roles",
|
ProjectRoles = "project_roles",
|
||||||
ProjectKeys = "project_keys"
|
ProjectKeys = "project_keys",
|
||||||
|
Secret = "secrets",
|
||||||
|
SecretBlindIndex = "secret_blind_indexes",
|
||||||
|
SecretVersion = "secret_versions",
|
||||||
|
SecretFolder = "secret_folders",
|
||||||
|
SecretImport = "secret_imports",
|
||||||
|
SecretTag = "secret_tags",
|
||||||
|
JnSecretTag = "secret_tag_junction",
|
||||||
|
JnSecretVersionTag = "secret_version_tag_junction"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
@@ -58,3 +66,8 @@ export enum SecretKeyEncoding {
|
|||||||
BASE64 = "base64",
|
BASE64 = "base64",
|
||||||
HEX = "hex"
|
HEX = "hex"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SecretType {
|
||||||
|
Shared = "shared",
|
||||||
|
Personal = "personal"
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const ProjectEnvironmentsSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
|
position: z.number(),
|
||||||
projectId: z.string().uuid(),
|
projectId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
|
|||||||
24
backend-pg/src/db/schemas/secret-blind-indexes.ts
Normal file
24
backend-pg/src/db/schemas/secret-blind-indexes.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretBlindIndexesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
encryptedSaltCipherText: z.string(),
|
||||||
|
saltIV: z.string(),
|
||||||
|
saltTag: z.string(),
|
||||||
|
algorithm: z.string().default('aes-256-gcm'),
|
||||||
|
keyEncoding: z.string().default('utf8'),
|
||||||
|
projectId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretBlindIndexes = z.infer<typeof SecretBlindIndexesSchema>;
|
||||||
|
export type TSecretBlindIndexesInsert = Omit<TSecretBlindIndexes, TImmutableDBKeys>;
|
||||||
|
export type TSecretBlindIndexesUpdate = Partial<Omit<TSecretBlindIndexes, TImmutableDBKeys>>;
|
||||||
22
backend-pg/src/db/schemas/secret-folders.ts
Normal file
22
backend-pg/src/db/schemas/secret-folders.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretFoldersSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
version: z.number().default(1).nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
envId: z.string().uuid(),
|
||||||
|
parentId: z.string().uuid().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
||||||
|
export type TSecretFoldersInsert = Omit<TSecretFolders, TImmutableDBKeys>;
|
||||||
|
export type TSecretFoldersUpdate = Partial<Omit<TSecretFolders, TImmutableDBKeys>>;
|
||||||
23
backend-pg/src/db/schemas/secret-imports.ts
Normal file
23
backend-pg/src/db/schemas/secret-imports.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretImportsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
version: z.number().default(1).nullable().optional(),
|
||||||
|
importPath: z.string(),
|
||||||
|
importEnv: z.string().uuid(),
|
||||||
|
position: z.number(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretImports = z.infer<typeof SecretImportsSchema>;
|
||||||
|
export type TSecretImportsInsert = Omit<TSecretImports, TImmutableDBKeys>;
|
||||||
|
export type TSecretImportsUpdate = Partial<Omit<TSecretImports, TImmutableDBKeys>>;
|
||||||
18
backend-pg/src/db/schemas/secret-tag-junction.ts
Normal file
18
backend-pg/src/db/schemas/secret-tag-junction.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretTagJunctionSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
secretsId: z.string().uuid(),
|
||||||
|
secret_tagsId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretTagJunction = z.infer<typeof SecretTagJunctionSchema>;
|
||||||
|
export type TSecretTagJunctionInsert = Omit<TSecretTagJunction, TImmutableDBKeys>;
|
||||||
|
export type TSecretTagJunctionUpdate = Partial<Omit<TSecretTagJunction, TImmutableDBKeys>>;
|
||||||
23
backend-pg/src/db/schemas/secret-tags.ts
Normal file
23
backend-pg/src/db/schemas/secret-tags.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretTagsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
tagColor: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
createdBy: z.string().uuid(),
|
||||||
|
projectId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretTags = z.infer<typeof SecretTagsSchema>;
|
||||||
|
export type TSecretTagsInsert = Omit<TSecretTags, TImmutableDBKeys>;
|
||||||
|
export type TSecretTagsUpdate = Partial<Omit<TSecretTags, TImmutableDBKeys>>;
|
||||||
39
backend-pg/src/db/schemas/secret-versions.ts
Normal file
39
backend-pg/src/db/schemas/secret-versions.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretVersionsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
version: z.number().default(1).nullable().optional(),
|
||||||
|
type: z.string().default('shared'),
|
||||||
|
secretBlindIndex: z.string(),
|
||||||
|
secretKeyCiphertext: z.string(),
|
||||||
|
secretKeyIV: z.string(),
|
||||||
|
secretKeyTag: z.string(),
|
||||||
|
secretValueCiphertext: z.string(),
|
||||||
|
secretValueIV: z.string(),
|
||||||
|
secretValueTag: z.string(),
|
||||||
|
secretCommentCiphertext: z.string().nullable().optional(),
|
||||||
|
secretCommentIV: z.string().nullable().optional(),
|
||||||
|
secretCommentTag: z.string().nullable().optional(),
|
||||||
|
secretReminderNotice: z.string().nullable().optional(),
|
||||||
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
|
algorithm: z.string().default('aes-256-gcm'),
|
||||||
|
keyEncoding: z.string().default('utf8'),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
secretId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretVersions = z.infer<typeof SecretVersionsSchema>;
|
||||||
|
export type TSecretVersionsInsert = Omit<TSecretVersions, TImmutableDBKeys>;
|
||||||
|
export type TSecretVersionsUpdate = Partial<Omit<TSecretVersions, TImmutableDBKeys>>;
|
||||||
38
backend-pg/src/db/schemas/secrets.ts
Normal file
38
backend-pg/src/db/schemas/secrets.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
version: z.number().default(1).nullable().optional(),
|
||||||
|
type: z.string().default('shared'),
|
||||||
|
secretBlindIndex: z.string(),
|
||||||
|
secretKeyCiphertext: z.string(),
|
||||||
|
secretKeyIV: z.string(),
|
||||||
|
secretKeyTag: z.string(),
|
||||||
|
secretValueCiphertext: z.string(),
|
||||||
|
secretValueIV: z.string(),
|
||||||
|
secretValueTag: z.string(),
|
||||||
|
secretCommentCiphertext: z.string().nullable().optional(),
|
||||||
|
secretCommentIV: z.string().nullable().optional(),
|
||||||
|
secretCommentTag: z.string().nullable().optional(),
|
||||||
|
secretReminderNotice: z.string().nullable().optional(),
|
||||||
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
|
algorithm: z.string().default('aes-256-gcm'),
|
||||||
|
keyEncoding: z.string().default('utf8'),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecrets = z.infer<typeof SecretsSchema>;
|
||||||
|
export type TSecretsInsert = Omit<TSecrets, TImmutableDBKeys>;
|
||||||
|
export type TSecretsUpdate = Partial<Omit<TSecrets, TImmutableDBKeys>>;
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "./schemas";
|
||||||
|
|
||||||
export const createJunctionTable = (
|
export const createJunctionTable = (
|
||||||
knex: Knex,
|
knex: Knex,
|
||||||
tableName: string,
|
tableName: TableName,
|
||||||
table1Name: string,
|
table1Name: TableName,
|
||||||
table2Name: string
|
table2Name: TableName
|
||||||
) =>
|
) =>
|
||||||
knex.schema.createTable(tableName, (table) => {
|
knex.schema.createTable(tableName, (table) => {
|
||||||
table.increments(); // Primary key
|
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
table.integer(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1
|
table.uuid(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1
|
||||||
table.integer(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2
|
table.uuid(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2
|
||||||
table.foreign(`${table1Name}Id`).references("id").inTable(table2Name);
|
table.foreign(`${table1Name}Id`).references("id").inTable(table2Name);
|
||||||
table.foreign(`${table2Name}Id`).references("id").inTable(table1Name);
|
table.foreign(`${table2Name}Id`).references("id").inTable(table1Name);
|
||||||
});
|
});
|
||||||
|
|||||||
184
backend-pg/src/lib/crypto/encryption.ts
Normal file
184
backend-pg/src/lib/crypto/encryption.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import * as argon2 from "argon2";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
import naclUtils from "tweetnacl-util";
|
||||||
|
|
||||||
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export type TDecryptSymmetricInput = {
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
export const IV_BYTES_SIZE = 12;
|
||||||
|
export const BLOCK_SIZE_BYTES_16 = 16;
|
||||||
|
|
||||||
|
export const decryptSymmetric = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => {
|
||||||
|
const secretKey = crypto.createSecretKey(key, "base64");
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
secretKey,
|
||||||
|
Buffer.from(iv, "base64")
|
||||||
|
);
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||||
|
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
||||||
|
cleartext += decipher.final("utf8");
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptSymmetric = (plaintext: string, key: string) => {
|
||||||
|
const iv = crypto.randomBytes(IV_BYTES_SIZE);
|
||||||
|
|
||||||
|
const secretKey = crypto.createSecretKey(key, "base64");
|
||||||
|
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, secretKey, iv);
|
||||||
|
|
||||||
|
let ciphertext = cipher.update(plaintext, "utf8", "base64");
|
||||||
|
ciphertext += cipher.final("base64");
|
||||||
|
|
||||||
|
return {
|
||||||
|
ciphertext,
|
||||||
|
iv: iv.toString("base64"),
|
||||||
|
tag: cipher.getAuthTag().toString("base64")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => {
|
||||||
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
|
||||||
|
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
|
||||||
|
|
||||||
|
let ciphertext = cipher.update(plaintext, "utf8", "base64");
|
||||||
|
ciphertext += cipher.final("base64");
|
||||||
|
|
||||||
|
return {
|
||||||
|
ciphertext,
|
||||||
|
iv: iv.toString("base64"),
|
||||||
|
tag: cipher.getAuthTag().toString("base64")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSymmetric128BitHexKeyUTF8 = ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
}: TDecryptSymmetricInput): string => {
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
key,
|
||||||
|
Buffer.from(iv, "base64")
|
||||||
|
);
|
||||||
|
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||||
|
|
||||||
|
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
||||||
|
cleartext += decipher.final("utf8");
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptAsymmetric = (plaintext: string, publicKey: string, privateKey: string) => {
|
||||||
|
const nonce = nacl.randomBytes(24);
|
||||||
|
const ciphertext = nacl.box(
|
||||||
|
naclUtils.decodeUTF8(plaintext),
|
||||||
|
nonce,
|
||||||
|
naclUtils.decodeBase64(publicKey),
|
||||||
|
naclUtils.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ciphertext: naclUtils.encodeBase64(ciphertext),
|
||||||
|
nonce: naclUtils.encodeBase64(nonce)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDecryptAsymmetricInput = {
|
||||||
|
ciphertext: string;
|
||||||
|
nonce: string;
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptAsymmetric = ({
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
}: TDecryptAsymmetricInput) => {
|
||||||
|
const plaintext: Uint8Array | null = nacl.box.open(
|
||||||
|
naclUtils.decodeBase64(ciphertext),
|
||||||
|
naclUtils.decodeBase64(nonce),
|
||||||
|
naclUtils.decodeBase64(publicKey),
|
||||||
|
naclUtils.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (plaintext == null) throw Error("Invalid ciphertext or keys");
|
||||||
|
|
||||||
|
return naclUtils.encodeUTF8(plaintext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGenSecretBlindIndex = {
|
||||||
|
secretName: string;
|
||||||
|
keyEncoding: SecretKeyEncoding;
|
||||||
|
rootEncryptionKey?: string;
|
||||||
|
encryptionKey?: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
ciphertext: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildSecretBlindIndexFromName = async ({
|
||||||
|
secretName,
|
||||||
|
ciphertext,
|
||||||
|
keyEncoding,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
encryptionKey,
|
||||||
|
rootEncryptionKey
|
||||||
|
}: TGenSecretBlindIndex) => {
|
||||||
|
if (!encryptionKey && !rootEncryptionKey) throw new Error("Missing secret blind index key");
|
||||||
|
let salt = "";
|
||||||
|
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
|
||||||
|
salt = decryptSymmetric({ iv, ciphertext, key: rootEncryptionKey, tag });
|
||||||
|
} else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
|
||||||
|
salt = decryptSymmetric128BitHexKeyUTF8({ iv, ciphertext, key: encryptionKey, tag });
|
||||||
|
}
|
||||||
|
if (!salt) throw new Error("Missing secret blind index key");
|
||||||
|
|
||||||
|
const secretBlindIndex = await argon2.hash(secretName, {
|
||||||
|
type: argon2.argon2id,
|
||||||
|
salt: Buffer.from(salt, "base64"),
|
||||||
|
saltLength: 16, // default 16 bytes
|
||||||
|
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||||
|
hashLength: 32,
|
||||||
|
parallelism: 1,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretBlindIndex.toString("base64");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSecretBlindIndex = (rootEncryptionKey?: string, encryptionKey?: string) => {
|
||||||
|
if (!encryptionKey && !rootEncryptionKey) throw new Error("Atleast one encryption key needed");
|
||||||
|
const salt = crypto.randomBytes(16).toString("base64");
|
||||||
|
if (rootEncryptionKey) {
|
||||||
|
const data = encryptSymmetric(salt, rootEncryptionKey);
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.BASE64
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (encryptionKey) {
|
||||||
|
const data = encryptSymmetric128BitHexKeyUTF8(salt, encryptionKey);
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error("Failed to generate blind index due to encryption key missing");
|
||||||
|
};
|
||||||
@@ -1 +1,11 @@
|
|||||||
|
export {
|
||||||
|
buildSecretBlindIndexFromName,
|
||||||
|
createSecretBlindIndex,
|
||||||
|
decryptAsymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
|
encryptAsymmetric,
|
||||||
|
encryptSymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8
|
||||||
|
} from "./encryption";
|
||||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ export class BadRequestError extends Error {
|
|||||||
|
|
||||||
error: unknown;
|
error: unknown;
|
||||||
|
|
||||||
constructor({ name, error, message }: { message?: string; name: string; error?: unknown }) {
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||||
super(message ?? "The request is invalid");
|
super(message ?? "The request is invalid");
|
||||||
this.name = name;
|
this.name = name || "";
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { Tables } from "knex/types/tables";
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
import { TableName } from "@app/db/schemas";
|
|
||||||
|
|
||||||
import { DatabaseError } from "../errors";
|
import { DatabaseError } from "../errors";
|
||||||
|
|
||||||
export * from "./join";
|
export * from "./join";
|
||||||
@@ -19,7 +17,7 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
|||||||
// What is ormify
|
// What is ormify
|
||||||
// It is to inject typical operations like find, findOne, update, delete, create
|
// It is to inject typical operations like find, findOne, update, delete, create
|
||||||
// This will avoid writing most common ones each time
|
// This will avoid writing most common ones each time
|
||||||
export const ormify = <DbOps extends object, Tname extends TableName>(
|
export const ormify = <DbOps extends object, Tname extends keyof Tables>(
|
||||||
db: Knex,
|
db: Knex,
|
||||||
tableName: Tname,
|
tableName: Tname,
|
||||||
dal?: DbOps
|
dal?: DbOps
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const mergeOneToManyRelation = <
|
|||||||
prevPkId = pk;
|
prevPkId = pk;
|
||||||
prevPkIndex += 1;
|
prevPkIndex += 1;
|
||||||
}
|
}
|
||||||
console.log(prevPkIndex, prevPkId);
|
|
||||||
groupedRecord[prevPkIndex][childKey].push(childMapper(row));
|
groupedRecord[prevPkIndex][childKey].push(childMapper(row));
|
||||||
}
|
}
|
||||||
return groupedRecord;
|
return groupedRecord;
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ import { projectMembershipDalFactory } from "@app/services/project-membership/pr
|
|||||||
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
import { projectRoleDalFactory } from "@app/services/project-role/project-role-dal";
|
import { projectRoleDalFactory } from "@app/services/project-role/project-role-dal";
|
||||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { secretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal";
|
||||||
|
import { secretDalFactory } from "@app/services/secret/secret-dal";
|
||||||
|
import { secretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { secretVersionDalFactory } from "@app/services/secret/secret-version-dal";
|
||||||
|
import { secretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
|
import { secretImportDalFactory } from "@app/services/secret-import/secret-import-dal";
|
||||||
|
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
import { superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -35,10 +43,10 @@ import { userDalFactory } from "@app/services/user/user-dal";
|
|||||||
import { userServiceFactory } from "@app/services/user/user-service";
|
import { userServiceFactory } from "@app/services/user/user-service";
|
||||||
|
|
||||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||||
|
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||||
import { registerV1Routes } from "./v1";
|
import { registerV1Routes } from "./v1";
|
||||||
import { registerV2Routes } from "./v2";
|
import { registerV2Routes } from "./v2";
|
||||||
import { registerV3Routes } from "./v3";
|
import { registerV3Routes } from "./v3";
|
||||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
|
||||||
|
|
||||||
export const registerRoutes = async (
|
export const registerRoutes = async (
|
||||||
server: FastifyZodProvider,
|
server: FastifyZodProvider,
|
||||||
@@ -60,6 +68,12 @@ export const registerRoutes = async (
|
|||||||
const projectEnvDal = projectEnvDalFactory(db);
|
const projectEnvDal = projectEnvDalFactory(db);
|
||||||
const projectKeyDal = projectKeyDalFactory(db);
|
const projectKeyDal = projectKeyDalFactory(db);
|
||||||
|
|
||||||
|
const secretDal = secretDalFactory(db);
|
||||||
|
const folderDal = secretFolderDalFactory(db);
|
||||||
|
const secretImportDal = secretImportDalFactory(db);
|
||||||
|
const secretVersionDal = secretVersionDalFactory(db);
|
||||||
|
const secretBlindIndexDal = secretBlindIndexDalFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDal = permissionDalFactory(db);
|
const permissionDal = permissionDalFactory(db);
|
||||||
|
|
||||||
@@ -104,8 +118,10 @@ export const registerRoutes = async (
|
|||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDal,
|
projectDal,
|
||||||
|
secretBlindIndexDal,
|
||||||
projectEnvDal,
|
projectEnvDal,
|
||||||
projectMembershipDal
|
projectMembershipDal,
|
||||||
|
folderDal
|
||||||
});
|
});
|
||||||
const projectMembershipService = projectMembershipServiceFactory({
|
const projectMembershipService = projectMembershipServiceFactory({
|
||||||
projectMembershipDal,
|
projectMembershipDal,
|
||||||
@@ -125,6 +141,25 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDal });
|
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDal });
|
||||||
|
|
||||||
|
const secretService = secretServiceFactory({
|
||||||
|
folderDal,
|
||||||
|
secretVersionDal,
|
||||||
|
secretBlindIndexDal,
|
||||||
|
permissionService,
|
||||||
|
secretDal
|
||||||
|
});
|
||||||
|
const folderService = secretFolderServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
folderDal,
|
||||||
|
projectEnvDal
|
||||||
|
});
|
||||||
|
const secretImportService = secretImportServiceFactory({
|
||||||
|
projectEnvDal,
|
||||||
|
folderDal,
|
||||||
|
permissionService,
|
||||||
|
secretImportDal
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
// inject all services
|
// inject all services
|
||||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||||
@@ -142,7 +177,10 @@ export const registerRoutes = async (
|
|||||||
projectMembership: projectMembershipService,
|
projectMembership: projectMembershipService,
|
||||||
projectKey: projectKeyService,
|
projectKey: projectKeyService,
|
||||||
projectEnv: projectEnvService,
|
projectEnv: projectEnvService,
|
||||||
projectRole: projectRoleService
|
projectRole: projectRoleService,
|
||||||
|
secret: secretService,
|
||||||
|
folder: folderService,
|
||||||
|
secretImport: secretImportService
|
||||||
});
|
});
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
@@ -169,6 +207,7 @@ export const registerRoutes = async (
|
|||||||
},
|
},
|
||||||
handler: () => {
|
handler: () => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
message: "Ok" as const,
|
message: "Ok" as const,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { registerProjectEnvRouter } from "./project-env-router";
|
|||||||
import { registerProjectKeyRouter } from "./project-key-router";
|
import { registerProjectKeyRouter } from "./project-key-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
|
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||||
|
import { registerSecretImportRouter } from "./secret-import-router";
|
||||||
import { registerSsoRouter } from "./sso-router";
|
import { registerSsoRouter } from "./sso-router";
|
||||||
import { registerUserActionRouter } from "./user-action-router";
|
import { registerUserActionRouter } from "./user-action-router";
|
||||||
import { registerUserRouter } from "./user-router";
|
import { registerUserRouter } from "./user-router";
|
||||||
@@ -20,6 +22,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserRouter, { prefix: "/user" });
|
await server.register(registerUserRouter, { prefix: "/user" });
|
||||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
|
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
|
||||||
|
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (projectServer) => {
|
async (projectServer) => {
|
||||||
|
|||||||
121
backend-pg/src/server/routes/v1/secret-folder-router.ts
Normal file
121
backend-pg/src/server/routes/v1/secret-folder-router.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretFoldersSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSecretFolderRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
name: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
folder: SecretFoldersSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const folder = await server.services.folder.createFolder({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { folder };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:folderId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
folderId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
name: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
folder: SecretFoldersSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const folder = await server.services.folder.updateFolder({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.body,
|
||||||
|
id: req.params.folderId
|
||||||
|
});
|
||||||
|
return { folder };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:folderId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
folderId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
folder: SecretFoldersSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const folder = await server.services.folder.deleteFolder({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.body,
|
||||||
|
id: req.params.folderId
|
||||||
|
});
|
||||||
|
return { folder };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
folders: SecretFoldersSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const folders = await server.services.folder.getFolders({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
return { folders };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
152
backend-pg/src/server/routes/v1/secret-import-router.ts
Normal file
152
backend-pg/src/server/routes/v1/secret-import-router.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretImportsSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSecretImportRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/"),
|
||||||
|
import: z.object({
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
secretImport: SecretImportsSchema.omit({ importEnv: true }).merge(
|
||||||
|
z.object({
|
||||||
|
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretImport = await server.services.secretImport.createImport({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.body,
|
||||||
|
data: req.body.import
|
||||||
|
});
|
||||||
|
return { message: "Successfully created secret import", secretImport };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretImportId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretImportId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/"),
|
||||||
|
import: z.object({
|
||||||
|
environment: z.string().trim().optional(),
|
||||||
|
path: z.string().trim().optional(),
|
||||||
|
position: z.number().optional()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
secretImport: SecretImportsSchema.omit({ importEnv: true }).merge(
|
||||||
|
z.object({
|
||||||
|
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretImport = await server.services.secretImport.updateImport({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.secretImportId,
|
||||||
|
...req.body,
|
||||||
|
data: req.body.import
|
||||||
|
});
|
||||||
|
return { message: "Successfully updated secret import", secretImport };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretImportId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretImportId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
secretImport: SecretImportsSchema.omit({ importEnv: true }).merge(
|
||||||
|
z.object({
|
||||||
|
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretImport = await server.services.secretImport.deleteImport({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.secretImportId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { message: "Successfully deleted secret import", secretImport };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
secretImports: SecretImportsSchema.omit({ importEnv: true })
|
||||||
|
.merge(
|
||||||
|
z.object({
|
||||||
|
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretImports = await server.services.secretImport.getImports({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
return { message: "Successfully fetched secret imports", secretImports };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectKeysSchema } from "@app/db/schemas";
|
import { ProjectKeysSchema } from "@app/db/schemas";
|
||||||
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";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { registerLoginRouter } from "./login-router";
|
import { registerLoginRouter } from "./login-router";
|
||||||
|
import { registerSecretRouter } from "./secret-router";
|
||||||
import { registerSignupRouter } from "./signup-router";
|
import { registerSignupRouter } from "./signup-router";
|
||||||
import { registerUserRouter } from "./user-router";
|
import { registerUserRouter } from "./user-router";
|
||||||
|
|
||||||
@@ -6,4 +7,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerSignupRouter, { prefix: "/signup" });
|
await server.register(registerSignupRouter, { prefix: "/signup" });
|
||||||
await server.register(registerLoginRouter, { prefix: "/auth" });
|
await server.register(registerLoginRouter, { prefix: "/auth" });
|
||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
};
|
};
|
||||||
|
|||||||
372
backend-pg/src/server/routes/v3/secret-router.ts
Normal file
372
backend-pg/src/server/routes/v3/secret-router.ts
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretsSchema,SecretType } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
include_imports: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secrets = await server.services.secret.getSecrets({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
environment: req.query.environment,
|
||||||
|
projectId: req.query.workspaceId,
|
||||||
|
path: req.query.secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretName",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
|
include_imports: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secret: SecretsSchema.omit({ secretBlindIndex: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secret = await server.services.secret.getASecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
environment: req.query.environment,
|
||||||
|
projectId: req.query.workspaceId,
|
||||||
|
path: req.query.secretPath,
|
||||||
|
secretName: req.params.secretName,
|
||||||
|
type: req.query.type
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretName",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
secretKeyCiphertext: z.string().trim(),
|
||||||
|
secretKeyIV: z.string().trim(),
|
||||||
|
secretKeyTag: z.string().trim(),
|
||||||
|
secretValueCiphertext: z.string().trim(),
|
||||||
|
secretValueIV: z.string().trim(),
|
||||||
|
secretValueTag: z.string().trim(),
|
||||||
|
secretCommentCiphertext: z.string().trim().optional(),
|
||||||
|
secretCommentIV: z.string().trim().optional(),
|
||||||
|
secretCommentTag: z.string().trim().optional(),
|
||||||
|
metadata: z.record(z.string()).optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().optional()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secret: SecretsSchema.omit({ secretBlindIndex: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secret = await server.services.secret.createSecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
type: req.body.type,
|
||||||
|
environment: req.body.environment,
|
||||||
|
secretName: req.params.secretName,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
secretKeyIV: req.body.secretKeyIV,
|
||||||
|
secretKeyTag: req.body.secretKeyTag,
|
||||||
|
secretKeyCiphertext: req.body.secretKeyCiphertext,
|
||||||
|
secretValueIV: req.body.secretValueIV,
|
||||||
|
secretValueTag: req.body.secretValueTag,
|
||||||
|
secretValueCiphertext: req.body.secretValueCiphertext,
|
||||||
|
secretCommentIV: req.body.secretCommentIV,
|
||||||
|
secretCommentTag: req.body.secretCommentTag,
|
||||||
|
secretCommentCiphertext: req.body.secretCommentCiphertext,
|
||||||
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
|
metadata: req.body.metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretName",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretId: z.string().trim().optional(),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
secretValueCiphertext: z.string().trim(),
|
||||||
|
secretValueIV: z.string().trim(),
|
||||||
|
secretValueTag: z.string().trim(),
|
||||||
|
secretCommentCiphertext: z.string().trim().optional(),
|
||||||
|
secretCommentIV: z.string().trim().optional(),
|
||||||
|
secretCommentTag: z.string().trim().optional(),
|
||||||
|
secretReminderRepeatDays: z.number().min(1).max(365).optional().nullable(),
|
||||||
|
secretReminderNote: z.string().trim().nullable().optional(),
|
||||||
|
tags: z.string().array().optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().optional(),
|
||||||
|
// to update secret name
|
||||||
|
secretName: z.string().trim().optional(),
|
||||||
|
secretKeyIV: z.string().trim().optional(),
|
||||||
|
secretKeyTag: z.string().trim().optional(),
|
||||||
|
secretKeyCiphertext: z.string().trim().optional(),
|
||||||
|
metadata: z.record(z.string()).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secret: SecretsSchema.omit({ secretBlindIndex: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secret = await server.services.secret.updateSecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
type: req.body.type,
|
||||||
|
environment: req.body.environment,
|
||||||
|
secretName: req.params.secretName,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
secretKeyIV: req.body.secretKeyIV,
|
||||||
|
secretKeyTag: req.body.secretKeyTag,
|
||||||
|
secretKeyCiphertext: req.body.secretKeyCiphertext,
|
||||||
|
secretValueIV: req.body.secretValueIV,
|
||||||
|
secretValueTag: req.body.secretValueTag,
|
||||||
|
secretValueCiphertext: req.body.secretValueCiphertext,
|
||||||
|
secretCommentIV: req.body.secretCommentIV,
|
||||||
|
secretCommentTag: req.body.secretCommentTag,
|
||||||
|
secretCommentCiphertext: req.body.secretCommentCiphertext,
|
||||||
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
|
metadata: req.body.metadata,
|
||||||
|
secretReminderRepeatDays: req.body.secretReminderRepeatDays,
|
||||||
|
secretReminderNote: req.body.secretReminderNote,
|
||||||
|
newSecretName: req.body.secretName
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretName",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
secretId: z.string().trim().optional(),
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secret: SecretsSchema.omit({ secretBlindIndex: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secret = await server.services.secret.deleteSecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
type: req.body.type,
|
||||||
|
environment: req.body.environment,
|
||||||
|
secretName: req.params.secretName,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
secretId: req.body.secretId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/batch",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretName: z.string().trim(),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
|
secretKeyCiphertext: z.string().trim(),
|
||||||
|
secretKeyIV: z.string().trim(),
|
||||||
|
secretKeyTag: z.string().trim(),
|
||||||
|
secretValueCiphertext: z.string().trim(),
|
||||||
|
secretValueIV: z.string().trim(),
|
||||||
|
secretValueTag: z.string().trim(),
|
||||||
|
secretCommentCiphertext: z.string().trim().optional(),
|
||||||
|
secretCommentIV: z.string().trim().optional(),
|
||||||
|
secretCommentTag: z.string().trim().optional(),
|
||||||
|
metadata: z.record(z.string()).optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secrets = await server.services.secret.createManySecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
environment: req.body.environment,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
secrets: req.body.secrets
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/batch",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretName: z.string().trim(),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
|
secretValueCiphertext: z.string().trim(),
|
||||||
|
secretValueIV: z.string().trim(),
|
||||||
|
secretValueTag: z.string().trim(),
|
||||||
|
secretKeyCiphertext: z.string().trim(),
|
||||||
|
secretKeyIV: z.string().trim(),
|
||||||
|
secretKeyTag: z.string().trim(),
|
||||||
|
secretCommentCiphertext: z.string().trim().optional(),
|
||||||
|
secretCommentIV: z.string().trim().optional(),
|
||||||
|
secretCommentTag: z.string().trim().optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().optional(),
|
||||||
|
tags: z.string().array().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secrets = await server.services.secret.updateManySecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
environment: req.body.environment,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
secrets: req.body.secrets
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/batch",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretName: z.string().trim(),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secrets = await server.services.secret.deleteManySecret({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
environment: req.body.environment,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
secrets: req.body.secrets
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
TResetPasswordViaBackupKeyDTO
|
TResetPasswordViaBackupKeyDTO
|
||||||
} from "./auth-password-type";
|
} from "./auth-password-type";
|
||||||
import { AuthTokenType } from "./auth-type";
|
import { AuthTokenType } from "./auth-type";
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
|
||||||
|
|
||||||
type TAuthPasswordServiceFactoryDep = {
|
type TAuthPasswordServiceFactoryDep = {
|
||||||
authDal: TAuthDalFactory;
|
authDal: TAuthDalFactory;
|
||||||
|
|||||||
@@ -1,10 +1,63 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectEnvDalFactory = ReturnType<typeof projectEnvDalFactory>;
|
export type TProjectEnvDalFactory = ReturnType<typeof projectEnvDalFactory>;
|
||||||
|
|
||||||
export const projectEnvDalFactory = (db: TDbClient) => {
|
export const projectEnvDalFactory = (db: TDbClient) => {
|
||||||
const projectEnvOrm = ormify(db, TableName.Environment);
|
const projectEnvOrm = ormify(db, TableName.Environment);
|
||||||
return projectEnvOrm;
|
|
||||||
|
const findBySlugs = async (projectId: string, env: string[], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const envs = await (tx || db)(TableName.Environment)
|
||||||
|
.where("projectId", projectId)
|
||||||
|
.whereIn("slug", env);
|
||||||
|
return envs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by slugs" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// we are using postion based sorting as its a small list
|
||||||
|
// this will return the last value of the position in a folder with secret imports
|
||||||
|
const findLastEnvPosition = async (projectId: string, tx?: Knex) => {
|
||||||
|
const lastPos = await (tx || db)(TableName.Environment)
|
||||||
|
.where({ projectId })
|
||||||
|
.max("position")
|
||||||
|
.first();
|
||||||
|
return lastPos?.position || 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const incrementLastPosition = async (
|
||||||
|
projectId: string,
|
||||||
|
startPos: number,
|
||||||
|
increment = 1,
|
||||||
|
tx?: Knex
|
||||||
|
) =>
|
||||||
|
(tx || db)(TableName.Environment)
|
||||||
|
.where("projectId", projectId)
|
||||||
|
.where("postion", ">=", startPos)
|
||||||
|
.increment("position", increment);
|
||||||
|
|
||||||
|
const decrementLastPosition = async (
|
||||||
|
projectId: string,
|
||||||
|
startPos: number,
|
||||||
|
decrement = 1,
|
||||||
|
tx?: Knex
|
||||||
|
) =>
|
||||||
|
(tx || db)(TableName.Environment)
|
||||||
|
.where("projectId", projectId)
|
||||||
|
.where("postion", ">", startPos)
|
||||||
|
.decrement("position", decrement);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...projectEnvOrm,
|
||||||
|
findBySlugs,
|
||||||
|
findLastEnvPosition,
|
||||||
|
decrementLastPosition,
|
||||||
|
incrementLastPosition
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TProjectEnvDalFactory } from "./project-env-dal";
|
import { TProjectEnvDalFactory } from "./project-env-dal";
|
||||||
import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types";
|
import { TCreateEnvDTO, TDeleteEnvDTO, TReorderEnvDTO, TUpdateEnvDTO } from "./project-env-types";
|
||||||
|
|
||||||
type TProjectEnvServiceFactoryDep = {
|
type TProjectEnvServiceFactoryDep = {
|
||||||
projectEnvDal: TProjectEnvDalFactory;
|
projectEnvDal: TProjectEnvDalFactory;
|
||||||
@@ -36,7 +36,11 @@ export const projectEnvServiceFactory = ({
|
|||||||
name: "Create envv"
|
name: "Create envv"
|
||||||
});
|
});
|
||||||
|
|
||||||
const env = await projectEnvDal.create({ slug, name, projectId });
|
const env = await projectEnvDal.transaction(async (tx) => {
|
||||||
|
const lastPos = await projectEnvDal.findLastEnvPosition(projectId, tx);
|
||||||
|
const doc = await projectEnvDal.create({ slug, name, projectId, position: lastPos }, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
return env;
|
return env;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,13 +79,44 @@ export const projectEnvServiceFactory = ({
|
|||||||
ProjectPermissionSub.Environments
|
ProjectPermissionSub.Environments
|
||||||
);
|
);
|
||||||
|
|
||||||
const [env] = await projectEnvDal.delete({ id, projectId });
|
const env = await projectEnvDal.transaction(async (tx) => {
|
||||||
|
const [doc] = await projectEnvDal.delete({ id, projectId }, tx);
|
||||||
|
if (!doc)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Env doesn't exist",
|
||||||
|
name: "Re-order env"
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectEnvDal.decrementLastPosition(projectId, doc.position, 1, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
return env;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorderEnvironment = async ({ projectId, id, actorId, actor, pos }: TReorderEnvDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Environments
|
||||||
|
);
|
||||||
|
|
||||||
|
const [env] = await projectEnvDal.transaction(async (tx) => {
|
||||||
|
await projectEnvDal.incrementLastPosition(projectId, pos, 1, tx);
|
||||||
|
return projectEnvDal.update({ id, projectId }, { position: pos }, tx);
|
||||||
|
});
|
||||||
|
if (!env)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Env doesn't exist",
|
||||||
|
name: "Re-order env"
|
||||||
|
});
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
updateEnvironment,
|
updateEnvironment,
|
||||||
deleteEnvironment
|
deleteEnvironment,
|
||||||
|
reorderEnvironment
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ export type TUpdateEnvDTO = {
|
|||||||
export type TDeleteEnvDTO = {
|
export type TDeleteEnvDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TReorderEnvDTO = {
|
||||||
|
id: string;
|
||||||
|
pos: number;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
|
|
||||||
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TSecretBlindIndexDalFactory } from "../secret/secret-blind-index-dal";
|
||||||
|
import { ROOT_FOLDER_NAME, TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TProjectDalFactory } from "./project-dal";
|
import { TProjectDalFactory } from "./project-dal";
|
||||||
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
||||||
|
|
||||||
@@ -24,8 +28,10 @@ const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDal: TProjectDalFactory;
|
projectDal: TProjectDalFactory;
|
||||||
|
folderDal: Pick<TSecretFolderDalFactory, "insertMany">;
|
||||||
projectEnvDal: Pick<TProjectEnvDalFactory, "insertMany">;
|
projectEnvDal: Pick<TProjectEnvDalFactory, "insertMany">;
|
||||||
projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">;
|
projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">;
|
||||||
|
secretBlindIndexDal: Pick<TSecretBlindIndexDalFactory, "create">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,6 +40,8 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDal,
|
projectDal,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
folderDal,
|
||||||
|
secretBlindIndexDal,
|
||||||
projectMembershipDal,
|
projectMembershipDal,
|
||||||
projectEnvDal
|
projectEnvDal
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
@@ -47,9 +55,12 @@ export const projectServiceFactory = ({
|
|||||||
OrgPermissionSubjects.Workspace
|
OrgPermissionSubjects.Workspace
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
|
||||||
// TODO(backend-pg): licence server
|
// TODO(backend-pg): licence server
|
||||||
const newProject = projectDal.transaction(async (tx) => {
|
const newProject = projectDal.transaction(async (tx) => {
|
||||||
const project = await projectDal.create({ name: workspaceName, orgId }, tx);
|
const project = await projectDal.create({ name: workspaceName, orgId }, tx);
|
||||||
|
// set user as admin member for proeject
|
||||||
await projectMembershipDal.create(
|
await projectMembershipDal.create(
|
||||||
{
|
{
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
@@ -58,8 +69,25 @@ export const projectServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
// generate the blind index for project
|
||||||
|
await secretBlindIndexDal.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
keyEncoding: blindIndex.keyEncoding,
|
||||||
|
saltIV: blindIndex.iv,
|
||||||
|
saltTag: blindIndex.tag,
|
||||||
|
algorithm: blindIndex.algorithm,
|
||||||
|
encryptedSaltCipherText: blindIndex.ciphertext
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
// set default environments and root folder for provided environments
|
||||||
const envs = await projectEnvDal.insertMany(
|
const envs = await projectEnvDal.insertMany(
|
||||||
DEFAULT_PROJECT_ENVS.map((el) => ({ ...el, projectId: project.id })),
|
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await folderDal.insertMany(
|
||||||
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
return { ...project, environments: envs };
|
return { ...project, environments: envs };
|
||||||
|
|||||||
40
backend-pg/src/services/secret-folder/secret-folder-dal.ts
Normal file
40
backend-pg/src/services/secret-folder/secret-folder-dal.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName,TSecretFolders } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSecretFolderDalFactory = ReturnType<typeof secretFolderDalFactory>;
|
||||||
|
// never change this. If u do write a migration for it
|
||||||
|
export const ROOT_FOLDER_NAME = "root";
|
||||||
|
export const secretFolderDalFactory = (db: TDbClient) => {
|
||||||
|
const secretFolderOrm = ormify(db, TableName.SecretFolder);
|
||||||
|
|
||||||
|
const findBySecretPath = async (
|
||||||
|
projectId: string,
|
||||||
|
environment: string,
|
||||||
|
path: string,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const folder: TSecretFolders | undefined = await (tx || db)(TableName.SecretFolder)
|
||||||
|
.join(
|
||||||
|
TableName.Environment,
|
||||||
|
`${TableName.SecretFolder}.envId`,
|
||||||
|
`${TableName.Environment}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId)
|
||||||
|
.where(`${TableName.Environment}.slug`, environment)
|
||||||
|
.where(`${TableName.SecretFolder}.name`, "root")
|
||||||
|
.select(`${TableName.SecretFolder}.*`)
|
||||||
|
.first();
|
||||||
|
return folder;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by secret path" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...secretFolderOrm, findBySecretPath };
|
||||||
|
};
|
||||||
132
backend-pg/src/services/secret-folder/secret-folder-service.ts
Normal file
132
backend-pg/src/services/secret-folder/secret-folder-service.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
||||||
|
import { ROOT_FOLDER_NAME, TSecretFolderDalFactory } from "./secret-folder-dal";
|
||||||
|
import {
|
||||||
|
TCreateFolderDTO,
|
||||||
|
TDeleteFolderDTO,
|
||||||
|
TGetFolderDTO,
|
||||||
|
TUpdateFolderDTO
|
||||||
|
} from "./secret-folder-types";
|
||||||
|
|
||||||
|
type TSecretFolderServiceFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
folderDal: TSecretFolderDalFactory;
|
||||||
|
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
|
||||||
|
|
||||||
|
export const secretFolderServiceFactory = ({
|
||||||
|
folderDal,
|
||||||
|
permissionService,
|
||||||
|
projectEnvDal
|
||||||
|
}: TSecretFolderServiceFactoryDep) => {
|
||||||
|
const createFolder = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
name,
|
||||||
|
environment,
|
||||||
|
path
|
||||||
|
}: TCreateFolderDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||||
|
if (!env)
|
||||||
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
|
const folder = await folderDal.transaction(async (tx) => {
|
||||||
|
const doc = await folderDal.create({ name, envId: env.id, version: 1 }, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFolder = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
name,
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
id
|
||||||
|
}: TUpdateFolderDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||||
|
if (!env)
|
||||||
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
|
const folder = await folderDal.transaction(async (tx) => {
|
||||||
|
const [doc] = await folderDal.update({ envId: env.id, id }, { name, version: 1 }, tx);
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Update folder" });
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFolder = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
id
|
||||||
|
}: TDeleteFolderDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||||
|
if (!env)
|
||||||
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
|
const folder = await folderDal.transaction(async (tx) => {
|
||||||
|
const [doc] = await folderDal.delete({ envId: env.id, id }, tx);
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" });
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFolders = async ({ projectId, actor, actorId, environment }: TGetFolderDTO) => {
|
||||||
|
// folder list is allowed to be read by anyone
|
||||||
|
// permission to check does user has access
|
||||||
|
await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
|
||||||
|
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||||
|
if (!env)
|
||||||
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
|
const folders = await folderDal.find({ envId: env.id, parentId: null });
|
||||||
|
return folders.filter(({ name }) => name !== ROOT_FOLDER_NAME);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createFolder,
|
||||||
|
updateFolder,
|
||||||
|
deleteFolder,
|
||||||
|
getFolders
|
||||||
|
};
|
||||||
|
};
|
||||||
25
backend-pg/src/services/secret-folder/secret-folder-types.ts
Normal file
25
backend-pg/src/services/secret-folder/secret-folder-types.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateFolderDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateFolderDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteFolderDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetFolderDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
} & TProjectPermission;
|
||||||
83
backend-pg/src/services/secret-import/secret-import-dal.ts
Normal file
83
backend-pg/src/services/secret-import/secret-import-dal.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName,TSecretImports } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSecretImportDalFactory = ReturnType<typeof secretImportDalFactory>;
|
||||||
|
|
||||||
|
export const secretImportDalFactory = (db: TDbClient) => {
|
||||||
|
const secretImportOrm = ormify(db, TableName.SecretImport);
|
||||||
|
|
||||||
|
// we are using postion based sorting as its a small list
|
||||||
|
// this will return the last value of the position in a folder with secret imports
|
||||||
|
const findLastImportPosition = async (folderId: string, tx?: Knex) => {
|
||||||
|
const lastPos = await (tx || db)(TableName.SecretImport)
|
||||||
|
.where({ folderId })
|
||||||
|
.max({ position: "position" })
|
||||||
|
.first();
|
||||||
|
return lastPos?.position || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAllPosition = async (folderId: string, pos: number, targetPos: number, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
if (targetPos === -1) {
|
||||||
|
// this means delete
|
||||||
|
await (tx || db)(TableName.SecretImport)
|
||||||
|
.where({ folderId })
|
||||||
|
.andWhere("position", ">", pos)
|
||||||
|
.decrement("position", 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetPos > pos) {
|
||||||
|
await (tx || db)(TableName.SecretImport)
|
||||||
|
.where({ folderId })
|
||||||
|
.where("position", "<=", targetPos)
|
||||||
|
.andWhere("position", ">", pos)
|
||||||
|
.decrement("position", 1);
|
||||||
|
} else {
|
||||||
|
await (tx || db)(TableName.SecretImport)
|
||||||
|
.where({ folderId })
|
||||||
|
.where("position", ">=", targetPos)
|
||||||
|
.andWhere("position", "<", pos)
|
||||||
|
.increment("position", 1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update position" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const find = async (filter: Partial<TSecretImports>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db)(TableName.SecretImport)
|
||||||
|
.where(filter)
|
||||||
|
.join(
|
||||||
|
TableName.Environment,
|
||||||
|
`${TableName.SecretImport}.importEnv`,
|
||||||
|
`${TableName.Environment}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
|
||||||
|
db.ref("slug").withSchema(TableName.Environment),
|
||||||
|
db.ref("name").withSchema(TableName.Environment),
|
||||||
|
db.ref("id").withSchema(TableName.Environment).as("envId")
|
||||||
|
)
|
||||||
|
.orderBy("position", "asc");
|
||||||
|
return docs.map(({ envId, slug, name, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
importEnv: { id: envId, slug, name }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find secret imports" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secretImportOrm,
|
||||||
|
find,
|
||||||
|
findLastImportPosition,
|
||||||
|
updateAllPosition
|
||||||
|
};
|
||||||
|
};
|
||||||
177
backend-pg/src/services/secret-import/secret-import-service.ts
Normal file
177
backend-pg/src/services/secret-import/secret-import-service.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretImportDalFactory } from "./secret-import-dal";
|
||||||
|
import {
|
||||||
|
TCreateSecretImportDTO,
|
||||||
|
TDeleteSecretImportDTO,
|
||||||
|
TGetSecretImportsDTO,
|
||||||
|
TUpdateSecretImportDTO
|
||||||
|
} from "./secret-import-types";
|
||||||
|
|
||||||
|
type TSecretImportServiceFactoryDep = {
|
||||||
|
secretImportDal: TSecretImportDalFactory;
|
||||||
|
folderDal: TSecretFolderDalFactory;
|
||||||
|
projectEnvDal: TProjectEnvDalFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
|
||||||
|
|
||||||
|
export type TSecretImportServiceFactory = ReturnType<typeof secretImportServiceFactory>;
|
||||||
|
|
||||||
|
export const secretImportServiceFactory = ({
|
||||||
|
secretImportDal,
|
||||||
|
projectEnvDal,
|
||||||
|
permissionService,
|
||||||
|
folderDal
|
||||||
|
}: TSecretImportServiceFactoryDep) => {
|
||||||
|
const createImport = async ({
|
||||||
|
environment,
|
||||||
|
data,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
path
|
||||||
|
}: TCreateSecretImportDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
||||||
|
|
||||||
|
const [importEnv] = await projectEnvDal.findBySlugs(projectId, [data.environment]);
|
||||||
|
if (!importEnv)
|
||||||
|
throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
|
||||||
|
const secImport = await secretImportDal.transaction(async (tx) => {
|
||||||
|
const lastPos = await secretImportDal.findLastImportPosition(folder.id, tx);
|
||||||
|
return secretImportDal.create(
|
||||||
|
{
|
||||||
|
folderId: folder.id,
|
||||||
|
position: lastPos + 1,
|
||||||
|
importEnv: importEnv.id,
|
||||||
|
importPath: data.path
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...secImport, importEnv };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateImport = async ({
|
||||||
|
path,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
data,
|
||||||
|
id
|
||||||
|
}: TUpdateSecretImportDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" });
|
||||||
|
|
||||||
|
const secImpDoc = await secretImportDal.findOne({ folderId: folder.id, id });
|
||||||
|
if (!secImpDoc) throw ERR_SEC_IMP_NOT_FOUND;
|
||||||
|
|
||||||
|
const importedEnv = data.environment // this is get env information of new one or old one
|
||||||
|
? (await projectEnvDal.findBySlugs(projectId, [data.environment]))?.[0]
|
||||||
|
: await projectEnvDal.findById(secImpDoc.importEnv);
|
||||||
|
if (!importedEnv)
|
||||||
|
throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
|
||||||
|
const updatedSecImport = await secretImportDal.transaction(async (tx) => {
|
||||||
|
const secImp = await secretImportDal.findOne({ folderId: folder.id, id });
|
||||||
|
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
|
||||||
|
if (data.position) {
|
||||||
|
await secretImportDal.updateAllPosition(folder.id, secImp.position, data.position, tx);
|
||||||
|
}
|
||||||
|
const [doc] = await secretImportDal.update(
|
||||||
|
{ id, folderId: folder.id },
|
||||||
|
{
|
||||||
|
position: data?.position,
|
||||||
|
importEnv: data?.environment ? importedEnv.id : undefined,
|
||||||
|
importPath: data?.path
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
return { ...updatedSecImport, importEnv: importedEnv };
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteImport = async ({
|
||||||
|
path,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
id
|
||||||
|
}: TDeleteSecretImportDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Delete import" });
|
||||||
|
|
||||||
|
const secImport = await secretImportDal.transaction(async (tx) => {
|
||||||
|
const [doc] = await secretImportDal.delete({ folderId: folder.id, id }, tx);
|
||||||
|
if (!doc)
|
||||||
|
throw new BadRequestError({ name: "Sec imp del", message: "Secret import doc not found" });
|
||||||
|
await secretImportDal.updateAllPosition(folder.id, doc.position, -1, tx);
|
||||||
|
|
||||||
|
const importEnv = await projectEnvDal.findById(doc.importEnv);
|
||||||
|
if (!importEnv)
|
||||||
|
throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
return { ...doc, importEnv };
|
||||||
|
});
|
||||||
|
return secImport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImports = async ({
|
||||||
|
path,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId
|
||||||
|
}: TGetSecretImportsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
|
||||||
|
|
||||||
|
const secImports = await secretImportDal.find({ folderId: folder.id });
|
||||||
|
return secImports;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
createImport,
|
||||||
|
updateImport,
|
||||||
|
deleteImport,
|
||||||
|
getImports
|
||||||
|
};
|
||||||
|
};
|
||||||
28
backend-pg/src/services/secret-import/secret-import-types.ts
Normal file
28
backend-pg/src/services/secret-import/secret-import-types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateSecretImportDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
data: {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateSecretImportDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
data: Partial<{ environment: string; path: string; position: number }>;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteSecretImportDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetSecretImportsDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
} & TProjectPermission;
|
||||||
10
backend-pg/src/services/secret/secret-blind-index-dal.ts
Normal file
10
backend-pg/src/services/secret/secret-blind-index-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSecretBlindIndexDalFactory = ReturnType<typeof secretBlindIndexDalFactory>;
|
||||||
|
|
||||||
|
export const secretBlindIndexDalFactory = (db: TDbClient) => {
|
||||||
|
const secretBlindIndexOrm = ormify(db, TableName.SecretBlindIndex);
|
||||||
|
return secretBlindIndexOrm;
|
||||||
|
};
|
||||||
110
backend-pg/src/services/secret/secret-dal.ts
Normal file
110
backend-pg/src/services/secret/secret-dal.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { SecretType, TableName, TSecrets, TSecretsInsert,TSecretsUpdate } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSecretDalFactory = ReturnType<typeof secretDalFactory>;
|
||||||
|
|
||||||
|
export const secretDalFactory = (db: TDbClient) => {
|
||||||
|
const secretOrm = ormify(db, TableName.Secret);
|
||||||
|
|
||||||
|
const update = async (
|
||||||
|
filter: Partial<TSecrets>,
|
||||||
|
data: Omit<TSecretsUpdate, "version">,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const sec = await (tx || db)(TableName.Secret)
|
||||||
|
.where(filter)
|
||||||
|
.update(data)
|
||||||
|
.increment("version", 1)
|
||||||
|
.returning("*");
|
||||||
|
return sec;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// the idea is to use postgres specific function
|
||||||
|
// insert with id this will cause a conflict then merge the data
|
||||||
|
const bulkUpdate = async (data: Array<TSecretsUpdate & { id: string }>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const secs = await (tx || db)(TableName.Secret)
|
||||||
|
.insert(data as TSecretsInsert[])
|
||||||
|
.onConflict("id")
|
||||||
|
.merge()
|
||||||
|
.returning("*");
|
||||||
|
return secs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMany = async (
|
||||||
|
data: Array<{ blindIndex: string; type: SecretType }>,
|
||||||
|
folderId: string,
|
||||||
|
userId: string,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const deletedSecrets = await (tx || db)(TableName.Secret)
|
||||||
|
.where({ folderId })
|
||||||
|
.where((bd) => {
|
||||||
|
data.forEach((el) => {
|
||||||
|
bd.orWhere({
|
||||||
|
secretBlindIndex: el.blindIndex,
|
||||||
|
type: el.type,
|
||||||
|
userId: el.type === SecretType.Personal ? userId : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.delete()
|
||||||
|
.returning("*");
|
||||||
|
return deletedSecrets;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "delete many secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const sec = await (tx || db)(TableName.Secret)
|
||||||
|
.where({ folderId })
|
||||||
|
.where((bd) => {
|
||||||
|
bd.whereNull("userId").orWhere({ userId: userId || null });
|
||||||
|
});
|
||||||
|
return sec;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get all secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByBlindIndexes = async (
|
||||||
|
folderId: string,
|
||||||
|
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
|
||||||
|
userId?: string,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
if (!blindIndexes.length) return [];
|
||||||
|
try {
|
||||||
|
const secrets = await (tx || db)(TableName.Secret)
|
||||||
|
.where({ folderId })
|
||||||
|
.where((bd) => {
|
||||||
|
blindIndexes.forEach((el) => {
|
||||||
|
bd.orWhere({
|
||||||
|
secretBlindIndex: el.blindIndex,
|
||||||
|
type: el.type,
|
||||||
|
userId: el.type === SecretType.Personal ? userId : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return secrets;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "find by blind indexes" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
|
||||||
|
};
|
||||||
589
backend-pg/src/services/secret/secret-service.ts
Normal file
589
backend-pg/src/services/secret/secret-service.ts
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SecretEncryptionAlgo,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
SecretType,
|
||||||
|
TSecretBlindIndexes,
|
||||||
|
TSecrets
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { buildSecretBlindIndexFromName } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretBlindIndexDalFactory } from "./secret-blind-index-dal";
|
||||||
|
import { TSecretDalFactory } from "./secret-dal";
|
||||||
|
import {
|
||||||
|
TCreateBulkSecretDTO,
|
||||||
|
TCreateSecretDTO,
|
||||||
|
TDeleteBulkSecretDTO,
|
||||||
|
TDeleteSecretDTO,
|
||||||
|
TGetASecretDTO,
|
||||||
|
TGetSecretsDTO,
|
||||||
|
TUpdateBulkSecretDTO,
|
||||||
|
TUpdateSecretDTO
|
||||||
|
} from "./secret-types";
|
||||||
|
import { TSecretVersionDalFactory } from "./secret-version-dal";
|
||||||
|
|
||||||
|
type TSecretServiceFactoryDep = {
|
||||||
|
secretDal: TSecretDalFactory;
|
||||||
|
secretVersionDal: TSecretVersionDalFactory;
|
||||||
|
folderDal: TSecretFolderDalFactory;
|
||||||
|
secretBlindIndexDal: TSecretBlindIndexDalFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
||||||
|
|
||||||
|
export const secretServiceFactory = ({
|
||||||
|
secretDal,
|
||||||
|
secretVersionDal,
|
||||||
|
folderDal,
|
||||||
|
secretBlindIndexDal,
|
||||||
|
permissionService
|
||||||
|
}: TSecretServiceFactoryDep) => {
|
||||||
|
const generateSecretBlindIndexBySalt = async (
|
||||||
|
secretName: string,
|
||||||
|
secretBlindIndexDoc: TSecretBlindIndexes
|
||||||
|
) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const secretBlindIndex = await buildSecretBlindIndexFromName({
|
||||||
|
secretName,
|
||||||
|
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
|
||||||
|
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
|
||||||
|
encryptionKey: appCfg.ENCRYPTION_KEY,
|
||||||
|
tag: secretBlindIndexDoc.saltTag,
|
||||||
|
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
|
||||||
|
iv: secretBlindIndexDoc.saltIV
|
||||||
|
});
|
||||||
|
return secretBlindIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
// utility function to get secret blind index data
|
||||||
|
const generateSecretBlindIndexByName = async (projectId: string, secretName: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const secretBlindIndexDoc = await secretBlindIndexDal.findOne({ projectId });
|
||||||
|
if (!secretBlindIndexDoc)
|
||||||
|
throw new BadRequestError({ message: "Blind index not found", name: "Create secret" });
|
||||||
|
|
||||||
|
const secretBlindIndex = await buildSecretBlindIndexFromName({
|
||||||
|
secretName,
|
||||||
|
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
|
||||||
|
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
|
||||||
|
encryptionKey: appCfg.ENCRYPTION_KEY,
|
||||||
|
tag: secretBlindIndexDoc.saltTag,
|
||||||
|
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
|
||||||
|
iv: secretBlindIndexDoc.saltIV
|
||||||
|
});
|
||||||
|
if (!secretBlindIndex) throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
return secretBlindIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSecret = async ({
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
...inputSecret
|
||||||
|
}: TCreateSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexByName(
|
||||||
|
projectId,
|
||||||
|
inputSecret.secretName
|
||||||
|
);
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const existingSecret = await secretDal.findOne({
|
||||||
|
secretBlindIndex,
|
||||||
|
folderId,
|
||||||
|
type: inputSecret.type,
|
||||||
|
userId: inputSecret.type === SecretType.Personal ? actorId : null
|
||||||
|
});
|
||||||
|
if (existingSecret) throw new BadRequestError({ message: "Secret already exist" });
|
||||||
|
// if user creating personal check its shared also exist
|
||||||
|
if (inputSecret.type === SecretType.Personal) {
|
||||||
|
const sharedExist = await secretDal.findOne({
|
||||||
|
secretBlindIndex,
|
||||||
|
folderId,
|
||||||
|
type: SecretType.Shared
|
||||||
|
});
|
||||||
|
if (!sharedExist)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create personal secret override for no corresponding shared secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = await secretDal.transaction(async (tx) => {
|
||||||
|
const { secretName, type, ...el } = inputSecret;
|
||||||
|
const doc = await secretDal.create(
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
folderId,
|
||||||
|
secretBlindIndex,
|
||||||
|
type,
|
||||||
|
...el,
|
||||||
|
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await secretVersionDal.create(
|
||||||
|
{
|
||||||
|
secretBlindIndex,
|
||||||
|
folderId,
|
||||||
|
version: 1,
|
||||||
|
type,
|
||||||
|
...el,
|
||||||
|
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8,
|
||||||
|
secretId: doc.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||||
|
return secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSecret = async ({
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
...inputSecret
|
||||||
|
}: TUpdateSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const blindIndexDoc = await secretBlindIndexDal.findOne({ projectId });
|
||||||
|
if (!blindIndexDoc)
|
||||||
|
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
const oldBlindIndex = await generateSecretBlindIndexBySalt(
|
||||||
|
inputSecret.secretName,
|
||||||
|
blindIndexDoc
|
||||||
|
);
|
||||||
|
if (!oldBlindIndex) throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
let newSecretNameBlindIndex: string;
|
||||||
|
if (inputSecret?.newSecretName && inputSecret.type === SecretType.Shared) {
|
||||||
|
newSecretNameBlindIndex = await generateSecretBlindIndexBySalt(
|
||||||
|
inputSecret.newSecretName,
|
||||||
|
blindIndexDoc
|
||||||
|
);
|
||||||
|
const doesSecretExist = await secretDal.findOne({
|
||||||
|
secretBlindIndex: newSecretNameBlindIndex,
|
||||||
|
folderId,
|
||||||
|
type: inputSecret.type
|
||||||
|
});
|
||||||
|
if (doesSecretExist) {
|
||||||
|
throw new BadRequestError({ message: "Secret with the provided name already exist" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSecret = await secretDal.transaction(async (tx) => {
|
||||||
|
const { secretName, ...el } = inputSecret;
|
||||||
|
const [doc] = await secretDal.update(
|
||||||
|
{
|
||||||
|
secretBlindIndex: oldBlindIndex,
|
||||||
|
folderId,
|
||||||
|
type: inputSecret.type,
|
||||||
|
userId: inputSecret.type === SecretType.Personal ? actorId : null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretBlindIndex: newSecretNameBlindIndex,
|
||||||
|
...el
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const { id, createdAt, updatedAt, ...newVersion } = doc;
|
||||||
|
await secretVersionDal.create(
|
||||||
|
{
|
||||||
|
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
||||||
|
secretId: doc.id,
|
||||||
|
...newVersion
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||||
|
return updatedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSecret = async ({
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
...inputSecret
|
||||||
|
}: TDeleteSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexByName(
|
||||||
|
projectId,
|
||||||
|
inputSecret.secretName
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const deletedSecret = await secretDal.transaction(async (tx) => {
|
||||||
|
const [doc] = await secretDal.delete(
|
||||||
|
{
|
||||||
|
secretBlindIndex,
|
||||||
|
folderId,
|
||||||
|
type: inputSecret.type,
|
||||||
|
userId: inputSecret.type === SecretType.Personal ? actorId : null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||||
|
return deletedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecrets = async ({ actorId, path, environment, projectId, actor }: TGetSecretsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const secrets = await secretDal.findByFolderId(folderId, actorId);
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getASecret = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
secretName
|
||||||
|
}: TGetASecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const secretBlindIndex = await generateSecretBlindIndexByName(projectId, secretName);
|
||||||
|
|
||||||
|
const secret = await secretDal.findOne({
|
||||||
|
folderId,
|
||||||
|
type,
|
||||||
|
userId: type === SecretType.Personal ? actorId : null,
|
||||||
|
secretBlindIndex
|
||||||
|
});
|
||||||
|
if (!secret) throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createManySecret = async ({
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
secrets: inputSecrets
|
||||||
|
}: TCreateBulkSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const blindIndexDoc = await secretBlindIndexDal.findOne({ projectId });
|
||||||
|
if (!blindIndexDoc)
|
||||||
|
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
|
||||||
|
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||||
|
const secretBlindIndexes = await Promise.all(
|
||||||
|
inputSecrets.map(({ secretName }) =>
|
||||||
|
generateSecretBlindIndexBySalt(secretName, blindIndexDoc)
|
||||||
|
)
|
||||||
|
).then((blindIndexes) =>
|
||||||
|
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
prev[inputSecrets[i].secretName] = curr;
|
||||||
|
secretBlindIndexToKey[curr] = inputSecrets[i].secretName;
|
||||||
|
return prev;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const exists = await secretDal.findByBlindIndexes(
|
||||||
|
folderId,
|
||||||
|
inputSecrets.map(({ type, secretName }) => ({
|
||||||
|
blindIndex: secretBlindIndexes[secretName],
|
||||||
|
type
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
if (exists.length) throw new BadRequestError({ message: "Secret already exist" });
|
||||||
|
|
||||||
|
const secrets = await secretDal.transaction(async (tx) => {
|
||||||
|
const newSecrets = await secretDal.insertMany(
|
||||||
|
inputSecrets.map(({ secretName, type, ...el }) => ({
|
||||||
|
version: 1,
|
||||||
|
folderId,
|
||||||
|
type,
|
||||||
|
secretBlindIndex: secretBlindIndexes[secretName],
|
||||||
|
...el,
|
||||||
|
userId: type === SecretType.Personal ? actorId : null,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await secretVersionDal.insertMany(
|
||||||
|
newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
secretId: id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return newSecrets;
|
||||||
|
});
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateManySecret = async ({
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
secrets: inputSecrets
|
||||||
|
}: TUpdateBulkSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const blindIndexDoc = await secretBlindIndexDal.findOne({ projectId });
|
||||||
|
if (!blindIndexDoc)
|
||||||
|
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
|
||||||
|
// get all blind index
|
||||||
|
// Find all those secrets
|
||||||
|
// if not throw not found
|
||||||
|
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||||
|
const secretBlindIndexes = await Promise.all(
|
||||||
|
inputSecrets.map(({ secretName }) =>
|
||||||
|
generateSecretBlindIndexBySalt(secretName, blindIndexDoc)
|
||||||
|
)
|
||||||
|
).then((blindIndexes) =>
|
||||||
|
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
prev[inputSecrets[i].secretName] = curr;
|
||||||
|
secretBlindIndexToKey[curr] = inputSecrets[i].secretName;
|
||||||
|
return prev;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretsToBeUpdated = await secretDal.findByBlindIndexes(
|
||||||
|
folderId,
|
||||||
|
inputSecrets.map(({ type, secretName }) => ({
|
||||||
|
blindIndex: secretBlindIndexes[secretName],
|
||||||
|
type
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
if (secretsToBeUpdated.length !== inputSecrets.length)
|
||||||
|
throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
|
||||||
|
// now find any secret that needs to update its name
|
||||||
|
// same process as above
|
||||||
|
const nameUpdatedSecrets = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName));
|
||||||
|
const newSecretBlindIndexes = await Promise.all(
|
||||||
|
nameUpdatedSecrets.map(({ newSecretName }) =>
|
||||||
|
generateSecretBlindIndexBySalt(newSecretName as string, blindIndexDoc)
|
||||||
|
)
|
||||||
|
).then((blindIndexes) =>
|
||||||
|
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
prev[nameUpdatedSecrets[i].secretName] = curr;
|
||||||
|
return prev;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
const secretsWithNewName = await secretDal.findByBlindIndexes(
|
||||||
|
folderId,
|
||||||
|
nameUpdatedSecrets.map(({ type, newSecretName }) => ({
|
||||||
|
blindIndex: newSecretBlindIndexes[newSecretName as string],
|
||||||
|
type
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
if (secretsWithNewName.length) throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
|
||||||
|
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, TSecrets>>(
|
||||||
|
(prev, curr) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
|
||||||
|
return prev;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const secrets = await secretDal.transaction(async (tx) => {
|
||||||
|
const newSecrets = await secretDal.bulkUpdate(
|
||||||
|
inputSecrets.map(({ secretName, type, ...el }) => {
|
||||||
|
const { version, updatedAt, ...info } =
|
||||||
|
secretsGroupedByBlindIndex[secretBlindIndexes[secretName]];
|
||||||
|
return {
|
||||||
|
version: (version || 0) + 1,
|
||||||
|
...info,
|
||||||
|
folderId,
|
||||||
|
type,
|
||||||
|
secretBlindIndex:
|
||||||
|
el?.newSecretName && newSecretBlindIndexes[el.newSecretName]
|
||||||
|
? newSecretBlindIndexes[el.newSecretName]
|
||||||
|
: secretBlindIndexes[secretName],
|
||||||
|
...el,
|
||||||
|
userId: type === SecretType.Personal ? actorId : null,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await secretVersionDal.insertMany(
|
||||||
|
newSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
secretId: id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return newSecrets;
|
||||||
|
});
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteManySecret = async ({
|
||||||
|
secrets: inputSecrets,
|
||||||
|
path,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId
|
||||||
|
}: TDeleteBulkSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const blindIndexDoc = await secretBlindIndexDal.findOne({ projectId });
|
||||||
|
if (!blindIndexDoc)
|
||||||
|
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
|
||||||
|
// get all blind index
|
||||||
|
// Find all those secrets
|
||||||
|
// if not throw not found
|
||||||
|
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||||
|
const secretBlindIndexes = await Promise.all(
|
||||||
|
inputSecrets.map(({ secretName }) =>
|
||||||
|
generateSecretBlindIndexBySalt(secretName, blindIndexDoc)
|
||||||
|
)
|
||||||
|
).then((blindIndexes) =>
|
||||||
|
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
prev[inputSecrets[i].secretName] = curr;
|
||||||
|
secretBlindIndexToKey[curr] = inputSecrets[i].secretName;
|
||||||
|
return prev;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
// not find those secrets. if any of them not found throw an not found error
|
||||||
|
const secretsToBeDeleted = await secretDal.findByBlindIndexes(
|
||||||
|
folderId,
|
||||||
|
inputSecrets.map(({ type, secretName }) => ({
|
||||||
|
blindIndex: secretBlindIndexes[secretName],
|
||||||
|
type
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
if (secretsToBeDeleted.length !== inputSecrets.length)
|
||||||
|
throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
const secretsDeleted = await secretDal.transaction(async (tx) =>
|
||||||
|
secretDal.deleteMany(
|
||||||
|
inputSecrets.map(({ type, secretName }) => ({
|
||||||
|
blindIndex: secretBlindIndexes[secretName],
|
||||||
|
type
|
||||||
|
})),
|
||||||
|
folderId,
|
||||||
|
actorId,
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return secretsDeleted;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSecret,
|
||||||
|
deleteSecret,
|
||||||
|
updateSecret,
|
||||||
|
createManySecret,
|
||||||
|
updateManySecret,
|
||||||
|
deleteManySecret,
|
||||||
|
getASecret,
|
||||||
|
getSecrets
|
||||||
|
};
|
||||||
|
};
|
||||||
115
backend-pg/src/services/secret/secret-types.ts
Normal file
115
backend-pg/src/services/secret/secret-types.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { SecretType } from "@app/db/schemas";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateSecretDTO = {
|
||||||
|
secretName: string;
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
type: "shared" | "personal";
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext?: string;
|
||||||
|
secretCommentIV?: string;
|
||||||
|
secretCommentTag?: string;
|
||||||
|
skipMultilineEncoding?: boolean;
|
||||||
|
secretReminderRepeatDays?: number | null;
|
||||||
|
secretReminderNote?: string | null;
|
||||||
|
metadata?: {
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateSecretDTO = {
|
||||||
|
secretName: string;
|
||||||
|
path: string;
|
||||||
|
newSecretName?: string;
|
||||||
|
environment: string;
|
||||||
|
type: "shared" | "personal";
|
||||||
|
secretKeyCiphertext?: string;
|
||||||
|
secretKeyIV?: string;
|
||||||
|
secretKeyTag?: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext?: string;
|
||||||
|
secretCommentIV?: string;
|
||||||
|
secretCommentTag?: string;
|
||||||
|
skipMultilineEncoding?: boolean;
|
||||||
|
secretReminderRepeatDays?: number | null;
|
||||||
|
secretReminderNote?: string | null;
|
||||||
|
metadata?: {
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteSecretDTO = {
|
||||||
|
secretName: string;
|
||||||
|
secretId?: string;
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
type: "shared" | "personal";
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetSecretsDTO = {
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetASecretDTO = {
|
||||||
|
secretName: string;
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
type: "shared" | "personal";
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TCreateBulkSecretDTO = {
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
secrets: Array<{
|
||||||
|
secretName: string;
|
||||||
|
type: SecretType;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext?: string;
|
||||||
|
secretCommentIV?: string;
|
||||||
|
secretCommentTag?: string;
|
||||||
|
skipMultilineEncoding?: boolean;
|
||||||
|
metadata?: {
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateBulkSecretDTO = {
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
secrets: Array<{
|
||||||
|
type: SecretType;
|
||||||
|
secretName: string;
|
||||||
|
newSecretName?: string;
|
||||||
|
secretValueCiphertext?: string;
|
||||||
|
secretValueIV?: string;
|
||||||
|
secretValueTag?: string;
|
||||||
|
secretCommentCiphertext?: string;
|
||||||
|
secretCommentIV?: string;
|
||||||
|
secretCommentTag?: string;
|
||||||
|
skipMultilineEncoding?: boolean;
|
||||||
|
}>;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteBulkSecretDTO = {
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
secrets: Array<{
|
||||||
|
type: SecretType;
|
||||||
|
secretName: string;
|
||||||
|
}>;
|
||||||
|
} & TProjectPermission;
|
||||||
10
backend-pg/src/services/secret/secret-version-dal.ts
Normal file
10
backend-pg/src/services/secret/secret-version-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSecretVersionDalFactory = ReturnType<typeof secretVersionDalFactory>;
|
||||||
|
|
||||||
|
export const secretVersionDalFactory = (db: TDbClient) => {
|
||||||
|
const secretVersionOrm = ormify(db, TableName.SecretVersion);
|
||||||
|
return secretVersionOrm;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TGetUserProjectPermissionDTO,
|
TGetUserProjectPermissionDTO,
|
||||||
TOrgRole,
|
TOrgRole,
|
||||||
TPermission,
|
TPermission,
|
||||||
|
TProjectPermission,
|
||||||
TProjectRole
|
TProjectRole
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ const getProjectRoles = async (projectId: string) => {
|
|||||||
}>(`/api/ee/v1/workspace/${projectId}/roles`);
|
}>(`/api/ee/v1/workspace/${projectId}/roles`);
|
||||||
return data.data.roles.map(({ permissions, ...el }) => ({
|
return data.data.roles.map(({ permissions, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
permissions: unpackRules(permissions as PackRule<TPermission>[])
|
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type TProjectRole = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions: TPermission[];
|
permissions: TProjectPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgRole = {
|
export type TOrgRole = {
|
||||||
@@ -67,13 +67,13 @@ export type TCreateProjectRoleDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
permissions: TPermission[];
|
permissions: TProjectPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateProjectRoleDTO = {
|
export type TUpdateProjectRoleDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
id: string;
|
id: string;
|
||||||
} & Partial<Omit<TCreateOrgRoleDTO, "orgId">>;
|
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
|
||||||
|
|
||||||
export type TDeleteProjectRoleDTO = {
|
export type TDeleteProjectRoleDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -20,25 +20,25 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const queryKeys = {
|
const queryKeys = {
|
||||||
getSecretFolders: ({ workspaceId, environment, directory }: TGetProjectFoldersDTO) =>
|
getSecretFolders: ({ projectId, environment, path }: TGetProjectFoldersDTO) =>
|
||||||
["secret-folders", { workspaceId, environment, directory }] as const
|
["secret-folders", { projectId, environment, path }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchProjectFolders = async (workspaceId: string, environment: string, directory = "/") => {
|
const fetchProjectFolders = async (projectId: string, environment: string, path = "/") => {
|
||||||
const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", {
|
const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", {
|
||||||
params: {
|
params: {
|
||||||
workspaceId,
|
projectId,
|
||||||
environment,
|
environment,
|
||||||
directory
|
path
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data.folders;
|
return data.folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProjectFolders = ({
|
export const useGetProjectFolders = ({
|
||||||
workspaceId,
|
projectId,
|
||||||
environment,
|
environment,
|
||||||
directory = "/",
|
path = "/",
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetProjectFoldersDTO & {
|
}: TGetProjectFoldersDTO & {
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
@@ -53,21 +53,21 @@ export const useGetProjectFolders = ({
|
|||||||
}) =>
|
}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
...options,
|
...options,
|
||||||
queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
|
queryKey: queryKeys.getSecretFolders({ projectId, environment, path }),
|
||||||
enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
|
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
|
||||||
queryFn: async () => fetchProjectFolders(workspaceId, environment, directory)
|
queryFn: async () => fetchProjectFolders(projectId, environment, path)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetFoldersByEnv = ({
|
export const useGetFoldersByEnv = ({
|
||||||
directory = "/",
|
path = "/",
|
||||||
workspaceId,
|
projectId,
|
||||||
environments
|
environments
|
||||||
}: TGetFoldersByEnvDTO) => {
|
}: TGetFoldersByEnvDTO) => {
|
||||||
const folders = useQueries({
|
const folders = useQueries({
|
||||||
queries: environments.map((environment) => ({
|
queries: environments.map((environment) => ({
|
||||||
queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
|
queryKey: queryKeys.getSecretFolders({ projectId, environment, path }),
|
||||||
queryFn: async () => fetchProjectFolders(workspaceId, environment, directory),
|
queryFn: async () => fetchProjectFolders(projectId, environment, path),
|
||||||
enabled: Boolean(workspaceId) && Boolean(environment)
|
enabled: Boolean(projectId) && Boolean(environment)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,15 +105,13 @@ export const useCreateFolder = () => {
|
|||||||
const { data } = await apiRequest.post("/api/v1/folders", dto);
|
const { data } = await apiRequest.post("/api/v1/folders", dto);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
onSuccess: (_, { projectId, environment, path }) => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
queryKeys.getSecretFolders({ workspaceId, environment, directory })
|
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretSnapshotKeys.list({ workspaceId, environment, directory })
|
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
);
|
|
||||||
queryClient.invalidateQueries(
|
|
||||||
secretSnapshotKeys.count({ workspaceId, environment, directory })
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -123,24 +121,22 @@ export const useUpdateFolder = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TUpdateFolderDTO>({
|
return useMutation<{}, {}, TUpdateFolderDTO>({
|
||||||
mutationFn: async ({ directory = "/", folderName, name, environment, workspaceId }) => {
|
mutationFn: async ({ path = "/", folderId, name, environment, projectId }) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/folders/${folderName}`, {
|
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
|
||||||
name,
|
name,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
projectId,
|
||||||
directory
|
path
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
onSuccess: (_, { projectId, environment, path }) => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
queryKeys.getSecretFolders({ workspaceId, environment, directory })
|
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretSnapshotKeys.list({ workspaceId, environment, directory })
|
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
);
|
|
||||||
queryClient.invalidateQueries(
|
|
||||||
secretSnapshotKeys.count({ workspaceId, environment, directory })
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -150,25 +146,23 @@ export const useDeleteFolder = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TDeleteFolderDTO>({
|
return useMutation<{}, {}, TDeleteFolderDTO>({
|
||||||
mutationFn: async ({ directory = "/", folderName, environment, workspaceId }) => {
|
mutationFn: async ({ path = "/", folderId, environment, projectId }) => {
|
||||||
const { data } = await apiRequest.delete(`/api/v1/folders/${folderName}`, {
|
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, {
|
||||||
data: {
|
data: {
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
projectId,
|
||||||
directory
|
path
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { directory = "/", workspaceId, environment }) => {
|
onSuccess: (_, { path = "/", projectId, environment }) => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
queryKeys.getSecretFolders({ workspaceId, environment, directory })
|
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretSnapshotKeys.list({ workspaceId, environment, directory })
|
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
);
|
|
||||||
queryClient.invalidateQueries(
|
|
||||||
secretSnapshotKeys.count({ workspaceId, environment, directory })
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,35 +4,35 @@ export type TSecretFolder = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TGetProjectFoldersDTO = {
|
export type TGetProjectFoldersDTO = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetFoldersByEnvDTO = {
|
export type TGetFoldersByEnvDTO = {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateFolderDTO = {
|
export type TCreateFolderDTO = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
folderName: string;
|
name: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateFolderDTO = {
|
export type TUpdateFolderDTO = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
name: string;
|
name: string;
|
||||||
folderName: string;
|
folderId: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteFolderDTO = {
|
export type TDeleteFolderDTO = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
folderName: string;
|
folderId: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ export const useCreateSecretImport = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TCreateSecretImportDTO>({
|
return useMutation<{}, {}, TCreateSecretImportDTO>({
|
||||||
mutationFn: async ({ secretImport, environment, workspaceId, directory }) => {
|
mutationFn: async ({ import: secretImport, environment, projectId, path }) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/secret-imports", {
|
const { data } = await apiRequest.post("/api/v1/secret-imports", {
|
||||||
secretImport,
|
import: secretImport,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
projectId,
|
||||||
directory
|
path
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
onSuccess: (_, { environment, projectId, path }) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
|
secretImportKeys.getProjectSecretImports({ projectId, environment, path })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
|
secretImportKeys.getSecretImportSecrets({ projectId, environment, path })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -33,21 +33,21 @@ export const useUpdateSecretImport = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
||||||
mutationFn: async ({ environment, workspaceId, directory, secretImports, id }) => {
|
mutationFn: async ({ environment, import: secretImports, projectId, path, id }) => {
|
||||||
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/secret-imports/${id}`, {
|
||||||
secretImports,
|
import: secretImports,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
path,
|
||||||
directory
|
projectId
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
onSuccess: (_, { environment, projectId, path }) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
|
secretImportKeys.getProjectSecretImports({ projectId, path, environment })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
|
secretImportKeys.getSecretImportSecrets({ environment, path, projectId })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -57,21 +57,22 @@ export const useDeleteSecretImport = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
||||||
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
|
mutationFn: async ({ id, projectId, path, environment }) => {
|
||||||
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
||||||
data: {
|
data: {
|
||||||
secretImportPath,
|
projectId,
|
||||||
secretImportEnv
|
path,
|
||||||
|
environment
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
onSuccess: (_, { projectId, environment, path }) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
|
secretImportKeys.getProjectSecretImports({ projectId, environment, path })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
|
secretImportKeys.getSecretImportSecrets({ projectId, environment, path })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,44 +7,44 @@ import {
|
|||||||
} from "@app/components/utilities/cryptography/crypto";
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImports } from "./types";
|
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImport } from "./types";
|
||||||
|
|
||||||
export const secretImportKeys = {
|
export const secretImportKeys = {
|
||||||
getProjectSecretImports: ({ environment, workspaceId, directory }: TGetSecretImports) =>
|
getProjectSecretImports: ({ environment, projectId, path }: TGetSecretImports) =>
|
||||||
[{ workspaceId, directory, environment }, "secrets-imports"] as const,
|
[{ projectId, path, environment }, "secrets-imports"] as const,
|
||||||
getSecretImportSecrets: ({
|
getSecretImportSecrets: ({
|
||||||
workspaceId,
|
|
||||||
environment,
|
environment,
|
||||||
directory
|
projectId,
|
||||||
|
path
|
||||||
}: Omit<TGetImportedSecrets, "decryptFileKey">) =>
|
}: Omit<TGetImportedSecrets, "decryptFileKey">) =>
|
||||||
[{ workspaceId, environment, directory }, "secrets-import-sec"] as const
|
[{ environment, path, projectId }, "secrets-import-sec"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSecretImport = async ({ workspaceId, environment, directory }: TGetSecretImports) => {
|
const fetchSecretImport = async ({ projectId, environment, path = "/" }: TGetSecretImports) => {
|
||||||
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
|
const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>(
|
||||||
"/api/v1/secret-imports",
|
"/api/v1/secret-imports",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
workspaceId,
|
projectId,
|
||||||
environment,
|
environment,
|
||||||
directory
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.secretImport;
|
return data.secretImports;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetSecretImports = ({
|
export const useGetSecretImports = ({
|
||||||
workspaceId,
|
|
||||||
environment,
|
environment,
|
||||||
directory = "/",
|
path = "/",
|
||||||
|
projectId,
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetSecretImports & {
|
}: TGetSecretImports & {
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
TSecretImports,
|
TSecretImport[],
|
||||||
unknown,
|
unknown,
|
||||||
TSecretImports,
|
TSecretImport[],
|
||||||
ReturnType<typeof secretImportKeys.getProjectSecretImports>
|
ReturnType<typeof secretImportKeys.getProjectSecretImports>
|
||||||
>,
|
>,
|
||||||
"queryKey" | "queryFn"
|
"queryKey" | "queryFn"
|
||||||
@@ -52,9 +52,9 @@ export const useGetSecretImports = ({
|
|||||||
}) =>
|
}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
...options,
|
...options,
|
||||||
queryKey: secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory }),
|
queryKey: secretImportKeys.getProjectSecretImports({ environment, projectId, path }),
|
||||||
enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
|
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
|
||||||
queryFn: () => fetchSecretImport({ workspaceId, environment, directory })
|
queryFn: () => fetchSecretImport({ path, projectId, environment })
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchImportedSecrets = async (
|
const fetchImportedSecrets = async (
|
||||||
@@ -76,10 +76,10 @@ const fetchImportedSecrets = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useGetImportedSecrets = ({
|
export const useGetImportedSecrets = ({
|
||||||
workspaceId,
|
|
||||||
environment,
|
environment,
|
||||||
decryptFileKey,
|
decryptFileKey,
|
||||||
directory,
|
path,
|
||||||
|
projectId,
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetImportedSecrets & {
|
}: TGetImportedSecrets & {
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
@@ -94,16 +94,16 @@ export const useGetImportedSecrets = ({
|
|||||||
}) =>
|
}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
enabled:
|
enabled:
|
||||||
Boolean(workspaceId) &&
|
Boolean(projectId) &&
|
||||||
Boolean(environment) &&
|
Boolean(environment) &&
|
||||||
Boolean(decryptFileKey) &&
|
Boolean(decryptFileKey) &&
|
||||||
(options?.enabled ?? true),
|
(options?.enabled ?? true),
|
||||||
queryKey: secretImportKeys.getSecretImportSecrets({
|
queryKey: secretImportKeys.getSecretImportSecrets({
|
||||||
workspaceId,
|
|
||||||
environment,
|
environment,
|
||||||
directory
|
path,
|
||||||
|
projectId
|
||||||
}),
|
}),
|
||||||
queryFn: () => fetchImportedSecrets(workspaceId, environment, directory),
|
queryFn: () => fetchImportedSecrets(projectId, environment, path),
|
||||||
select: useCallback(
|
select: useCallback(
|
||||||
(data: TImportedSecrets[]) => {
|
(data: TImportedSecrets[]) => {
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { UserWsKeyPair } from "../keys/types";
|
import { UserWsKeyPair } from "../keys/types";
|
||||||
import { EncryptedSecret } from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
|
|
||||||
export type TSecretImports = {
|
export type TSecretImport = {
|
||||||
id: string;
|
id: string;
|
||||||
workspaceId: string;
|
|
||||||
environment: string;
|
|
||||||
folderId: string;
|
folderId: string;
|
||||||
imports: Array<{ environment: string; secretPath: string }>;
|
importPath: string;
|
||||||
|
importEnv: {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
position: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
@@ -19,44 +23,43 @@ export type TImportedSecrets = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretImports = {
|
export type TGetSecretImports = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetImportedSecrets = {
|
export type TGetImportedSecrets = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSecretImportDTO = {
|
export type TCreateSecretImportDTO = {
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
secretImport: {
|
import: {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
path: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSecretImportDTO = {
|
export type TUpdateSecretImportDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
secretImports: Array<{
|
import: Partial<{
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
path: string;
|
||||||
|
position: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteSecretImportDTO = {
|
export type TDeleteSecretImportDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
workspaceId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory?: string;
|
path?: string;
|
||||||
secretImportPath: string;
|
|
||||||
secretImportEnv: string;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type {
|
|||||||
} from "./secretApprovalRequest/types";
|
} from "./secretApprovalRequest/types";
|
||||||
export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types";
|
export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types";
|
||||||
export type { TSecretFolder } from "./secretFolders/types";
|
export type { TSecretFolder } from "./secretFolders/types";
|
||||||
export type { TImportedSecrets, TSecretImports } from "./secretImports/types";
|
export type { TImportedSecrets, TSecretImport } from "./secretImports/types";
|
||||||
export type {
|
export type {
|
||||||
TGetSecretRotationProviders,
|
TGetSecretRotationProviders,
|
||||||
TProviderTemplate,
|
TProviderTemplate,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
|||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||||
{(roles as TProjectRole[])?.map((role) => {
|
{roles?.map((role) => {
|
||||||
const { id, name, slug } = role;
|
const { id, name, slug } = role;
|
||||||
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
|
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const multiEnvForm2Api = (
|
|||||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||||
// if any of them is set in all push it without any condition
|
// if any of them is set in all push it without any condition
|
||||||
PERMISSION_ACTIONS.forEach((action) => {
|
PERMISSION_ACTIONS.forEach((action) => {
|
||||||
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isFullAccess) {
|
if (!isFullAccess) {
|
||||||
@@ -139,7 +139,7 @@ const multiEnvForm2Api = (
|
|||||||
if (formVal[slug]?.secretPath)
|
if (formVal[slug]?.secretPath)
|
||||||
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
||||||
|
|
||||||
permissions.push({ action, subject, conditions });
|
permissions.push({ action, subject: [subject], conditions });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -156,7 +156,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
} else {
|
} else {
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||||
if (isAllowed) {
|
if (isAllowed) {
|
||||||
permissions.push({ subject: rule, action });
|
permissions.push({ subject: [rule], action });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ export const SecretMainPage = () => {
|
|||||||
});
|
});
|
||||||
// fetch folders
|
// fetch folders
|
||||||
const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({
|
const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
directory: secretPath
|
path: secretPath
|
||||||
});
|
});
|
||||||
// fetch secret imports
|
// fetch secret imports
|
||||||
const {
|
const {
|
||||||
@@ -98,9 +98,9 @@ export const SecretMainPage = () => {
|
|||||||
isLoading: isSecretImportsLoading,
|
isLoading: isSecretImportsLoading,
|
||||||
isFetching: isSecretImportsFetching
|
isFetching: isSecretImportsFetching
|
||||||
} = useGetSecretImports({
|
} = useGetSecretImports({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
options: {
|
options: {
|
||||||
enabled: canReadSecret
|
enabled: canReadSecret
|
||||||
}
|
}
|
||||||
@@ -108,15 +108,15 @@ export const SecretMainPage = () => {
|
|||||||
|
|
||||||
// fetch imported secrets to show user the overriden ones
|
// fetch imported secrets to show user the overriden ones
|
||||||
const { data: importedSecrets } = useGetImportedSecrets({
|
const { data: importedSecrets } = useGetImportedSecrets({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
decryptFileKey: decryptFileKey!,
|
decryptFileKey: decryptFileKey!,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
options: {
|
options: {
|
||||||
enabled: canReadSecret
|
enabled: canReadSecret
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// fetch tags
|
// fech tags
|
||||||
const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : "");
|
const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : "");
|
||||||
|
|
||||||
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
|
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
|
||||||
@@ -146,7 +146,7 @@ export const SecretMainPage = () => {
|
|||||||
isPaused: !canDoReadRollback
|
isPaused: !canDoReadRollback
|
||||||
});
|
});
|
||||||
|
|
||||||
const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.imports?.length);
|
const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.length);
|
||||||
|
|
||||||
const handleSortToggle = () =>
|
const handleSortToggle = () =>
|
||||||
setSortDir((state) => (state === SortDir.ASC ? SortDir.DESC : SortDir.ASC));
|
setSortDir((state) => (state === SortDir.ASC ? SortDir.DESC : SortDir.ASC));
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ export const ActionBar = ({
|
|||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string) => {
|
||||||
try {
|
try {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
folderName,
|
name: folderName,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
});
|
});
|
||||||
handlePopUpClose("addFolder");
|
handlePopUpClose("addFolder");
|
||||||
createNotification({
|
createNotification({
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ export const CreateSecretImportForm = ({
|
|||||||
try {
|
try {
|
||||||
await createSecretImport({
|
await createSecretImport({
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
secretImport: {
|
import: {
|
||||||
environment: importedEnv,
|
environment: importedEnv,
|
||||||
secretPath: importedSecPath
|
path: importedSecPath
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -42,12 +42,13 @@ export const FolderListView = ({
|
|||||||
|
|
||||||
const handleFolderUpdate = async (newFolderName: string) => {
|
const handleFolderUpdate = async (newFolderName: string) => {
|
||||||
try {
|
try {
|
||||||
|
const { id: folderId } = popUp.updateFolder.data as TSecretFolder;
|
||||||
await updateFolder({
|
await updateFolder({
|
||||||
folderName: popUp.updateFolder.data as string,
|
folderId,
|
||||||
name: newFolderName,
|
name: newFolderName,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
});
|
});
|
||||||
handlePopUpClose("updateFolder");
|
handlePopUpClose("updateFolder");
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -65,11 +66,12 @@ export const FolderListView = ({
|
|||||||
|
|
||||||
const handleFolderDelete = async () => {
|
const handleFolderDelete = async () => {
|
||||||
try {
|
try {
|
||||||
|
const { id: folderId } = popUp.deleteFolder.data as TSecretFolder;
|
||||||
await deleteFolder({
|
await deleteFolder({
|
||||||
folderName: popUp.deleteFolder.data as string,
|
folderId,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
});
|
});
|
||||||
handlePopUpClose("deleteFolder");
|
handlePopUpClose("deleteFolder");
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -106,13 +108,13 @@ export const FolderListView = ({
|
|||||||
.map(({ name, id }) => (
|
.map(({ name, id }) => (
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className="flex group border-b border-mineshaft-600 hover:bg-mineshaft-700 cursor-pointer"
|
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||||
>
|
>
|
||||||
<div className="w-11 px-5 py-3 text-yellow-700 flex items-center">
|
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
|
||||||
<FontAwesomeIcon icon={faFolder} />
|
<FontAwesomeIcon icon={faFolder} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex-grow px-4 py-3 flex items-center"
|
className="flex flex-grow items-center px-4 py-3"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
@@ -122,7 +124,7 @@ export const FolderListView = ({
|
|||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-3 flex items-center space-x-4 border-l border-mineshaft-600">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
@@ -134,8 +136,8 @@ export const FolderListView = ({
|
|||||||
ariaLabel="edit-folder"
|
ariaLabel="edit-folder"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="group-hover:opacity-100 opacity-0 p-0"
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
onClick={() => handlePopUpOpen("updateFolder", name)}
|
onClick={() => handlePopUpOpen("updateFolder", { id, name })}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||||
@@ -153,8 +155,8 @@ export const FolderListView = ({
|
|||||||
ariaLabel="delete-folder"
|
ariaLabel="delete-folder"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="md"
|
size="md"
|
||||||
className="group-hover:opacity-100 opacity-0 p-0"
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
onClick={() => handlePopUpOpen("deleteFolder", name)}
|
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||||
@@ -171,14 +173,14 @@ export const FolderListView = ({
|
|||||||
<ModalContent title="Edit Folder">
|
<ModalContent title="Edit Folder">
|
||||||
<FolderForm
|
<FolderForm
|
||||||
isEdit
|
isEdit
|
||||||
defaultFolderName={popUp.updateFolder.data as string}
|
defaultFolderName={(popUp.updateFolder?.data as TSecretFolder)?.name}
|
||||||
onUpdateFolder={handleFolderUpdate}
|
onUpdateFolder={handleFolderUpdate}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteFolder.isOpen}
|
isOpen={popUp.deleteFolder.isOpen}
|
||||||
deleteKey={popUp.deleteFolder?.data as string}
|
deleteKey={(popUp.deleteFolder?.data as TSecretFolder)?.name}
|
||||||
title="Do you want to delete this folder?"
|
title="Do you want to delete this folder?"
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||||
onDeleteApproved={handleFolderDelete}
|
onDeleteApproved={handleFolderDelete}
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { EmptyState, IconButton, SecretInput, TableContainer } from "@app/components/v2";
|
import { EmptyState, IconButton, SecretInput, TableContainer } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDelete: (environment: string, secretPath: string) => void;
|
onDelete: () => void;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
importedEnv: string;
|
importEnvName: string;
|
||||||
importedSecPath: string;
|
importEnvPath: string;
|
||||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// to show the environment and folder icon
|
// to show the environment and folder icon
|
||||||
@@ -31,7 +32,7 @@ export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: st
|
|||||||
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
|
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
|
||||||
{secretPath && (
|
{secretPath && (
|
||||||
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
|
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-green-700 text-md" />
|
<FontAwesomeIcon icon={faFolder} className="text-md text-green-700" />
|
||||||
<span>{secretPath}</span>
|
<span>{secretPath}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -39,9 +40,10 @@ export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: st
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const SecretImportItem = ({
|
export const SecretImportItem = ({
|
||||||
importedEnv,
|
|
||||||
importedSecPath,
|
|
||||||
onDelete,
|
onDelete,
|
||||||
|
id,
|
||||||
|
importEnvName,
|
||||||
|
importEnvPath,
|
||||||
importedSecrets = [],
|
importedSecrets = [],
|
||||||
searchTerm = "",
|
searchTerm = "",
|
||||||
secretPath,
|
secretPath,
|
||||||
@@ -49,10 +51,8 @@ export const SecretImportItem = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isExpanded, setIsExpanded] = useToggle();
|
const [isExpanded, setIsExpanded] = useToggle();
|
||||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||||
id: `${importedEnv}-${importedSecPath}`
|
id
|
||||||
});
|
});
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||||
@@ -80,7 +80,7 @@ export const SecretImportItem = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex group border-b border-mineshaft-600 hover:bg-mineshaft-700 cursor-pointer"
|
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||||
role="button"
|
role="button"
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -88,13 +88,13 @@ export const SecretImportItem = ({
|
|||||||
onClick={() => setIsExpanded.toggle()}
|
onClick={() => setIsExpanded.toggle()}
|
||||||
onKeyDown={() => setIsExpanded.toggle()}
|
onKeyDown={() => setIsExpanded.toggle()}
|
||||||
>
|
>
|
||||||
<div className="w-12 px-4 py-2 flex items-center text-green-700">
|
<div className="flex w-12 items-center px-4 py-2 text-green-700">
|
||||||
<FontAwesomeIcon icon={faFileImport} />
|
<FontAwesomeIcon icon={faFileImport} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow px-4 py-2 flex items-center">
|
<div className="flex flex-grow items-center px-4 py-2">
|
||||||
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
|
<EnvFolderIcon env={importEnvName || ""} secretPath={importEnvPath} />
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2 flex items-center space-x-4 border-l border-mineshaft-600">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
@@ -107,7 +107,7 @@ export const SecretImportItem = ({
|
|||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="expand"
|
ariaLabel="expand"
|
||||||
className="group-hover:opacity-100 opacity-0 p-0"
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
@@ -128,10 +128,10 @@ export const SecretImportItem = ({
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
ariaLabel="delete"
|
ariaLabel="delete"
|
||||||
className="group-hover:opacity-100 opacity-0 p-0"
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
onDelete(importedEnv, importedSecPath);
|
onDelete();
|
||||||
}}
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
@@ -167,7 +167,7 @@ export const SecretImportItem = ({
|
|||||||
{importedSecrets
|
{importedSecrets
|
||||||
.filter((secret) => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
|
.filter((secret) => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
|
||||||
.map(({ key, value, overriden }, index) => (
|
.map(({ key, value, overriden }, index) => (
|
||||||
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
|
<tr key={`${id}-${key}-${index + 1}`}>
|
||||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||||
{key}
|
{key}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { DeleteActionModal } from "@app/components/v2";
|
|||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
|
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
|
||||||
import { TSecretImports } from "@app/hooks/api/secretImports/types";
|
import { TSecretImport } from "@app/hooks/api/secretImports/types";
|
||||||
import { DecryptedSecret } from "@app/hooks/api/types";
|
import { DecryptedSecret } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { SecretImportItem } from "./SecretImportItem";
|
import { SecretImportItem } from "./SecretImportItem";
|
||||||
@@ -76,17 +76,15 @@ type Props = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
secretImports?: TSecretImports;
|
secretImports?: TSecretImport[];
|
||||||
isFetching?: boolean;
|
isFetching?: boolean;
|
||||||
secrets?: DecryptedSecret[];
|
secrets?: DecryptedSecret[];
|
||||||
importedSecrets?: TImportedSecrets;
|
importedSecrets?: TImportedSecrets;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TDeleteSecretImport = { environment: string; secretPath: string };
|
|
||||||
|
|
||||||
export const SecretImportListView = ({
|
export const SecretImportListView = ({
|
||||||
secretImports,
|
secretImports = [],
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretPath,
|
secretPath,
|
||||||
@@ -107,21 +105,11 @@ export const SecretImportListView = ({
|
|||||||
useSensor(KeyboardSensor, {})
|
useSensor(KeyboardSensor, {})
|
||||||
);
|
);
|
||||||
|
|
||||||
const [items, setItems] = useState(
|
const [items, setItems] = useState(secretImports);
|
||||||
(secretImports?.imports || [])?.map((dto) => ({
|
|
||||||
id: `${dto.environment}-${dto.secretPath}`,
|
|
||||||
...dto
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetching) {
|
if (!isFetching) {
|
||||||
setItems(
|
setItems(secretImports);
|
||||||
(secretImports?.imports || [])?.map((dto) => ({
|
|
||||||
id: `${dto.environment}-${dto.secretPath}`,
|
|
||||||
...dto
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [isFetching]);
|
}, [isFetching]);
|
||||||
|
|
||||||
@@ -129,24 +117,19 @@ export const SecretImportListView = ({
|
|||||||
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
||||||
|
|
||||||
const handleSecretImportDelete = async () => {
|
const handleSecretImportDelete = async () => {
|
||||||
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
|
const { id: secretImportId } = popUp.deleteSecretImport?.data as { id: string };
|
||||||
?.data as TDeleteSecretImport;
|
|
||||||
try {
|
try {
|
||||||
if (secretImports?.id) {
|
await deleteSecretImport({
|
||||||
await deleteSecretImport({
|
projectId: workspaceId,
|
||||||
workspaceId,
|
environment,
|
||||||
environment,
|
path: secretPath,
|
||||||
directory: secretPath,
|
id: secretImportId
|
||||||
id: secretImports?.id,
|
});
|
||||||
secretImportEnv: importEnv,
|
handlePopUpClose("deleteSecretImport");
|
||||||
secretImportPath: impSecPath
|
createNotification({
|
||||||
});
|
type: "success",
|
||||||
handlePopUpClose("deleteSecretImport");
|
text: "Successfully removed secret link"
|
||||||
createNotification({
|
});
|
||||||
type: "success",
|
|
||||||
text: "Successfully removed secret link"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -163,11 +146,13 @@ export const SecretImportListView = ({
|
|||||||
const newImportOrder = arrayMove(items, oldIndex, newIndex);
|
const newImportOrder = arrayMove(items, oldIndex, newIndex);
|
||||||
setItems(newImportOrder);
|
setItems(newImportOrder);
|
||||||
updateSecretImport({
|
updateSecretImport({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
directory: secretPath,
|
path: secretPath,
|
||||||
id: secretImports?.id || "",
|
id: active.id as string,
|
||||||
secretImports: newImportOrder
|
import: {
|
||||||
|
position: newIndex + 1
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -181,26 +166,28 @@ export const SecretImportListView = ({
|
|||||||
modifiers={[restrictToVerticalAxis]}
|
modifiers={[restrictToVerticalAxis]}
|
||||||
>
|
>
|
||||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
{items?.map(({ secretPath: importedSecPath, environment: importedEnv }) => (
|
{items?.map((item) => {
|
||||||
<SecretImportItem
|
const { importPath, importEnv, id } = item;
|
||||||
searchTerm={searchTerm}
|
return (
|
||||||
key={`${importedEnv}-${importedSecPath}`}
|
<SecretImportItem
|
||||||
importedEnv={importedEnv}
|
searchTerm={searchTerm}
|
||||||
importedSecPath={importedSecPath}
|
key={`imported-env-${id}`}
|
||||||
importedSecrets={computeImportedSecretRows(
|
id={id}
|
||||||
importedEnv,
|
importEnvPath={importPath}
|
||||||
importedSecPath,
|
importEnvName={importEnv.name}
|
||||||
importedSecrets,
|
importedSecrets={computeImportedSecretRows(
|
||||||
secrets,
|
importEnv.slug,
|
||||||
environments
|
importPath,
|
||||||
)}
|
importedSecrets,
|
||||||
secretPath={secretPath}
|
secrets,
|
||||||
environment={environment}
|
environments
|
||||||
onDelete={(env, secPath) =>
|
)}
|
||||||
handlePopUpOpen("deleteSecretImport", { environment: env, secretPath: secPath })
|
secretPath={secretPath}
|
||||||
}
|
environment={environment}
|
||||||
/>
|
onDelete={() => handlePopUpOpen("deleteSecretImport", item)}
|
||||||
))}
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
@@ -208,8 +195,8 @@ export const SecretImportListView = ({
|
|||||||
deleteKey="unlink"
|
deleteKey="unlink"
|
||||||
title="Do you want to remove this secret import?"
|
title="Do you want to remove this secret import?"
|
||||||
subTitle={`This will unlink secrets from environment ${
|
subTitle={`This will unlink secrets from environment ${
|
||||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
|
||||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||||
onDeleteApproved={handleSecretImportDelete}
|
onDeleteApproved={handleSecretImportDelete}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -208,8 +208,8 @@ export const SecretListView = ({
|
|||||||
} = modSecret;
|
} = modSecret;
|
||||||
const hasKeyChanged = oldKey !== key;
|
const hasKeyChanged = oldKey !== key;
|
||||||
|
|
||||||
const tagIds = tags.map(({ id }) => id);
|
const tagIds = tags?.map(({ id }) => id);
|
||||||
const oldTagIds = orgSecret.tags.map(({ id }) => id);
|
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
|
||||||
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
|
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
|
||||||
const isSharedSecUnchanged =
|
const isSharedSecUnchanged =
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export const SecretOverviewPage = () => {
|
|||||||
decryptFileKey: latestFileKey!
|
decryptFileKey: latestFileKey!
|
||||||
});
|
});
|
||||||
const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({
|
const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
environments: userAvailableEnvs.map(({ slug }) => slug),
|
path: secretPath,
|
||||||
directory: secretPath
|
environments: userAvailableEnvs.map(({ slug }) => slug)
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
@@ -119,10 +119,10 @@ export const SecretOverviewPage = () => {
|
|||||||
const folderName = pathSegment.at(-1);
|
const folderName = pathSegment.at(-1);
|
||||||
if (folderName && parentPath) {
|
if (folderName && parentPath) {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
|
path: secretPath,
|
||||||
environment: env,
|
environment: env,
|
||||||
directory: parentPath,
|
name: folderName
|
||||||
folderName
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,10 +216,10 @@ export const SecretOverviewPage = () => {
|
|||||||
const folderName = path.at(-1);
|
const folderName = path.at(-1);
|
||||||
if (folderName && directory) {
|
if (folderName && directory) {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
environment: slug,
|
environment: slug,
|
||||||
directory,
|
path: secretPath,
|
||||||
folderName
|
name: folderName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user