mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
21 Commits
misc/add-d
...
daniel/gho
Author | SHA1 | Date | |
---|---|---|---|
e2a3876e7f | |||
96a19506d5 | |||
2898e9646e | |||
46e10d95c1 | |||
2bc7a180c8 | |||
bb45407e1f | |||
c5297c47cf | |||
1fc7a4bcc8 | |||
c741d35d3e | |||
3dad3361eb | |||
38181d26a5 | |||
2c2f71061c | |||
71ad0f3099 | |||
8d8b6f52df | |||
94f554f48f | |||
b5e8884195 | |||
2434734d8f | |||
3f8d36734a | |||
7587007d73 | |||
b71316019f | |||
89acfda65f |
37
backend/src/db/migrations/20240202093209_ghost_user.ts
Normal file
37
backend/src/db/migrations/20240202093209_ghost_user.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ProjectVersion, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "ghost");
|
||||||
|
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
|
||||||
|
|
||||||
|
if (!hasGhostUserColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
t.boolean("ghost").defaultTo(false).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasProjectVersionColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.string("version").defaultTo(ProjectVersion.V1).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "ghost");
|
||||||
|
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
|
||||||
|
|
||||||
|
if (hasGhostUserColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
t.dropColumn("ghost");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasProjectVersionColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.dropColumn("version");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -111,6 +111,11 @@ export enum SecretType {
|
|||||||
Personal = "personal"
|
Personal = "personal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectVersion {
|
||||||
|
V1 = "v1",
|
||||||
|
V2 = "v2"
|
||||||
|
}
|
||||||
|
|
||||||
export enum IdentityAuthMethod {
|
export enum IdentityAuthMethod {
|
||||||
Univeral = "universal-auth"
|
Univeral = "universal-auth"
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ export const ProjectsSchema = z.object({
|
|||||||
autoCapitalization: z.boolean().default(true).nullable().optional(),
|
autoCapitalization: z.boolean().default(true).nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
version: z.string().default("v1")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
@ -19,7 +19,8 @@ export const UsersSchema = z.object({
|
|||||||
mfaMethods: z.string().array().nullable().optional(),
|
mfaMethods: z.string().array().nullable().optional(),
|
||||||
devices: z.unknown().nullable().optional(),
|
devices: z.unknown().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
ghost: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable import/no-mutable-exports */
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import argon2, { argon2id } from "argon2";
|
import argon2, { argon2id } from "argon2";
|
||||||
@ -14,9 +15,12 @@ import {
|
|||||||
|
|
||||||
import { TUserEncryptionKeys } from "./schemas";
|
import { TUserEncryptionKeys } from "./schemas";
|
||||||
|
|
||||||
|
export let userPrivateKey: string | undefined;
|
||||||
|
export let userPublicKey: string | undefined;
|
||||||
|
|
||||||
export const seedData1 = {
|
export const seedData1 = {
|
||||||
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
||||||
email: "test@localhost.local",
|
email: process.env.TEST_USER_EMAIL || "test@localhost.local",
|
||||||
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
|
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
|
||||||
organization: {
|
organization: {
|
||||||
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
|
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
|
||||||
@ -33,6 +37,12 @@ export const seedData1 = {
|
|||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
||||||
|
},
|
||||||
|
|
||||||
|
// We set these values during user creation, and later re-use them during project seeding.
|
||||||
|
encryptionKeys: {
|
||||||
|
publicKey: "",
|
||||||
|
privateKey: ""
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
|
|
||||||
import { AuthMethod } from "../../services/auth/auth-type";
|
import { AuthMethod } from "../../services/auth/auth-type";
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
import { generateUserSrpKeys, seedData1 } from "../seed-data";
|
import { seedData1 } from "../seed-data";
|
||||||
|
|
||||||
export async function seed(knex: Knex): Promise<void> {
|
export async function seed(knex: Knex): Promise<void> {
|
||||||
// Deletes ALL existing entries
|
// Deletes ALL existing entries
|
||||||
@ -18,6 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
id: seedData1.id,
|
id: seedData1.id,
|
||||||
email: seedData1.email,
|
email: seedData1.email,
|
||||||
superAdmin: true,
|
superAdmin: true,
|
||||||
|
ghost: false,
|
||||||
firstName: "test",
|
firstName: "test",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethods: [AuthMethod.EMAIL],
|
authMethods: [AuthMethod.EMAIL],
|
||||||
@ -29,7 +36,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
])
|
])
|
||||||
.returning("*");
|
.returning("*");
|
||||||
|
|
||||||
const encKeys = await generateUserSrpKeys(seedData1.password);
|
const encKeys = await generateUserSrpKeys(seedData1.email, seedData1.password);
|
||||||
// password: testInfisical@1
|
// password: testInfisical@1
|
||||||
await knex(TableName.UserEncryptionKey).insert([
|
await knex(TableName.UserEncryptionKey).insert([
|
||||||
{
|
{
|
||||||
@ -58,4 +65,9 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
refreshVersion: 1,
|
refreshVersion: 1,
|
||||||
lastUsed: new Date()
|
lastUsed: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
seedData1.encryptionKeys = {
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
|
privateKey: encKeys.plainPrivateKey
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas";
|
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas";
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
|
/* eslint-disable simple-import-sort/imports */
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import crypto from "crypto";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { createSecretBlindIndex, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
|
|
||||||
import { OrgMembershipRole, TableName } from "../schemas";
|
import { OrgMembershipRole, TableName } from "../schemas";
|
||||||
import { seedData1 } from "../seed-data";
|
import { seedData1 } from "../seed-data";
|
||||||
|
import { getConfig, initEnvConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
{ name: "Development", slug: "dev" },
|
{ name: "Development", slug: "dev" },
|
||||||
@ -10,6 +18,8 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export async function seed(knex: Knex): Promise<void> {
|
export async function seed(knex: Knex): Promise<void> {
|
||||||
|
initEnvConfig();
|
||||||
|
const appCfg = getConfig();
|
||||||
// Deletes ALL existing entries
|
// Deletes ALL existing entries
|
||||||
await knex(TableName.Project).del();
|
await knex(TableName.Project).del();
|
||||||
await knex(TableName.Environment).del();
|
await knex(TableName.Environment).del();
|
||||||
@ -21,14 +31,38 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
orgId: seedData1.organization.id,
|
orgId: seedData1.organization.id,
|
||||||
slug: "first-project",
|
slug: "first-project",
|
||||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
||||||
id: seedData1.project.id
|
id: seedData1.project.id,
|
||||||
|
version: "v1"
|
||||||
})
|
})
|
||||||
.returning("*");
|
.returning("*");
|
||||||
|
|
||||||
// await knex(TableName.ProjectKeys).insert({
|
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
|
||||||
// projectId: project.id,
|
|
||||||
// senderId: seedData1.id
|
await knex(TableName.SecretBlindIndex).insert({
|
||||||
// });
|
projectId: project.id,
|
||||||
|
algorithm: blindIndex.algorithm,
|
||||||
|
keyEncoding: blindIndex.keyEncoding,
|
||||||
|
saltIV: blindIndex.iv,
|
||||||
|
encryptedSaltCipherText: blindIndex.ciphertext,
|
||||||
|
saltTag: blindIndex.tag
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex"); // Project key
|
||||||
|
// const encKeys = await generateUserSrpKeys(seedData1.email, seedData1.password); // User keys
|
||||||
|
|
||||||
|
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
|
||||||
|
randomBytes,
|
||||||
|
seedData1.encryptionKeys.publicKey,
|
||||||
|
seedData1.encryptionKeys.privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex(TableName.ProjectKeys).insert({
|
||||||
|
projectId: project.id,
|
||||||
|
senderId: seedData1.id,
|
||||||
|
receiverId: seedData1.id,
|
||||||
|
encryptedKey: encryptedProjectKey,
|
||||||
|
nonce: encryptedProjectKeyIv
|
||||||
|
});
|
||||||
|
|
||||||
await knex(TableName.ProjectMembership).insert({
|
await knex(TableName.ProjectMembership).insert({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
@ -57,6 +57,7 @@ export const auditLogServiceFactory = ({
|
|||||||
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
||||||
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return auditLogQueue.pushToLog(data);
|
return auditLogQueue.pushToLog(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas";
|
import {
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
SecretApprovalRequestsSecretsSchema,
|
||||||
|
TableName,
|
||||||
|
TSecretApprovalRequestsSecrets,
|
||||||
|
TSecretApprovalRequestsSecretsUpdate,
|
||||||
|
TSecretTags
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
|
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
|
||||||
@ -11,6 +17,27 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
||||||
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
||||||
|
|
||||||
|
const bulkUpdateNoVersionIncrement = async (
|
||||||
|
data: Array<{ filter: Partial<TSecretApprovalRequestsSecrets>; data: TSecretApprovalRequestsSecretsUpdate }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secs = await Promise.all(
|
||||||
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
|
const [doc] = await (tx || db)(TableName.SecretApprovalRequestSecret)
|
||||||
|
.where(filter)
|
||||||
|
.update(updateData)
|
||||||
|
.returning("*");
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||||
|
return doc;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return secs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db)({
|
const doc = await (tx || db)({
|
||||||
@ -190,6 +217,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
...secretApprovalRequestSecretOrm,
|
...secretApprovalRequestSecretOrm,
|
||||||
findByRequestId,
|
findByRequestId,
|
||||||
|
bulkUpdateNoVersionIncrement,
|
||||||
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -10,3 +10,4 @@ export {
|
|||||||
generateAsymmetricKeyPair
|
generateAsymmetricKeyPair
|
||||||
} from "./encryption";
|
} from "./encryption";
|
||||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||||
|
export { decodeBase64, encodeBase64 } from "tweetnacl-util";
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
|
import argon2, { argon2id } from "argon2";
|
||||||
|
import crypto from "crypto";
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
import { encodeBase64 } from "tweetnacl-util";
|
||||||
|
|
||||||
|
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
|
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||||
|
|
||||||
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
@ -24,3 +32,97 @@ export const srpCheckClientProof = async (
|
|||||||
server.setClientPublicKey(clientPublicKey);
|
server.setClientPublicKey(clientPublicKey);
|
||||||
return server.checkClientProof(clientProof);
|
return server.checkClientProof(clientProof);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FOR GHOST USER STUFF
|
||||||
|
export const generateUserSrpKeys = async (email: string, password: string) => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
|
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||||
|
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const client = new jsrp.client();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
client.init({ username: email, password }, () => resolve(null));
|
||||||
|
});
|
||||||
|
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>((resolve, reject) => {
|
||||||
|
client.createVerifier((err, res) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
return resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const derivedKey = await argon2.hash(password, {
|
||||||
|
salt: Buffer.from(salt),
|
||||||
|
memoryCost: 65536,
|
||||||
|
timeCost: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLength: 32,
|
||||||
|
type: argon2id,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
// create encrypted private key by encrypting the private
|
||||||
|
// key with the symmetric key [key]
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = encryptSymmetric(privateKey, key.toString("base64"));
|
||||||
|
|
||||||
|
// create the protected key by encrypting the symmetric key
|
||||||
|
// [key] with the derived key
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
protectedKey,
|
||||||
|
plainPrivateKey: privateKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
||||||
|
const derivedKey = await argon2.hash(password, {
|
||||||
|
salt: Buffer.from(user.salt),
|
||||||
|
memoryCost: 65536,
|
||||||
|
timeCost: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLength: 32,
|
||||||
|
type: argon2id,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
const key = decryptSymmetric({
|
||||||
|
ciphertext: user.protectedKey!,
|
||||||
|
iv: user.protectedKeyIV!,
|
||||||
|
tag: user.protectedKeyTag!,
|
||||||
|
key: derivedKey.toString("base64")
|
||||||
|
});
|
||||||
|
const privateKey = decryptSymmetric({
|
||||||
|
ciphertext: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
return privateKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
||||||
|
return { nonce, ciphertext };
|
||||||
|
};
|
||||||
|
60
backend/src/lib/project/index.ts
Normal file
60
backend/src/lib/project/index.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
|
import { decryptAsymmetric, encryptAsymmetric } from "../crypto";
|
||||||
|
|
||||||
|
type AddUserToWsDTO = {
|
||||||
|
decryptKey: TProjectKeys & { sender: { publicKey: string } };
|
||||||
|
userPrivateKey: string;
|
||||||
|
members: {
|
||||||
|
orgMembershipId: string;
|
||||||
|
projectMembershipRole: ProjectMembershipRole;
|
||||||
|
userPublicKey: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWsMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: decryptKey.encryptedKey,
|
||||||
|
nonce: decryptKey.nonce,
|
||||||
|
publicKey: decryptKey.sender.publicKey,
|
||||||
|
privateKey: userPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
|
||||||
|
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
||||||
|
key,
|
||||||
|
userPublicKey,
|
||||||
|
userPrivateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
orgMembershipId,
|
||||||
|
projectRole: projectMembershipRole,
|
||||||
|
workspaceEncryptedKey: inviteeCipherText,
|
||||||
|
workspaceEncryptedNonce: inviteeNonce
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return newWsMembers;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TCreateProjectKeyDTO = {
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createProjectKey = ({ publicKey, privateKey }: TCreateProjectKeyDTO) => {
|
||||||
|
// 3. Create a random key that we'll use as the project key.
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
// 4. Encrypt the project key with the users key pair.
|
||||||
|
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
|
||||||
|
randomBytes,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||||
|
};
|
126
backend/src/lib/secret/index.ts
Normal file
126
backend/src/lib/secret/index.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding, TProjectKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
|
import { decryptAsymmetric, decryptSymmetric } from "../crypto";
|
||||||
|
import { decryptSymmetric128BitHexKeyUTF8, TDecryptSymmetricInput } from "../crypto/encryption";
|
||||||
|
|
||||||
|
export enum SecretDocType {
|
||||||
|
Secret = "secret",
|
||||||
|
SecretVersion = "secretVersion",
|
||||||
|
ApprovalSecret = "approvalSecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PartialSecretSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
secretKeyCiphertext: z.string(),
|
||||||
|
secretKeyIV: z.string(),
|
||||||
|
secretKeyTag: z.string(),
|
||||||
|
|
||||||
|
secretValueCiphertext: z.string(),
|
||||||
|
secretValueIV: z.string(),
|
||||||
|
secretValueTag: z.string(),
|
||||||
|
|
||||||
|
secretCommentCiphertext: z.string().nullish(),
|
||||||
|
secretCommentIV: z.string().nullish(),
|
||||||
|
secretCommentTag: z.string().nullish(),
|
||||||
|
|
||||||
|
docType: z.nativeEnum(SecretDocType),
|
||||||
|
|
||||||
|
keyEncoding: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const PartialDecryptedSecretSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
secretKey: z.string(),
|
||||||
|
secretValue: z.string(),
|
||||||
|
secretComment: z.string().optional(),
|
||||||
|
|
||||||
|
docType: z.nativeEnum(SecretDocType)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TPartialSecret = z.infer<typeof PartialSecretSchema>;
|
||||||
|
export type TPartialDecryptedSecret = z.infer<typeof PartialDecryptedSecretSchema>;
|
||||||
|
|
||||||
|
const symmetricDecrypt = ({
|
||||||
|
keyEncoding,
|
||||||
|
ciphertext,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
key,
|
||||||
|
isApprovalSecret
|
||||||
|
}: TDecryptSymmetricInput & { keyEncoding: SecretKeyEncoding; isApprovalSecret: boolean }) => {
|
||||||
|
if (keyEncoding === SecretKeyEncoding.UTF8 || isApprovalSecret) {
|
||||||
|
const data = decryptSymmetric128BitHexKeyUTF8({ key, iv, tag, ciphertext });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (keyEncoding === SecretKeyEncoding.BASE64) {
|
||||||
|
const data = decryptSymmetric({ key, iv, tag, ciphertext });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Missing both encryption keys");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSecrets = (
|
||||||
|
encryptedSecrets: TPartialSecret[],
|
||||||
|
privateKey: string,
|
||||||
|
latestKey: TProjectKeys & {
|
||||||
|
sender: {
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const secrets: TPartialDecryptedSecret[] = [];
|
||||||
|
|
||||||
|
encryptedSecrets.forEach((encSecret) => {
|
||||||
|
const secretKey = symmetricDecrypt({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key,
|
||||||
|
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
|
||||||
|
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = symmetricDecrypt({
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
iv: encSecret.secretValueIV,
|
||||||
|
tag: encSecret.secretValueTag,
|
||||||
|
key,
|
||||||
|
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
|
||||||
|
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretComment =
|
||||||
|
encSecret.secretCommentCiphertext && encSecret.secretCommentIV && encSecret.secretCommentTag
|
||||||
|
? symmetricDecrypt({
|
||||||
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
|
iv: encSecret.secretCommentIV,
|
||||||
|
tag: encSecret.secretCommentTag,
|
||||||
|
key,
|
||||||
|
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
|
||||||
|
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
|
||||||
|
})
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const decryptedSecret: TPartialDecryptedSecret = {
|
||||||
|
id: encSecret.id,
|
||||||
|
secretKey,
|
||||||
|
secretValue,
|
||||||
|
secretComment,
|
||||||
|
docType: encSecret.docType
|
||||||
|
};
|
||||||
|
|
||||||
|
secrets.push(decryptedSecret);
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
@ -266,19 +266,13 @@ export const registerRoutes = async (
|
|||||||
secretScanningDAL,
|
secretScanningDAL,
|
||||||
secretScanningQueue
|
secretScanningQueue
|
||||||
});
|
});
|
||||||
const projectService = projectServiceFactory({
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
|
||||||
permissionService,
|
|
||||||
projectDAL,
|
|
||||||
secretBlindIndexDAL,
|
|
||||||
projectEnvDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
folderDAL,
|
|
||||||
licenseService
|
|
||||||
});
|
|
||||||
const projectMembershipService = projectMembershipServiceFactory({
|
const projectMembershipService = projectMembershipServiceFactory({
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
@ -286,6 +280,31 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const projectKeyService = projectKeyServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
const projectService = projectServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
identityProjectDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretDAL,
|
||||||
|
orgDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL: sarSecretDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
userDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
orgService,
|
||||||
|
projectMembershipDAL,
|
||||||
|
folderDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@ -293,11 +312,7 @@ export const registerRoutes = async (
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
});
|
});
|
||||||
const projectKeyService = projectKeyServiceFactory({
|
|
||||||
permissionService,
|
|
||||||
projectKeyDAL,
|
|
||||||
projectMembershipDAL
|
|
||||||
});
|
|
||||||
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
|
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
|
||||||
|
|
||||||
const snapshotService = secretSnapshotServiceFactory({
|
const snapshotService = secretSnapshotServiceFactory({
|
||||||
@ -334,7 +349,6 @@ export const registerRoutes = async (
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectBotsSchema } from "@app/db/schemas";
|
import { ProjectBotsSchema } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@ -26,6 +27,16 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const project = await server.services.project.getAProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (project.version === "v2") {
|
||||||
|
throw new BadRequestError({ message: "Failed to find bot, project has E2EE disabled" });
|
||||||
|
}
|
||||||
|
|
||||||
const bot = await server.services.projectBot.findBotByProjectId({
|
const bot = await server.services.projectBot.findBotByProjectId({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -65,6 +76,12 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const project = await server.services.projectBot.findProjectByBotId(req.params.botId);
|
||||||
|
|
||||||
|
if (project?.version === "v2") {
|
||||||
|
throw new BadRequestError({ message: "Failed to set bot active, project has E2EE disabled" });
|
||||||
|
}
|
||||||
|
|
||||||
const bot = await server.services.projectBot.setBotActiveState({
|
const bot = await server.services.projectBot.setBotActiveState({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
@ -48,6 +48,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await projectRouter.register(registerProjectMembershipRouter);
|
await projectRouter.register(registerProjectMembershipRouter);
|
||||||
await projectRouter.register(registerSecretTagRouter);
|
await projectRouter.register(registerSecretTagRouter);
|
||||||
},
|
},
|
||||||
|
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrgMembershipsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import {
|
||||||
|
OrgMembershipsSchema,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectMembershipsSchema,
|
||||||
|
UserEncryptionKeysSchema,
|
||||||
|
UsersSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -71,7 +77,10 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
members: req.body.members
|
members: req.body.members.map((member) => ({
|
||||||
|
...member,
|
||||||
|
projectRole: ProjectMembershipRole.Member
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
@ -128,32 +128,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
|
||||||
url: "/",
|
|
||||||
method: "POST",
|
|
||||||
schema: {
|
|
||||||
body: z.object({
|
|
||||||
workspaceName: z.string().trim(),
|
|
||||||
organizationId: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
workspace: projectWithEnv
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const workspace = await server.services.project.createProject({
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
orgId: req.body.organizationId,
|
|
||||||
workspaceName: req.body.workspaceName
|
|
||||||
});
|
|
||||||
return { workspace };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId",
|
url: "/:workspaceId",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -242,6 +216,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Is this actually used..?
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/invite-signup",
|
url: "/:workspaceId/invite-signup",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -254,20 +229,22 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
invitee: UsersSchema,
|
invitees: UsersSchema.array(),
|
||||||
latestKey: ProjectKeysSchema.optional()
|
latestKey: ProjectKeysSchema.optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({
|
const { invitees, latestKey } = await server.services.projectMembership.inviteUserToProject({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
email: req.body.email
|
emails: [req.body.email]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const invitee of invitees) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
@ -279,7 +256,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { invitee, latestKey };
|
}
|
||||||
|
return { invitees, latestKey };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
|
|||||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||||
import { registerMfaRouter } from "./mfa-router";
|
import { registerMfaRouter } from "./mfa-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerServiceTokenRouter } from "./service-token-router";
|
import { registerServiceTokenRouter } from "./service-token-router";
|
||||||
import { registerUserRouter } from "./user-router";
|
import { registerUserRouter } from "./user-router";
|
||||||
@ -21,6 +22,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (projectServer) => {
|
async (projectServer) => {
|
||||||
await projectServer.register(registerProjectRouter);
|
await projectServer.register(registerProjectRouter);
|
||||||
await projectServer.register(registerIdentityProjectRouter);
|
await projectServer.register(registerIdentityProjectRouter);
|
||||||
|
await projectServer.register(registerProjectMembershipRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
51
backend/src/server/routes/v2/project-membership-router.ts
Normal file
51
backend/src/server/routes/v2/project-membership-router.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
|
||||||
|
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:projectId/memberships",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
emails: z.string().email().array()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
memberships: ProjectMembershipsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
emails: req.body.emails
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
||||||
|
metadata: memberships.map(({ userId, id }) => ({
|
||||||
|
userId: userId || "",
|
||||||
|
membershipId: id,
|
||||||
|
email: ""
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { memberships };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,17 +1,26 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectKeysSchema } from "@app/db/schemas";
|
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const projectWithEnv = ProjectsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
_id: z.string(),
|
||||||
|
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
|
/* Get project key */
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/encrypted-key",
|
url: "/:projectId/encrypted-key",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: ProjectKeysSchema.merge(
|
200: ProjectKeysSchema.merge(
|
||||||
@ -28,12 +37,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
const key = await server.services.projectKey.getLatestProjectKey({
|
const key = await server.services.projectKey.getLatestProjectKey({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
projectId: req.params.workspaceId
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_WORKSPACE_KEY,
|
type: EventType.GET_WORKSPACE_KEY,
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -45,4 +54,60 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:projectId/upgrade",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
|
||||||
|
body: z.object({
|
||||||
|
userPrivateKey: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.project.upgradeProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
userPrivateKey: req.body.userPrivateKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Create new project */
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectName: z.string().trim(),
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
project: projectWithEnv
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const project = await server.services.project.createProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
workspaceName: req.body.projectName
|
||||||
|
});
|
||||||
|
|
||||||
|
return { project };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -224,7 +224,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
|||||||
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
|
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
|
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], ghost: false });
|
||||||
}
|
}
|
||||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
const isUserCompleted = user.isAccepted;
|
const isUserCompleted = user.isAccepted;
|
||||||
|
@ -74,7 +74,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
);
|
)
|
||||||
|
.where({ ghost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
||||||
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
...data,
|
...data,
|
||||||
user: { email, firstName, lastName, id: userId, publicKey }
|
user: { email, firstName, lastName, id: userId, publicKey }
|
||||||
@ -84,6 +85,76 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgMembersByEmail = async (orgId: string, emails: string[]) => {
|
||||||
|
try {
|
||||||
|
const members = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("role").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("status").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
)
|
||||||
|
.whereIn("email", emails);
|
||||||
|
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
user: { email, firstName, lastName, id: userId, publicKey }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all org members" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOrgGhostUser = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const [member] = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("role").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("status").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
)
|
||||||
|
.where({ ghost: true });
|
||||||
|
return member;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ghostUserExists = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const [member] = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
|
||||||
|
.where({ ghost: true });
|
||||||
|
return !!member;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
|
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
|
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
|
||||||
@ -181,6 +252,9 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
findAllOrgMembers,
|
findAllOrgMembers,
|
||||||
findOrgById,
|
findOrgById,
|
||||||
findAllOrgsByUserId,
|
findAllOrgsByUserId,
|
||||||
|
ghostUserExists,
|
||||||
|
findOrgMembersByEmail,
|
||||||
|
findOrgGhostUser,
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
deleteById,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import crypto from "crypto";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
@ -11,6 +14,7 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
@ -28,6 +32,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
|
|||||||
import {
|
import {
|
||||||
TDeleteOrgMembershipDTO,
|
TDeleteOrgMembershipDTO,
|
||||||
TFindAllWorkspacesDTO,
|
TFindAllWorkspacesDTO,
|
||||||
|
TFindOrgMembersByEmailDTO,
|
||||||
TInviteUserToOrgDTO,
|
TInviteUserToOrgDTO,
|
||||||
TUpdateOrgMembershipDTO,
|
TUpdateOrgMembershipDTO,
|
||||||
TVerifyUserToOrgDTO
|
TVerifyUserToOrgDTO
|
||||||
@ -92,6 +97,15 @@ export const orgServiceFactory = ({
|
|||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const members = await orgDAL.findOrgMembersByEmail(orgId, emails);
|
||||||
|
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
@ -117,6 +131,54 @@ export const orgServiceFactory = ({
|
|||||||
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addGhostUser = async (orgId: string, tx?: Knex) => {
|
||||||
|
const email = `ghost@${nanoid(8)}-${orgId}.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
|
||||||
|
const password = crypto.randomBytes(128).toString("hex");
|
||||||
|
|
||||||
|
const user = await userDAL.create(
|
||||||
|
{
|
||||||
|
ghost: true,
|
||||||
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
email,
|
||||||
|
isAccepted: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const encKeys = await generateUserSrpKeys(email, password);
|
||||||
|
|
||||||
|
await userDAL.upsertUserEncryptionKey(
|
||||||
|
user.id,
|
||||||
|
{
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey: encKeys.protectedKey,
|
||||||
|
protectedKeyIV: encKeys.protectedKeyIV,
|
||||||
|
protectedKeyTag: encKeys.protectedKeyTag,
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||||
|
iv: encKeys.encryptedPrivateKeyIV,
|
||||||
|
tag: encKeys.encryptedPrivateKeyTag,
|
||||||
|
salt: encKeys.salt,
|
||||||
|
verifier: encKeys.verifier
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const createMembershipData = {
|
||||||
|
orgId,
|
||||||
|
userId: user.id,
|
||||||
|
role: OrgMembershipRole.Admin,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
};
|
||||||
|
|
||||||
|
await orgDAL.createMembership(createMembershipData, tx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
keys: encKeys
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update organization settings
|
* Update organization settings
|
||||||
* */
|
* */
|
||||||
@ -294,7 +356,8 @@ export const orgServiceFactory = ({
|
|||||||
{
|
{
|
||||||
email: inviteeEmail,
|
email: inviteeEmail,
|
||||||
isAccepted: false,
|
isAccepted: false,
|
||||||
authMethods: [AuthMethod.EMAIL]
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
ghost: false
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -444,10 +507,12 @@ export const orgServiceFactory = ({
|
|||||||
inviteUserToOrganization,
|
inviteUserToOrganization,
|
||||||
verifyUserToOrg,
|
verifyUserToOrg,
|
||||||
updateOrgName,
|
updateOrgName,
|
||||||
|
findOrgMembersByEmail,
|
||||||
createOrganization,
|
createOrganization,
|
||||||
deleteOrganizationById,
|
deleteOrganizationById,
|
||||||
deleteOrgMembership,
|
deleteOrgMembership,
|
||||||
findAllWorkspaces,
|
findAllWorkspaces,
|
||||||
|
addGhostUser,
|
||||||
updateOrgMembership,
|
updateOrgMembership,
|
||||||
// incident contacts
|
// incident contacts
|
||||||
findIncidentContacts,
|
findIncidentContacts,
|
||||||
|
@ -25,6 +25,13 @@ export type TVerifyUserToOrgDTO = {
|
|||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TFindOrgMembersByEmailDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
orgId: string;
|
||||||
|
emails: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type TFindAllWorkspacesDTO = {
|
export type TFindAllWorkspacesDTO = {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
|
@ -27,5 +27,19 @@ export const projectBotDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...projectBotOrm, findOne };
|
const findProjectByBotId = async (botId: string) => {
|
||||||
|
try {
|
||||||
|
const project = await db(TableName.ProjectBot)
|
||||||
|
.where({ [`${TableName.ProjectBot}.id` as "id"]: botId })
|
||||||
|
.join(TableName.Project, `${TableName.ProjectBot}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Project))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return project || null;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project by bot id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectBotOrm, findOne, findProjectByBotId };
|
||||||
};
|
};
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import {
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
decryptAsymmetric,
|
|
||||||
decryptSymmetric,
|
|
||||||
decryptSymmetric128BitHexKeyUTF8,
|
|
||||||
encryptSymmetric,
|
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
|
||||||
generateAsymmetricKeyPair
|
|
||||||
} from "@app/lib/crypto";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
|
||||||
|
|
||||||
import { TProjectBotDALFactory } from "./project-bot-dal";
|
import { TProjectBotDALFactory } from "./project-bot-dal";
|
||||||
import { TSetActiveStateDTO } from "./project-bot-types";
|
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
|
||||||
|
|
||||||
type TProjectBotServiceFactoryDep = {
|
type TProjectBotServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
@ -26,63 +19,50 @@ type TProjectBotServiceFactoryDep = {
|
|||||||
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
||||||
|
|
||||||
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
|
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
|
||||||
const getBotKey = async (projectId: string) => {
|
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
|
||||||
const appCfg = getConfig();
|
infisicalSymmetricDecrypt({
|
||||||
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const getBotKey = async (projectId: string) => {
|
||||||
const bot = await projectBotDAL.findOne({ projectId });
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
||||||
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
||||||
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||||
throw new BadRequestError({ message: "Encryption key missing" });
|
throw new BadRequestError({ message: "Encryption key missing" });
|
||||||
|
|
||||||
if (rootEncryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.BASE64) {
|
const botPrivateKey = getBotPrivateKey({ bot });
|
||||||
const privateKeyBot = decryptSymmetric({
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
|
||||||
key: rootEncryptionKey
|
|
||||||
});
|
|
||||||
return decryptAsymmetric({
|
|
||||||
ciphertext: bot.encryptedProjectKey,
|
|
||||||
privateKey: privateKeyBot,
|
|
||||||
nonce: bot.encryptedProjectKeyNonce,
|
|
||||||
publicKey: bot.sender.publicKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (encryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.UTF8) {
|
|
||||||
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
|
||||||
key: encryptionKey
|
|
||||||
});
|
|
||||||
return decryptAsymmetric({
|
|
||||||
ciphertext: bot.encryptedProjectKey,
|
|
||||||
privateKey: privateKeyBot,
|
|
||||||
nonce: bot.encryptedProjectKeyNonce,
|
|
||||||
publicKey: bot.sender.publicKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestError({
|
return decryptAsymmetric({
|
||||||
message: "Failed to obtain bot copy of workspace key needed for operation"
|
ciphertext: bot.encryptedProjectKey,
|
||||||
|
privateKey: botPrivateKey,
|
||||||
|
nonce: bot.encryptedProjectKeyNonce,
|
||||||
|
publicKey: bot.sender.publicKey
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const findBotByProjectId = async ({ actorId, actor, projectId }: TProjectPermission) => {
|
const findBotByProjectId = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
projectId,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
botKey
|
||||||
|
}: TFindBotByProjectIdDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const bot = await projectBotDAL.transaction(async (tx) => {
|
const bot = await projectBotDAL.transaction(async (tx) => {
|
||||||
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
||||||
if (doc) return doc;
|
if (doc) return doc;
|
||||||
|
|
||||||
const { publicKey, privateKey } = generateAsymmetricKeyPair();
|
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
|
||||||
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
|
||||||
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey);
|
||||||
|
|
||||||
return projectBotDAL.create(
|
return projectBotDAL.create(
|
||||||
{
|
{
|
||||||
name: "Infisical Bot",
|
name: "Infisical Bot",
|
||||||
@ -91,36 +71,30 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
iv,
|
iv,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey: ciphertext,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
publicKey,
|
publicKey: keys.publicKey,
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
algorithm,
|
||||||
keyEncoding: SecretKeyEncoding.BASE64
|
keyEncoding: encoding,
|
||||||
|
...(botKey && {
|
||||||
|
encryptedProjectKey: botKey.encryptedKey,
|
||||||
|
encryptedProjectKeyNonce: botKey.nonce
|
||||||
|
})
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if (appCfg.ENCRYPTION_KEY) {
|
|
||||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY);
|
|
||||||
return projectBotDAL.create(
|
|
||||||
{
|
|
||||||
name: "Infisical Bot",
|
|
||||||
projectId,
|
|
||||||
tag,
|
|
||||||
iv,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
isActive: false,
|
|
||||||
publicKey,
|
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
||||||
keyEncoding: SecretKeyEncoding.UTF8
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
|
|
||||||
});
|
});
|
||||||
return bot;
|
return bot;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBotActiveState = async ({ actor, botId, botKey, actorId, isActive }: TSetActiveStateDTO) => {
|
const findProjectByBotId = async (botId: string) => {
|
||||||
|
try {
|
||||||
|
const bot = await projectBotDAL.findProjectByBotId(botId);
|
||||||
|
return bot;
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadRequestError({ message: "Failed to find bot by ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBotActiveState = async ({ actor, botId, botKey, actorId, isActive }: TSetActiveStateDTO, tx?: Knex) => {
|
||||||
const bot = await projectBotDAL.findById(botId);
|
const bot = await projectBotDAL.findById(botId);
|
||||||
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
||||||
|
|
||||||
@ -131,12 +105,16 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
if (!botKey?.nonce || !botKey?.encryptedKey) {
|
if (!botKey?.nonce || !botKey?.encryptedKey) {
|
||||||
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
||||||
}
|
}
|
||||||
const doc = await projectBotDAL.updateById(botId, {
|
const doc = await projectBotDAL.updateById(
|
||||||
|
botId,
|
||||||
|
{
|
||||||
isActive: true,
|
isActive: true,
|
||||||
encryptedProjectKey: botKey.encryptedKey,
|
encryptedProjectKey: botKey.encryptedKey,
|
||||||
encryptedProjectKeyNonce: botKey.nonce,
|
encryptedProjectKeyNonce: botKey.nonce,
|
||||||
senderId: actorId
|
senderId: actorId
|
||||||
});
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
|
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
@ -153,6 +131,8 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
return {
|
return {
|
||||||
findBotByProjectId,
|
findBotByProjectId,
|
||||||
setBotActiveState,
|
setBotActiveState,
|
||||||
|
getBotPrivateKey,
|
||||||
|
findProjectByBotId,
|
||||||
getBotKey
|
getBotKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { TProjectBots } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TSetActiveStateDTO = {
|
export type TSetActiveStateDTO = {
|
||||||
@ -8,3 +10,21 @@ export type TSetActiveStateDTO = {
|
|||||||
};
|
};
|
||||||
botId: string;
|
botId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TFindBotByProjectIdDTO = {
|
||||||
|
privateKey?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
botKey?: {
|
||||||
|
nonce: string;
|
||||||
|
encryptedKey: string;
|
||||||
|
};
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetPrivateKeyDTO = {
|
||||||
|
// encoding: SecretKeyEncoding;
|
||||||
|
// nonce: string;
|
||||||
|
// tag: string;
|
||||||
|
// encryptedPrivateKey: string;
|
||||||
|
|
||||||
|
bot: TProjectBots;
|
||||||
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TProjectKeys } from "@app/db/schemas";
|
import { TableName, TProjectKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
@ -10,10 +12,11 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findLatestProjectKey = async (
|
const findLatestProjectKey = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string
|
projectId: string,
|
||||||
|
tx?: Knex
|
||||||
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
||||||
try {
|
try {
|
||||||
const projectKey = await db(TableName.ProjectKeys)
|
const projectKey = await (tx || db)(TableName.ProjectKeys)
|
||||||
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
.where({ projectId, receiverId: userId })
|
.where({ projectId, receiverId: userId })
|
||||||
@ -29,9 +32,9 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAllProjectUserPubKeys = async (projectId: string) => {
|
const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const pubKeys = await db(TableName.ProjectMembership)
|
const pubKeys = await (tx || db)(TableName.ProjectMembership)
|
||||||
.where({ projectId })
|
.where({ projectId })
|
||||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@ -21,14 +22,10 @@ export const projectKeyServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
permissionService
|
permissionService
|
||||||
}: TProjectKeyServiceFactoryDep) => {
|
}: TProjectKeyServiceFactoryDep) => {
|
||||||
const uploadProjectKeys = async ({
|
const uploadProjectKeys = async (
|
||||||
receiverId,
|
{ receiverId, actor, actorId, projectId, nonce, encryptedKey }: TUploadProjectKeyDTO,
|
||||||
actor,
|
tx?: Knex
|
||||||
actorId,
|
) => {
|
||||||
projectId,
|
|
||||||
nonce,
|
|
||||||
encryptedKey
|
|
||||||
}: TUploadProjectKeyDTO) => {
|
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
@ -42,7 +39,7 @@ export const projectKeyServiceFactory = ({
|
|||||||
name: "Upload project keys"
|
name: "Upload project keys"
|
||||||
});
|
});
|
||||||
|
|
||||||
await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId });
|
await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId }, tx);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLatestProjectKey = async ({ actorId, projectId, actor }: TGetLatestProjectKeyDTO) => {
|
const getLatestProjectKey = async ({ actorId, projectId, actor }: TGetLatestProjectKeyDTO) => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
||||||
|
|
||||||
@ -24,20 +24,37 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("role").withSchema(TableName.ProjectMembership),
|
db.ref("role").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("roleId").withSchema(TableName.ProjectMembership),
|
db.ref("roleId").withSchema(TableName.ProjectMembership),
|
||||||
|
db.ref("ghost").withSchema(TableName.Users),
|
||||||
db.ref("email").withSchema(TableName.Users),
|
db.ref("email").withSchema(TableName.Users),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||||
db.ref("firstName").withSchema(TableName.Users),
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId")
|
db.ref("id").withSchema(TableName.Users).as("userId")
|
||||||
);
|
);
|
||||||
return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({
|
// .where({ ghost: false });
|
||||||
|
return members.map(({ email, firstName, lastName, publicKey, ghost, ...data }) => ({
|
||||||
...data,
|
...data,
|
||||||
user: { email, firstName, lastName, id: data.userId, publicKey }
|
user: { email, firstName, lastName, id: data.userId, publicKey, ghost }
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all project members" });
|
throw new DatabaseError({ error, name: "Find all project members" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...projectMemberOrm, findAllProjectMembers };
|
const findProjectGhostUser = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const ghostUser = await db(TableName.ProjectMembership)
|
||||||
|
.where({ projectId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Users))
|
||||||
|
.where({ ghost: true })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return ghostUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project ghost user" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser };
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,26 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
import {
|
||||||
|
OrgMembershipStatus,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TableName,
|
||||||
|
TProjectMemberships,
|
||||||
|
TUsers
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
import { createWsMembers } from "@app/lib/project";
|
||||||
|
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
@ -17,6 +28,7 @@ import { TUserDALFactory } from "../user/user-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
import {
|
import {
|
||||||
TAddUsersToWorkspaceDTO,
|
TAddUsersToWorkspaceDTO,
|
||||||
|
TAddUsersToWorkspaceNonE2EEDTO,
|
||||||
TDeleteProjectMembershipDTO,
|
TDeleteProjectMembershipDTO,
|
||||||
TGetProjectMembershipDTO,
|
TGetProjectMembershipDTO,
|
||||||
TInviteUserToProjectDTO,
|
TInviteUserToProjectDTO,
|
||||||
@ -26,11 +38,12 @@ import {
|
|||||||
type TProjectMembershipServiceFactoryDep = {
|
type TProjectMembershipServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId">;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
@ -42,6 +55,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@ -55,14 +69,17 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return projectMembershipDAL.findAllProjectMembers(projectId);
|
return projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inviteUserToProject = async ({ actorId, actor, projectId, email }: TInviteUserToProjectDTO) => {
|
const inviteUserToProject = async ({ actorId, actor, projectId, emails }: TInviteUserToProjectDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const invitees: TUsers[] = [];
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
const invitee = await userDAL.findOne({ email });
|
const invitee = await userDAL.findOne({ email });
|
||||||
if (!invitee || !invitee.isAccepted)
|
if (!invitee || !invitee.isAccepted)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Faield to validate invitee",
|
message: "Failed to validate invitee",
|
||||||
name: "Invite user to project"
|
name: "Invite user to project"
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,31 +105,38 @@ export const projectMembershipServiceFactory = ({
|
|||||||
name: "Invite user to project"
|
name: "Invite user to project"
|
||||||
});
|
});
|
||||||
|
|
||||||
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
|
||||||
await projectMembershipDAL.create({
|
await projectMembershipDAL.create({
|
||||||
userId: invitee.id,
|
userId: invitee.id,
|
||||||
projectId,
|
projectId,
|
||||||
role: ProjectMembershipRole.Member
|
role: ProjectMembershipRole.Member
|
||||||
});
|
});
|
||||||
|
|
||||||
const sender = await userDAL.findById(actorId);
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
subjectLine: "Infisical workspace invitation",
|
subjectLine: "Infisical workspace invitation",
|
||||||
recipients: [invitee.email],
|
recipients: [invitee.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: sender.firstName,
|
|
||||||
inviterEmail: sender.email,
|
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { invitee, latestKey };
|
invitees.push(invitee);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
||||||
|
|
||||||
|
return { invitees, latestKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUsersToProject = async ({ projectId, actorId, actor, members }: TAddUsersToWorkspaceDTO) => {
|
const addUsersToProject = async ({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
members,
|
||||||
|
sendEmails = true
|
||||||
|
}: TAddUsersToWorkspaceDTO) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
@ -134,11 +158,16 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
await projectMembershipDAL.insertMany(
|
await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ userId }) => ({
|
orgMembers.map(({ userId, id: membershipId }) => {
|
||||||
|
const role =
|
||||||
|
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
|
||||||
|
|
||||||
|
return {
|
||||||
projectId,
|
projectId,
|
||||||
userId: userId as string,
|
userId: userId as string,
|
||||||
role: ProjectMembershipRole.Member
|
role
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
||||||
@ -153,22 +182,133 @@ export const projectMembershipServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const sender = await userDAL.findById(actorId);
|
|
||||||
|
if (sendEmails) {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
subjectLine: "Infisical workspace invitation",
|
subjectLine: "Infisical workspace invitation",
|
||||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: sender.firstName,
|
|
||||||
inviterEmail: sender.email,
|
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return orgMembers;
|
return orgMembers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addUsersToProjectNonE2EE = async ({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
emails,
|
||||||
|
sendEmails = true
|
||||||
|
}: TAddUsersToWorkspaceNonE2EEDTO) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails);
|
||||||
|
|
||||||
|
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" });
|
||||||
|
|
||||||
|
const existingMembers = await projectMembershipDAL.find({
|
||||||
|
projectId,
|
||||||
|
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||||
|
});
|
||||||
|
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||||
|
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||||
|
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find ghost user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find ghost user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find bot"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const newWsMembers = createWsMembers({
|
||||||
|
decryptKey: ghostUserLatestKey,
|
||||||
|
userPrivateKey: botPrivateKey,
|
||||||
|
members: orgMembers.map((membership) => ({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Member,
|
||||||
|
userPublicKey: membership.user.publicKey
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const members: TProjectMemberships[] = [];
|
||||||
|
|
||||||
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
const result = await projectMembershipDAL.insertMany(
|
||||||
|
orgMembers.map(({ user, id: membershipId }) => {
|
||||||
|
const role =
|
||||||
|
orgMembers.find((membership) => membership.id === membershipId)?.role || ProjectMembershipRole.Member;
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId,
|
||||||
|
userId: user.id,
|
||||||
|
role
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
members.push(...result);
|
||||||
|
|
||||||
|
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||||
|
await projectKeyDAL.insertMany(
|
||||||
|
orgMembers.map(({ user, id }) => ({
|
||||||
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sendEmails) {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
|
subjectLine: "Infisical workspace invitation",
|
||||||
|
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean),
|
||||||
|
substitutions: {
|
||||||
|
workspaceName: project.name,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
const updateProjectMembership = async ({
|
const updateProjectMembership = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@ -179,6 +319,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||||
|
|
||||||
|
if (membershipUser?.ghost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unauthorized member update",
|
||||||
|
name: "Update project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||||
if (isCustomRole) {
|
if (isCustomRole) {
|
||||||
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
@ -208,6 +357,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||||
|
|
||||||
|
if (membershipUser?.ghost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot delete ghost",
|
||||||
|
name: "Delete project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
||||||
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
||||||
@ -220,6 +378,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
inviteUserToProject,
|
inviteUserToProject,
|
||||||
updateProjectMembership,
|
updateProjectMembership,
|
||||||
|
addUsersToProjectNonE2EE,
|
||||||
deleteProjectMembership,
|
deleteProjectMembership,
|
||||||
addUsersToProject
|
addUsersToProject
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||||
|
|
||||||
export type TInviteUserToProjectDTO = {
|
export type TInviteUserToProjectDTO = {
|
||||||
email: string;
|
emails: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectMembershipDTO = {
|
export type TUpdateProjectMembershipDTO = {
|
||||||
@ -16,9 +17,16 @@ export type TDeleteProjectMembershipDTO = {
|
|||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TAddUsersToWorkspaceDTO = {
|
export type TAddUsersToWorkspaceDTO = {
|
||||||
|
sendEmails?: boolean;
|
||||||
members: {
|
members: {
|
||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
workspaceEncryptedKey: string;
|
workspaceEncryptedKey: string;
|
||||||
workspaceEncryptedNonce: string;
|
workspaceEncryptedNonce: string;
|
||||||
|
projectRole: ProjectMembershipRole;
|
||||||
}[];
|
}[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TAddUsersToWorkspaceNonE2EEDTO = {
|
||||||
|
sendEmails?: boolean;
|
||||||
|
emails: string[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
@ -48,6 +48,20 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectGhostUser = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const ghostUser = await db(TableName.ProjectMembership)
|
||||||
|
.where({ projectId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Users))
|
||||||
|
.where({ ghost: true })
|
||||||
|
.first();
|
||||||
|
return ghostUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project ghost user" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db(TableName.IdentityProjectMembership)
|
const workspaces = await db(TableName.IdentityProjectMembership)
|
||||||
@ -128,6 +142,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
...projectOrm,
|
...projectOrm,
|
||||||
findAllProjects,
|
findAllProjects,
|
||||||
findAllProjectsByIdentity,
|
findAllProjectsByIdentity,
|
||||||
|
findProjectGhostUser,
|
||||||
findProjectById
|
findProjectById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,22 +1,46 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TSecrets } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
|
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
|
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import {
|
||||||
|
decryptAsymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
infisicalSymmetricDecrypt,
|
||||||
|
infisicalSymmetricEncypt
|
||||||
|
} from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { createProjectKey, createWsMembers } from "@app/lib/project";
|
||||||
|
import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, TUpgradeProjectDTO } from "./project-types";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
{ name: "Development", slug: "dev" },
|
{ name: "Development", slug: "dev" },
|
||||||
@ -26,11 +50,22 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
userDAL: TUserDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
|
secretVersionDAL: TSecretVersionDALFactory;
|
||||||
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
|
orgDAL: TOrgDALFactory;
|
||||||
|
secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
|
||||||
|
secretApprovalSecretDAL: TSecretApprovalRequestSecretDALFactory;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
|
secretDAL: TSecretDALFactory;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,8 +73,19 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
orgService,
|
||||||
|
orgDAL,
|
||||||
|
identityProjectDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
secretDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@ -49,7 +95,7 @@ export const projectServiceFactory = ({
|
|||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
* */
|
* */
|
||||||
const createProject = async ({ orgId, actor, actorId, workspaceName }: TCreateProjectDTO) => {
|
const createProject = async ({ orgId, actor, actorId, workspaceName }: TCreateProjectDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@ -64,20 +110,28 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newProject = projectDAL.transaction(async (tx) => {
|
const results = await projectDAL.transaction(async (tx) => {
|
||||||
|
const ghostUser = await orgService.addGhostUser(orgId, tx);
|
||||||
|
|
||||||
const project = await projectDAL.create(
|
const project = await projectDAL.create(
|
||||||
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
|
{
|
||||||
|
name: workspaceName,
|
||||||
|
orgId,
|
||||||
|
slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||||
|
version: ProjectVersion.V2
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
// set user as admin member for proeject
|
// set ghost user as admin of project
|
||||||
await projectMembershipDAL.create(
|
await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: actorId,
|
userId: ghostUser.user.id,
|
||||||
role: ProjectMembershipRole.Admin,
|
role: ProjectMembershipRole.Admin,
|
||||||
projectId: project.id
|
projectId: project.id
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// generate the blind index for project
|
// generate the blind index for project
|
||||||
await secretBlindIndexDAL.create(
|
await secretBlindIndexDAL.create(
|
||||||
{
|
{
|
||||||
@ -99,11 +153,154 @@ export const projectServiceFactory = ({
|
|||||||
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
// _id for backward compat
|
|
||||||
return { ...project, environments: envs, _id: project.id };
|
// 3. Create a random key that we'll use as the project key.
|
||||||
|
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
privateKey: ghostUser.keys.plainPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
return newProject;
|
// 4. Save the project key for the ghost user.
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
receiverId: ghostUser.user.id,
|
||||||
|
encryptedKey: encryptedProjectKey,
|
||||||
|
nonce: encryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.user.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create & a bot for the project
|
||||||
|
await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the ghost users latest key
|
||||||
|
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|
||||||
|
if (!latestKey) {
|
||||||
|
throw new Error("Latest key not found for user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project is being created by a user, add the user to the project as an admin
|
||||||
|
if (actor === ActorType.USER) {
|
||||||
|
// Find public key of user
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(actorId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [projectAdmin] = createWsMembers({
|
||||||
|
decryptKey: latestKey,
|
||||||
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: orgMembership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a membership for the user
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: user.id,
|
||||||
|
role: projectAdmin.projectRole
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a project key for the user
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
||||||
|
nonce: projectAdmin.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project is being created by an identity, add the identity to the project as an admin
|
||||||
|
else if (actor === ActorType.IDENTITY) {
|
||||||
|
// Find identity org membership
|
||||||
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne(
|
||||||
|
{
|
||||||
|
identityId: actorId,
|
||||||
|
orgId: project.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// If identity org membership not found, throw error
|
||||||
|
if (!identityOrgMembership) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find identity with id ${actorId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the role permission for the identity
|
||||||
|
// IS THIS CORRECT?
|
||||||
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
ProjectMembershipRole.Admin,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPrivilege)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
|
await identityProjectDAL.create(
|
||||||
|
{
|
||||||
|
identityId: actorId,
|
||||||
|
projectId: project.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
environments: envs,
|
||||||
|
_id: project.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findProjectGhostUser = async (projectId: string) => {
|
||||||
|
const user = await projectMembershipDAL.findProjectGhostUser(projectId);
|
||||||
|
|
||||||
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProject = async ({ actor, actorId, projectId }: TDeleteProjectDTO) => {
|
const deleteProject = async ({ actor, actorId, projectId }: TDeleteProjectDTO) => {
|
||||||
@ -145,12 +342,361 @@ export const projectServiceFactory = ({
|
|||||||
return updatedProject;
|
return updatedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
|
||||||
|
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
|
if (membership?.role !== ProjectMembershipRole.Admin) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "User must be admin"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Get the existing project
|
||||||
|
2. Get the existing project keys
|
||||||
|
4. Get all the project envs & folders
|
||||||
|
5. Get ALL secrets within the project
|
||||||
|
6. Create a new ghost user
|
||||||
|
7. Create a project membership for the ghost user
|
||||||
|
8. Get the existing bot, and the existing project keys for the members of the project
|
||||||
|
9. IF a bot already exists for the project, delete it!
|
||||||
|
10. Delete all the existing project keys
|
||||||
|
11. Create a project key for the ghost user
|
||||||
|
12. Find the newly created ghost user's latest key
|
||||||
|
|
||||||
|
|
||||||
|
FOR EACH OF THE OLD PROJECT KEYS (loop):
|
||||||
|
13. Find the user based on the key.receiverId.
|
||||||
|
14. Find the org membership for the user.
|
||||||
|
15. Create a new project key for the user.
|
||||||
|
|
||||||
|
|
||||||
|
16. Encrypt the ghost user's private key
|
||||||
|
17. Create a new bot, and set the public/private key of the bot, to the ghost user's public/private key.
|
||||||
|
18. Add the workspace key to the bot
|
||||||
|
19. Decrypt the secrets with the old project key
|
||||||
|
20. Get the newly created bot's private key, and workspace key (we do it this way to test as many steps of the bot process as possible)
|
||||||
|
21. Get the workspace key from the bot
|
||||||
|
|
||||||
|
|
||||||
|
FOR EACH DECRYPTED SECRET (loop):
|
||||||
|
22. Re-encrypt the secret value, secret key, and secret comment with the NEW project key from the bot.
|
||||||
|
23. Update the secret in the database with the new encrypted values.
|
||||||
|
|
||||||
|
|
||||||
|
24. Transaction ends. If there were no errors. All changes are applied.
|
||||||
|
25. API route returns 200 OK.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const project = await projectDAL.findOne({ id: projectId, version: ProjectVersion.V1 });
|
||||||
|
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
||||||
|
if (!project || !oldProjectKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project or project key not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectEnvs = await projectEnvDAL.find({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"projectEnvs",
|
||||||
|
projectEnvs.map((e) => e.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectFolders = await folderDAL.find({
|
||||||
|
$in: {
|
||||||
|
envId: projectEnvs.map((env) => env.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all the secrets within the project (as encrypted)
|
||||||
|
const secrets: TPartialSecret[] = [];
|
||||||
|
for (const folder of projectFolders) {
|
||||||
|
const folderSecrets = await secretDAL.find({ folderId: folder.id });
|
||||||
|
|
||||||
|
const folderSecretVersions = await secretVersionDAL.find({
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
const approvalRequests = await secretApprovalRequestDAL.find({
|
||||||
|
status: RequestState.Open,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
const approvalSecrets = await secretApprovalSecretDAL.find({
|
||||||
|
$in: {
|
||||||
|
requestId: approvalRequests.map((el) => el.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret })));
|
||||||
|
secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion })));
|
||||||
|
secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
|
||||||
|
|
||||||
|
if (secrets.length !== decryptedSecrets.length) {
|
||||||
|
throw new Error("Failed to decrypt some secret versions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the existing bot and the existing project keys for the members of the project
|
||||||
|
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
|
||||||
|
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
|
||||||
|
|
||||||
|
// TRANSACTION START
|
||||||
|
await projectDAL.transaction(async (tx) => {
|
||||||
|
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
|
||||||
|
|
||||||
|
// Create a ghost user
|
||||||
|
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
|
||||||
|
|
||||||
|
// Create a project key
|
||||||
|
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
privateKey: ghostUser.keys.plainPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Creating new project key for ghost user");
|
||||||
|
// Create a new project key for the GHOST
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
receiverId: ghostUser.user.id,
|
||||||
|
encryptedKey: newEncryptedProjectKey,
|
||||||
|
nonce: newEncryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.user.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a membership for the ghost user
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: ghostUser.user.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a bot already exists, delete it
|
||||||
|
if (existingBot) {
|
||||||
|
console.log("Deleting existing bot");
|
||||||
|
await projectBotDAL.delete({ id: existingBot.id }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Deleting old project keys");
|
||||||
|
// Delete all the existing project keys
|
||||||
|
await projectKeyDAL.delete(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
$in: {
|
||||||
|
id: existingProjectKeys.map((key) => key.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Finding latest key for ghost user");
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new Error("User latest key not found (V2 Upgrade)");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Creating new project keys for old members");
|
||||||
|
|
||||||
|
const newProjectMembers: {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
senderId: string;
|
||||||
|
receiverId: string;
|
||||||
|
projectId: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const key of existingProjectKeys) {
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
|
||||||
|
|
||||||
|
if (!user || !orgMembership) {
|
||||||
|
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [newMember] = createWsMembers({
|
||||||
|
decryptKey: ghostUserLatestKey,
|
||||||
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: orgMembership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
newProjectMembers.push({
|
||||||
|
encryptedKey: newMember.workspaceEncryptedKey,
|
||||||
|
nonce: newMember.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create project keys for all the old members
|
||||||
|
await projectKeyDAL.insertMany(newProjectMembers, tx);
|
||||||
|
|
||||||
|
// Encrypt the bot private key (which is the same as the ghost user)
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create a bot for the project
|
||||||
|
const newBot = await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
encryptedProjectKey: newEncryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Updating secrets with new project key");
|
||||||
|
console.log("Got decrypted secrets");
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: newBot.iv,
|
||||||
|
tag: newBot.tag,
|
||||||
|
ciphertext: newBot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const botKey = decryptAsymmetric({
|
||||||
|
ciphertext: newBot.encryptedProjectKey!,
|
||||||
|
privateKey: botPrivateKey,
|
||||||
|
nonce: newBot.encryptedProjectKeyNonce!,
|
||||||
|
publicKey: ghostUser.keys.publicKey
|
||||||
|
});
|
||||||
|
|
||||||
|
type TPartialSecret = Pick<
|
||||||
|
TSecrets,
|
||||||
|
| "id"
|
||||||
|
| "secretKeyCiphertext"
|
||||||
|
| "secretKeyIV"
|
||||||
|
| "secretKeyTag"
|
||||||
|
| "secretValueCiphertext"
|
||||||
|
| "secretValueIV"
|
||||||
|
| "secretValueTag"
|
||||||
|
| "secretCommentCiphertext"
|
||||||
|
| "secretCommentIV"
|
||||||
|
| "secretCommentTag"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const updatedSecrets: TPartialSecret[] = [];
|
||||||
|
const updatedSecretVersions: TPartialSecret[] = [];
|
||||||
|
const updatedSecretApprovals: TPartialSecret[] = [];
|
||||||
|
for (const rawSecret of decryptedSecrets) {
|
||||||
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey);
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey);
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id: rawSecret.id,
|
||||||
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (rawSecret.docType === SecretDocType.Secret) {
|
||||||
|
updatedSecrets.push(payload);
|
||||||
|
} else if (rawSecret.docType === SecretDocType.SecretVersion) {
|
||||||
|
updatedSecretVersions.push(payload);
|
||||||
|
} else if (rawSecret.docType === SecretDocType.ApprovalSecret) {
|
||||||
|
updatedSecretApprovals.push(payload);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown secret type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
[
|
||||||
|
...updatedSecrets.map((secret) => ({
|
||||||
|
filter: { id: secret.id },
|
||||||
|
data: {
|
||||||
|
...secret,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
[
|
||||||
|
...updatedSecretVersions.map((version) => ({
|
||||||
|
filter: { id: version.id },
|
||||||
|
data: {
|
||||||
|
...version,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
[
|
||||||
|
...updatedSecretApprovals.map((approval) => ({
|
||||||
|
filter: {
|
||||||
|
id: approval.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...approval,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secretUpdates.length !== updatedSecrets.length) {
|
||||||
|
throw new Error("Failed to update some secrets");
|
||||||
|
}
|
||||||
|
if (secretVersionUpdates.length !== updatedSecretVersions.length) {
|
||||||
|
throw new Error("Failed to update some secret versions");
|
||||||
|
}
|
||||||
|
if (secretApprovalUpdates.length !== updatedSecretApprovals.length) {
|
||||||
|
throw new Error("Failed to update some secret approvals");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Transaction was successful");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
getProjects,
|
getProjects,
|
||||||
|
findProjectGhostUser,
|
||||||
getAProject,
|
getAProject,
|
||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
updateName
|
updateName,
|
||||||
|
upgradeProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
export type TCreateProjectDTO = {
|
export type TCreateProjectDTO = {
|
||||||
@ -18,3 +20,7 @@ export type TGetProjectDTO = {
|
|||||||
actorId: string;
|
actorId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpgradeProjectDTO = {
|
||||||
|
userPrivateKey: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
@ -22,7 +22,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
// the idea is to use postgres specific function
|
// the idea is to use postgres specific function
|
||||||
// insert with id this will cause a conflict then merge the data
|
// insert with id this will cause a conflict then merge the data
|
||||||
const bulkUpdate = async (data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>, tx?: Knex) => {
|
const bulkUpdate = async (
|
||||||
|
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
|
||||||
|
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const secs = await Promise.all(
|
const secs = await Promise.all(
|
||||||
data.map(async ({ filter, data: updateData }) => {
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
@ -41,6 +45,24 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkUpdateNoVersionIncrement = async (
|
||||||
|
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secs = await Promise.all(
|
||||||
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
|
const [doc] = await (tx || db)(TableName.Secret).where(filter).update(updateData).returning("*");
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||||
|
return doc;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return secs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteMany = async (
|
const deleteMany = async (
|
||||||
data: Array<{ blindIndex: string; type: SecretType }>,
|
data: Array<{ blindIndex: string; type: SecretType }>,
|
||||||
folderId: string,
|
folderId: string,
|
||||||
@ -139,5 +161,13 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
|
return {
|
||||||
|
...secretOrm,
|
||||||
|
update,
|
||||||
|
bulkUpdate,
|
||||||
|
deleteMany,
|
||||||
|
bulkUpdateNoVersionIncrement,
|
||||||
|
findByFolderId,
|
||||||
|
findByBlindIndexes
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretVersions } from "@app/db/schemas";
|
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
|
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
|
||||||
@ -36,6 +36,46 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkUpdate = async (
|
||||||
|
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secs = await Promise.all(
|
||||||
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
|
const [doc] = await (tx || db)(TableName.SecretVersion)
|
||||||
|
.where(filter)
|
||||||
|
.update(updateData)
|
||||||
|
// .increment("version", 1) // TODO: Is this really needed?
|
||||||
|
.returning("*");
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||||
|
return doc;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return secs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkUpdateNoVersionIncrement = async (
|
||||||
|
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secs = await Promise.all(
|
||||||
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
|
const [doc] = await (tx || db)(TableName.SecretVersion).where(filter).update(updateData).returning("*");
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||||
|
return doc;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return secs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
|
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
|
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
|
||||||
@ -59,5 +99,11 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
|
return {
|
||||||
|
...secretVersionOrm,
|
||||||
|
findLatestVersionMany,
|
||||||
|
bulkUpdate,
|
||||||
|
findLatestVersionByFolderId,
|
||||||
|
bulkUpdateNoVersionIncrement
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
<title>Project Invitation</title>
|
<title>Project Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Join your team on Infisical</h2>
|
<h2>Join your team on Infisical</h2>
|
||||||
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
|
<p>You have been invited to a new Infisical project — {{workspaceName}}</p>
|
||||||
<a href="{{callback_url}}">Join now</a>
|
<a href="{{callback_url}}">Join now</a>
|
||||||
<h3>What is Infisical?</h3>
|
<h3>What is Infisical?</h3>
|
||||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
||||||
|
and configs.</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -47,6 +47,17 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
|
||||||
|
try {
|
||||||
|
return await db(TableName.ProjectMembership)
|
||||||
|
.where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.first();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find user by project membership id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||||
@ -111,6 +122,7 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
findUserEncKeyByEmail,
|
findUserEncKeyByEmail,
|
||||||
findUserEncKeyByUserId,
|
findUserEncKeyByUserId,
|
||||||
updateUserEncryptionByUserId,
|
updateUserEncryptionByUserId,
|
||||||
|
findUserByProjectMembershipId,
|
||||||
upsertUserEncryptionKey,
|
upsertUserEncryptionKey,
|
||||||
createUserEncryption,
|
createUserEncryption,
|
||||||
findOneUserAction,
|
findOneUserAction,
|
||||||
|
@ -14,11 +14,11 @@ nacl.util = require("tweetnacl-util");
|
|||||||
const generateKeyPair = () => {
|
const generateKeyPair = () => {
|
||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
||||||
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
||||||
});
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type EncryptAsymmetricProps = {
|
type EncryptAsymmetricProps = {
|
||||||
plaintext: string;
|
plaintext: string;
|
||||||
@ -33,23 +33,15 @@ type EncryptAsymmetricProps = {
|
|||||||
* @param {String} - base64-encoded Nacl private key
|
* @param {String} - base64-encoded Nacl private key
|
||||||
* @param {String} - base64-encoded Nacl public key
|
* @param {String} - base64-encoded Nacl public key
|
||||||
*/
|
*/
|
||||||
const verifyPrivateKey = ({
|
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
|
||||||
privateKey,
|
|
||||||
publicKey
|
|
||||||
}: {
|
|
||||||
privateKey: string;
|
|
||||||
publicKey: string;
|
|
||||||
}) => {
|
|
||||||
const derivedPublicKey = nacl.util.encodeBase64(
|
const derivedPublicKey = nacl.util.encodeBase64(
|
||||||
nacl.box.keyPair.fromSecretKey(
|
nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey
|
||||||
nacl.util.decodeBase64(privateKey)
|
|
||||||
).publicKey
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (derivedPublicKey !== publicKey) {
|
if (derivedPublicKey !== publicKey) {
|
||||||
throw new Error("Failed to verify private key");
|
throw new Error("Failed to verify private key");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a key from password [password] and salt [salt] using Argon2id
|
* Derive a key from password [password] and salt [salt] using Argon2id
|
||||||
@ -232,4 +224,5 @@ export {
|
|||||||
encryptAssymmetric,
|
encryptAssymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
generateKeyPair,
|
generateKeyPair,
|
||||||
verifyPrivateKey};
|
verifyPrivateKey
|
||||||
|
};
|
||||||
|
@ -56,7 +56,6 @@ const encryptSecrets = async ({
|
|||||||
publicKey: wsKey.sender.publicKey,
|
publicKey: wsKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// case: a (shared) key does not exist for the workspace
|
// case: a (shared) key does not exist for the workspace
|
||||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
@ -116,7 +115,6 @@ const encryptSecrets = async ({
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error while encrypting secrets");
|
console.log("Error while encrypting secrets");
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ const encKeyKeys = {
|
|||||||
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
|
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUserWsKey = async (workspaceID: string) => {
|
export const fetchUserWsKey = async (projectId: string) => {
|
||||||
const { data } = await apiRequest.get<UserWsKeyPair>(
|
const { data } = await apiRequest.get<UserWsKeyPair>(
|
||||||
`/api/v2/workspace/${workspaceID}/encrypted-key`
|
`/api/v2/workspace/${projectId}/encrypted-key`
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -22,7 +22,7 @@ export * from "./secrets/types";
|
|||||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||||
export type { SubscriptionPlan } from "./subscriptions/types";
|
export type { SubscriptionPlan } from "./subscriptions/types";
|
||||||
export type { WsTag } from "./tags/types";
|
export type { WsTag } from "./tags/types";
|
||||||
export type { AddUserToWsDTO, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
|
export type { AddUserToWsDTOE2EE, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
|
||||||
export type { TWebhook } from "./webhooks/types";
|
export type { TWebhook } from "./webhooks/types";
|
||||||
export type {
|
export type {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export { useAddUserToWs } from "./mutation";
|
export { useAddUserToWsE2EE, useAddUserToWsNonE2EE } from "./mutation";
|
||||||
export {
|
export {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToOrg,
|
useAddUserToOrg,
|
||||||
|
@ -7,12 +7,12 @@ import {
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
import { AddUserToWsDTO } from "./types";
|
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
||||||
|
|
||||||
export const useAddUserToWs = () => {
|
export const useAddUserToWsE2EE = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, AddUserToWsDTO>({
|
return useMutation<{}, {}, AddUserToWsDTOE2EE>({
|
||||||
mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => {
|
mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => {
|
||||||
// assymmetrically decrypt symmetric key with local private key
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@ -45,3 +45,19 @@ export const useAddUserToWs = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAddUserToWsNonE2EE = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
||||||
|
mutationFn: async ({ projectId, emails }) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
||||||
|
emails
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -63,7 +63,7 @@ export type TProjectMembership = {
|
|||||||
|
|
||||||
export type TWorkspaceUser = OrgUser;
|
export type TWorkspaceUser = OrgUser;
|
||||||
|
|
||||||
export type AddUserToWsDTO = {
|
export type AddUserToWsDTOE2EE = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
decryptKey: UserWsKeyPair;
|
decryptKey: UserWsKeyPair;
|
||||||
userPrivateKey: string;
|
userPrivateKey: string;
|
||||||
@ -73,6 +73,11 @@ export type AddUserToWsDTO = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AddUserToWsDTONonE2EE = {
|
||||||
|
projectId: string;
|
||||||
|
emails: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateOrgUserRoleDTO = {
|
export type UpdateOrgUserRoleDTO = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
|
@ -21,5 +21,6 @@ export {
|
|||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
useUpdateIdentityWorkspaceRole,
|
useUpdateIdentityWorkspaceRole,
|
||||||
useUpdateUserWorkspaceRole,
|
useUpdateUserWorkspaceRole,
|
||||||
useUpdateWsEnvironment
|
useUpdateWsEnvironment,
|
||||||
|
useUpgradeProject
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
@ -61,6 +61,21 @@ export const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
|||||||
return secrets;
|
return secrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpgradeProject = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, { projectId: string; privateKey: string }>({
|
||||||
|
mutationFn: ({ projectId, privateKey }) => {
|
||||||
|
return apiRequest.post(`/api/v2/workspace/${projectId}/upgrade`, {
|
||||||
|
userPrivateKey: privateKey
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUserWorkspaces = async () => {
|
const fetchUserWorkspaces = async () => {
|
||||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||||
return data.workspaces;
|
return data.workspaces;
|
||||||
@ -158,19 +173,19 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
|||||||
|
|
||||||
export const createWorkspace = ({
|
export const createWorkspace = ({
|
||||||
organizationId,
|
organizationId,
|
||||||
workspaceName
|
projectName
|
||||||
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
|
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||||
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
|
return apiRequest.post("/api/v2/workspace", { projectName, organizationId });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
|
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||||
mutationFn: async ({ organizationId, workspaceName }) =>
|
mutationFn: async ({ organizationId, projectName }) =>
|
||||||
createWorkspace({
|
createWorkspace({
|
||||||
organizationId,
|
organizationId,
|
||||||
workspaceName
|
projectName
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
@ -285,11 +300,13 @@ export const useAddUserToWorkspace = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => {
|
mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => {
|
||||||
const {
|
const {
|
||||||
data: { invitee, latestKey }
|
data: { invitees, latestKey }
|
||||||
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
|
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, {
|
||||||
|
emails: [email]
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invitee,
|
invitees,
|
||||||
latestKey
|
latestKey
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -3,8 +3,10 @@ export type Workspace = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
version: "v1" | "v2";
|
||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = {
|
export type WorkspaceEnv = {
|
||||||
@ -25,7 +27,7 @@ export type NameWorkspaceSecretsDTO = {
|
|||||||
|
|
||||||
// mutation dto
|
// mutation dto
|
||||||
export type CreateWorkspaceDTO = {
|
export type CreateWorkspaceDTO = {
|
||||||
workspaceName: string;
|
projectName: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
/* eslint-disable vars-on-top */
|
/* eslint-disable vars-on-top */
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
/* eslint-disable func-names */
|
/* eslint-disable func-names */
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
@ -34,7 +33,6 @@ import * as yup from "yup";
|
|||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@ -62,16 +60,14 @@ import {
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToWs,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction
|
||||||
useUploadWsKey
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
|
||||||
import { CreateOrgModal } from "@app/views/Org/components";
|
import { CreateOrgModal } from "@app/views/Org/components";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@ -129,8 +125,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
: true;
|
: true;
|
||||||
|
|
||||||
const createWs = useCreateWorkspace();
|
const createWs = useCreateWorkspace();
|
||||||
const uploadWsKey = useUploadWsKey();
|
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||||
const addWsUser = useAddUserToWs();
|
|
||||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
@ -220,53 +216,32 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg) return;
|
||||||
|
if (!user) return;
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
workspace: { id: newWorkspaceId }
|
project: { id: newProjectId }
|
||||||
}
|
}
|
||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
organizationId: currentOrg?.id,
|
organizationId: currentOrg.id,
|
||||||
workspaceName: name
|
projectName: name
|
||||||
});
|
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
|
||||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: randomBytes,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadWsKey.mutateAsync({
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce,
|
|
||||||
userId: user?.id,
|
|
||||||
workspaceId: newWorkspaceId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
// not using hooks because need at this point only
|
|
||||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||||
const decryptKey = await fetchUserWsKey(newWorkspaceId);
|
|
||||||
await addWsUser.mutateAsync({
|
await addUsersToProject.mutateAsync({
|
||||||
workspaceId: newWorkspaceId,
|
emails: orgUsers
|
||||||
decryptKey,
|
.map((member) => member.user.email)
|
||||||
userPrivateKey: PRIVATE_KEY,
|
.filter((email) => email !== user.email),
|
||||||
members: orgUsers
|
projectId: newProjectId
|
||||||
.filter(
|
|
||||||
({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
|
|
||||||
)
|
|
||||||
.map(({ user: orgUser, id: orgMembershipId }) => ({
|
|
||||||
userPublicKey: orgUser.publicKey,
|
|
||||||
orgMembershipId
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||||
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -23,7 +21,7 @@ import {
|
|||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
faPlug,
|
faPlug,
|
||||||
faPlus,
|
faPlus,
|
||||||
faUserPlus,
|
faUserPlus
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
@ -33,7 +31,6 @@ import * as yup from "yup";
|
|||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@ -54,12 +51,11 @@ import {
|
|||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import {
|
import {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToWs,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction
|
||||||
useUploadWsKey
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@ -475,7 +471,7 @@ const OrganizationPage = withPermission(
|
|||||||
const currentOrg = String(router.query.id);
|
const currentOrg = String(router.query.id);
|
||||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === currentOrg) || [];
|
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === currentOrg) || [];
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const addWsUser = useAddUserToWs();
|
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
@ -497,61 +493,36 @@ const OrganizationPage = withPermission(
|
|||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
const createWs = useCreateWorkspace();
|
const createWs = useCreateWorkspace();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const uploadWsKey = useUploadWsKey();
|
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
|
||||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
|
if (!user) return;
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
workspace: { id: newWorkspaceId }
|
project: { id: newProjectId }
|
||||||
}
|
}
|
||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
organizationId: currentOrg,
|
organizationId: currentOrg,
|
||||||
workspaceName: name
|
projectName: name
|
||||||
});
|
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
|
||||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: randomBytes,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadWsKey.mutateAsync({
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce,
|
|
||||||
userId: user?.id,
|
|
||||||
workspaceId: newWorkspaceId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
// not using hooks because need at this point only
|
|
||||||
const orgUsers = await fetchOrgUsers(currentOrg);
|
const orgUsers = await fetchOrgUsers(currentOrg);
|
||||||
const decryptKey = await fetchUserWsKey(newWorkspaceId);
|
|
||||||
const members = orgUsers
|
await addUsersToProject.mutateAsync({
|
||||||
.filter(
|
emails: orgUsers
|
||||||
({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
|
.map((member) => member.user.email)
|
||||||
)
|
.filter((email) => email !== user.email),
|
||||||
.map(({ user: orgUser, id: orgMembershipId }) => ({
|
projectId: newProjectId
|
||||||
userPublicKey: orgUser.publicKey,
|
|
||||||
orgMembershipId
|
|
||||||
}));
|
|
||||||
if (members.length) {
|
|
||||||
await addWsUser.mutateAsync({
|
|
||||||
workspaceId: newWorkspaceId,
|
|
||||||
decryptKey,
|
|
||||||
userPrivateKey: PRIVATE_KEY,
|
|
||||||
members
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
@ -735,7 +706,7 @@ const OrganizationPage = withPermission(
|
|||||||
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
||||||
30 * 24 * 60 * 60 * 1000
|
30 * 24 * 60 * 60 * 1000
|
||||||
) && (
|
) && (
|
||||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-6 pb-0 text-3xl">
|
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<LearningItemSquare
|
<LearningItemSquare
|
||||||
|
@ -44,7 +44,8 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useAddUserToWs,
|
useAddUserToWsE2EE,
|
||||||
|
useAddUserToWsNonE2EE,
|
||||||
useDeleteUserFromWorkspace,
|
useDeleteUserFromWorkspace,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetProjectRoles,
|
useGetProjectRoles,
|
||||||
@ -95,12 +96,14 @@ export const MemberListTab = () => {
|
|||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWs();
|
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
||||||
|
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
||||||
const { mutateAsync: uploadWsKey } = useUploadWsKey();
|
const { mutateAsync: uploadWsKey } = useUploadWsKey();
|
||||||
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||||
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
||||||
|
|
||||||
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
||||||
|
if (!currentWorkspace) return;
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
// TODO(akhilmhdh): Move to memory storage
|
// TODO(akhilmhdh): Move to memory storage
|
||||||
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
|
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
|
||||||
@ -114,12 +117,24 @@ export const MemberListTab = () => {
|
|||||||
if (!orgUser) return;
|
if (!orgUser) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (currentWorkspace.version === "v1") {
|
||||||
await addUserToWorkspace({
|
await addUserToWorkspace({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
userPrivateKey,
|
userPrivateKey,
|
||||||
decryptKey: wsKey,
|
decryptKey: wsKey,
|
||||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
||||||
});
|
});
|
||||||
|
} else if (currentWorkspace.version === "v2") {
|
||||||
|
await addUserToWorkspaceNonE2EE({
|
||||||
|
projectId: workspaceId,
|
||||||
|
emails: [orgUser.user.email]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to add user to project, unknown project type",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully added user to the project",
|
text: "Successfully added user to the project",
|
||||||
type: "success"
|
type: "success"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -14,6 +14,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
Button,
|
Button,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
@ -37,7 +39,8 @@ import {
|
|||||||
useGetFoldersByEnv,
|
useGetFoldersByEnv,
|
||||||
useGetProjectSecretsAllEnv,
|
useGetProjectSecretsAllEnv,
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useUpdateSecretV3
|
useUpdateSecretV3,
|
||||||
|
useUpgradeProject
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
@ -104,6 +107,7 @@ export const SecretOverviewPage = () => {
|
|||||||
environments: userAvailableEnvs.map(({ slug }) => slug)
|
environments: userAvailableEnvs.map(({ slug }) => slug)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const upgradeProject = useUpgradeProject();
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
@ -197,6 +201,24 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUpgradeProject = useCallback(async () => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Private key not found"
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await upgradeProject.mutateAsync({
|
||||||
|
projectId: workspaceId,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleResetSearch = () => setSearchFilter("");
|
const handleResetSearch = () => setSearchFilter("");
|
||||||
|
|
||||||
const handleFolderClick = (path: string) => {
|
const handleFolderClick = (path: string) => {
|
||||||
@ -315,6 +337,23 @@ export const SecretOverviewPage = () => {
|
|||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{currentWorkspace?.version === "v1" && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<Alert variant="danger">
|
||||||
|
<AlertDescription className="prose">
|
||||||
|
Upgrade your project. More filler text More filler text More filler text More filler
|
||||||
|
text More filler text More filler text More filler text More filler text More filler
|
||||||
|
text More filler text More filler text More filler text{" "}
|
||||||
|
</AlertDescription>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Button isLoading={upgradeProject.isLoading} onClick={onUpgradeProject}>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<div className="mt-8 flex items-center justify-between">
|
||||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
<div className="w-80">
|
<div className="w-80">
|
||||||
|
Reference in New Issue
Block a user