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-ui": "^1.10.1",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"argon2": "^0.31.2",
|
||||
"axios": "^1.6.2",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
@@ -38,6 +39,8 @@
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
},
|
||||
@@ -940,6 +943,14 @@
|
||||
"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": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -2269,6 +2280,25 @@
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -8571,6 +8601,16 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"argon2": "^0.31.2",
|
||||
"axios": "^1.6.2",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
@@ -91,6 +92,8 @@
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import promptSync from "prompt-sync";
|
||||
|
||||
const prompt = promptSync();
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
const migrationName = prompt("Enter name for migration: ");
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { readdirSync } from "fs";
|
||||
import path from "path";
|
||||
import promptSync from "prompt-sync";
|
||||
|
||||
const prompt = promptSync();
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
const migrationName = prompt("Enter name for seedfile: ");
|
||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
|
||||
|
||||
@@ -5,7 +5,7 @@ import knex from "knex";
|
||||
import { writeFileSync } from "fs";
|
||||
import promptSync from "prompt-sync";
|
||||
|
||||
const prompt = promptSync();
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
dotenv.config({
|
||||
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 { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-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 { TAuthTokenServiceFactory } from "@app/services/token/token-service";
|
||||
import { TUserDalFactory } from "@app/services/user/user-dal";
|
||||
@@ -64,6 +67,9 @@ declare module "fastify" {
|
||||
projectEnv: TProjectEnvServiceFactory;
|
||||
projectKey: TProjectKeyServiceFactory;
|
||||
projectRole: TProjectRoleServiceFactory;
|
||||
secret: TSecretServiceFactory;
|
||||
secretImport: TSecretImportServiceFactory;
|
||||
folder: TSecretFolderServiceFactory;
|
||||
};
|
||||
|
||||
// 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,
|
||||
TProjectsInsert,
|
||||
TProjectsUpdate,
|
||||
TSecretBlindIndexes,
|
||||
TSecretBlindIndexesInsert,
|
||||
TSecretBlindIndexesUpdate,
|
||||
TSecretFolders,
|
||||
TSecretFoldersInsert,
|
||||
TSecretFoldersUpdate,
|
||||
TSecretImports,
|
||||
TSecretImportsInsert,
|
||||
TSecretImportsUpdate,
|
||||
TSecrets,
|
||||
TSecretsInsert,
|
||||
TSecretsUpdate,
|
||||
TSecretTagJunction,
|
||||
TSecretTagJunctionInsert,
|
||||
TSecretTagJunctionUpdate,
|
||||
TSecretTags,
|
||||
TSecretTagsInsert,
|
||||
TSecretTagsUpdate,
|
||||
TSecretVersions,
|
||||
TSecretVersionsInsert,
|
||||
TSecretVersionsUpdate,
|
||||
TSuperAdmin,
|
||||
TSuperAdminInsert,
|
||||
TSuperAdminUpdate,
|
||||
@@ -125,5 +146,36 @@ declare module "knex/types/tables" {
|
||||
TProjectKeysInsert,
|
||||
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.string("name").notNullable();
|
||||
t.string("slug").notNullable();
|
||||
t.integer("position").notNullable();
|
||||
t.uuid("projectId").notNullable();
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -34,9 +40,9 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.text("nonce").notNullable();
|
||||
t.uuid("receiverId").notNullable();
|
||||
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
|
||||
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.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
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-roles";
|
||||
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 "./user-actions";
|
||||
export * from "./user-encryption-keys";
|
||||
|
||||
@@ -17,7 +17,15 @@ export enum TableName {
|
||||
Environment = "project_environments",
|
||||
ProjectMembership = "project_memberships",
|
||||
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";
|
||||
@@ -58,3 +66,8 @@ export enum SecretKeyEncoding {
|
||||
BASE64 = "base64",
|
||||
HEX = "hex"
|
||||
}
|
||||
|
||||
export enum SecretType {
|
||||
Shared = "shared",
|
||||
Personal = "personal"
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export const ProjectEnvironmentsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
position: z.number(),
|
||||
projectId: z.string().uuid(),
|
||||
createdAt: 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 { TableName } from "./schemas";
|
||||
|
||||
export const createJunctionTable = (
|
||||
knex: Knex,
|
||||
tableName: string,
|
||||
table1Name: string,
|
||||
table2Name: string
|
||||
tableName: TableName,
|
||||
table1Name: TableName,
|
||||
table2Name: TableName
|
||||
) =>
|
||||
knex.schema.createTable(tableName, (table) => {
|
||||
table.increments(); // Primary key
|
||||
table.integer(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1
|
||||
table.integer(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2
|
||||
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
table.uuid(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1
|
||||
table.uuid(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2
|
||||
table.foreign(`${table1Name}Id`).references("id").inTable(table2Name);
|
||||
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";
|
||||
|
||||
@@ -28,9 +28,9 @@ export class BadRequestError extends Error {
|
||||
|
||||
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");
|
||||
this.name = name;
|
||||
this.name = name || "";
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
import { DatabaseError } from "../errors";
|
||||
|
||||
export * from "./join";
|
||||
@@ -19,7 +17,7 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||
// What is ormify
|
||||
// It is to inject typical operations like find, findOne, update, delete, create
|
||||
// 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,
|
||||
tableName: Tname,
|
||||
dal?: DbOps
|
||||
|
||||
@@ -25,7 +25,6 @@ export const mergeOneToManyRelation = <
|
||||
prevPkId = pk;
|
||||
prevPkIndex += 1;
|
||||
}
|
||||
console.log(prevPkIndex, prevPkId);
|
||||
groupedRecord[prevPkIndex][childKey].push(childMapper(row));
|
||||
}
|
||||
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 { projectRoleDalFactory } from "@app/services/project-role/project-role-dal";
|
||||
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 { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
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 { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
|
||||
export const registerRoutes = async (
|
||||
server: FastifyZodProvider,
|
||||
@@ -60,6 +68,12 @@ export const registerRoutes = async (
|
||||
const projectEnvDal = projectEnvDalFactory(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
|
||||
const permissionDal = permissionDalFactory(db);
|
||||
|
||||
@@ -104,8 +118,10 @@ export const registerRoutes = async (
|
||||
const projectService = projectServiceFactory({
|
||||
permissionService,
|
||||
projectDal,
|
||||
secretBlindIndexDal,
|
||||
projectEnvDal,
|
||||
projectMembershipDal
|
||||
projectMembershipDal,
|
||||
folderDal
|
||||
});
|
||||
const projectMembershipService = projectMembershipServiceFactory({
|
||||
projectMembershipDal,
|
||||
@@ -125,6 +141,25 @@ export const registerRoutes = async (
|
||||
});
|
||||
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();
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -142,7 +177,10 @@ export const registerRoutes = async (
|
||||
projectMembership: projectMembershipService,
|
||||
projectKey: projectKeyService,
|
||||
projectEnv: projectEnvService,
|
||||
projectRole: projectRoleService
|
||||
projectRole: projectRoleService,
|
||||
secret: secretService,
|
||||
folder: folderService,
|
||||
secretImport: secretImportService
|
||||
});
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
@@ -169,6 +207,7 @@ export const registerRoutes = async (
|
||||
},
|
||||
handler: () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
return {
|
||||
date: new Date(),
|
||||
message: "Ok" as const,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { registerProjectEnvRouter } from "./project-env-router";
|
||||
import { registerProjectKeyRouter } from "./project-key-router";
|
||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||
import { registerProjectRouter } from "./project-router";
|
||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||
import { registerSecretImportRouter } from "./secret-import-router";
|
||||
import { registerSsoRouter } from "./sso-router";
|
||||
import { registerUserActionRouter } from "./user-action-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(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
|
||||
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
|
||||
|
||||
await server.register(
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { z } from "zod";
|
||||
|
||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { registerLoginRouter } from "./login-router";
|
||||
import { registerSecretRouter } from "./secret-router";
|
||||
import { registerSignupRouter } from "./signup-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(registerLoginRouter, { prefix: "/auth" });
|
||||
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 { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
TResetPasswordViaBackupKeyDTO
|
||||
} from "./auth-password-type";
|
||||
import { AuthTokenType } from "./auth-type";
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
|
||||
type TAuthPasswordServiceFactoryDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
|
||||
@@ -1,10 +1,63 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TProjectEnvDalFactory = ReturnType<typeof projectEnvDalFactory>;
|
||||
|
||||
export const projectEnvDalFactory = (db: TDbClient) => {
|
||||
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 { TProjectEnvDalFactory } from "./project-env-dal";
|
||||
import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types";
|
||||
import { TCreateEnvDTO, TDeleteEnvDTO, TReorderEnvDTO, TUpdateEnvDTO } from "./project-env-types";
|
||||
|
||||
type TProjectEnvServiceFactoryDep = {
|
||||
projectEnvDal: TProjectEnvDalFactory;
|
||||
@@ -36,7 +36,11 @@ export const projectEnvServiceFactory = ({
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -75,13 +79,44 @@ export const projectEnvServiceFactory = ({
|
||||
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 {
|
||||
createEnvironment,
|
||||
updateEnvironment,
|
||||
deleteEnvironment
|
||||
deleteEnvironment,
|
||||
reorderEnvironment
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,3 +14,8 @@ export type TUpdateEnvDTO = {
|
||||
export type TDeleteEnvDTO = {
|
||||
id: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TReorderEnvDTO = {
|
||||
id: string;
|
||||
pos: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -10,9 +10,13 @@ import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} 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 { 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 { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
||||
|
||||
@@ -24,8 +28,10 @@ const DEFAULT_PROJECT_ENVS = [
|
||||
|
||||
type TProjectServiceFactoryDep = {
|
||||
projectDal: TProjectDalFactory;
|
||||
folderDal: Pick<TSecretFolderDalFactory, "insertMany">;
|
||||
projectEnvDal: Pick<TProjectEnvDalFactory, "insertMany">;
|
||||
projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">;
|
||||
secretBlindIndexDal: Pick<TSecretBlindIndexDalFactory, "create">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
@@ -34,6 +40,8 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
export const projectServiceFactory = ({
|
||||
projectDal,
|
||||
permissionService,
|
||||
folderDal,
|
||||
secretBlindIndexDal,
|
||||
projectMembershipDal,
|
||||
projectEnvDal
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
@@ -47,9 +55,12 @@ export const projectServiceFactory = ({
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
|
||||
// TODO(backend-pg): licence server
|
||||
const newProject = projectDal.transaction(async (tx) => {
|
||||
const project = await projectDal.create({ name: workspaceName, orgId }, tx);
|
||||
// set user as admin member for proeject
|
||||
await projectMembershipDal.create(
|
||||
{
|
||||
userId: actorId,
|
||||
@@ -58,8 +69,25 @@ export const projectServiceFactory = ({
|
||||
},
|
||||
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(
|
||||
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
|
||||
);
|
||||
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,
|
||||
TOrgRole,
|
||||
TPermission,
|
||||
TProjectPermission,
|
||||
TProjectRole
|
||||
} from "./types";
|
||||
|
||||
@@ -50,7 +51,7 @@ const getProjectRoles = async (projectId: string) => {
|
||||
}>(`/api/ee/v1/workspace/${projectId}/roles`);
|
||||
return data.data.roles.map(({ permissions, ...el }) => ({
|
||||
...el,
|
||||
permissions: unpackRules(permissions as PackRule<TPermission>[])
|
||||
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export type TProjectRole = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
description?: string;
|
||||
permissions: TPermission[];
|
||||
permissions: TProjectPermission[];
|
||||
};
|
||||
|
||||
export type TOrgRole = {
|
||||
@@ -67,13 +67,13 @@ export type TCreateProjectRoleDTO = {
|
||||
name: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
permissions: TPermission[];
|
||||
permissions: TProjectPermission[];
|
||||
};
|
||||
|
||||
export type TUpdateProjectRoleDTO = {
|
||||
projectId: string;
|
||||
id: string;
|
||||
} & Partial<Omit<TCreateOrgRoleDTO, "orgId">>;
|
||||
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
|
||||
|
||||
export type TDeleteProjectRoleDTO = {
|
||||
projectId: string;
|
||||
|
||||
@@ -20,25 +20,25 @@ import {
|
||||
} from "./types";
|
||||
|
||||
const queryKeys = {
|
||||
getSecretFolders: ({ workspaceId, environment, directory }: TGetProjectFoldersDTO) =>
|
||||
["secret-folders", { workspaceId, environment, directory }] as const
|
||||
getSecretFolders: ({ projectId, environment, path }: TGetProjectFoldersDTO) =>
|
||||
["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", {
|
||||
params: {
|
||||
workspaceId,
|
||||
projectId,
|
||||
environment,
|
||||
directory
|
||||
path
|
||||
}
|
||||
});
|
||||
return data.folders;
|
||||
};
|
||||
|
||||
export const useGetProjectFolders = ({
|
||||
workspaceId,
|
||||
projectId,
|
||||
environment,
|
||||
directory = "/",
|
||||
path = "/",
|
||||
options = {}
|
||||
}: TGetProjectFoldersDTO & {
|
||||
options?: Omit<
|
||||
@@ -53,21 +53,21 @@ export const useGetProjectFolders = ({
|
||||
}) =>
|
||||
useQuery({
|
||||
...options,
|
||||
queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
|
||||
queryFn: async () => fetchProjectFolders(workspaceId, environment, directory)
|
||||
queryKey: queryKeys.getSecretFolders({ projectId, environment, path }),
|
||||
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
|
||||
queryFn: async () => fetchProjectFolders(projectId, environment, path)
|
||||
});
|
||||
|
||||
export const useGetFoldersByEnv = ({
|
||||
directory = "/",
|
||||
workspaceId,
|
||||
path = "/",
|
||||
projectId,
|
||||
environments
|
||||
}: TGetFoldersByEnvDTO) => {
|
||||
const folders = useQueries({
|
||||
queries: environments.map((environment) => ({
|
||||
queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
|
||||
queryFn: async () => fetchProjectFolders(workspaceId, environment, directory),
|
||||
enabled: Boolean(workspaceId) && Boolean(environment)
|
||||
queryKey: queryKeys.getSecretFolders({ projectId, environment, path }),
|
||||
queryFn: async () => fetchProjectFolders(projectId, environment, path),
|
||||
enabled: Boolean(projectId) && Boolean(environment)
|
||||
}))
|
||||
});
|
||||
|
||||
@@ -105,15 +105,13 @@ export const useCreateFolder = () => {
|
||||
const { data } = await apiRequest.post("/api/v1/folders", dto);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
||||
onSuccess: (_, { projectId, environment, path }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
|
||||
queryClient.invalidateQueries(
|
||||
queryKeys.getSecretFolders({ workspaceId, environment, directory })
|
||||
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.list({ workspaceId, environment, directory })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.count({ workspaceId, environment, directory })
|
||||
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -123,24 +121,22 @@ export const useUpdateFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateFolderDTO>({
|
||||
mutationFn: async ({ directory = "/", folderName, name, environment, workspaceId }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/folders/${folderName}`, {
|
||||
mutationFn: async ({ path = "/", folderId, name, environment, projectId }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
|
||||
name,
|
||||
environment,
|
||||
workspaceId,
|
||||
directory
|
||||
projectId,
|
||||
path
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
||||
onSuccess: (_, { projectId, environment, path }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
|
||||
queryClient.invalidateQueries(
|
||||
queryKeys.getSecretFolders({ workspaceId, environment, directory })
|
||||
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.list({ workspaceId, environment, directory })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.count({ workspaceId, environment, directory })
|
||||
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -150,25 +146,23 @@ export const useDeleteFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteFolderDTO>({
|
||||
mutationFn: async ({ directory = "/", folderName, environment, workspaceId }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/folders/${folderName}`, {
|
||||
mutationFn: async ({ path = "/", folderId, environment, projectId }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, {
|
||||
data: {
|
||||
environment,
|
||||
workspaceId,
|
||||
directory
|
||||
projectId,
|
||||
path
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { directory = "/", workspaceId, environment }) => {
|
||||
onSuccess: (_, { path = "/", projectId, environment }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
|
||||
queryClient.invalidateQueries(
|
||||
queryKeys.getSecretFolders({ workspaceId, environment, directory })
|
||||
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.list({ workspaceId, environment, directory })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.count({ workspaceId, environment, directory })
|
||||
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,35 +4,35 @@ export type TSecretFolder = {
|
||||
};
|
||||
|
||||
export type TGetProjectFoldersDTO = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
directory?: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type TGetFoldersByEnvDTO = {
|
||||
environments: string[];
|
||||
workspaceId: string;
|
||||
directory?: string;
|
||||
projectId: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type TCreateFolderDTO = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
folderName: string;
|
||||
directory?: string;
|
||||
name: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type TUpdateFolderDTO = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
name: string;
|
||||
folderName: string;
|
||||
directory?: string;
|
||||
folderId: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type TDeleteFolderDTO = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
folderName: string;
|
||||
directory?: string;
|
||||
folderId: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
@@ -9,21 +9,21 @@ export const useCreateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
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", {
|
||||
secretImport,
|
||||
import: secretImport,
|
||||
environment,
|
||||
workspaceId,
|
||||
directory
|
||||
projectId,
|
||||
path
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
||||
onSuccess: (_, { environment, projectId, path }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
|
||||
secretImportKeys.getProjectSecretImports({ projectId, environment, path })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
|
||||
secretImportKeys.getSecretImportSecrets({ projectId, environment, path })
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -33,21 +33,21 @@ export const useUpdateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, directory, secretImports, id }) => {
|
||||
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
|
||||
secretImports,
|
||||
mutationFn: async ({ environment, import: secretImports, projectId, path, id }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/secret-imports/${id}`, {
|
||||
import: secretImports,
|
||||
environment,
|
||||
workspaceId,
|
||||
directory
|
||||
path,
|
||||
projectId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
||||
onSuccess: (_, { environment, projectId, path }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
|
||||
secretImportKeys.getProjectSecretImports({ projectId, path, environment })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
|
||||
secretImportKeys.getSecretImportSecrets({ environment, path, projectId })
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -57,21 +57,22 @@ export const useDeleteSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
||||
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
|
||||
mutationFn: async ({ id, projectId, path, environment }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
||||
data: {
|
||||
secretImportPath,
|
||||
secretImportEnv
|
||||
projectId,
|
||||
path,
|
||||
environment
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, directory }) => {
|
||||
onSuccess: (_, { projectId, environment, path }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
|
||||
secretImportKeys.getProjectSecretImports({ projectId, environment, path })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
|
||||
secretImportKeys.getSecretImportSecrets({ projectId, environment, path })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,44 +7,44 @@ import {
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImports } from "./types";
|
||||
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImport } from "./types";
|
||||
|
||||
export const secretImportKeys = {
|
||||
getProjectSecretImports: ({ environment, workspaceId, directory }: TGetSecretImports) =>
|
||||
[{ workspaceId, directory, environment }, "secrets-imports"] as const,
|
||||
getProjectSecretImports: ({ environment, projectId, path }: TGetSecretImports) =>
|
||||
[{ projectId, path, environment }, "secrets-imports"] as const,
|
||||
getSecretImportSecrets: ({
|
||||
workspaceId,
|
||||
environment,
|
||||
directory
|
||||
projectId,
|
||||
path
|
||||
}: 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 { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
|
||||
const fetchSecretImport = async ({ projectId, environment, path = "/" }: TGetSecretImports) => {
|
||||
const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>(
|
||||
"/api/v1/secret-imports",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
projectId,
|
||||
environment,
|
||||
directory
|
||||
path
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secretImport;
|
||||
return data.secretImports;
|
||||
};
|
||||
|
||||
export const useGetSecretImports = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
directory = "/",
|
||||
path = "/",
|
||||
projectId,
|
||||
options = {}
|
||||
}: TGetSecretImports & {
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretImports,
|
||||
TSecretImport[],
|
||||
unknown,
|
||||
TSecretImports,
|
||||
TSecretImport[],
|
||||
ReturnType<typeof secretImportKeys.getProjectSecretImports>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
@@ -52,9 +52,9 @@ export const useGetSecretImports = ({
|
||||
}) =>
|
||||
useQuery({
|
||||
...options,
|
||||
queryKey: secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory }),
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
|
||||
queryFn: () => fetchSecretImport({ workspaceId, environment, directory })
|
||||
queryKey: secretImportKeys.getProjectSecretImports({ environment, projectId, path }),
|
||||
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
|
||||
queryFn: () => fetchSecretImport({ path, projectId, environment })
|
||||
});
|
||||
|
||||
const fetchImportedSecrets = async (
|
||||
@@ -76,10 +76,10 @@ const fetchImportedSecrets = async (
|
||||
};
|
||||
|
||||
export const useGetImportedSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
decryptFileKey,
|
||||
directory,
|
||||
path,
|
||||
projectId,
|
||||
options = {}
|
||||
}: TGetImportedSecrets & {
|
||||
options?: Omit<
|
||||
@@ -94,16 +94,16 @@ export const useGetImportedSecrets = ({
|
||||
}) =>
|
||||
useQuery({
|
||||
enabled:
|
||||
Boolean(workspaceId) &&
|
||||
Boolean(projectId) &&
|
||||
Boolean(environment) &&
|
||||
Boolean(decryptFileKey) &&
|
||||
(options?.enabled ?? true),
|
||||
queryKey: secretImportKeys.getSecretImportSecrets({
|
||||
workspaceId,
|
||||
environment,
|
||||
directory
|
||||
path,
|
||||
projectId
|
||||
}),
|
||||
queryFn: () => fetchImportedSecrets(workspaceId, environment, directory),
|
||||
queryFn: () => fetchImportedSecrets(projectId, environment, path),
|
||||
select: useCallback(
|
||||
(data: TImportedSecrets[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
|
||||
export type TSecretImports = {
|
||||
export type TSecretImport = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
imports: Array<{ environment: string; secretPath: string }>;
|
||||
importPath: string;
|
||||
importEnv: {
|
||||
name: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
position: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -19,44 +23,43 @@ export type TImportedSecrets = {
|
||||
};
|
||||
|
||||
export type TGetSecretImports = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
directory?: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type TGetImportedSecrets = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
directory?: string;
|
||||
path?: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type TCreateSecretImportDTO = {
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
directory?: string;
|
||||
secretImport: {
|
||||
path?: string;
|
||||
import: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
directory?: string;
|
||||
secretImports: Array<{
|
||||
path?: string;
|
||||
import: Partial<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
path: string;
|
||||
position: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TDeleteSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
directory?: string;
|
||||
secretImportPath: string;
|
||||
secretImportEnv: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ export type {
|
||||
} from "./secretApprovalRequest/types";
|
||||
export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types";
|
||||
export type { TSecretFolder } from "./secretFolders/types";
|
||||
export type { TImportedSecrets, TSecretImports } from "./secretImports/types";
|
||||
export type { TImportedSecrets, TSecretImport } from "./secretImports/types";
|
||||
export type {
|
||||
TGetSecretRotationProviders,
|
||||
TProviderTemplate,
|
||||
|
||||
@@ -89,7 +89,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{(roles as TProjectRole[])?.map((role) => {
|
||||
{roles?.map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ const multiEnvForm2Api = (
|
||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||
// if any of them is set in all push it without any condition
|
||||
PERMISSION_ACTIONS.forEach((action) => {
|
||||
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
||||
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] });
|
||||
});
|
||||
|
||||
if (!isFullAccess) {
|
||||
@@ -139,7 +139,7 @@ const multiEnvForm2Api = (
|
||||
if (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 {
|
||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||
if (isAllowed) {
|
||||
permissions.push({ subject: rule, action });
|
||||
permissions.push({ subject: [rule], action });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ export const SecretMainPage = () => {
|
||||
});
|
||||
// fetch folders
|
||||
const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||
workspaceId,
|
||||
projectId: workspaceId,
|
||||
environment,
|
||||
directory: secretPath
|
||||
path: secretPath
|
||||
});
|
||||
// fetch secret imports
|
||||
const {
|
||||
@@ -98,9 +98,9 @@ export const SecretMainPage = () => {
|
||||
isLoading: isSecretImportsLoading,
|
||||
isFetching: isSecretImportsFetching
|
||||
} = useGetSecretImports({
|
||||
workspaceId,
|
||||
projectId: workspaceId,
|
||||
environment,
|
||||
directory: secretPath,
|
||||
path: secretPath,
|
||||
options: {
|
||||
enabled: canReadSecret
|
||||
}
|
||||
@@ -108,15 +108,15 @@ export const SecretMainPage = () => {
|
||||
|
||||
// fetch imported secrets to show user the overriden ones
|
||||
const { data: importedSecrets } = useGetImportedSecrets({
|
||||
workspaceId,
|
||||
projectId: workspaceId,
|
||||
environment,
|
||||
decryptFileKey: decryptFileKey!,
|
||||
directory: secretPath,
|
||||
path: secretPath,
|
||||
options: {
|
||||
enabled: canReadSecret
|
||||
}
|
||||
});
|
||||
// fetch tags
|
||||
// fech tags
|
||||
const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : "");
|
||||
|
||||
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
|
||||
@@ -146,7 +146,7 @@ export const SecretMainPage = () => {
|
||||
isPaused: !canDoReadRollback
|
||||
});
|
||||
|
||||
const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.imports?.length);
|
||||
const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.length);
|
||||
|
||||
const handleSortToggle = () =>
|
||||
setSortDir((state) => (state === SortDir.ASC ? SortDir.DESC : SortDir.ASC));
|
||||
|
||||
@@ -112,10 +112,10 @@ export const ActionBar = ({
|
||||
const handleFolderCreate = async (folderName: string) => {
|
||||
try {
|
||||
await createFolder({
|
||||
folderName,
|
||||
directory: secretPath,
|
||||
name: folderName,
|
||||
path: secretPath,
|
||||
environment,
|
||||
workspaceId
|
||||
projectId: workspaceId
|
||||
});
|
||||
handlePopUpClose("addFolder");
|
||||
createNotification({
|
||||
|
||||
@@ -66,11 +66,11 @@ export const CreateSecretImportForm = ({
|
||||
try {
|
||||
await createSecretImport({
|
||||
environment,
|
||||
workspaceId,
|
||||
directory: secretPath,
|
||||
secretImport: {
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
import: {
|
||||
environment: importedEnv,
|
||||
secretPath: importedSecPath
|
||||
path: importedSecPath
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
|
||||
@@ -42,12 +42,13 @@ export const FolderListView = ({
|
||||
|
||||
const handleFolderUpdate = async (newFolderName: string) => {
|
||||
try {
|
||||
const { id: folderId } = popUp.updateFolder.data as TSecretFolder;
|
||||
await updateFolder({
|
||||
folderName: popUp.updateFolder.data as string,
|
||||
folderId,
|
||||
name: newFolderName,
|
||||
directory: secretPath,
|
||||
path: secretPath,
|
||||
environment,
|
||||
workspaceId
|
||||
projectId: workspaceId
|
||||
});
|
||||
handlePopUpClose("updateFolder");
|
||||
createNotification({
|
||||
@@ -65,11 +66,12 @@ export const FolderListView = ({
|
||||
|
||||
const handleFolderDelete = async () => {
|
||||
try {
|
||||
const { id: folderId } = popUp.deleteFolder.data as TSecretFolder;
|
||||
await deleteFolder({
|
||||
folderName: popUp.deleteFolder.data as string,
|
||||
directory: secretPath,
|
||||
folderId,
|
||||
path: secretPath,
|
||||
environment,
|
||||
workspaceId
|
||||
projectId: workspaceId
|
||||
});
|
||||
handlePopUpClose("deleteFolder");
|
||||
createNotification({
|
||||
@@ -106,13 +108,13 @@ export const FolderListView = ({
|
||||
.map(({ name, id }) => (
|
||||
<div
|
||||
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} />
|
||||
</div>
|
||||
<div
|
||||
className="flex-grow px-4 py-3 flex items-center"
|
||||
className="flex flex-grow items-center px-4 py-3"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
@@ -122,7 +124,7 @@ export const FolderListView = ({
|
||||
>
|
||||
{name}
|
||||
</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
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
@@ -134,8 +136,8 @@ export const FolderListView = ({
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
onClick={() => handlePopUpOpen("updateFolder", name)}
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("updateFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
@@ -153,8 +155,8 @@ export const FolderListView = ({
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
onClick={() => handlePopUpOpen("deleteFolder", name)}
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
@@ -171,14 +173,14 @@ export const FolderListView = ({
|
||||
<ModalContent title="Edit Folder">
|
||||
<FolderForm
|
||||
isEdit
|
||||
defaultFolderName={popUp.updateFolder.data as string}
|
||||
defaultFolderName={(popUp.updateFolder?.data as TSecretFolder)?.name}
|
||||
onUpdateFolder={handleFolderUpdate}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
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?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
onDeleteApproved={handleFolderDelete}
|
||||
|
||||
@@ -12,17 +12,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
onDelete: (environment: string, secretPath: string) => void;
|
||||
onDelete: () => void;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
importedEnv: string;
|
||||
importedSecPath: string;
|
||||
importEnvName: string;
|
||||
importEnvPath: string;
|
||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||
searchTerm: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
// 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>
|
||||
{secretPath && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@@ -39,9 +40,10 @@ export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: st
|
||||
);
|
||||
|
||||
export const SecretImportItem = ({
|
||||
importedEnv,
|
||||
importedSecPath,
|
||||
onDelete,
|
||||
id,
|
||||
importEnvName,
|
||||
importEnvPath,
|
||||
importedSecrets = [],
|
||||
searchTerm = "",
|
||||
secretPath,
|
||||
@@ -49,10 +51,8 @@ export const SecretImportItem = ({
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useToggle();
|
||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: `${importedEnv}-${importedSecPath}`
|
||||
id
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||
@@ -80,7 +80,7 @@ export const SecretImportItem = ({
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
ref={setNodeRef}
|
||||
tabIndex={0}
|
||||
@@ -88,13 +88,13 @@ export const SecretImportItem = ({
|
||||
onClick={() => 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} />
|
||||
</div>
|
||||
<div className="flex-grow px-4 py-2 flex items-center">
|
||||
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
|
||||
<div className="flex flex-grow items-center px-4 py-2">
|
||||
<EnvFolderIcon env={importEnvName || ""} secretPath={importEnvPath} />
|
||||
</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
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
@@ -107,7 +107,7 @@ export const SecretImportItem = ({
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="expand"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
isDisabled={!isAllowed}
|
||||
@@ -128,10 +128,10 @@ export const SecretImportItem = ({
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onDelete(importedEnv, importedSecPath);
|
||||
onDelete();
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
@@ -167,7 +167,7 @@ export const SecretImportItem = ({
|
||||
{importedSecrets
|
||||
.filter((secret) => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
|
||||
.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" }}>
|
||||
{key}
|
||||
</td>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { DeleteActionModal } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
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 { SecretImportItem } from "./SecretImportItem";
|
||||
@@ -76,17 +76,15 @@ type Props = {
|
||||
environment: string;
|
||||
workspaceId: string;
|
||||
secretPath?: string;
|
||||
secretImports?: TSecretImports;
|
||||
secretImports?: TSecretImport[];
|
||||
isFetching?: boolean;
|
||||
secrets?: DecryptedSecret[];
|
||||
importedSecrets?: TImportedSecrets;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
type TDeleteSecretImport = { environment: string; secretPath: string };
|
||||
|
||||
export const SecretImportListView = ({
|
||||
secretImports,
|
||||
secretImports = [],
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@@ -107,21 +105,11 @@ export const SecretImportListView = ({
|
||||
useSensor(KeyboardSensor, {})
|
||||
);
|
||||
|
||||
const [items, setItems] = useState(
|
||||
(secretImports?.imports || [])?.map((dto) => ({
|
||||
id: `${dto.environment}-${dto.secretPath}`,
|
||||
...dto
|
||||
}))
|
||||
);
|
||||
const [items, setItems] = useState(secretImports);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
setItems(
|
||||
(secretImports?.imports || [])?.map((dto) => ({
|
||||
id: `${dto.environment}-${dto.secretPath}`,
|
||||
...dto
|
||||
}))
|
||||
);
|
||||
setItems(secretImports);
|
||||
}
|
||||
}, [isFetching]);
|
||||
|
||||
@@ -129,24 +117,19 @@ export const SecretImportListView = ({
|
||||
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
||||
|
||||
const handleSecretImportDelete = async () => {
|
||||
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
|
||||
?.data as TDeleteSecretImport;
|
||||
const { id: secretImportId } = popUp.deleteSecretImport?.data as { id: string };
|
||||
try {
|
||||
if (secretImports?.id) {
|
||||
await deleteSecretImport({
|
||||
workspaceId,
|
||||
environment,
|
||||
directory: secretPath,
|
||||
id: secretImports?.id,
|
||||
secretImportEnv: importEnv,
|
||||
secretImportPath: impSecPath
|
||||
});
|
||||
handlePopUpClose("deleteSecretImport");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed secret link"
|
||||
});
|
||||
}
|
||||
await deleteSecretImport({
|
||||
projectId: workspaceId,
|
||||
environment,
|
||||
path: secretPath,
|
||||
id: secretImportId
|
||||
});
|
||||
handlePopUpClose("deleteSecretImport");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed secret link"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
@@ -163,11 +146,13 @@ export const SecretImportListView = ({
|
||||
const newImportOrder = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newImportOrder);
|
||||
updateSecretImport({
|
||||
workspaceId,
|
||||
projectId: workspaceId,
|
||||
environment,
|
||||
directory: secretPath,
|
||||
id: secretImports?.id || "",
|
||||
secretImports: newImportOrder
|
||||
path: secretPath,
|
||||
id: active.id as string,
|
||||
import: {
|
||||
position: newIndex + 1
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -181,26 +166,28 @@ export const SecretImportListView = ({
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items?.map(({ secretPath: importedSecPath, environment: importedEnv }) => (
|
||||
<SecretImportItem
|
||||
searchTerm={searchTerm}
|
||||
key={`${importedEnv}-${importedSecPath}`}
|
||||
importedEnv={importedEnv}
|
||||
importedSecPath={importedSecPath}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importedEnv,
|
||||
importedSecPath,
|
||||
importedSecrets,
|
||||
secrets,
|
||||
environments
|
||||
)}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
onDelete={(env, secPath) =>
|
||||
handlePopUpOpen("deleteSecretImport", { environment: env, secretPath: secPath })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{items?.map((item) => {
|
||||
const { importPath, importEnv, id } = item;
|
||||
return (
|
||||
<SecretImportItem
|
||||
searchTerm={searchTerm}
|
||||
key={`imported-env-${id}`}
|
||||
id={id}
|
||||
importEnvPath={importPath}
|
||||
importEnvName={importEnv.name}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importEnv.slug,
|
||||
importPath,
|
||||
importedSecrets,
|
||||
secrets,
|
||||
environments
|
||||
)}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
onDelete={() => handlePopUpOpen("deleteSecretImport", item)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<DeleteActionModal
|
||||
@@ -208,8 +195,8 @@ export const SecretImportListView = ({
|
||||
deleteKey="unlink"
|
||||
title="Do you want to remove this secret import?"
|
||||
subTitle={`This will unlink secrets from environment ${
|
||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
||||
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
|
||||
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||
onDeleteApproved={handleSecretImportDelete}
|
||||
/>
|
||||
|
||||
@@ -208,8 +208,8 @@ export const SecretListView = ({
|
||||
} = modSecret;
|
||||
const hasKeyChanged = oldKey !== key;
|
||||
|
||||
const tagIds = tags.map(({ id }) => id);
|
||||
const oldTagIds = orgSecret.tags.map(({ id }) => id);
|
||||
const tagIds = tags?.map(({ id }) => id);
|
||||
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
|
||||
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
|
||||
const isSharedSecUnchanged =
|
||||
(
|
||||
|
||||
@@ -99,9 +99,9 @@ export const SecretOverviewPage = () => {
|
||||
decryptFileKey: latestFileKey!
|
||||
});
|
||||
const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({
|
||||
workspaceId,
|
||||
environments: userAvailableEnvs.map(({ slug }) => slug),
|
||||
directory: secretPath
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
environments: userAvailableEnvs.map(({ slug }) => slug)
|
||||
});
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
@@ -119,10 +119,10 @@ export const SecretOverviewPage = () => {
|
||||
const folderName = pathSegment.at(-1);
|
||||
if (folderName && parentPath) {
|
||||
await createFolder({
|
||||
workspaceId,
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
environment: env,
|
||||
directory: parentPath,
|
||||
folderName
|
||||
name: folderName
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -216,10 +216,10 @@ export const SecretOverviewPage = () => {
|
||||
const folderName = path.at(-1);
|
||||
if (folderName && directory) {
|
||||
await createFolder({
|
||||
workspaceId,
|
||||
projectId: workspaceId,
|
||||
environment: slug,
|
||||
directory,
|
||||
folderName
|
||||
path: secretPath,
|
||||
name: folderName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user