diff --git a/pg-migrator/src/.gitignore b/pg-migrator/.gitignore similarity index 100% rename from pg-migrator/src/.gitignore rename to pg-migrator/.gitignore diff --git a/pg-migrator/package-lock.json b/pg-migrator/package-lock.json index 8560ceeee..e86d82d5c 100644 --- a/pg-migrator/package-lock.json +++ b/pg-migrator/package-lock.json @@ -9,21 +9,35 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@casl/ability": "^6.5.0", "dotenv": "^16.3.1", "knex": "^3.1.0", "level": "^8.0.0", "mongoose": "^8.0.4", "pg": "^8.11.3", "prompt-sync": "^4.2.0", + "uuid": "^9.0.1", "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.11.0", "@types/prompt-sync": "^4.2.3", + "@types/uuid": "^9.0.7", "tsx": "^4.7.0", "typescript": "^5.3.3" } }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", @@ -414,6 +428,12 @@ "integrity": "sha512-Ox77gCSx0YyeakGt/qfOZUSFNSSi+sh3ABoGOiCwiO2KODx492BJnUm9oIXS+AHJtqp12iM4RduY6viTJ9bYwA==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -428,6 +448,37 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/abstract-level": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz", @@ -1325,6 +1376,18 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/pg-migrator/package.json b/pg-migrator/package.json index 9ffa16086..64f431a9d 100644 --- a/pg-migrator/package.json +++ b/pg-migrator/package.json @@ -4,23 +4,27 @@ "description": "", "main": "index.js", "scripts": { - "migration": "tsx src/index.ts" + "migration": "tsx src/index.ts", + "rollback": "tsx src/rollback.ts" }, "author": "", "license": "ISC", "devDependencies": { "@types/node": "^20.11.0", "@types/prompt-sync": "^4.2.3", + "@types/uuid": "^9.0.7", "tsx": "^4.7.0", "typescript": "^5.3.3" }, "dependencies": { + "@casl/ability": "^6.5.0", "dotenv": "^16.3.1", "knex": "^3.1.0", "level": "^8.0.0", "mongoose": "^8.0.4", "pg": "^8.11.3", "prompt-sync": "^4.2.0", + "uuid": "^9.0.1", "zod": "^3.22.4" } } diff --git a/pg-migrator/src/folder.ts b/pg-migrator/src/folder.ts new file mode 100644 index 000000000..f6d8d507f --- /dev/null +++ b/pg-migrator/src/folder.ts @@ -0,0 +1,33 @@ +import { TFolderSchema } from "./models"; + +export const folderBfsTraversal = async ( + root: TFolderSchema, + callback: ( + data: TFolderSchema & { parentId: string | null }, + ) => void | Promise, +) => { + const queue = [root]; + while (queue.length) { + const folder = queue.pop() as TFolderSchema & { parentId: null }; + callback(folder); + queue.push( + ...folder.children.map((el) => ({ + ...el, + parentId: folder.id, + })), + ); + } +}; + +export const flattenFolders = (folders: TFolderSchema) => { + const flattened: { + id: string; + parentId: string | null; + name: string; + version: number; + }[] = []; + folderBfsTraversal(folders, ({ name, version, parentId, id }) => { + flattened.push({ name, version, parentId, id }); + }); + return flattened; +}; diff --git a/pg-migrator/src/index.ts b/pg-migrator/src/index.ts index 2f402eeba..b439040bd 100644 --- a/pg-migrator/src/index.ts +++ b/pg-migrator/src/index.ts @@ -1,13 +1,187 @@ import promptSync from "prompt-sync"; -import mongoose from "mongoose"; +import mongoose, { Model } from "mongoose"; import dotenv from "dotenv"; -import knex from "knex"; +import knex, { Knex } from "knex"; import path from "path"; import { Level } from "level"; -import { IUser, User } from "./models"; -import { TUsers, TUsersInsert, TableName } from "./schemas"; +import { packRules } from "@casl/ability/extra"; +import { + APIKeyData, + BackupPrivateKey, + Bot, + BotKey, + BotOrg, + Folder, + FolderVersion, + GitAppInstallationSession, + GitAppOrganizationInstallation, + GitRisks, + Identity, + IdentityAccessToken, + IdentityMembership, + IdentityMembershipOrg, + IdentityUniversalAuth, + IdentityUniversalAuthClientSecret, + IncidentContactOrg, + Integration, + IntegrationAuth, + Key, + Membership, + MembershipOrg, + Organization, + Role, + SSOConfig, + Secret, + SecretApprovalPolicy, + SecretApprovalRequest, + SecretBlindIndexData, + SecretImport, + SecretSnapshot, + SecretVersion, + ServiceTokenData, + Tag, + TrustedIP, + User, + UserAction, + Webhook, + Workspace, +} from "./models"; +import { TableName } from "./schemas"; +import { v4 as uuidV4 } from "uuid"; +import { Tables } from "knex/types/tables"; +import { ServerConfig } from "./models/serverConfig"; +import { flattenFolders } from "./folder"; +import { SecretRotation } from "./models/secretRotation"; + +enum SecretEncryptionAlgo { + AES_256_GCM = "aes-256-gcm", +} + +enum SecretKeyEncoding { + UTF8 = "utf8", + BASE64 = "base64", + HEX = "hex", +} const kdb = new Level("./db", { valueEncoding: "json" }); +const getFolderVersionKey = (folderId: string, version: number) => + `${folderId}:${version}`; +/** + * Sorts an array of items into groups. The return value is a map where the keys are + * the group ids the given getGroupId function produced and the value is an array of + * each item in that group. + */ +export const groupBy = ( + array: readonly T[], + getGroupId: (item: T) => Key, +): Record => + array.reduce( + (acc, item) => { + const groupId = getGroupId(item); + if (!acc[groupId]) acc[groupId] = []; + acc[groupId].push(item); + return acc; + }, + {} as Record, + ); + +const migrateCollection = async < + T extends {}, + K extends keyof Tables, + R extends (keyof Tables[K]["base"])[] = [], +>({ + db, + postPgProcessing, + preProcessing, + mongooseCollection, + postgresTableName, + returnKeys, + filter, +}: { + db: Knex; + returnKeys: R; + postPgProcessing?: ( + preData: T[], + data: Pick[], + ) => void | Promise; + preProcessing: ( + data: T, + ) => + | Tables[K]["base"] + | Tables[K]["base"][] + | Promise + | undefined; + postgresTableName: K; + mongooseCollection: Model; + filter?: Record; +}) => { + const mongooseDoc: T[] = []; + const pgDoc: Tables[K]["base"][] = []; + + console.log("Starting migration of ", mongooseCollection.modelName); + const totalMongoCount = await mongooseCollection.countDocuments(); + console.log("Total documents", totalMongoCount); + console.log("Total batches", Math.ceil(totalMongoCount / 1000)); + let batch = 1; + + for await (const doc of mongooseCollection + .find(filter || {}) + .cursor({ batchSize: 100 })) { + mongooseDoc.push(doc); + const preProcessedData = await preProcessing( + doc.toObject({ virtuals: true }), + ); + if (preProcessedData) { + if (Array.isArray(preProcessedData)) { + pgDoc.push( + ...(preProcessedData.filter(Boolean) as Tables[K]["base"][]), + ); + } else { + pgDoc.push(preProcessedData); + } + } + if (mongooseDoc.length >= 1000) { + console.log("Batch No.:", batch); + if (!pgDoc.length) + console.log("Skipping this batch due to empty pre processor", batch); + if (pgDoc.length) { + const newUserIds = await db.transaction(async (tx) => { + return (await tx + .batchInsert(postgresTableName, pgDoc as any) + .returning(returnKeys as any)) as Pick< + Tables[K]["base"], + R[number] + >[]; + }); + await postPgProcessing?.(mongooseDoc, newUserIds); + } + batch += 1; + mongooseDoc.slice(0, mongooseDoc.length); + pgDoc.slice(0, pgDoc.length); + } + } + if (mongooseDoc.length) { + console.log("Batch No.:", batch); + if (!pgDoc.length) + console.log("Skipping this batch due to empty pre processor", batch); + if (pgDoc.length) { + const newUserIds = await db.transaction(async (tx) => { + return (await tx + .batchInsert(postgresTableName, pgDoc as any) + .returning(returnKeys as any)) as Pick< + Tables[K]["base"], + R[number] + >[]; + }); + await postPgProcessing?.(mongooseDoc, newUserIds); + } + batch += 1; + mongooseDoc.slice(0, mongooseDoc.length); + pgDoc.slice(0, pgDoc.length); + } + + console.log("Finished migration of ", mongooseCollection.modelName); +}; const main = async () => { try { @@ -40,35 +214,1438 @@ const main = async () => { console.log("Connected successfully to postgres"); await db.raw("select 1+1 as result"); + console.log("Starting rolling back to latest, comment this out later"); + await db.migrate.rollback({}, true); + kdb.clear(); + console.log("Rolling back completed"); + console.log("Executing migration"); await db.migrate.latest(); + console.log("Completed migration"); - console.log("Starting to insert users"); - const users: IUser[] = []; - const newUsers: TUsersInsert[] = []; - for await (const doc of User.find().cursor({ batchSize: 100 })) { - users.push(doc); - newUsers.push({ - firstName: doc.firstName, - email: doc.email, - devices: doc.devices, - lastName: doc.lastName, - isAccepted: Boolean(doc.publicKey), - superAdmin: doc.superAdmin, - authMethods: doc.authMethods, - isMfaEnabled: doc.isMfaEnabled, - }); - if (users.length >= 1000) { - const newUserIds = await db.transaction(async (tx) => { - return await tx - .batchInsert(TableName.Users, newUsers) - .returning("id"); + const userKv = kdb.sublevel(TableName.Users); + await migrateCollection({ + db, + mongooseCollection: User, + postgresTableName: TableName.Users, + returnKeys: ["id", "email"], + preProcessing: (doc) => { + const id = uuidV4(); + userKv.put(doc.id, id); + return { + id, + firstName: doc.firstName, + email: doc.email, + devices: JSON.stringify(doc.devices), + lastName: doc.lastName, + isAccepted: Boolean(doc.publicKey), + superAdmin: doc.superAdmin, + authMethods: doc.authMethods, + isMfaEnabled: doc.isMfaEnabled, + createdAt: new Date(doc.createdAt), + updatedAt: new Date(doc.updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: User, + postgresTableName: TableName.UserEncryptionKey, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + if (!doc.publicKey || !doc.encryptedPrivateKey || !doc.iv) return; + const userId = await userKv.get(doc.id.toString()); + + return { + id, + iv: doc.iv, + tag: doc.tag as string, + salt: doc.salt as string, + verifier: doc.verifier as string, + publicKey: doc.publicKey, + userId, + protectedKey: doc.protectedKey as string, + protectedKeyIV: doc.protectedKeyIV as string, + protectedKeyTag: doc.protectedKeyTag as string, + encryptedPrivateKey: doc.encryptedPrivateKey as string, + encryptionVersion: doc.encryptionVersion as number, + // for change password + clientPublicKey: null, + serverPrivateKey: null, + }; + }, + }); + // skipping user auth token and token sessions + // lets reset and ask users to login + + await migrateCollection({ + db, + mongooseCollection: BackupPrivateKey, + postgresTableName: TableName.BackupPrivateKey, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const userId = await userKv.get(doc.user.toString()); + + return { + id, + encryptedPrivateKey: doc.encryptedPrivateKey, + userId, + verifier: doc.verifier, + salt: doc.salt, + tag: doc.tag, + iv: doc.iv, + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8, + createdAt: new Date(doc.createdAt), + updatedAt: new Date(doc.updatedAt), + }; + }, + }); + + const orgKv = kdb.sublevel(TableName.Organization); + await migrateCollection({ + db, + mongooseCollection: Organization, + postgresTableName: TableName.Organization, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await orgKv.put(doc._id.toString(), id); + return { + id, + name: doc.name, + customerId: doc.customerId, + createdAt: new Date(doc.createdAt), + updatedAt: new Date(doc.updatedAt), + }; + }, + }); + + const orgRoleKv = kdb.sublevel(TableName.OrgRoles); + await migrateCollection({ + db, + filter: { isOrgRole: true }, + mongooseCollection: Role, + postgresTableName: TableName.OrgRoles, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + await orgRoleKv.put(doc._id.toString(), id); + return { + id, + name: doc.name, + orgId, + description: doc.description, + slug: doc.slug, + permissions: doc.permissions + ? JSON.stringify(packRules(doc.permissions as any)) + : null, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: MembershipOrg, + postgresTableName: TableName.OrgMembership, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + const userId = doc?.user ? await userKv.get(doc.user.toString()) : null; + const roleId = doc.customRole + ? await orgRoleKv.get(doc.customRole.toString()) + : null; + + return { + id, + role: doc.role, + orgId, + roleId, + userId, + status: doc.status, + inviteEmail: doc.inviteEmail, + createdAt: new Date(doc.createdAt), + updatedAt: new Date(doc.updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: IncidentContactOrg, + postgresTableName: TableName.IncidentContact, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + return { + id, + email: doc.email, + orgId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: UserAction, + postgresTableName: TableName.UserAction, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const userId = await userKv.get(doc.user.toString()); + return { + id, + userId, + action: doc.action, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: ServerConfig, + postgresTableName: TableName.SuperAdmin, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + return { + id, + allowSignUp: doc.allowSignUp, + initialized: doc.initialized, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: APIKeyData, + postgresTableName: TableName.ApiKey, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const userId = await userKv.get(doc.user.toString()); + // expired tokens can be removed + if (new Date(doc.expiresAt) < new Date()) return; + return { + id, + userId, + name: doc.name, + lastUsed: doc.lastUsed, + secretHash: doc.secretHash, + expiresAt: doc.expiresAt, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: Workspace, + postgresTableName: TableName.Project, + returnKeys: ["id"], + preProcessing: async (doc) => { + const orgId = await orgKv.get(doc.organization.toString()); + // expired tokens can be removed + // cannot use this uuid for the org id + return { + id: doc._id.toString(), + name: doc.name, + orgId, + autoCapitalization: doc.autoCapitalization, + createdAt: new Date(), + updatedAt: new Date(), + }; + }, + }); + + console.log( + "Migrating environments from Mongo Project -> Pg Environment Table", + ); + const envPKv = kdb.sublevel(TableName.Environment); + + const getEnvId = (workspace: string, environment: string) => { + const envKv = envPKv.sublevel(workspace); + return envKv.get(environment); + }; + const getFolderKv = (workspace: string, environment: string) => { + const envKv = envPKv.sublevel(workspace); + return envKv.sublevel(environment); + }; + + await migrateCollection({ + db, + mongooseCollection: Workspace, + postgresTableName: TableName.Environment, + returnKeys: ["id"], + preProcessing: async (doc) => { + // to we scope environments into each project then map each slug with respective id + const envKv = envPKv.sublevel(doc._id.toString()); + // expired tokens can be removed + // cannot use this uuid for the org id + return doc.environments.map((env, index) => { + const id = uuidV4(); + envKv.put(env.slug, id); + return { + id, + name: env.name, + slug: env.slug, + position: index + 1, + projectId: doc._id.toString(), + createdAt: new Date(), + updatedAt: new Date(), + }; }); - console.log(newUserIds.length); - users.slice(0, users.length); - newUsers.slice(0, newUsers.length); - } - } + }, + }); + + console.log("Creating root folders for all environments"); + await migrateCollection({ + db, + mongooseCollection: Workspace, + postgresTableName: TableName.SecretFolder, + returnKeys: ["id"], + preProcessing: async (doc) => + Promise.all( + doc.environments.map(async (env) => { + const id = uuidV4(); + const envId = await getEnvId(doc._id.toString(), env.slug); + const folderKv = getFolderKv(doc._id.toString(), env.slug); + await folderKv.put("root", id); + return { + id, + name: "root", + envId, + version: 1, + createdAt: new Date(), + updatedAt: new Date(), + }; + }), + ), + }); + + await migrateCollection({ + db, + mongooseCollection: Key, + postgresTableName: TableName.ProjectKeys, + returnKeys: ["id"], + preProcessing: async (doc) => { + // expired tokens can be removed + // cannot use this uuid for the org id + const id = uuidV4(); + const senderId = (await userKv.get(doc.sender.toString())) || null; + const receiverId = await userKv.get(doc.receiver.toString()); + if (!receiverId) return; + + return { + id, + projectId: doc.workspace.toString(), + senderId, + nonce: doc.nonce, + encryptedKey: doc.encryptedKey, + receiverId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + console.log("Migrating roles from Mongo Project -> Pg Project Role Table"); + const projectRoleKv = kdb.sublevel(TableName.ProjectRoles); + await migrateCollection({ + db, + filter: { isOrgRole: false }, + mongooseCollection: Role, + postgresTableName: TableName.ProjectRoles, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await projectRoleKv.put(doc._id.toString(), id); + return { + id, + name: doc.name, + projectId: doc.workspace.toString(), + description: doc.description, + slug: doc.slug, + permissions: doc.permissions + ? JSON.stringify(packRules(doc.permissions as any)) + : null, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const projectMembKv = kdb.sublevel(TableName.ProjectMembership); + await migrateCollection({ + db, + mongooseCollection: Membership, + postgresTableName: TableName.ProjectMembership, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const userId = await userKv.get(doc.user.toString()); + const roleId = doc.customRole + ? await projectRoleKv.get(doc.customRole.toString()) + : null; + await projectMembKv.put(doc._id.toString(), id); + + return { + id, + role: doc.role, + roleId, + projectId: doc.workspace.toString(), + userId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: Folder, + postgresTableName: TableName.SecretFolder, + returnKeys: ["id"], + preProcessing: async (doc) => { + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + const folders = flattenFolders(doc.nodes); + + const pgFolder = await Promise.all( + folders + // already has been created + .filter(({ id }) => id !== "root") + .map(async (folder) => { + const { name, version } = folder; + const id = uuidV4(); + await folderKv.put(folder.id, id); + const parentId = folder.parentId + ? await folderKv.get(folder.parentId) + : null; + return { + name, + version, + id, + parentId, + envId, + createdAt: (doc as any).createdAt, + updatedAt: (doc as any).updatedAt, + }; + }), + ); + + return pgFolder; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: FolderVersion, + postgresTableName: TableName.SecretFolderVersion, + returnKeys: ["id"], + preProcessing: async (doc) => { + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + const rootFolders = (doc?.nodes?.children || []).map( + ({ name, version, id }) => ({ + name, + version, + id, + }), + ); + + const pgFolder = await Promise.all( + rootFolders.map(async (folder) => { + const { name, version } = folder; + const id = uuidV4(); + await folderKv.put(getFolderVersionKey(folder.id, version), id); + const folderId = await folderKv.get(folder.id); + return { + name, + version, + id, + folderId, + envId, + createdAt: (doc as any).createdAt, + updatedAt: (doc as any).updatedAt, + }; + }), + ); + + return pgFolder; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: SecretImport, + postgresTableName: TableName.SecretImport, + returnKeys: ["id"], + preProcessing: async (doc) => { + const envKv = envPKv.sublevel(doc.workspace.toString()); + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const folderId = await folderKv.get(doc.folderId); + + return Promise.all( + doc.imports.map(async ({ environment, secretPath }, index) => { + const id = uuidV4(); + const importEnv = await envKv.get(environment); + return { + id, + folderId, + position: index + 1, + version: 1, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + importEnv, + importPath: secretPath, + }; + }), + ); + }, + }); + + const tagKv = kdb.sublevel(TableName.SecretTag); + await migrateCollection({ + db, + mongooseCollection: Tag, + postgresTableName: TableName.SecretTag, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await tagKv.put(doc._id.toString(), id); + const createdBy = await userKv.get(doc.user.toString()); + + return { + id, + name: doc.name, + slug: doc.slug, + color: doc.tagColor, + projectId: doc.workspace.toString(), + createdBy: createdBy || null, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: SecretBlindIndexData, + postgresTableName: TableName.SecretBlindIndex, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + + return { + id, + projectId: doc.workspace.toString(), + saltIV: doc.saltIV, + saltTag: doc.saltTag, + algorithm: doc.algorithm, + keyEncoding: doc.keyEncoding, + encryptedSaltCipherText: doc.encryptedSaltCiphertext, + createdAt: new Date(), + updatedAt: new Date(), + }; + }, + }); + + const secKv = kdb.sublevel(TableName.Secret); + await migrateCollection({ + db, + mongooseCollection: Secret, + postgresTableName: TableName.Secret, + returnKeys: ["id"], + preProcessing: async (doc) => { + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const folderId = await folderKv.get(doc.folder || "root"); + + const userId = doc.user ? await userKv.get(doc.user.toString()) : null; + const id = uuidV4(); + await secKv.put(doc._id.toString(), id); + + return { + id, + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + folderId, + type: doc.type, + version: doc.version, + secretReminderRepeatDays: doc.secretReminderRepeatDays, + userId, + metadata: doc.metadata, + secretKeyIV: doc.secretKeyIV, + secretKeyTag: doc.secretKeyTag, + secretKeyCiphertext: doc.secretKeyCiphertext, + secretValueIV: doc.secretValueIV, + secretValueTag: doc.secretValueTag, + secretValueCiphertext: doc.secretValueCiphertext, + secretBlindIndex: doc.secretBlindIndex, + secretCommentIV: doc.secretCommentIV, + secretCommentTag: doc.secretCommentTag, + secretCommentCiphertext: doc.secretCommentCiphertext, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + console.log( + "Migrating secret tags from Mongo Secret.tags -> Pg SecretTag table", + ); + await migrateCollection({ + db, + mongooseCollection: Secret, + postgresTableName: TableName.JnSecretTag, + returnKeys: ["id"], + preProcessing: async (doc) => { + return Promise.all( + (doc.tags || [])?.map(async (tagId) => { + const id = uuidV4(); + const secretId = await secKv.get(doc._id.toString()); + const secretTagId = await tagKv.get(tagId); + return { + id, + [`${TableName.Secret}Id`]: secretId, + [`${TableName.SecretTag}Id`]: secretTagId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }), + ); + }, + }); + + const secVerKv = kdb.sublevel(TableName.SecretVersion); + await migrateCollection({ + db, + mongooseCollection: SecretVersion, + postgresTableName: TableName.SecretVersion, + returnKeys: ["id"], + preProcessing: async (doc) => { + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + const folderId = await folderKv.get(doc.folder || "root"); + const userId = doc.user ? await userKv.get(doc.user.toString()) : null; + + const id = uuidV4(); + await secVerKv.put(doc._id.toString(), id); + const secretId = await secKv.get(doc.secret.toString()); + // comment and reminder are not saved in secret version of mongo + return { + id, + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + folderId, + envId, + type: doc.type, + version: doc.version, + userId, + secretId, + secretKeyIV: doc.secretKeyIV, + secretKeyTag: doc.secretKeyTag, + secretKeyCiphertext: doc.secretKeyCiphertext, + secretValueIV: doc.secretValueIV, + secretValueTag: doc.secretValueTag, + secretValueCiphertext: doc.secretValueCiphertext, + secretBlindIndex: doc.secretBlindIndex, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const projectBotKv = kdb.sublevel(TableName.ProjectBot); + await migrateCollection({ + db, + mongooseCollection: Bot, + postgresTableName: TableName.ProjectBot, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await projectBotKv.put(doc._id.toString(), id); + const botKey = await BotKey.findOne({ + workspace: doc.workspace, + }).lean(); + const senderId = botKey?.sender + ? await userKv.get(botKey.sender.toString()) + : null; + return { + id, + algorithm: doc.algorithm, + keyEncoding: doc.keyEncoding, + projectId: doc.workspace.toString(), + name: doc.name, + iv: doc.iv, + tag: doc.tag, + senderId, + isActive: doc.isActive, + publicKey: doc.publicKey, + encryptedProjectKey: botKey?.encryptedKey || null, + encryptedProjectKeyNonce: botKey?.nonce || null, + encryptedPrivateKey: doc.encryptedPrivateKey, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const integrationAuthKv = kdb.sublevel(TableName.IntegrationAuth); + await migrateCollection({ + db, + mongooseCollection: IntegrationAuth, + postgresTableName: TableName.IntegrationAuth, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await integrationAuthKv.put(doc._id.toString(), id); + return { + id, + projectId: doc.workspace.toString(), + keyEncoding: doc.keyEncoding || SecretKeyEncoding.UTF8, + algorithm: doc.algorithm || SecretEncryptionAlgo.AES_256_GCM, + metadata: doc.metadata, + url: doc.url, + teamId: doc.teamId, + accessIV: doc.accessIV, + accessTag: doc.accessTag, + accessCiphertext: doc.accessCiphertext, + accountId: doc.accountId, + namespace: doc.namespace, + refreshIV: doc.refreshIV, + refreshTag: doc.refreshTag, + refreshCiphertext: doc.refreshCiphertext, + integration: doc.integration, + accessIdIV: doc.accessIdIV, + accessIdTag: doc.accessIdTag, + accessIdCiphertext: doc.accessIdCiphertext, + accessExpiresAt: doc.accessExpiresAt, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: Integration, + postgresTableName: TableName.Integration, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + const integrationAuthId = await integrationAuthKv.get( + doc.integrationAuth.toString(), + ); + + return { + id, + integration: doc.integration, + url: doc.url, + metadata: doc.metadata, + isActive: doc.isActive, + secretPath: doc.secretPath, + integrationAuthId, + app: doc.app, + envId, + path: doc.path, + appId: doc.appId, + owner: doc.owner, + scope: doc.scope, + region: doc.region, + targetService: doc.targetService, + targetServiceId: doc.targetServiceId, + targetEnvironment: doc.targetEnvironment, + targetEnvironmentId: doc.targetEnvironmentId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: ServiceTokenData, + postgresTableName: TableName.ServiceToken, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const userId = await userKv.get(doc.user.toString()); + return { + id, + projectId: doc.workspace.toString(), + name: doc.name, + createdBy: userId, + iv: doc.iv, + tag: doc.tag, + scopes: JSON.stringify(doc.scopes), + lastUsed: doc.lastUsed, + secretHash: doc.secretHash, + expiresAt: doc.expiresAt, + permissions: doc.permissions, + encryptedKey: doc.encryptedKey, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: Webhook, + postgresTableName: TableName.Webhook, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + return { + id, + iv: doc.iv, + envId, + secretPath: doc.secretPath, + url: doc.url, + algorithm: doc.algorithm || SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: doc.keyEncoding || SecretKeyEncoding.UTF8, + tag: doc.tag, + isDisabled: doc.isDisabled, + lastStatus: doc.lastStatus, + encryptedSecretKey: doc.encryptedSecretKey, + lastRunErrorMessage: doc.lastRunErrorMessage, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const identityKv = kdb.sublevel(TableName.Identity); + await migrateCollection({ + db, + mongooseCollection: Identity, + postgresTableName: TableName.Identity, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await identityKv.put(doc._id.toString(), id); + return { + id, + name: doc.name, + authMethod: doc.authMethod, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const identityUaKv = kdb.sublevel(TableName.IdentityUniversalAuth); + await migrateCollection({ + db, + mongooseCollection: IdentityUniversalAuth, + postgresTableName: TableName.IdentityUniversalAuth, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const identityId = await identityKv.get(doc.identity.toString()); + await identityUaKv.put(doc._id.toString(), id); + return { + id, + identityId, + clientId: doc.clientId, + accessTokenTTL: doc.accessTokenTTL, + accessTokenMaxTTL: doc.accessTokenMaxTTL, + accessTokenTrustedIps: JSON.stringify( + doc.accessTokenTrustedIps || [], + ), + accessTokenNumUsesLimit: doc.accessTokenNumUsesLimit, + clientSecretTrustedIps: JSON.stringify( + doc.clientSecretTrustedIps || [], + ), + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const identityUaClientSecKv = kdb.sublevel( + TableName.IdentityUaClientSecret, + ); + await migrateCollection({ + db, + mongooseCollection: IdentityUniversalAuthClientSecret, + postgresTableName: TableName.IdentityUaClientSecret, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const identityUAId = await identityUaKv.get( + doc.identityUniversalAuth.toString(), + ); + await identityUaClientSecKv.put(doc._id.toString(), id); + return { + id, + identityUAId, + description: doc.description, + clientSecretTTL: doc.clientSecretTTL, + clientSecretHash: doc.clientSecretHash, + clientSecretPrefix: doc.clientSecretPrefix, + clientSecretNumUses: doc.clientSecretNumUses, + isClientSecretRevoked: doc.isClientSecretRevoked, + clientSecretLastUsedAt: doc.clientSecretLastUsedAt, + clientSecretNumUsesLimit: doc.clientSecretNumUsesLimit, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: IdentityAccessToken, + postgresTableName: TableName.IdentityAccessToken, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const identityUAClientSecretId = doc?.identityUniversalAuthClientSecret + ? await identityUaClientSecKv.get( + doc.identityUniversalAuthClientSecret.toString(), + ) + : null; + const identityId = await identityKv.get(doc.identity.toString()); + return { + id, + accessTokenNumUsesLimit: doc.accessTokenNumUsesLimit, + accessTokenMaxTTL: doc.accessTokenMaxTTL, + accessTokenTTL: doc.accessTokenTTL, + identityId, + accessTokenNumUses: doc.accessTokenNumUses, + isAccessTokenRevoked: doc.isAccessTokenRevoked, + accessTokenLastUsedAt: doc.accessTokenLastUsedAt, + accessTokenLastRenewedAt: doc.accessTokenLastRenewedAt, + identityUAClientSecretId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: IdentityMembershipOrg, + postgresTableName: TableName.IdentityOrgMembership, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + const identityId = await identityKv.get(doc.identity.toString()); + const roleId = doc.customRole + ? await orgRoleKv.get(doc.customRole.toString()) + : null; + + return { + id, + role: doc.role, + orgId, + identityId, + roleId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: IdentityMembership, + postgresTableName: TableName.IdentityProjectMembership, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const identityId = await identityKv.get(doc.identity.toString()); + const roleId = doc.customRole + ? await projectRoleKv.get(doc.customRole.toString()) + : null; + + return { + id, + role: doc.role, + identityId, + projectId: doc.workspace.toString(), + roleId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const sapKv = kdb.sublevel(TableName.SecretApprovalPolicy); + await migrateCollection({ + db, + mongooseCollection: SecretApprovalPolicy, + postgresTableName: TableName.SecretApprovalPolicy, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + sapKv.put(doc._id.toString(), id); + + return { + id, + name: doc.name, + envId, + approvals: doc.approvals, + secretPath: doc.secretPath, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + console.log( + "Migration secret approval policy approvers -> Pg sap approvers table", + ); + await migrateCollection({ + db, + mongooseCollection: SecretApprovalPolicy, + postgresTableName: TableName.SapApprover, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const policyId = await sapKv.get(doc._id.toString()); + + return Promise.all( + doc.approvers.map(async (membId) => { + const approverId = await projectMembKv.get(membId.toString()); + return { + id, + policyId, + approverId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }), + ); + }, + }); + + const secRotationKv = kdb.sublevel(TableName.SecretRotation); + await migrateCollection({ + db, + mongooseCollection: SecretRotation, + postgresTableName: TableName.SecretRotation, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await secRotationKv.put(doc._id.toString(), id); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + + return { + id, + envId, + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + secretPath: doc.secretPath, + status: doc.status, + interval: doc.interval, + provider: doc.provider, + encryptedData: doc.encryptedData, + encryptedDataIV: doc.encryptedDataIV, + encryptedDataTag: doc.encryptedDataTag, + lastRotatedAt: doc.lastRotatedAt ? new Date(doc.lastRotatedAt) : null, + statusMessage: doc.statusMessage, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + console.log( + "Migration secret rotation outputs from mongo to postgres secret rotation output table", + ); + await migrateCollection({ + db, + mongooseCollection: SecretRotation, + postgresTableName: TableName.SecretRotationOutput, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const rotationId = await secRotationKv.get(doc._id.toString()); + + return Promise.all( + doc.outputs.map(async ({ key, secret }) => { + const secretId = await secKv + .get(secret.toString()) + .catch(() => null); + if (!secretId) return; + + return { + id, + key, + secretId, + rotationId, + }; + }), + ); + }, + }); + + await migrateCollection({ + db, + mongooseCollection: SSOConfig, + postgresTableName: TableName.SamlConfig, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + + return { + id, + isActive: doc.isActive, + orgId, + certIV: doc.certIV, + certTag: doc.certTag, + issuerIV: doc.issuerIV, + issuerTag: doc.issuerTag, + authProvider: doc.authProvider, + entryPointIV: doc.entryPointIV, + entryPointTag: doc.entryPointTag, + encryptedEntryPoint: doc.encryptedEntryPoint, + encryptedCert: doc.encryptedCert, + encryptedIssuer: doc.encryptedIssuer, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: BotOrg, + postgresTableName: TableName.OrgBot, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + + return { + id, + orgId, + name: doc.name, + encryptedPrivateKey: doc.encryptedPrivateKey, + publicKey: doc.publicKey, + privateKeyIV: doc.privateKeyIV, + privateKeyTag: doc.privateKeyTag, + symmetricKeyIV: doc.symmetricKeyIV, + symmetricKeyTag: doc.symmetricKeyTag, + privateKeyAlgorithm: doc.privateKeyAlgorithm, + encryptedSymmetricKey: doc.encryptedSymmetricKey, + privateKeyKeyEncoding: doc.privateKeyKeyEncoding, + symmetricKeyAlgorithm: doc.symmetricKeyAlgorithm, + symmetricKeyKeyEncoding: doc.symmetricKeyKeyEncoding, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: GitAppInstallationSession, + postgresTableName: TableName.GitAppInstallSession, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + const userId = await userKv.get(doc.user.toString()); + + return { + id, + orgId, + userId, + sessionId: doc.sessionId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: GitAppOrganizationInstallation, + postgresTableName: TableName.GitAppOrg, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organizationId); + const userId = await userKv.get(doc.user.toString()); + + return { + id, + orgId, + userId, + installationId: doc.installationId, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: GitRisks, + postgresTableName: TableName.SecretScanningGitRisk, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const orgId = await orgKv.get(doc.organization.toString()); + + return { + id, + orgId, + installationId: doc.installationId, + status: doc.status, + tags: doc.tags, + date: doc.date, + file: doc.file, + email: doc.email, + author: doc.author, + commit: doc.commit, + ruleID: doc.ruleID, + endLine: doc.endLine, + entropy: doc.entropy, + message: doc.message, + endColumn: doc.endColumn, + riskOwner: doc.riskOwner, + startLine: doc.startLine, + isResolved: doc.isResolved, + pusherName: doc.pusher.name, + description: doc.description, + fingerprint: doc.fingerprint, + fingerPrintWithoutCommitId: doc.fingerPrintWithoutCommitId, + pusherEmail: doc.pusher.email, + startColumn: doc.startColumn, + symlinkFile: doc.symlinkFile, + repositoryId: doc.repositoryId, + repositoryLink: doc.repositoryLink, + isFalsePositive: doc.isFalsePositive, + repositoryFullName: doc.repositoryFullName, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + await migrateCollection({ + db, + mongooseCollection: TrustedIP, + postgresTableName: TableName.TrustedIps, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + + return { + id, + projectId: doc.workspace.toString(), + type: doc.type, + prefix: doc.prefix, + comment: doc.comment, + ipAddress: doc.ipAddress, + isActive: doc.isActive, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + const snapKv = kdb.sublevel(TableName.Snapshot); + await migrateCollection({ + db, + mongooseCollection: SecretSnapshot, + postgresTableName: TableName.Snapshot, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await snapKv.put(doc._id.toString(), id); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const folderId = await folderKv.get(doc.folderId).catch(async () => { + // this folder may not exist now in tree then create a new id and assign it + const newId = uuidV4(); + await folderKv.put(doc.folderId, newId); + return newId; + }); + + return { + id, + envId, + folderId, + createdAt: (doc as any).createdAt, + updatedAt: (doc as any).updatedAt, + }; + }, + }); + + console.log("Migrating secret snapshot secrets"); + await migrateCollection({ + db, + mongooseCollection: SecretSnapshot, + postgresTableName: TableName.SnapshotSecret, + returnKeys: ["id"], + preProcessing: async (doc) => { + const snapshotId = await snapKv.get(doc._id.toString()); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + + return Promise.all( + doc.secretVersions.map(async (secVer) => { + const id = uuidV4(); + const secretVersionId = await secVerKv.get(secVer.toString()); + return { + id, + envId, + snapshotId, + secretVersionId, + createdAt: (doc as any).createdAt, + updatedAt: (doc as any).updatedAt, + }; + }), + ); + }, + }); + + console.log("Migrating secret snapshot folders"); + await migrateCollection({ + db, + mongooseCollection: SecretSnapshot, + postgresTableName: TableName.SnapshotFolder, + returnKeys: ["id"], + preProcessing: async (doc) => { + const snapshotId = await snapKv.get(doc._id.toString()); + const envId = await getEnvId(doc.workspace.toString(), doc.environment); + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const folderVersion = await FolderVersion.findById(doc.folderVersion); + if (!folderVersion) return; + + return Promise.all( + folderVersion.nodes.children.map(async (folderVer) => { + const id = uuidV4(); + const folderVersionId = await folderKv.get( + getFolderVersionKey(folderVer.id, folderVer.version), + ); + return { + id, + envId, + snapshotId, + folderVersionId, + createdAt: (doc as any).createdAt, + updatedAt: (doc as any).updatedAt, + }; + }), + ); + }, + }); + + const sarKv = kdb.sublevel(TableName.SecretApprovalRequest); + await migrateCollection({ + db, + mongooseCollection: SecretApprovalRequest, + postgresTableName: TableName.SecretApprovalRequest, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + await sarKv.put(doc._id.toString(), id); + const policyId = await sapKv.get(doc.policy.toString()); + + const folderKv = getFolderKv(doc.workspace.toString(), doc.environment); + const folderId = await folderKv.get(doc.folderId); + + const committerId = await projectMembKv.get(doc.committer.toString()); + const statusChangeBy = doc.statusChangeBy + ? await projectMembKv.get(doc.statusChangeBy.toString()) + : null; + return { + id, + policyId, + hasMerged: doc.hasMerged, + status: doc.status, + conflicts: JSON.stringify(doc.conflicts), + slug: doc.slug, + folderId, + committerId, + statusChangeBy, + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }, + }); + + console.log( + "Migrating Mongo Secret approval request reviewers -> Pg Secret Approval Request reviewers table", + ); + await migrateCollection({ + db, + mongooseCollection: SecretApprovalRequest, + postgresTableName: TableName.SarReviewer, + returnKeys: ["id"], + preProcessing: async (doc) => { + const id = uuidV4(); + const requestId = await sarKv.get(doc._id.toString()); + + return Promise.all( + doc.reviewers.map(async ({ status, member }) => { + return { + id, + status, + requestId, + member: await projectMembKv.get(member.toString()), + createdAt: new Date((doc as any).createdAt), + updatedAt: new Date((doc as any).updatedAt), + }; + }), + ); + }, + }); + + // console.log( + // "Migrating Mongo Secret approval request secrets -> Pg Secret Approval Request secrets table", + // ); + // await migrateCollection({ + // db, + // mongooseCollection: SecretApprovalRequest, + // postgresTableName: TableName.SarSecret, + // returnKeys: ["id"], + // preProcessing: async (doc) => { + // const id = uuidV4(); + // const requestId = await sarKv.get(doc._id.toString()); + // + // return Promise.all( + // doc.commits.map(async (commit) => { + // if (commit.op === CommitType.CREATE) { + // return { + // id, + // requestId, + // secretBlindIndex: commit.newVersion.secretBlindIndex, + // createdAt: new Date((doc as any).createdAt), + // updatedAt: new Date((doc as any).updatedAt), + // }; + // } + // }), + // ); + // }, + // }); process.exit(1); } catch (error) { diff --git a/pg-migrator/src/migrations/20231218103423_secret.ts b/pg-migrator/src/migrations/20231218103423_secret.ts index a29dccfc8..c955ee50f 100644 --- a/pg-migrator/src/migrations/20231218103423_secret.ts +++ b/pg-migrator/src/migrations/20231218103423_secret.ts @@ -1,7 +1,16 @@ import { Knex } from "knex"; -import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas"; -import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; +import { + SecretEncryptionAlgo, + SecretKeyEncoding, + SecretType, + TableName, +} from "../schemas"; +import { + createJunctionTable, + createOnUpdateTrigger, + dropOnUpdateTrigger, +} from "../utils"; export async function up(knex: Knex): Promise { if (!(await knex.schema.hasTable(TableName.SecretBlindIndex))) { @@ -10,10 +19,15 @@ export async function up(knex: Knex): Promise { t.text("encryptedSaltCipherText").notNullable(); t.text("saltIV").notNullable(); t.text("saltTag").notNullable(); - t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM); + t.string("algorithm") + .notNullable() + .defaultTo(SecretEncryptionAlgo.AES_256_GCM); t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8); t.string("projectId").notNullable().unique(); - t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.foreign("projectId") + .references("id") + .inTable(TableName.Project) + .onDelete("CASCADE"); t.timestamps(true, true, true); }); } @@ -27,7 +41,8 @@ export async function up(knex: Knex): Promise { // t.text("secretKeyHash").notNullable(); // t.text("secretValueHash"); // t.text("secretCommentHash"); - t.text("secretBlindIndex").notNullable(); + // this is required but for backward compatiability we are making it nullable + t.text("secretBlindIndex"); t.text("secretKeyCiphertext").notNullable(); t.text("secretKeyIV").notNullable(); t.text("secretKeyTag").notNullable(); @@ -40,19 +55,32 @@ export async function up(knex: Knex): Promise { t.string("secretReminderNote"); t.integer("secretReminderRepeatDays"); t.boolean("skipMultilineEncoding").defaultTo(false); - t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM); + t.string("algorithm") + .notNullable() + .defaultTo(SecretEncryptionAlgo.AES_256_GCM); t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8); t.jsonb("metadata"); t.uuid("userId"); - t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.foreign("userId") + .references("id") + .inTable(TableName.Users) + .onDelete("CASCADE"); t.uuid("folderId").notNullable(); - t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE"); + t.foreign("folderId") + .references("id") + .inTable(TableName.SecretFolder) + .onDelete("CASCADE"); t.timestamps(true, true, true); }); } await createOnUpdateTrigger(knex, TableName.Secret); // many to many relation between tags - await createJunctionTable(knex, TableName.JnSecretTag, TableName.Secret, TableName.SecretTag); + await createJunctionTable( + knex, + TableName.JnSecretTag, + TableName.Secret, + TableName.SecretTag, + ); } export async function down(knex: Knex): Promise { diff --git a/pg-migrator/src/migrations/20231220052508_secret-version.ts b/pg-migrator/src/migrations/20231220052508_secret-version.ts index 4b515b8e1..4290f0ec3 100644 --- a/pg-migrator/src/migrations/20231220052508_secret-version.ts +++ b/pg-migrator/src/migrations/20231220052508_secret-version.ts @@ -1,7 +1,16 @@ import { Knex } from "knex"; -import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas"; -import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; +import { + SecretEncryptionAlgo, + SecretKeyEncoding, + SecretType, + TableName, +} from "../schemas"; +import { + createJunctionTable, + createOnUpdateTrigger, + dropOnUpdateTrigger, +} from "../utils"; export async function up(knex: Knex): Promise { if (!(await knex.schema.hasTable(TableName.SecretVersion))) { @@ -9,7 +18,7 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.integer("version").defaultTo(1).notNullable(); t.string("type").notNullable().defaultTo(SecretType.Shared); - t.text("secretBlindIndex").notNullable(); + t.text("secretBlindIndex"); t.text("secretKeyCiphertext").notNullable(); t.text("secretKeyIV").notNullable(); t.text("secretKeyTag").notNullable(); @@ -22,17 +31,25 @@ export async function up(knex: Knex): Promise { t.string("secretReminderNote"); t.integer("secretReminderRepeatDays"); t.boolean("skipMultilineEncoding").defaultTo(false); - t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM); + t.string("algorithm") + .notNullable() + .defaultTo(SecretEncryptionAlgo.AES_256_GCM); t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8); t.jsonb("metadata"); // to avoid orphan rows t.uuid("envId"); - t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.foreign("envId") + .references("id") + .inTable(TableName.Environment) + .onDelete("CASCADE"); t.uuid("secretId").notNullable(); t.uuid("folderId").notNullable(); // t.foreign("secretId").references("id").inTable(TableName.Secret).onDelete("SET NULL"); t.uuid("userId"); - t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.foreign("userId") + .references("id") + .inTable(TableName.Users) + .onDelete("CASCADE"); t.timestamps(true, true, true); }); } @@ -42,7 +59,7 @@ export async function up(knex: Knex): Promise { knex, TableName.JnSecretVersionTag, TableName.SecretVersion, - TableName.SecretTag + TableName.SecretTag, ); } diff --git a/pg-migrator/src/migrations/20231228075011_identity-access-token.ts b/pg-migrator/src/migrations/20231228075011_identity-access-token.ts index 1a5953fcd..fccbe2730 100644 --- a/pg-migrator/src/migrations/20231228075011_identity-access-token.ts +++ b/pg-migrator/src/migrations/20231228075011_identity-access-token.ts @@ -7,7 +7,6 @@ export async function up(knex: Knex): Promise { if (!(await knex.schema.hasTable(TableName.IdentityAccessToken))) { await knex.schema.createTable(TableName.IdentityAccessToken, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); - t.string("authType").notNullable(); t.integer("accessTokenTTL").defaultTo(2592000).notNullable(); // 30 days second t.integer("accessTokenMaxTTL").defaultTo(2592000).notNullable(); t.integer("accessTokenNumUses").defaultTo(0).notNullable(); @@ -21,7 +20,10 @@ export async function up(knex: Knex): Promise { .inTable(TableName.IdentityUaClientSecret) .onDelete("CASCADE"); t.uuid("identityId").notNullable(); - t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.foreign("identityId") + .references("id") + .inTable(TableName.Identity) + .onDelete("CASCADE"); t.timestamps(true, true, true); }); } diff --git a/pg-migrator/src/models/apiKeyData.ts b/pg-migrator/src/models/apiKeyData.ts index 0b88c5ddb..67a962b25 100644 --- a/pg-migrator/src/models/apiKeyData.ts +++ b/pg-migrator/src/models/apiKeyData.ts @@ -28,7 +28,7 @@ const apiKeyDataSchema = new Schema( secretHash: { type: String, required: true, - select: false, + }, }, { diff --git a/pg-migrator/src/models/backupPrivateKey.ts b/pg-migrator/src/models/backupPrivateKey.ts index 09df1dda7..47e4b8909 100644 --- a/pg-migrator/src/models/backupPrivateKey.ts +++ b/pg-migrator/src/models/backupPrivateKey.ts @@ -1,74 +1,67 @@ import { Schema, Types, model } from "mongoose"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, -} from "../variables"; export interface IBackupPrivateKey { - _id: Types.ObjectId; - user: Types.ObjectId; - encryptedPrivateKey: string; - iv: string; - tag: string; - salt: string; - algorithm: string; - keyEncoding: "base64" | "utf8"; - verifier: string; + _id: Types.ObjectId; + user: Types.ObjectId; + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + algorithm: string; + keyEncoding: "base64" | "utf8"; + verifier: string; + createdAt: string; + updatedAt: string; } const backupPrivateKeySchema = new Schema( - { - user: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - encryptedPrivateKey: { - type: String, - select: false, - required: true, - }, - iv: { - type: String, - select: false, - required: true, - }, - tag: { - type: String, - select: false, - required: true, - }, - algorithm: { // the encryption algorithm used - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true, - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true, - }, - salt: { - type: String, - select: false, - required: true, - }, - verifier: { - type: String, - select: false, - required: true, - }, - }, - { - timestamps: true, - } + { + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + encryptedPrivateKey: { + type: String, + + required: true, + }, + iv: { + type: String, + + required: true, + }, + tag: { + type: String, + + required: true, + }, + algorithm: { + // the encryption algorithm used + type: String, + required: true, + }, + keyEncoding: { + type: String, + required: true, + }, + salt: { + type: String, + + required: true, + }, + verifier: { + type: String, + + required: true, + }, + }, + { + timestamps: true, + }, ); export const BackupPrivateKey = model( - "BackupPrivateKey", - backupPrivateKeySchema + "BackupPrivateKey", + backupPrivateKeySchema, ); diff --git a/pg-migrator/src/models/bot.ts b/pg-migrator/src/models/bot.ts index 5a5c83b13..1d024191c 100644 --- a/pg-migrator/src/models/bot.ts +++ b/pg-migrator/src/models/bot.ts @@ -1,77 +1,68 @@ import { Schema, Types, model } from "mongoose"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, -} from "../variables"; export interface IBot { - _id: Types.ObjectId; - name: string; - workspace: Types.ObjectId; - isActive: boolean; - publicKey: string; - encryptedPrivateKey: string; - iv: string; - tag: string; - algorithm: "aes-256-gcm"; - keyEncoding: "base64" | "utf8"; + _id: Types.ObjectId; + name: string; + workspace: Types.ObjectId; + isActive: boolean; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; + algorithm: "aes-256-gcm"; + keyEncoding: "base64" | "utf8"; } const botSchema = new Schema( - { - name: { - type: String, - required: true, - }, - workspace: { - type: Schema.Types.ObjectId, - ref: "Workspace", - required: true, - }, - isActive: { - type: Boolean, - required: true, - default: false, - }, - publicKey: { - type: String, - required: true, - }, - encryptedPrivateKey: { - type: String, - required: true, - select: false, - }, - iv: { - type: String, - required: true, - select: false, - }, - tag: { - type: String, - required: true, - select: false, - }, - algorithm: { // the encryption algorithm used - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true, - select: false, - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true, - select: false, - }, - }, - { - timestamps: true, - } + { + name: { + type: String, + required: true, + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + isActive: { + type: Boolean, + required: true, + default: false, + }, + publicKey: { + type: String, + required: true, + }, + encryptedPrivateKey: { + type: String, + required: true, + + }, + iv: { + type: String, + required: true, + + }, + tag: { + type: String, + required: true, + + }, + algorithm: { + // the encryption algorithm used + type: String, + required: true, + + }, + keyEncoding: { + type: String, + required: true, + + }, + }, + { + timestamps: true, + }, ); -export const Bot = model("Bot", botSchema); \ No newline at end of file +export const Bot = model("Bot", botSchema); diff --git a/pg-migrator/src/models/botOrg.ts b/pg-migrator/src/models/botOrg.ts index 177294ef9..9da98e6c9 100644 --- a/pg-migrator/src/models/botOrg.ts +++ b/pg-migrator/src/models/botOrg.ts @@ -1,96 +1,81 @@ import { Schema, Types, model } from "mongoose"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, -} from "../variables"; export interface IBotOrg { - _id: Types.ObjectId; - name: string; - organization: Types.ObjectId; - publicKey: string; - encryptedSymmetricKey: string; - symmetricKeyIV: string; - symmetricKeyTag: string; - symmetricKeyAlgorithm: "aes-256-gcm"; - symmetricKeyKeyEncoding: "base64" | "utf8"; - encryptedPrivateKey: string; - privateKeyIV: string; - privateKeyTag: string; - privateKeyAlgorithm: "aes-256-gcm"; - privateKeyKeyEncoding: "base64" | "utf8"; + _id: Types.ObjectId; + name: string; + organization: Types.ObjectId; + publicKey: string; + encryptedSymmetricKey: string; + symmetricKeyIV: string; + symmetricKeyTag: string; + symmetricKeyAlgorithm: "aes-256-gcm"; + symmetricKeyKeyEncoding: "base64" | "utf8"; + encryptedPrivateKey: string; + privateKeyIV: string; + privateKeyTag: string; + privateKeyAlgorithm: "aes-256-gcm"; + privateKeyKeyEncoding: "base64" | "utf8"; } const botOrgSchema = new Schema( - { - name: { - type: String, - required: true, - }, - organization: { - type: Schema.Types.ObjectId, - ref: "Organization", - required: true, - }, - publicKey: { - type: String, - required: true, - }, - encryptedSymmetricKey: { - type: String, - required: true - }, - symmetricKeyIV: { - type: String, - required: true - }, - symmetricKeyTag: { - type: String, - required: true - }, - symmetricKeyAlgorithm: { - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true - }, - symmetricKeyKeyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true - }, - encryptedPrivateKey: { - type: String, - required: true - }, - privateKeyIV: { - type: String, - required: true - }, - privateKeyTag: { - type: String, - required: true - }, - privateKeyAlgorithm: { - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true - }, - privateKeyKeyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true - }, - }, - { - timestamps: true, - } + { + name: { + type: String, + required: true, + }, + organization: { + type: Schema.Types.ObjectId, + ref: "Organization", + required: true, + }, + publicKey: { + type: String, + required: true, + }, + encryptedSymmetricKey: { + type: String, + required: true, + }, + symmetricKeyIV: { + type: String, + required: true, + }, + symmetricKeyTag: { + type: String, + required: true, + }, + symmetricKeyAlgorithm: { + type: String, + required: true, + }, + symmetricKeyKeyEncoding: { + type: String, + required: true, + }, + encryptedPrivateKey: { + type: String, + required: true, + }, + privateKeyIV: { + type: String, + required: true, + }, + privateKeyTag: { + type: String, + required: true, + }, + privateKeyAlgorithm: { + type: String, + required: true, + }, + privateKeyKeyEncoding: { + type: String, + required: true, + }, + }, + { + timestamps: true, + }, ); -export const BotOrg = model("BotOrg", botOrgSchema); \ No newline at end of file +export const BotOrg = model("BotOrg", botOrgSchema); diff --git a/pg-migrator/src/models/identityAccessToken.ts b/pg-migrator/src/models/identityAccessToken.ts index 82b2e6778..d65c6cbce 100644 --- a/pg-migrator/src/models/identityAccessToken.ts +++ b/pg-migrator/src/models/identityAccessToken.ts @@ -1,104 +1,100 @@ import { Document, Schema, Types, model } from "mongoose"; import { IIdentityTrustedIp } from "./identity"; -import { IPType } from "../ee/models/trustedIp"; export interface IIdentityAccessToken extends Document { - _id: Types.ObjectId; - identity: Types.ObjectId; - identityUniversalAuthClientSecret?: Types.ObjectId; - accessTokenLastUsedAt?: Date; - accessTokenLastRenewedAt?: Date; - accessTokenNumUses: number; - accessTokenNumUsesLimit: number; - accessTokenTTL: number; - accessTokenMaxTTL: number; - accessTokenTrustedIps: Array; - isAccessTokenRevoked: boolean; - updatedAt: Date; - createdAt: Date; + _id: Types.ObjectId; + identity: Types.ObjectId; + identityUniversalAuthClientSecret?: Types.ObjectId; + accessTokenLastUsedAt?: Date; + accessTokenLastRenewedAt?: Date; + accessTokenNumUses: number; + accessTokenNumUsesLimit: number; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenTrustedIps: Array; + isAccessTokenRevoked: boolean; + updatedAt: Date; + createdAt: Date; } const identityAccessTokenSchema = new Schema( - { - identity: { - type: Schema.Types.ObjectId, - ref: "Identity", - required: false - }, - identityUniversalAuthClientSecret: { - type: Schema.Types.ObjectId, - ref: "IdentityUniversalAuthClientSecret", - required: false - }, - accessTokenLastUsedAt: { - type: Date, - required: false - }, - accessTokenLastRenewedAt: { - type: Date, - required: false - }, - accessTokenNumUses: { - // number of times access token has been used - type: Number, - default: 0, - required: true - }, - accessTokenNumUsesLimit: { - // number of times access token can be used for - type: Number, - default: 0, // default: used as many times as needed - required: true - }, - accessTokenTTL: { // seconds - // incremental lifetime - type: Number, - default: 2592000, // 30 days - required: true - }, - accessTokenMaxTTL: { // seconds - // max lifetime - type: Number, - default: 2592000, // 30 days - required: true - }, - accessTokenTrustedIps: { - type: [ - { - ipAddress: { - type: String, - required: true - }, - type: { - type: String, - enum: [ - IPType.IPV4, - IPType.IPV6 - ], - required: true - }, - prefix: { - type: Number, - required: false - } - } - ], - default: [{ - ipAddress: "0.0.0.0", - type: IPType.IPV4.toString(), - prefix: 0 - }], - required: true - }, - isAccessTokenRevoked: { - type: Boolean, - default: false, - required: true - }, + { + identity: { + type: Schema.Types.ObjectId, + ref: "Identity", + required: false, }, - { - timestamps: true - } + identityUniversalAuthClientSecret: { + type: Schema.Types.ObjectId, + ref: "IdentityUniversalAuthClientSecret", + required: false, + }, + accessTokenLastUsedAt: { + type: Date, + required: false, + }, + accessTokenLastRenewedAt: { + type: Date, + required: false, + }, + accessTokenNumUses: { + // number of times access token has been used + type: Number, + default: 0, + required: true, + }, + accessTokenNumUsesLimit: { + // number of times access token can be used for + type: Number, + default: 0, // default: used as many times as needed + required: true, + }, + accessTokenTTL: { + // seconds + // incremental lifetime + type: Number, + default: 2592000, // 30 days + required: true, + }, + accessTokenMaxTTL: { + // seconds + // max lifetime + type: Number, + default: 2592000, // 30 days + required: true, + }, + accessTokenTrustedIps: { + type: [ + { + ipAddress: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + prefix: { + type: Number, + required: false, + }, + }, + ], + default: [], + required: true, + }, + isAccessTokenRevoked: { + type: Boolean, + default: false, + required: true, + }, + }, + { + timestamps: true, + }, ); -export const IdentityAccessToken = model("IdentityAccessToken", identityAccessTokenSchema); +export const IdentityAccessToken = model( + "IdentityAccessToken", + identityAccessTokenSchema, +); diff --git a/pg-migrator/src/models/identityMembership.ts b/pg-migrator/src/models/identityMembership.ts index 4fedfe909..4c4a12eba 100644 --- a/pg-migrator/src/models/identityMembership.ts +++ b/pg-migrator/src/models/identityMembership.ts @@ -1,5 +1,4 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables"; export interface IIdentityMembership { _id: Types.ObjectId; @@ -13,7 +12,7 @@ const identityMembershipSchema = new Schema( { identity: { type: Schema.Types.ObjectId, - ref: "Identity" + ref: "Identity", }, workspace: { type: Schema.Types.ObjectId, @@ -23,17 +22,19 @@ const identityMembershipSchema = new Schema( }, role: { type: String, - enum: [ADMIN, MEMBER, VIEWER, CUSTOM, NO_ACCESS], - required: true + required: true, }, customRole: { type: Schema.Types.ObjectId, - ref: "Role" - } + ref: "Role", + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const IdentityMembership = model("IdentityMembership", identityMembershipSchema); \ No newline at end of file +export const IdentityMembership = model( + "IdentityMembership", + identityMembershipSchema, +); diff --git a/pg-migrator/src/models/identityMembershipOrg.ts b/pg-migrator/src/models/identityMembershipOrg.ts index 8da8693c4..87699467f 100644 --- a/pg-migrator/src/models/identityMembershipOrg.ts +++ b/pg-migrator/src/models/identityMembershipOrg.ts @@ -1,5 +1,4 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables"; export interface IIdentityMembershipOrg { _id: Types.ObjectId; @@ -13,25 +12,27 @@ const identityMembershipOrgSchema = new Schema( { identity: { type: Schema.Types.ObjectId, - ref: "Identity" + ref: "Identity", }, organization: { type: Schema.Types.ObjectId, - ref: "Organization" + ref: "Organization", }, role: { type: String, - enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM], - required: true + required: true, }, customRole: { type: Schema.Types.ObjectId, - ref: "Role" - } + ref: "Role", + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const IdentityMembershipOrg = model("IdentityMembershipOrg", identityMembershipOrgSchema); \ No newline at end of file +export const IdentityMembershipOrg = model( + "IdentityMembershipOrg", + identityMembershipOrgSchema, +); diff --git a/pg-migrator/src/models/identityUniversalAuth.ts b/pg-migrator/src/models/identityUniversalAuth.ts index 89fb46a95..18b14775a 100644 --- a/pg-migrator/src/models/identityUniversalAuth.ts +++ b/pg-migrator/src/models/identityUniversalAuth.ts @@ -1,107 +1,99 @@ import { Document, Schema, Types, model } from "mongoose"; -import { IPType } from "../ee/models"; -import { IIdentityTrustedIp } from "./identity"; export interface IIdentityUniversalAuth extends Document { - _id: Types.ObjectId; - identity: Types.ObjectId; - clientId: string; - clientSecretTrustedIps: Array; - accessTokenTTL: number; - accessTokenMaxTTL: number; - accessTokenNumUsesLimit: number; - accessTokenTrustedIps: Array; + _id: Types.ObjectId; + identity: Types.ObjectId; + clientId: string; + clientSecretTrustedIps: Array<{}>; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array<{}>; } const identityUniversalAuthSchema = new Schema( - { - identity: { - type: Schema.Types.ObjectId, - ref: "Identity", - required: true - }, - clientId: { - type: String, - required: true - }, - clientSecretTrustedIps: { - type: [ - { - ipAddress: { - type: String, - required: true - }, - type: { - type: String, - enum: [ - IPType.IPV4, - IPType.IPV6 - ], - required: true - }, - prefix: { - type: Number, - required: false - } - } - ], - default: [{ - ipAddress: "0.0.0.0", - type: IPType.IPV4.toString(), - prefix: 0 - }], - required: true - }, - accessTokenTTL: { // seconds - // incremental lifetime - type: Number, - default: 7200, - required: true - }, - accessTokenMaxTTL: { // seconds - // max lifetime - type: Number, - default: 7200, - required: true - }, - accessTokenNumUsesLimit: { - // number of times access token can be used for - type: Number, - default: 0, // default: used as many times as needed - required: true - }, - accessTokenTrustedIps: { - type: [ - { - ipAddress: { - type: String, - required: true - }, - type: { - type: String, - enum: [ - IPType.IPV4, - IPType.IPV6 - ], - required: true - }, - prefix: { - type: Number, - required: false - } - } - ], - default: [{ - ipAddress: "0.0.0.0", - type: IPType.IPV4.toString(), - prefix: 0 - }], - required: true - } + { + identity: { + type: Schema.Types.ObjectId, + ref: "Identity", + required: true, }, - { - timestamps: true - } + clientId: { + type: String, + required: true, + }, + clientSecretTrustedIps: { + type: [ + { + ipAddress: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + prefix: { + type: Number, + required: false, + }, + }, + ], + default: [ + { + ipAddress: "0.0.0.0", + prefix: 0, + }, + ], + required: true, + }, + accessTokenTTL: { + // seconds + // incremental lifetime + type: Number, + default: 7200, + required: true, + }, + accessTokenMaxTTL: { + // seconds + // max lifetime + type: Number, + default: 7200, + required: true, + }, + accessTokenNumUsesLimit: { + // number of times access token can be used for + type: Number, + default: 0, // default: used as many times as needed + required: true, + }, + accessTokenTrustedIps: { + type: [ + { + ipAddress: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + prefix: { + type: Number, + required: false, + }, + }, + ], + default: [], + required: true, + }, + }, + { + timestamps: true, + }, ); -export const IdentityUniversalAuth = model("IdentityUniversalAuth", identityUniversalAuthSchema); \ No newline at end of file +export const IdentityUniversalAuth = model( + "IdentityUniversalAuth", + identityUniversalAuthSchema, +); diff --git a/pg-migrator/src/models/integration/integration.ts b/pg-migrator/src/models/integration/integration.ts index eaaadec92..40578138d 100644 --- a/pg-migrator/src/models/integration/integration.ts +++ b/pg-migrator/src/models/integration/integration.ts @@ -1,35 +1,3 @@ -import { - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_BITBUCKET, - INTEGRATION_CHECKLY, - INTEGRATION_CIRCLECI, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_WORKERS, - INTEGRATION_CLOUD_66, - INTEGRATION_CODEFRESH, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_FLYIO, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_HASURA_CLOUD, - INTEGRATION_HEROKU, - INTEGRATION_LARAVELFORGE, - INTEGRATION_NETLIFY, - INTEGRATION_NORTHFLANK, - INTEGRATION_QOVERY, - INTEGRATION_RAILWAY, - INTEGRATION_RENDER, - INTEGRATION_SUPABASE, - INTEGRATION_TEAMCITY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, - INTEGRATION_WINDMILL -} from "../../variables"; import { Schema, Types, model } from "mongoose"; import { Metadata } from "./types"; @@ -90,127 +58,98 @@ const integrationSchema = new Schema( workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, environment: { type: String, - required: true + required: true, }, isActive: { type: Boolean, - required: true + required: true, }, url: { // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) type: String, - default: null + default: null, }, app: { // name of app in provider type: String, - default: null + default: null, }, appId: { // id of app in provider type: String, - default: null + default: null, }, targetEnvironment: { // target environment type: String, - default: null + default: null, }, targetEnvironmentId: { type: String, - default: null + default: null, }, targetService: { // railway-specific service // qovery-specific project type: String, - default: null + default: null, }, targetServiceId: { // railway-specific service // qovery specific project type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login type: String, - default: null + default: null, }, path: { // aws-parameter-store-specific path // (also) vercel preview-branch type: String, - default: null + default: null, }, region: { // aws-parameter-store-specific path type: String, - default: null + default: null, }, scope: { // qovery-specific scope type: String, - default: null + default: null, }, integration: { type: String, - enum: [ - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_QOVERY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_TEAMCITY, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_WORKERS, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_HASURA_CLOUD - ], - required: true + required: true, }, integrationAuth: { type: Schema.Types.ObjectId, ref: "IntegrationAuth", - required: true + required: true, }, secretPath: { type: String, required: true, - default: "/" + default: "/", }, metadata: { type: Schema.Types.Mixed, - default: {} - } + default: {}, + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const Integration = model("Integration", integrationSchema); +export const Integration = model( + "Integration", + integrationSchema, +); diff --git a/pg-migrator/src/models/integrationAuth/integrationAuth.ts b/pg-migrator/src/models/integrationAuth/integrationAuth.ts index da1e57268..1c2e6d450 100644 --- a/pg-migrator/src/models/integrationAuth/integrationAuth.ts +++ b/pg-migrator/src/models/integrationAuth/integrationAuth.ts @@ -1,36 +1,3 @@ -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_BITBUCKET, - INTEGRATION_CIRCLECI, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_WORKERS, - INTEGRATION_CLOUD_66, - INTEGRATION_CODEFRESH, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_FLYIO, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_HASURA_CLOUD, - INTEGRATION_HEROKU, - INTEGRATION_LARAVELFORGE, - INTEGRATION_NETLIFY, - INTEGRATION_NORTHFLANK, - INTEGRATION_RAILWAY, - INTEGRATION_RENDER, - INTEGRATION_SUPABASE, - INTEGRATION_TEAMCITY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, - INTEGRATION_WINDMILL -} from "../../variables"; import { Document, Schema, Types, model } from "mongoose"; import { IntegrationAuthMetadata } from "./types"; @@ -91,116 +58,87 @@ const integrationAuthSchema = new Schema( workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, integration: { type: String, - enum: [ - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_WORKERS, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_HASURA_CLOUD - ], - required: true + required: true, }, teamId: { // vercel-specific integration param - type: String + type: String, }, url: { // for any self-hosted integrations (e.g. self-hosted hashicorp-vault) - type: String + type: String, }, namespace: { // hashicorp-vault-specific integration param - type: String + type: String, }, accountId: { // netlify-specific integration param - type: String + type: String, }, refreshCiphertext: { type: String, - select: false + }, refreshIV: { type: String, - select: false + }, refreshTag: { type: String, - select: false + }, accessIdCiphertext: { type: String, - select: false + }, accessIdIV: { type: String, - select: false + }, accessIdTag: { type: String, - select: false + }, accessCiphertext: { type: String, - select: false + }, accessIV: { type: String, - select: false + }, accessTag: { type: String, - select: false + }, accessExpiresAt: { type: Date, - select: false + }, algorithm: { // the encryption algorithm used type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true + required: true, }, keyEncoding: { type: String, - enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], - required: true + required: true, }, metadata: { - type: Schema.Types.Mixed - } + type: Schema.Types.Mixed, + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const IntegrationAuth = model("IntegrationAuth", integrationAuthSchema); +export const IntegrationAuth = model( + "IntegrationAuth", + integrationAuthSchema, +); diff --git a/pg-migrator/src/models/membership.ts b/pg-migrator/src/models/membership.ts index c09fa2779..eb2787082 100644 --- a/pg-migrator/src/models/membership.ts +++ b/pg-migrator/src/models/membership.ts @@ -1,5 +1,4 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables"; export interface IMembershipPermission { environmentSlug: string; @@ -20,15 +19,15 @@ const membershipSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: "User" + ref: "User", }, inviteEmail: { - type: String + type: String, }, workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, deniedPermissions: { type: [ @@ -36,25 +35,24 @@ const membershipSchema = new Schema( environmentSlug: String, ability: { type: String, - enum: ["read", "write"] - } - } + enum: ["read", "write"], + }, + }, ], - default: [] + default: [], }, role: { type: String, - enum: [ADMIN, MEMBER, VIEWER, NO_ACCESS, CUSTOM], - required: true + required: true, }, customRole: { type: Schema.Types.ObjectId, - ref: "Role" - } + ref: "Role", + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const Membership = model("Membership", membershipSchema); \ No newline at end of file +export const Membership = model("Membership", membershipSchema); diff --git a/pg-migrator/src/models/membershipOrg.ts b/pg-migrator/src/models/membershipOrg.ts index 0d4a2f6b7..2f02fc28a 100644 --- a/pg-migrator/src/models/membershipOrg.ts +++ b/pg-migrator/src/models/membershipOrg.ts @@ -1,5 +1,4 @@ import { Document, Schema, Types, model } from "mongoose"; -import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, NO_ACCESS } from "../variables"; export interface IMembershipOrg extends Document { _id: Types.ObjectId; @@ -9,39 +8,42 @@ export interface IMembershipOrg extends Document { role: "admin" | "member" | "no-access" | "custom"; customRole: Types.ObjectId; status: "invited" | "accepted"; + createdAt: string; + updatedAt: string; } const membershipOrgSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: "User" + ref: "User", }, inviteEmail: { - type: String + type: String, }, organization: { type: Schema.Types.ObjectId, - ref: "Organization" + ref: "Organization", }, role: { type: String, - enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM], - required: true + required: true, }, status: { type: String, - enum: [INVITED, ACCEPTED], - required: true + required: true, }, customRole: { type: Schema.Types.ObjectId, - ref: "Role" - } + ref: "Role", + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const MembershipOrg = model("MembershipOrg", membershipOrgSchema); +export const MembershipOrg = model( + "MembershipOrg", + membershipOrgSchema, +); diff --git a/pg-migrator/src/models/organization.ts b/pg-migrator/src/models/organization.ts index 1ae3bcb45..31a27411b 100644 --- a/pg-migrator/src/models/organization.ts +++ b/pg-migrator/src/models/organization.ts @@ -1,24 +1,29 @@ import { Schema, Types, model } from "mongoose"; export interface IOrganization { - _id: Types.ObjectId; - name: string; - customerId?: string; + _id: Types.ObjectId; + name: string; + customerId?: string; + createdAt: string; + updatedAt: string; } const organizationSchema = new Schema( - { - name: { - type: String, - required: true, - }, - customerId: { - type: String, - }, - }, - { - timestamps: true, - } + { + name: { + type: String, + required: true, + }, + customerId: { + type: String, + }, + }, + { + timestamps: true, + }, ); -export const Organization = model("Organization", organizationSchema); \ No newline at end of file +export const Organization = model( + "Organization", + organizationSchema, +); diff --git a/pg-migrator/src/models/role.ts b/pg-migrator/src/models/role.ts index d3de1d3ae..536363312 100644 --- a/pg-migrator/src/models/role.ts +++ b/pg-migrator/src/models/role.ts @@ -9,45 +9,47 @@ export interface IRole { workspace: Types.ObjectId; organization: Types.ObjectId; isOrgRole: boolean; + createdAt: string; + updatedAt: string; } const roleSchema = new Schema( { name: { type: String, - required: true + required: true, }, organization: { type: Schema.Types.ObjectId, ref: "Organization", - required: true + required: true, }, workspace: { type: Schema.Types.ObjectId, - ref: "Workspace" + ref: "Workspace", }, isOrgRole: { type: Boolean, required: true, - select: false + }, description: { - type: String + type: String, }, slug: { type: String, - required: true + required: true, }, permissions: { type: Array, - required: true - } + required: true, + }, }, { - timestamps: true - } + timestamps: true, + }, ); roleSchema.index({ organization: 1, workspace: 1 }); -export const Role = model("Role", roleSchema); \ No newline at end of file +export const Role = model("Role", roleSchema); diff --git a/pg-migrator/src/models/secret.ts b/pg-migrator/src/models/secret.ts index 4c1400fa8..1dbb6a84e 100644 --- a/pg-migrator/src/models/secret.ts +++ b/pg-migrator/src/models/secret.ts @@ -1,11 +1,4 @@ import { Schema, Types, model } from "mongoose"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, - SECRET_PERSONAL, - SECRET_SHARED -} from "../variables"; export interface ISecret { _id: Types.ObjectId; @@ -48,123 +41,118 @@ const secretSchema = new Schema( version: { type: Number, required: true, - default: 1 + default: 1, }, workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, type: { type: String, - enum: [SECRET_SHARED, SECRET_PERSONAL], - required: true + required: true, }, user: { // user associated with the personal secret type: Schema.Types.ObjectId, - ref: "User" + ref: "User", }, tags: { ref: "Tag", type: [Schema.Types.ObjectId], - default: [] + default: [], }, environment: { type: String, - required: true + required: true, }, secretBlindIndex: { type: String, - select: false + }, secretKeyCiphertext: { type: String, - required: true + required: true, }, secretKeyIV: { type: String, // symmetric - required: true + required: true, }, secretKeyTag: { type: String, // symmetric - required: true + required: true, }, secretKeyHash: { - type: String + type: String, }, secretValueCiphertext: { type: String, - required: true + required: true, }, secretValueIV: { type: String, // symmetric - required: true + required: true, }, secretValueTag: { type: String, // symmetric - required: true + required: true, }, secretValueHash: { - type: String + type: String, }, secretCommentCiphertext: { type: String, - required: false + required: false, }, secretCommentIV: { type: String, // symmetric - required: false + required: false, }, secretCommentTag: { type: String, // symmetric - required: false + required: false, }, secretCommentHash: { type: String, - required: false + required: false, }, secretReminderRepeatDays: { type: Number, required: false, - default: null + default: null, }, secretReminderNote: { type: String, required: false, - default: null + default: null, }, skipMultilineEncoding: { type: Boolean, - required: false + required: false, }, algorithm: { // the encryption algorithm used type: String, - enum: [ALGORITHM_AES_256_GCM], required: true, - default: ALGORITHM_AES_256_GCM }, keyEncoding: { type: String, - enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], required: true, - default: ENCODING_SCHEME_UTF8 }, folder: { type: String, - default: "root" + default: "root", }, metadata: { - type: Schema.Types.Mixed - } + type: Schema.Types.Mixed, + }, }, { - timestamps: true - } + timestamps: true, + }, ); secretSchema.index({ tags: 1 }, { background: true }); diff --git a/pg-migrator/src/models/secretApprovalRequest.ts b/pg-migrator/src/models/secretApprovalRequest.ts index 24e8af39a..983163d1a 100644 --- a/pg-migrator/src/models/secretApprovalRequest.ts +++ b/pg-migrator/src/models/secretApprovalRequest.ts @@ -1,26 +1,17 @@ import { Schema, Types, model } from "mongoose"; -import { customAlphabet } from "nanoid"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8 -} from "../../variables"; export enum ApprovalStatus { PENDING = "pending", APPROVED = "approved", - REJECTED = "rejected" + REJECTED = "rejected", } export enum CommitType { DELETE = "delete", UPDATE = "update", - CREATE = "create" + CREATE = "create", } -const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; -const nanoId = customAlphabet(SLUG_ALPHABETS, 10); - export interface ISecretApprovalSecChange { _id: Types.ObjectId; version: number; @@ -50,7 +41,9 @@ export type ISecretCommits = Array< // on merge secretVersion: J; secret: T; - newVersion: Partial> & { _id: Types.ObjectId }; + newVersion: Partial> & { + _id: Types.ObjectId; + }; op: CommitType.UPDATE; } | { @@ -82,58 +75,54 @@ const secretApprovalSecretChangeSchema = new Schema({ version: { type: Number, default: 1, - required: true + required: true, }, secretBlindIndex: { type: String, - select: false + }, secretKeyCiphertext: { type: String, - required: true + required: true, }, secretKeyIV: { type: String, // symmetric - required: true + required: true, }, secretKeyTag: { type: String, // symmetric - required: true + required: true, }, secretValueCiphertext: { type: String, - required: true + required: true, }, secretValueIV: { type: String, // symmetric - required: true + required: true, }, secretValueTag: { type: String, // symmetric - required: true + required: true, }, skipMultilineEncoding: { type: Boolean, - required: false + required: false, }, algorithm: { // the encryption algorithm used type: String, - enum: [ALGORITHM_AES_256_GCM], required: true, - default: ALGORITHM_AES_256_GCM }, keyEncoding: { type: String, - enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], required: true, - default: ENCODING_SCHEME_UTF8 }, tags: { ref: "Tag", type: [Schema.Types.ObjectId], - default: [] - } + default: [], + }, }); const secretApprovalRequestSchema = new Schema( @@ -141,20 +130,19 @@ const secretApprovalRequestSchema = new Schema( workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, environment: { type: String, - required: true + required: true, }, folderId: { type: String, required: true, - default: "root" + default: "root", }, slug: { type: String, - default: () => nanoId() }, reviewers: { type: [ @@ -162,12 +150,16 @@ const secretApprovalRequestSchema = new Schema( member: { // user associated with the personal secret type: Schema.Types.ObjectId, - ref: "Membership" + ref: "Membership", }, - status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING } - } + status: { + type: String, + enum: ApprovalStatus, + default: ApprovalStatus.PENDING, + }, + }, ], - default: [] + default: [], }, policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" }, hasMerged: { type: Boolean, default: false }, @@ -179,25 +171,25 @@ const secretApprovalRequestSchema = new Schema( secret: { type: Types.ObjectId, ref: "Secret" }, newVersion: secretApprovalSecretChangeSchema, secretVersion: { type: Types.ObjectId, ref: "SecretVersion" }, - op: { type: String, enum: [CommitType], required: true } - } + op: { type: String, enum: [CommitType], required: true }, + }, ], conflicts: { type: [ { secretId: { type: String, required: true }, - op: { type: String, enum: [CommitType], required: true } - } + op: { type: String, enum: [CommitType], required: true }, + }, ], - default: [] - } + default: [], + }, }, { - timestamps: true - } + timestamps: true, + }, ); export const SecretApprovalRequest = model( "SecretApprovalRequest", - secretApprovalRequestSchema + secretApprovalRequestSchema, ); diff --git a/pg-migrator/src/models/secretBlindIndexData.ts b/pg-migrator/src/models/secretBlindIndexData.ts index da397d2c1..e1eea4c3b 100644 --- a/pg-migrator/src/models/secretBlindIndexData.ts +++ b/pg-migrator/src/models/secretBlindIndexData.ts @@ -1,58 +1,49 @@ import { Document, Schema, Types, model } from "mongoose"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, -} from "../variables"; export interface ISecretBlindIndexData extends Document { - _id: Types.ObjectId; - workspace: Types.ObjectId; - encryptedSaltCiphertext: string; - saltIV: string; - saltTag: string; - algorithm: "aes-256-gcm"; - keyEncoding: "base64" | "utf8" + _id: Types.ObjectId; + workspace: Types.ObjectId; + encryptedSaltCiphertext: string; + saltIV: string; + saltTag: string; + algorithm: "aes-256-gcm"; + keyEncoding: "base64" | "utf8"; } -const secretBlindIndexDataSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: "Workspace", - required: true, - }, - encryptedSaltCiphertext: { // TODO: make these select: false - type: String, - required: true, - }, - saltIV: { - type: String, - required: true, - }, - saltTag: { - type: String, - required: true, - }, - algorithm: { - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true, - select: false, - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true, - select: false, - }, - - } -); +const secretBlindIndexDataSchema = new Schema({ + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + encryptedSaltCiphertext: { + // TODO: make these + type: String, + required: true, + }, + saltIV: { + type: String, + required: true, + }, + saltTag: { + type: String, + required: true, + }, + algorithm: { + type: String, + required: true, + + }, + keyEncoding: { + type: String, + required: true, + + }, +}); secretBlindIndexDataSchema.index({ workspace: 1 }); -export const SecretBlindIndexData = model("SecretBlindIndexData", secretBlindIndexDataSchema); \ No newline at end of file +export const SecretBlindIndexData = model( + "SecretBlindIndexData", + secretBlindIndexDataSchema, +); diff --git a/pg-migrator/src/models/secretRotation.ts b/pg-migrator/src/models/secretRotation.ts new file mode 100644 index 000000000..238cef536 --- /dev/null +++ b/pg-migrator/src/models/secretRotation.ts @@ -0,0 +1,106 @@ +import { Document, Schema, model, Types } from "mongoose"; + +export interface ISecretRotation extends Document { + _id: Types.ObjectId; + name: string; + interval: number; + provider: string; + customProvider: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + secretPath: string; + outputs: Array<{ + key: string; + secret: Types.ObjectId; + }>; + status?: "success" | "failed"; + lastRotatedAt?: string; + statusMessage?: string; + encryptedData: string; + encryptedDataIV: string; + encryptedDataTag: string; + algorithm: string; + keyEncoding: string; +} +const secretRotationSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + }, + provider: { + type: String, + required: true, + }, + customProvider: { + type: Schema.Types.ObjectId, + ref: "SecretRotationProvider", + }, + environment: { + type: String, + required: true, + }, + secretPath: { + type: String, + required: true, + }, + interval: { + type: Number, + required: true, + }, + lastRotatedAt: { + type: String, + }, + status: { + type: String, + enum: ["success", "failed"], + }, + statusMessage: { + type: String, + }, + // encrypted data on input keys and secrets got + encryptedData: { + type: String, + select: false, + }, + encryptedDataIV: { + type: String, + select: false, + }, + encryptedDataTag: { + type: String, + select: false, + }, + algorithm: { + // the encryption algorithm used + type: String, + required: true, + select: false, + }, + keyEncoding: { + type: String, + required: true, + select: false, + }, + outputs: [ + { + key: { + type: String, + required: true, + }, + secret: { + type: Schema.Types.ObjectId, + ref: "Secret", + }, + }, + ], + }, + { + timestamps: true, + }, +); + +export const SecretRotation = model( + "SecretRotation", + secretRotationSchema, +); diff --git a/pg-migrator/src/models/secretSnapshot.ts b/pg-migrator/src/models/secretSnapshot.ts index 71d1b27e6..132329f4a 100644 --- a/pg-migrator/src/models/secretSnapshot.ts +++ b/pg-migrator/src/models/secretSnapshot.ts @@ -1,6 +1,8 @@ import { Schema, Types, model } from "mongoose"; export interface ISecretSnapshot { + id: string; + _id: string; workspace: Types.ObjectId; environment: string; folderId: string | "root"; @@ -43,10 +45,10 @@ const secretSnapshotSchema = new Schema( }, { timestamps: true, - } + }, ); export const SecretSnapshot = model( "SecretSnapshot", - secretSnapshotSchema -); \ No newline at end of file + secretSnapshotSchema, +); diff --git a/pg-migrator/src/models/secretVersion.ts b/pg-migrator/src/models/secretVersion.ts index 11ffa79ab..6922216d1 100644 --- a/pg-migrator/src/models/secretVersion.ts +++ b/pg-migrator/src/models/secretVersion.ts @@ -1,11 +1,4 @@ import { Schema, Types, model } from "mongoose"; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, - SECRET_PERSONAL, - SECRET_SHARED -} from "../../variables"; export interface ISecretVersion { _id: Types.ObjectId; @@ -37,96 +30,94 @@ const secretVersionSchema = new Schema( // could be deleted type: Schema.Types.ObjectId, ref: "Secret", - required: true + required: true, }, version: { type: Number, default: 1, - required: true + required: true, }, workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, type: { type: String, - enum: [SECRET_SHARED, SECRET_PERSONAL], - required: true + required: true, }, user: { // user associated with the personal secret type: Schema.Types.ObjectId, - ref: "User" + ref: "User", }, environment: { type: String, - required: true + required: true, }, isDeleted: { // consider removing field type: Boolean, default: false, - required: true + required: true, }, secretBlindIndex: { type: String, - select: false + }, secretKeyCiphertext: { type: String, - required: true + required: true, }, secretKeyIV: { type: String, // symmetric - required: true + required: true, }, secretKeyTag: { type: String, // symmetric - required: true + required: true, }, secretValueCiphertext: { type: String, - required: true + required: true, }, secretValueIV: { type: String, // symmetric - required: true + required: true, }, secretValueTag: { type: String, // symmetric - required: true + required: true, }, skipMultilineEncoding: { type: Boolean, - required: false + required: false, }, algorithm: { // the encryption algorithm used type: String, - enum: [ALGORITHM_AES_256_GCM], required: true, - default: ALGORITHM_AES_256_GCM }, keyEncoding: { type: String, - enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], required: true, - default: ENCODING_SCHEME_UTF8 }, folder: { type: String, - required: true + required: true, }, tags: { ref: "Tag", type: [Schema.Types.ObjectId], - default: [] - } + default: [], + }, }, { - timestamps: true - } + timestamps: true, + }, ); -export const SecretVersion = model("SecretVersion", secretVersionSchema); +export const SecretVersion = model( + "SecretVersion", + secretVersionSchema, +); diff --git a/pg-migrator/src/models/serviceTokenData.ts b/pg-migrator/src/models/serviceTokenData.ts index 735131703..a8357d689 100644 --- a/pg-migrator/src/models/serviceTokenData.ts +++ b/pg-migrator/src/models/serviceTokenData.ts @@ -65,19 +65,19 @@ const serviceTokenDataSchema = new Schema( secretHash: { type: String, required: true, - select: false + }, encryptedKey: { type: String, - select: false + }, iv: { type: String, - select: false + }, tag: { type: String, - select: false + }, permissions: { type: [String], diff --git a/pg-migrator/src/models/tokenData.ts b/pg-migrator/src/models/tokenData.ts index 2544c05f1..80c6016c1 100644 --- a/pg-migrator/src/models/tokenData.ts +++ b/pg-migrator/src/models/tokenData.ts @@ -35,7 +35,7 @@ const tokenDataSchema = new Schema({ }, tokenHash: { type: String, - select: false, + required: true, }, triesLeft: { diff --git a/pg-migrator/src/models/user.ts b/pg-migrator/src/models/user.ts index a3d73b8a2..08ce2bb46 100644 --- a/pg-migrator/src/models/user.ts +++ b/pg-migrator/src/models/user.ts @@ -7,7 +7,7 @@ export enum AuthMethod { GITLAB = "gitlab", OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", - JUMPCLOUD_SAML = "jumpcloud-saml" + JUMPCLOUD_SAML = "jumpcloud-saml", } export interface IUser extends Document { @@ -34,6 +34,8 @@ export interface IUser extends Document { ip: string; userAgent: string; }[]; + createdAt: string; + updatedAt: string; } const userSchema = new Schema( @@ -41,101 +43,101 @@ const userSchema = new Schema( authProvider: { // TODO field: deprecate type: String, - enum: AuthMethod + enum: AuthMethod, }, authMethods: { type: [ { type: String, - enum: AuthMethod - } + enum: AuthMethod, + }, ], default: [AuthMethod.EMAIL], - required: true + required: true, }, email: { type: String, required: true, - unique: true + unique: true, }, firstName: { - type: String + type: String, }, lastName: { - type: String + type: String, }, encryptionVersion: { type: Number, - select: false, - default: 1 // to resolve backward-compatibility issues + + default: 1, // to resolve backward-compatibility issues }, protectedKey: { // introduced as part of encryption version 2 type: String, - select: false + }, protectedKeyIV: { // introduced as part of encryption version 2 type: String, - select: false + }, protectedKeyTag: { // introduced as part of encryption version 2 type: String, - select: false + }, publicKey: { type: String, - select: false + }, encryptedPrivateKey: { type: String, - select: false + }, superAdmin: { - type: Boolean + type: Boolean, }, iv: { // iv of [encryptedPrivateKey] type: String, - select: false + }, tag: { // tag of [encryptedPrivateKey] type: String, - select: false + }, salt: { type: String, - select: false + }, verifier: { type: String, - select: false + }, isMfaEnabled: { type: Boolean, - default: false + default: false, }, mfaMethods: [ { - type: String - } + type: String, + }, ], devices: { type: [ { ip: String, - userAgent: String - } + userAgent: String, + }, ], default: [], - select: false - } + + }, }, { - timestamps: true - } + timestamps: true, + }, ); export const User = model("User", userSchema); diff --git a/pg-migrator/src/models/webhooks.ts b/pg-migrator/src/models/webhooks.ts index bef5e795a..db3eadf1e 100644 --- a/pg-migrator/src/models/webhooks.ts +++ b/pg-migrator/src/models/webhooks.ts @@ -1,5 +1,4 @@ import { Document, Schema, Types, model } from "mongoose"; -import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables"; export interface IWebhook extends Document { _id: Types.ObjectId; @@ -22,60 +21,58 @@ const WebhookSchema = new Schema( workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true + required: true, }, environment: { type: String, - required: true + required: true, }, secretPath: { type: String, required: true, - default: "/" + default: "/", }, url: { type: String, - required: true + required: true, }, lastStatus: { type: String, - enum: ["success", "failed"] + enum: ["success", "failed"], }, lastRunErrorMessage: { - type: String + type: String, }, isDisabled: { type: Boolean, - default: false + default: false, }, // used for webhook signature encryptedSecretKey: { type: String, - select: false + }, iv: { type: String, - select: false + }, tag: { type: String, - select: false + }, algorithm: { // the encryption algorithm used type: String, - enum: [ALGORITHM_AES_256_GCM], - select: false + }, keyEncoding: { type: String, - enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], - select: false - } + + }, }, { - timestamps: true - } + timestamps: true, + }, ); export const Webhook = model("Webhook", WebhookSchema); diff --git a/pg-migrator/src/rollback.ts b/pg-migrator/src/rollback.ts new file mode 100644 index 000000000..8d12bbddc --- /dev/null +++ b/pg-migrator/src/rollback.ts @@ -0,0 +1,29 @@ +import dotenv from "dotenv"; +import knex from "knex"; +import { Level } from "level"; +import path from "path"; + +const main = async () => { + dotenv.config(); + let postgres_url = process.env.POSTGRES_DB_URL; + + console.log("Checking postgres connection..."); + const db = knex({ + client: "pg", + connection: postgres_url, + migrations: { + directory: path.join(__dirname, "./migrations"), + extension: "ts", + tableName: "infisical_migrations", + }, + }); + console.log("Good to go with postgres"); + const kdb = new Level("./db", { valueEncoding: "json" }); + console.log("Starting rolling back to latest"); + await db.migrate.rollback({}, true); + console.log("Rolling back completed"); + kdb.clear(); + process.exit(0); +}; + +main(); diff --git a/pg-migrator/src/schemas/identity-access-tokens.ts b/pg-migrator/src/schemas/identity-access-tokens.ts index 1d8405427..a7a685cf9 100644 --- a/pg-migrator/src/schemas/identity-access-tokens.ts +++ b/pg-migrator/src/schemas/identity-access-tokens.ts @@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models"; export const IdentityAccessTokensSchema = z.object({ id: z.string().uuid(), - authType: z.string(), accessTokenTTL: z.number().default(2592000), accessTokenMaxTTL: z.number().default(2592000), accessTokenNumUses: z.number().default(0), @@ -24,5 +23,10 @@ export const IdentityAccessTokensSchema = z.object({ }); export type TIdentityAccessTokens = z.infer; -export type TIdentityAccessTokensInsert = Omit; -export type TIdentityAccessTokensUpdate = Partial>; +export type TIdentityAccessTokensInsert = Omit< + TIdentityAccessTokens, + TImmutableDBKeys +>; +export type TIdentityAccessTokensUpdate = Partial< + Omit +>; diff --git a/pg-migrator/src/schemas/secret-versions.ts b/pg-migrator/src/schemas/secret-versions.ts index 3a04a8cd9..aff51bd83 100644 --- a/pg-migrator/src/schemas/secret-versions.ts +++ b/pg-migrator/src/schemas/secret-versions.ts @@ -10,8 +10,8 @@ import { TImmutableDBKeys } from "./models"; export const SecretVersionsSchema = z.object({ id: z.string().uuid(), version: z.number().default(1), - type: z.string().default('shared'), - secretBlindIndex: z.string(), + type: z.string().default("shared"), + secretBlindIndex: z.string().nullable().optional(), secretKeyCiphertext: z.string(), secretKeyIV: z.string(), secretKeyTag: z.string(), @@ -24,8 +24,8 @@ export const SecretVersionsSchema = z.object({ secretReminderNote: z.string().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(), - algorithm: z.string().default('aes-256-gcm'), - keyEncoding: z.string().default('utf8'), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), metadata: z.unknown().nullable().optional(), envId: z.string().uuid().nullable().optional(), secretId: z.string().uuid(), @@ -37,4 +37,6 @@ export const SecretVersionsSchema = z.object({ export type TSecretVersions = z.infer; export type TSecretVersionsInsert = Omit; -export type TSecretVersionsUpdate = Partial>; +export type TSecretVersionsUpdate = Partial< + Omit +>; diff --git a/pg-migrator/src/schemas/secrets.ts b/pg-migrator/src/schemas/secrets.ts index a284ae770..646f9d070 100644 --- a/pg-migrator/src/schemas/secrets.ts +++ b/pg-migrator/src/schemas/secrets.ts @@ -10,8 +10,8 @@ import { TImmutableDBKeys } from "./models"; export const SecretsSchema = z.object({ id: z.string().uuid(), version: z.number().default(1), - type: z.string().default('shared'), - secretBlindIndex: z.string(), + type: z.string().default("shared"), + secretBlindIndex: z.string().nullable().optional(), secretKeyCiphertext: z.string(), secretKeyIV: z.string(), secretKeyTag: z.string(), @@ -24,8 +24,8 @@ export const SecretsSchema = z.object({ secretReminderNote: z.string().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(), - algorithm: z.string().default('aes-256-gcm'), - keyEncoding: z.string().default('utf8'), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), metadata: z.unknown().nullable().optional(), userId: z.string().uuid().nullable().optional(), folderId: z.string().uuid(), diff --git a/pg-migrator/src/utils.ts b/pg-migrator/src/utils.ts new file mode 100644 index 000000000..f82d52cec --- /dev/null +++ b/pg-migrator/src/utils.ts @@ -0,0 +1,45 @@ +import { Knex } from "knex"; + +import { TableName } from "./schemas"; + +export const createJunctionTable = ( + knex: Knex, + tableName: TableName, + table1Name: TableName, + table2Name: TableName +) => + knex.schema.createTable(tableName, (table) => { + table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + table.uuid(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1 + table.uuid(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2 + table.foreign(`${table1Name}Id`).references("id").inTable(table1Name); + table.foreign(`${table2Name}Id`).references("id").inTable(table2Name); + }); + +// one time logic +// this is a postgres function log to set updateAt to present time whenever row gets updated +export const createUpdateAtTriggerFunction = (knex: Knex) => + knex.raw(` +CREATE OR REPLACE FUNCTION on_update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW."updatedAt" = NOW(); +RETURN NEW; +END; +$$ LANGUAGE plpgsql; +`); + +export const dropUpdatedAtTriggerFunction = (knex: Knex) => + knex.raw(` +DROP FUNCTION IF EXISTS on_update_timestamp() CASCADE; +`); + +// we would be using this to apply updatedAt where ever we wanta +// remember to set `timestamps(true,true,true)` before this on schema +export const createOnUpdateTrigger = (knex: Knex, tableName: string) => + knex.raw(` +CREATE TRIGGER "${tableName}_updatedAt" +BEFORE UPDATE ON ${tableName} +FOR EACH ROW +EXECUTE PROCEDURE on_update_timestamp(); +`); + +export const dropOnUpdateTrigger = (knex: Knex, tableName: string) => + knex.raw(`DROP TRIGGER IF EXISTS "${tableName}_updatedAt" ON ${tableName}`);