mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): completed integration, integration auth and project bot api migration
This commit is contained in:
57
backend-pg/folder.ts
Normal file
57
backend-pg/folder.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { initDbConnection } from "./src/db";
|
||||
import { TableName } from "./src/db/schemas";
|
||||
import { selectAllTableCols } from "./src/lib/knex";
|
||||
|
||||
dotenv.config();
|
||||
const db = initDbConnection(process.env.DB_CONNECTION_URI);
|
||||
|
||||
const main = async () => {
|
||||
const folders = db
|
||||
.withRecursive("parent", (qb) => {
|
||||
qb.select({
|
||||
depth: 1,
|
||||
path: db.raw("'/'")
|
||||
})
|
||||
.select(selectAllTableCols(db, TableName.SecretFolder))
|
||||
.from(TableName.SecretFolder)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where({
|
||||
projectId: "01c10de1-8743-490f-9c8a-7a19c4dc72a9",
|
||||
parentId: null
|
||||
})
|
||||
.where(`${TableName.Environment}.slug`, "dev")
|
||||
.union((qb) =>
|
||||
qb
|
||||
.select({
|
||||
depth: db.raw("parent.depth + 1"),
|
||||
path: db.raw(
|
||||
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
|
||||
)
|
||||
})
|
||||
.select(selectAllTableCols(db, TableName.SecretFolder))
|
||||
.whereRaw(
|
||||
`depth = array_position(ARRAY[${[1, 2]
|
||||
.map((_) => "?")
|
||||
.join(",")}]::varchar[], secret_folders.name,depth)`,
|
||||
[...["ui", "design"]]
|
||||
)
|
||||
.from(TableName.SecretFolder)
|
||||
.join("parent", "parent.id", `${TableName.SecretFolder}.parentId`)
|
||||
);
|
||||
})
|
||||
.select("*")
|
||||
.from("parent")
|
||||
.orderBy("depth", "desc")
|
||||
.first();
|
||||
console.log(folders.toSQL());
|
||||
console.log(JSON.stringify(await folders, null, 4));
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
main();
|
||||
159
backend-pg/package-lock.json
generated
159
backend-pg/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"argon2": "^0.31.2",
|
||||
"axios": "^1.6.2",
|
||||
@@ -943,6 +944,149 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
|
||||
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz",
|
||||
"integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^4.0.0",
|
||||
"@octokit/graphql": "^7.0.0",
|
||||
"@octokit/request": "^8.0.2",
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"before-after-hook": "^2.2.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz",
|
||||
"integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz",
|
||||
"integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^8.0.1",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
|
||||
"integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw=="
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "9.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz",
|
||||
"integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-request-log": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.0.tgz",
|
||||
"integrity": "sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz",
|
||||
"integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "8.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz",
|
||||
"integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^9.0.0",
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz",
|
||||
"integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "20.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
|
||||
"integrity": "sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^5.0.0",
|
||||
"@octokit/plugin-paginate-rest": "^9.0.0",
|
||||
"@octokit/plugin-request-log": "^4.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "12.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz",
|
||||
"integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
@@ -2544,6 +2688,11 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||
@@ -3075,6 +3224,11 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
||||
@@ -8764,6 +8918,11 @@
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"argon2": "^0.31.2",
|
||||
"axios": "^1.6.2",
|
||||
|
||||
6
backend-pg/src/@types/fastify.d.ts
vendored
6
backend-pg/src/@types/fastify.d.ts
vendored
@@ -8,9 +8,12 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { AuthMode } from "@app/services/auth/auth-signup-type";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||
@@ -69,7 +72,10 @@ declare module "fastify" {
|
||||
projectRole: TProjectRoleServiceFactory;
|
||||
secret: TSecretServiceFactory;
|
||||
secretImport: TSecretImportServiceFactory;
|
||||
projectBot: TProjectBotServiceFactory;
|
||||
folder: TSecretFolderServiceFactory;
|
||||
integration: TIntegrationServiceFactory;
|
||||
integrationAuth: TIntegrationAuthServiceFactory;
|
||||
};
|
||||
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
|
||||
24
backend-pg/src/@types/knex.d.ts
vendored
24
backend-pg/src/@types/knex.d.ts
vendored
@@ -16,6 +16,12 @@ import {
|
||||
TIncidentContacts,
|
||||
TIncidentContactsInsert,
|
||||
TIncidentContactsUpdate,
|
||||
TIntegrationAuths,
|
||||
TIntegrationAuthsInsert,
|
||||
TIntegrationAuthsUpdate,
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate,
|
||||
@@ -25,6 +31,9 @@ import {
|
||||
TOrgRoles,
|
||||
TOrgRolesInsert,
|
||||
TOrgRolesUpdate,
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate,
|
||||
TProjectEnvironments,
|
||||
TProjectEnvironmentsInsert,
|
||||
TProjectEnvironmentsUpdate,
|
||||
@@ -136,6 +145,11 @@ declare module "knex/types/tables" {
|
||||
TProjectEnvironmentsInsert,
|
||||
TProjectEnvironmentsUpdate
|
||||
>;
|
||||
[TableName.ProjectBot]: Knex.CompositeTableType<
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate
|
||||
>;
|
||||
[TableName.ProjectRoles]: Knex.CompositeTableType<
|
||||
TProjectRoles,
|
||||
TProjectRolesInsert,
|
||||
@@ -172,6 +186,16 @@ declare module "knex/types/tables" {
|
||||
TSecretImportsInsert,
|
||||
TSecretImportsUpdate
|
||||
>;
|
||||
[TableName.Integration]: Knex.CompositeTableType<
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate
|
||||
>;
|
||||
[TableName.IntegrationAuth]: Knex.CompositeTableType<
|
||||
TIntegrationAuths,
|
||||
TIntegrationAuthsInsert,
|
||||
TIntegrationAuthsUpdate
|
||||
>;
|
||||
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
||||
TSecretTagJunction,
|
||||
TSecretTagJunctionInsert,
|
||||
|
||||
35
backend-pg/src/db/migrations/20231222092113_project-bot.ts
Normal file
35
backend-pg/src/db/migrations/20231222092113_project-bot.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectBot))) {
|
||||
await knex.schema.createTable(TableName.ProjectBot, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.boolean("isActive").defaultTo(false).notNullable();
|
||||
t.text("encryptedPrivateKey").notNullable();
|
||||
t.text("publicKey").notNullable();
|
||||
t.text("iv").notNullable();
|
||||
t.text("tag").notNullable();
|
||||
t.string("algorithm").notNullable();
|
||||
t.string("keyEncoding").notNullable();
|
||||
t.text("encryptedProjectKey");
|
||||
t.text("encryptedProjectKeyNonce");
|
||||
// one to one relationship
|
||||
t.uuid("projectId").notNullable().unique();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("senderId");
|
||||
t.foreign("senderId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectBot);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectBot);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectBot);
|
||||
}
|
||||
71
backend-pg/src/db/migrations/20231222172455_integration.ts
Normal file
71
backend-pg/src/db/migrations/20231222172455_integration.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IntegrationAuth))) {
|
||||
await knex.schema.createTable(TableName.IntegrationAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("integration").notNullable();
|
||||
t.string("teamId"); // vercel-specific
|
||||
t.string("url"); // for self hosted
|
||||
t.string("namespace"); // hashicorp specific
|
||||
t.string("accountId"); // netlify
|
||||
t.string("refreshCiphertext");
|
||||
t.string("refreshIV");
|
||||
t.string("refreshTag");
|
||||
t.string("accessIdCiphertext");
|
||||
t.string("accessIdIV");
|
||||
t.string("accessIdTag");
|
||||
t.string("accessCiphertext");
|
||||
t.string("accessIV");
|
||||
t.string("accessTag");
|
||||
t.datetime("accessExpiresAt");
|
||||
t.jsonb("metadata");
|
||||
t.string("algorithm").notNullable();
|
||||
t.string("keyEncoding").notNullable();
|
||||
t.uuid("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.IntegrationAuth);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.Integration))) {
|
||||
await knex.schema.createTable(TableName.Integration, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.boolean("isActive").notNullable();
|
||||
t.string("url"); // self hosted
|
||||
t.string("app"); // name of app in provider
|
||||
t.string("appId");
|
||||
t.string("targetEnvironment");
|
||||
t.string("targetEnvironmentId");
|
||||
t.string("targetService"); // railway - qovery specific
|
||||
t.string("targetServiceId");
|
||||
t.string("owner"); // github specific
|
||||
t.string("path"); // aws parameter store / vercel preview branch
|
||||
t.string("region"); // aws
|
||||
t.string("scope"); // qovery specific scope
|
||||
t.string("integration").notNullable();
|
||||
t.jsonb("metadata");
|
||||
t.uuid("integrationAuthId").notNullable();
|
||||
t.foreign("integrationAuthId")
|
||||
.references("id")
|
||||
.inTable(TableName.IntegrationAuth)
|
||||
.onDelete("CASCADE");
|
||||
t.uuid("envId").notNullable();
|
||||
t.string("secretPath").defaultTo("/").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.Integration);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IntegrationAuth);
|
||||
await knex.schema.dropTableIfExists(TableName.Integration);
|
||||
await dropOnUpdateTrigger(knex, TableName.IntegrationAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.Integration);
|
||||
}
|
||||
@@ -3,10 +3,13 @@ export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
export * from "./backup-private-key";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./models";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
|
||||
37
backend-pg/src/db/schemas/integration-auths.ts
Normal file
37
backend-pg/src/db/schemas/integration-auths.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IntegrationAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
integration: z.string(),
|
||||
teamId: z.string().nullable().optional(),
|
||||
url: z.string().nullable().optional(),
|
||||
namespace: z.string().nullable().optional(),
|
||||
accountId: z.string().nullable().optional(),
|
||||
refreshCiphertext: z.string().nullable().optional(),
|
||||
refreshIV: z.string().nullable().optional(),
|
||||
refreshTag: z.string().nullable().optional(),
|
||||
accessIdCiphertext: z.string().nullable().optional(),
|
||||
accessIdIV: z.string().nullable().optional(),
|
||||
accessIdTag: z.string().nullable().optional(),
|
||||
accessCiphertext: z.string().nullable().optional(),
|
||||
accessIV: z.string().nullable().optional(),
|
||||
accessTag: z.string().nullable().optional(),
|
||||
accessExpiresAt: z.date().nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
algorithm: z.string(),
|
||||
keyEncoding: z.string(),
|
||||
projectId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
|
||||
export type TIntegrationAuthsInsert = Omit<TIntegrationAuths, TImmutableDBKeys>;
|
||||
export type TIntegrationAuthsUpdate = Partial<Omit<TIntegrationAuths, TImmutableDBKeys>>;
|
||||
35
backend-pg/src/db/schemas/integrations.ts
Normal file
35
backend-pg/src/db/schemas/integrations.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IntegrationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
isActive: z.boolean(),
|
||||
url: z.string().nullable().optional(),
|
||||
app: z.string().nullable().optional(),
|
||||
appId: z.string().nullable().optional(),
|
||||
targetEnvironment: z.string().nullable().optional(),
|
||||
targetEnvironmentId: z.string().nullable().optional(),
|
||||
targetService: z.string().nullable().optional(),
|
||||
targetServiceId: z.string().nullable().optional(),
|
||||
owner: z.string().nullable().optional(),
|
||||
path: z.string().nullable().optional(),
|
||||
region: z.string().nullable().optional(),
|
||||
scope: z.string().nullable().optional(),
|
||||
integration: z.string(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
integrationAuthId: z.string().uuid(),
|
||||
envId: z.string().uuid(),
|
||||
secretPath: z.string().default('/'),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TIntegrations = z.infer<typeof IntegrationsSchema>;
|
||||
export type TIntegrationsInsert = Omit<TIntegrations, TImmutableDBKeys>;
|
||||
export type TIntegrationsUpdate = Partial<Omit<TIntegrations, TImmutableDBKeys>>;
|
||||
@@ -14,6 +14,7 @@ export enum TableName {
|
||||
SuperAdmin = "super_admin",
|
||||
ApiKey = "api_keys",
|
||||
Project = "projects",
|
||||
ProjectBot = "project_bots",
|
||||
Environment = "project_environments",
|
||||
ProjectMembership = "project_memberships",
|
||||
ProjectRoles = "project_roles",
|
||||
@@ -24,6 +25,8 @@ export enum TableName {
|
||||
SecretFolder = "secret_folders",
|
||||
SecretImport = "secret_imports",
|
||||
SecretTag = "secret_tags",
|
||||
Integration = "integrations",
|
||||
IntegrationAuth = "integration_auths",
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
JnSecretVersionTag = "secret_version_tag_junction"
|
||||
}
|
||||
|
||||
30
backend-pg/src/db/schemas/project-bots.ts
Normal file
30
backend-pg/src/db/schemas/project-bots.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectBotsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
isActive: z.boolean().default(false),
|
||||
encryptedPrivateKey: z.string(),
|
||||
publicKey: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
algorithm: z.string(),
|
||||
keyEncoding: z.string(),
|
||||
encryptedProjectKey: z.string().optional().nullable(),
|
||||
encryptedProjectKeyNonce: z.string().optional().nullable(),
|
||||
projectId: z.string().uuid(),
|
||||
senderId: z.string().uuid().optional().nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectBots = z.infer<typeof ProjectBotsSchema>;
|
||||
export type TProjectBotsInsert = Omit<TProjectBots, TImmutableDBKeys>;
|
||||
export type TProjectBotsUpdate = Partial<Omit<TProjectBots, TImmutableDBKeys>>;
|
||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
||||
|
||||
import { zpStr } from "../zod";
|
||||
|
||||
export const GITLAB_URL = "https://gitlab.com";
|
||||
|
||||
const zodStrBool = z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
@@ -46,7 +48,34 @@ const envSchema = z
|
||||
CLIENT_SECRET_GITHUB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_ID_GITLAB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GITLAB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_GITLAB_LOGIN_URL: zpStr(z.string().optional())
|
||||
CLIENT_GITLAB_LOGIN_URL: zpStr(z.string().optional().default(GITLAB_URL)),
|
||||
// integration client secrets
|
||||
// heroku
|
||||
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_HEROKU: zpStr(z.string().optional()),
|
||||
// vercel
|
||||
CLIENT_ID_VERCEL: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_VERCEL: zpStr(z.string().optional()),
|
||||
CLIENT_SLUG_VERCEL: zpStr(z.string().optional()),
|
||||
// netlify
|
||||
CLIENT_ID_NETLIFY: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_NETLIFY: zpStr(z.string().optional()),
|
||||
// bit bucket
|
||||
CLIENT_ID_BITBUCKET: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_BITBUCKET: zpStr(z.string().optional()),
|
||||
// gcp secret manager
|
||||
CLIENT_ID_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
|
||||
// github
|
||||
CLIENT_ID_GITHUB: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GITHUB: zpStr(z.string().optional()),
|
||||
// azure
|
||||
CLIENT_ID_AZURE: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_AZURE: zpStr(z.string().optional()),
|
||||
// google
|
||||
CLIENT_ID_GITLAB: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GITLAB: zpStr(z.string().optional()),
|
||||
URL_GITLAB_URL: zpStr(z.string().optional().default(GITLAB_URL))
|
||||
})
|
||||
.transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST) }));
|
||||
|
||||
|
||||
@@ -120,6 +120,15 @@ export const decryptAsymmetric = ({
|
||||
return naclUtils.encodeUTF8(plaintext);
|
||||
};
|
||||
|
||||
export const generateAsymmetricKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return {
|
||||
publicKey: naclUtils.encodeBase64(pair.publicKey),
|
||||
privateKey: naclUtils.encodeBase64(pair.secretKey)
|
||||
};
|
||||
};
|
||||
|
||||
export type TGenSecretBlindIndex = {
|
||||
secretName: string;
|
||||
keyEncoding: SecretKeyEncoding;
|
||||
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
} from "./encryption";
|
||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Knex } from "knex";
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
export const selectAllTableCols = <Tname extends keyof Tables>(db: Knex, tableName: Tname) =>
|
||||
db.ref("*").withSchema(tableName) as unknown as keyof Tables[Tname];
|
||||
export const selectAllTableCols = <Tname extends keyof Tables>(tableName: Tname) =>
|
||||
`${tableName}.*` as keyof Tables[Tname]["base"];
|
||||
|
||||
@@ -13,6 +13,10 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { tokenDalFactory } from "@app/services/auth-token/auth-token-dal";
|
||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { integrationDalFactory } from "@app/services/integration/integration-dal";
|
||||
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
import { integrationAuthDalFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { incidentContactDalFactory } from "@app/services/org/incident-contacts-dal";
|
||||
import { orgDalFactory } from "@app/services/org/org-dal";
|
||||
import { orgRoleDalFactory } from "@app/services/org/org-role-dal";
|
||||
@@ -20,6 +24,8 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||
import { projectDalFactory } from "@app/services/project/project-dal";
|
||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||
import { projectBotDalFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { projectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { projectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
import { projectKeyDalFactory } from "@app/services/project-key/project-key-dal";
|
||||
@@ -67,6 +73,7 @@ export const registerRoutes = async (
|
||||
const projectRoleDal = projectRoleDalFactory(db);
|
||||
const projectEnvDal = projectEnvDalFactory(db);
|
||||
const projectKeyDal = projectKeyDalFactory(db);
|
||||
const projectBotDal = projectBotDalFactory(db);
|
||||
|
||||
const secretDal = secretDalFactory(db);
|
||||
const folderDal = secretFolderDalFactory(db);
|
||||
@@ -74,6 +81,9 @@ export const registerRoutes = async (
|
||||
const secretVersionDal = secretVersionDalFactory(db);
|
||||
const secretBlindIndexDal = secretBlindIndexDalFactory(db);
|
||||
|
||||
const integrationDal = integrationDalFactory(db);
|
||||
const integrationAuthDal = integrationAuthDalFactory(db);
|
||||
|
||||
// ee db layer ops
|
||||
const permissionDal = permissionDalFactory(db);
|
||||
|
||||
@@ -159,6 +169,20 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
secretImportDal
|
||||
});
|
||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDal });
|
||||
const integrationService = integrationServiceFactory({
|
||||
permissionService,
|
||||
folderDal,
|
||||
integrationDal,
|
||||
integrationAuthDal
|
||||
});
|
||||
const integrationAuthService = integrationAuthServiceFactory({
|
||||
integrationAuthDal,
|
||||
integrationDal,
|
||||
permissionService,
|
||||
projectBotDal,
|
||||
projectBotService
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
// inject all services
|
||||
@@ -180,7 +204,10 @@ export const registerRoutes = async (
|
||||
projectRole: projectRoleService,
|
||||
secret: secretService,
|
||||
folder: folderService,
|
||||
secretImport: secretImportService
|
||||
secretImport: secretImportService,
|
||||
projectBot: projectBotService,
|
||||
integration: integrationService,
|
||||
integrationAuth: integrationAuthService
|
||||
});
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
|
||||
80
backend-pg/src/server/routes/v1/bot-router.ts
Normal file
80
backend-pg/src/server/routes/v1/bot-router.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectBotsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:projectId",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
bot: ProjectBotsSchema.pick({
|
||||
name: true,
|
||||
projectId: true,
|
||||
isActive: true,
|
||||
publicKey: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const bot = await server.services.projectBot.findBotByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
return { bot };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:botId/active",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
body: z.object({
|
||||
isActive: z.boolean(),
|
||||
botKey: z
|
||||
.object({
|
||||
nonce: z.string().trim().optional(),
|
||||
encryptedKey: z.string().trim().optional()
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
params: z.object({
|
||||
botId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
bot: ProjectBotsSchema.pick({
|
||||
name: true,
|
||||
projectId: true,
|
||||
isActive: true,
|
||||
publicKey: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const bot = await server.services.projectBot.setBotActiveState({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
botId: req.params.botId,
|
||||
botKey: req.body.botKey,
|
||||
isActive: req.body.isActive
|
||||
});
|
||||
return { bot };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
import { registerAdminRouter } from "./admin-router";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
import { registerIntegrationRouter } from "./integration-router";
|
||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||
import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
@@ -34,4 +37,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
|
||||
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||
};
|
||||
|
||||
565
backend-pg/src/server/routes/v1/integration-auth-router.ts
Normal file
565
backend-pg/src/server/routes/v1/integration-auth-router.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IntegrationAuthsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/integration-options",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
integrationOptions: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
image: z.string(),
|
||||
isAvailable: z.boolean().optional(),
|
||||
type: z.string(),
|
||||
clientId: z.string().optional(),
|
||||
docsLink: z.string().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async () => {
|
||||
const integrationOptions = await server.services.integrationAuth.getIntegrationOptions();
|
||||
return { integrationOptions };
|
||||
}
|
||||
});
|
||||
|
||||
const integrationPublicSchema = IntegrationAuthsSchema.pick({
|
||||
projectId: true,
|
||||
integration: true,
|
||||
teamId: true,
|
||||
url: true,
|
||||
namespace: true,
|
||||
accountId: true,
|
||||
metadata: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integrationAuth: integrationPublicSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const integrationAuth = await server.services.integrationAuth.getIntegrationAuth({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { integrationAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId",
|
||||
method: "DELETE",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integrationAuth: integrationPublicSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const integrationAuth = await server.services.integrationAuth.deleteIntegrationAuth({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { integrationAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/oauth-token",
|
||||
method: "POST",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
integration: z.string().trim(),
|
||||
accessId: z.string().trim().optional(),
|
||||
accessToken: z.string().trim().optional(),
|
||||
url: z.string().url().trim().optional(),
|
||||
namespace: z.string().trim().optional(),
|
||||
refreshToken: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integrationAuth: integrationPublicSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const integrationAuth = await server.services.integrationAuth.saveIntegrationToken({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body
|
||||
});
|
||||
return { integrationAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/apps",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
teamId: z.string().trim().optional(),
|
||||
workspaceSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
apps: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
appId: z.string().optional(),
|
||||
owner: z.string().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const apps = await server.services.integrationAuth.getIntegrationApps({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
...req.query
|
||||
});
|
||||
return { apps };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/teams",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
teams: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const teams = await server.services.integrationAuth.getIntegrationAuthTeams({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { teams };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/vercel/branches",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
appId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
branches: z.string().array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const branches = await server.services.integrationAuth.getVercelBranches({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
appId: req.query.appId
|
||||
});
|
||||
return { branches };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/checkly/groups",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
accountId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
groups: z.object({ name: z.string(), groupId: z.number() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const groups = await server.services.integrationAuth.getChecklyGroups({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
accountId: req.query.accountId
|
||||
});
|
||||
return { groups };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/qovery/orgs",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
orgs: z.object({ name: z.string(), orgId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const orgs = await server.services.integrationAuth.getQoveryOrgs({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { orgs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/qovery/projects",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
orgId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
projects: z.object({ name: z.string(), projectId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const projects = await server.services.integrationAuth.getQoveryProjects({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
orgId: req.query.orgId
|
||||
});
|
||||
return { projects };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/qovery/environments",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
environments: z.object({ name: z.string(), environmentId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const environments = await server.services.integrationAuth.getQoveryEnvs({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
projectId: req.query.projectId
|
||||
});
|
||||
return { environments };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/qovery/apps",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environmentId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
apps: z.object({ name: z.string(), appId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const apps = await server.services.integrationAuth.getQoveryApps({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
environmentId: req.query.environmentId
|
||||
});
|
||||
return { apps };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/qovery/containers",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environmentId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
containers: z.object({ name: z.string(), appId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const containers = await server.services.integrationAuth.getQoveryContainers({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
environmentId: req.query.environmentId
|
||||
});
|
||||
return { containers };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/qovery/jobs",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environmentId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
jobs: z.object({ name: z.string(), appId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const jobs = await server.services.integrationAuth.getQoveryJobs({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
environmentId: req.query.environmentId
|
||||
});
|
||||
return { jobs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/railway/environments",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
appId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
environments: z.object({ name: z.string(), environmentId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const environments = await server.services.integrationAuth.getRailwayEnvironments({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
appId: req.query.appId
|
||||
});
|
||||
return { environments };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/railway/services",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
appId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
services: z.object({ name: z.string(), serviceId: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const services = await server.services.integrationAuth.getRailwayServices({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
appId: req.query.appId
|
||||
});
|
||||
return { services };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/bitbucket/workspaces",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaces: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
uuid: z.string(),
|
||||
type: z.string(),
|
||||
is_private: z.boolean(),
|
||||
created_on: z.string(),
|
||||
updated_on: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const workspaces = await server.services.integrationAuth.getBitbucketWorkspaces({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { workspaces };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/northflank/secret-groups",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
appId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretGroups: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
groupId: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const secretGroups = await server.services.integrationAuth.getNorthFlankSecretGroups({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
appId: req.query.appId
|
||||
});
|
||||
return { secretGroups };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationAuthId/teamcity/build-configs",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
appId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
buildConfigs: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
buildConfigId: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const buildConfigs = await server.services.integrationAuth.getTeamcityBuildConfigs({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationAuthId,
|
||||
appId: req.query.appId
|
||||
});
|
||||
return { buildConfigs };
|
||||
}
|
||||
});
|
||||
};
|
||||
116
backend-pg/src/server/routes/v1/integration-router.ts
Normal file
116
backend-pg/src/server/routes/v1/integration-router.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IntegrationsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
integrationAuthId: z.string().trim(),
|
||||
app: z.string().trim().optional(),
|
||||
isActive: z.boolean(),
|
||||
appId: z.string().trim().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
sourceEnvironment: z.string().trim(),
|
||||
targetEnvironment: z.string().trim().optional(),
|
||||
targetEnvironmentId: z.string().trim().optional(),
|
||||
targetService: z.string().trim().optional(),
|
||||
targetServiceId: z.string().trim().optional(),
|
||||
owner: z.string().trim().optional(),
|
||||
path: z.string().trim().optional(),
|
||||
region: z.string().trim().optional(),
|
||||
scope: z.string().trim().optional(),
|
||||
metadata: z
|
||||
.object({
|
||||
secretPrefix: z.string().optional(),
|
||||
secretSuffix: z.string().optional(),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const integration = await server.services.integration.createIntegration({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
...req.body
|
||||
});
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
app: z.string().trim(),
|
||||
appId: z.string().trim(),
|
||||
isActive: z.boolean(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
targetEnvironment: z.string().trim(),
|
||||
owner: z.string().trim(),
|
||||
environment: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const integration = await server.services.integration.updateIntegration({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationId,
|
||||
...req.body
|
||||
});
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:integrationId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const integration = await server.services.integration.deleteIntegration({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
id: req.params.integrationId
|
||||
});
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(akhilmhdh-pg): manual sync
|
||||
};
|
||||
1161
backend-pg/src/services/integration-auth/integration-app-list.ts
Normal file
1161
backend-pg/src/services/integration-auth/integration-app-list.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIntegrationAuthDalFactory = ReturnType<typeof integrationAuthDalFactory>;
|
||||
|
||||
export const integrationAuthDalFactory = (db: TDbClient) => {
|
||||
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
|
||||
return integrationAuthOrm;
|
||||
};
|
||||
1008
backend-pg/src/services/integration-auth/integration-auth-service.ts
Normal file
1008
backend-pg/src/services/integration-auth/integration-auth-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetIntegrationAuthDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TOauthExchangeDTO = {
|
||||
integration: string;
|
||||
code: string;
|
||||
url: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSaveIntegrationAccessTokenDTO = {
|
||||
integration: string;
|
||||
accessId?: string;
|
||||
accessToken?: string;
|
||||
url?: string;
|
||||
namespace?: string;
|
||||
refreshToken?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TIntegrationAuthAppsDTO = {
|
||||
id: string;
|
||||
teamId?: string;
|
||||
workspaceSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthTeamsDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthVercelBranchesDTO = {
|
||||
id: string;
|
||||
appId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthChecklyGroupsDTO = {
|
||||
id: string;
|
||||
accountId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthQoveryOrgsDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthQoveryProjectDTO = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthQoveryEnvironmentsDTO = {
|
||||
id: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TIntegrationAuthQoveryScopesDTO = {
|
||||
id: string;
|
||||
environmentId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthRailwayEnvDTO = {
|
||||
id: string;
|
||||
appId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthRailwayServicesDTO = {
|
||||
id: string;
|
||||
appId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthBitbucketWorkspaceDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthNorthflankSecretGroupDTO = {
|
||||
id: string;
|
||||
appId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteIntegrationAuthDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
||||
id: string;
|
||||
appId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TVercelBranches = {
|
||||
ref: string;
|
||||
lastCommit: string;
|
||||
isProtected: boolean;
|
||||
};
|
||||
|
||||
export type TChecklyGroups = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TQoveryProjects = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TQoveryEnvironments = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TBitbucketWorkspace = {
|
||||
type: string;
|
||||
uuid: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
is_private: boolean;
|
||||
created_on: string;
|
||||
updated_on: string;
|
||||
};
|
||||
|
||||
export type TNorthflankSecretGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
priority: number;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TTeamCityBuildConfig = {
|
||||
id: string;
|
||||
name: string;
|
||||
projectName: string;
|
||||
projectId: string;
|
||||
href: string;
|
||||
webUrl: string;
|
||||
};
|
||||
363
backend-pg/src/services/integration-auth/integration-list.ts
Normal file
363
backend-pg/src/services/integration-auth/integration-list.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
export enum Integrations {
|
||||
AZURE_KEY_VAULT = "azure-key-vault",
|
||||
AWS_PARAMETER_STORE = "aws-parameter-store",
|
||||
AWS_SECRET_MANAGER = "aws-secret-manager",
|
||||
GCP_SECRET_MANAGER = "gcp-secret-manager",
|
||||
HEROKU = "heroku",
|
||||
VERCEL = "vercel",
|
||||
NETLIFY = "netlify",
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
RENDER = "render",
|
||||
RAILWAY = "railway",
|
||||
FLYIO = "flyio",
|
||||
LARAVELFORGE = "laravel-forge",
|
||||
CIRCLECI = "circleci",
|
||||
TRAVISCI = "travisci",
|
||||
TEAMCITY = "teamcity",
|
||||
SUPABASE = "supabase",
|
||||
CHECKLY = "checkly",
|
||||
QOVERY = "qovery",
|
||||
TERRAFORM_CLOUD = "terraform-cloud",
|
||||
HASHICORP_VAULT = "hashicorp-vault",
|
||||
CLOUDFLARE_PAGES = "cloudflare-pages",
|
||||
CLOUDFLARE_WORKERS = "cloudflare-workers",
|
||||
BITBUCKET = "bitbucket",
|
||||
CODEFRESH = "codefresh",
|
||||
WINDMILL = "windmill",
|
||||
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
|
||||
CLOUD_66 = "cloud-66",
|
||||
NORTHFLANK = "northflank",
|
||||
HASURA_CLOUD = "hasura-cloud"
|
||||
}
|
||||
|
||||
export enum IntegrationType {
|
||||
OAUTH2 = "oauth2"
|
||||
}
|
||||
|
||||
export enum IntegrationUrls {
|
||||
// integration oauth endpoints
|
||||
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
|
||||
AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token",
|
||||
VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token",
|
||||
NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token",
|
||||
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token",
|
||||
GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token",
|
||||
BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token",
|
||||
|
||||
// integration apps endpoints
|
||||
GCP_API_URL = "https://cloudresourcemanager.googleapis.com",
|
||||
HEROKU_API_URL = "https://api.heroku.com",
|
||||
GITLAB_URL = "https://gitlab.com",
|
||||
GITLAB_API_URL = `${GITLAB_URL}/api`,
|
||||
GITHUB_API_URL = "https://api.github.com",
|
||||
VERCEL_API_URL = "https://api.vercel.com",
|
||||
NETLIFY_API_URL = "https://api.netlify.com",
|
||||
RENDER_API_URL = "https://api.render.com",
|
||||
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
||||
FLYIO_API_URL = "https://api.fly.io/graphql",
|
||||
CIRCLECI_API_URL = "https://circleci.com/api",
|
||||
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
||||
SUPABASE_API_URL = "https://api.supabase.com",
|
||||
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
||||
CHECKLY_API_URL = "https://api.checklyhq.com",
|
||||
QOVERY_API_URL = "https://api.qovery.com",
|
||||
TERRAFORM_CLOUD_API_URL = "https://app.terraform.io",
|
||||
CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com",
|
||||
CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com",
|
||||
BITBUCKET_API_URL = "https://api.bitbucket.org",
|
||||
CODEFRESH_API_URL = "https://g.codefresh.io/api",
|
||||
WINDMILL_API_URL = "https://app.windmill.dev/api",
|
||||
DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com",
|
||||
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
||||
NORTHFLANK_API_URL = "https://api.northflank.com",
|
||||
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
||||
|
||||
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: "Heroku",
|
||||
slug: "heroku",
|
||||
image: "Heroku.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_HEROKU,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Vercel",
|
||||
slug: "vercel",
|
||||
image: "Vercel.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: "",
|
||||
clientSlug: appCfg.CLIENT_ID_VERCEL,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Netlify",
|
||||
slug: "netlify",
|
||||
image: "Netlify.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_NETLIFY,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
slug: "github",
|
||||
image: "GitHub.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_BITBUCKET,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Render",
|
||||
slug: "render",
|
||||
image: "Render.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Railway",
|
||||
slug: "railway",
|
||||
image: "Railway.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Fly.io",
|
||||
slug: "flyio",
|
||||
image: "Flyio.svg",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "AWS Parameter Store",
|
||||
slug: "aws-parameter-store",
|
||||
image: "Amazon Web Services.png",
|
||||
isAvailable: true,
|
||||
type: "custom",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Laravel Forge",
|
||||
slug: "laravel-forge",
|
||||
image: "Laravel Forge.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "AWS Secrets Manager",
|
||||
slug: "aws-secret-manager",
|
||||
image: "Amazon Web Services.png",
|
||||
isAvailable: true,
|
||||
type: "custom",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Azure Key Vault",
|
||||
slug: "azure-key-vault",
|
||||
image: "Microsoft Azure.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_AZURE,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Circle CI",
|
||||
slug: "circleci",
|
||||
image: "Circle CI.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "GitLab",
|
||||
slug: "gitlab",
|
||||
image: "GitLab.png",
|
||||
isAvailable: true,
|
||||
type: "custom",
|
||||
clientId: appCfg.CLIENT_ID_GITLAB,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Terraform Cloud",
|
||||
slug: "terraform-cloud",
|
||||
image: "Terraform Cloud.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
cliendId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Travis CI",
|
||||
slug: "travisci",
|
||||
image: "Travis CI.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "TeamCity",
|
||||
slug: "teamcity",
|
||||
image: "TeamCity.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Supabase",
|
||||
slug: "supabase",
|
||||
image: "Supabase.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Checkly",
|
||||
slug: "checkly",
|
||||
image: "Checkly.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Qovery",
|
||||
slug: "qovery",
|
||||
image: "Qovery.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "HashiCorp Vault",
|
||||
slug: "hashicorp-vault",
|
||||
image: "Vault.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "GCP Secret Manager",
|
||||
slug: "gcp-secret-manager",
|
||||
image: "Google Cloud Platform.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Cloudflare Pages",
|
||||
slug: "cloudflare-pages",
|
||||
image: "Cloudflare.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Cloudflare Workers",
|
||||
slug: "cloudflare-workers",
|
||||
image: "Cloudflare.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "BitBucket",
|
||||
slug: "bitbucket",
|
||||
image: "BitBucket.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_BITBUCKET,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Codefresh",
|
||||
slug: "codefresh",
|
||||
image: "Codefresh.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Windmill",
|
||||
slug: "windmill",
|
||||
image: "Windmill.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Digital Ocean App Platform",
|
||||
slug: "digital-ocean-app-platform",
|
||||
image: "Digital Ocean.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Cloud 66",
|
||||
slug: "cloud-66",
|
||||
image: "Cloud 66.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Northflank",
|
||||
slug: "northflank",
|
||||
image: "Northflank.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Hasura Cloud",
|
||||
slug: "hasura-cloud",
|
||||
image: "Hasura.svg",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
}
|
||||
];
|
||||
|
||||
return INTEGRATION_OPTIONS;
|
||||
};
|
||||
49
backend-pg/src/services/integration-auth/integration-team.ts
Normal file
49
backend-pg/src/services/integration-auth/integration-team.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { Integrations,IntegrationUrls } from "./integration-list";
|
||||
|
||||
type Team = {
|
||||
name: string;
|
||||
teamId: string;
|
||||
};
|
||||
const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: string }) => {
|
||||
const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL;
|
||||
|
||||
let teams: Team[] = [];
|
||||
const res = (
|
||||
await request.get(`${gitLabApiUrl}/v4/groups`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
teams = res.map((t: any) => ({
|
||||
name: t.name,
|
||||
teamId: t.id
|
||||
}));
|
||||
|
||||
return teams;
|
||||
};
|
||||
|
||||
export const getTeams = async ({
|
||||
accessToken,
|
||||
url,
|
||||
integration
|
||||
}: {
|
||||
accessToken: string;
|
||||
url?: string;
|
||||
integration: string;
|
||||
}) => {
|
||||
switch (integration) {
|
||||
case Integrations.GITLAB:
|
||||
return getTeamsGitLab({
|
||||
url: url as string,
|
||||
accessToken
|
||||
});
|
||||
default:
|
||||
throw new BadRequestError({ message: "Integration doesn't have team support" });
|
||||
}
|
||||
};
|
||||
725
backend-pg/src/services/integration-auth/integration-token.ts
Normal file
725
backend-pg/src/services/integration-auth/integration-token.ts
Normal file
@@ -0,0 +1,725 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { Integrations,IntegrationUrls } from "./integration-list";
|
||||
|
||||
type ExchangeCodeAzureResponse = {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
id_token: string;
|
||||
};
|
||||
|
||||
type ExchangeCodeGCPResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
type ExchangeCodeHerokuResponse = {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
user_id: string;
|
||||
session_nonce?: string;
|
||||
};
|
||||
|
||||
type ExchangeCodeVercelResponse = {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
installation_id: string;
|
||||
user_id: string;
|
||||
team_id?: string;
|
||||
};
|
||||
|
||||
type ExchangeCodeNetlifyResponse = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
type ExchangeCodeGithubResponse = {
|
||||
access_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
type ExchangeCodeGitlabResponse = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
type ExchangeCodeBitBucketResponse = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scopes: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
const exchangeCodeGCP = async ({ code }: { code: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER || !appCfg.CLIENT_ID_GCP_SECRET_MANAGER) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeGCPResponse = (
|
||||
await request.post(
|
||||
IntegrationUrls.GCP_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
|
||||
client_secret: appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/gcp-secret-manager/oauth2/callback`
|
||||
})
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
const exchangeCodeAzure = async ({ code }: { code: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
const res: ExchangeCodeAzureResponse = (
|
||||
await request.post(
|
||||
IntegrationUrls.AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
scope: "https://vault.azure.net/.default openid offline_access",
|
||||
client_id: appCfg.CLIENT_ID_AZURE,
|
||||
client_secret: appCfg.CLIENT_SECRET_AZURE,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_SECRET_HEROKU) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeHerokuResponse = (
|
||||
await request.post(
|
||||
IntegrationUrls.HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_secret: appCfg.CLIENT_SECRET_HEROKU
|
||||
})
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
* code-token exchange
|
||||
* @param {Object} obj1
|
||||
* @param {Object} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj2
|
||||
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_VERCEL || !appCfg.CLIENT_SECRET_VERCEL) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeVercelResponse = (
|
||||
await request.post(
|
||||
IntegrationUrls.VERCEL_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
code,
|
||||
client_id: appCfg.CLIENT_ID_VERCEL,
|
||||
client_secret: appCfg.CLIENT_SECRET_VERCEL,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/vercel/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: null,
|
||||
accessExpiresAt: null,
|
||||
teamId: res.team_id
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
* code-token exchange
|
||||
* @param {Object} obj1
|
||||
* @param {Object} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj2
|
||||
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_NETLIFY || !appCfg.CLIENT_SECRET_NETLIFY) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeNetlifyResponse = (
|
||||
await request.post(
|
||||
IntegrationUrls.NETLIFY_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: appCfg.CLIENT_ID_NETLIFY,
|
||||
client_secret: appCfg.CLIENT_SECRET_NETLIFY,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/netlify/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
|
||||
// akhilmhdh: commented out by me. Not sure why its being called but never used in prev codebase
|
||||
// const res2 = await request.get("https://api.netlify.com/api/v1/sites", {
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${res.access_token}`
|
||||
// }
|
||||
// });
|
||||
|
||||
const res3 = (
|
||||
await request.get("https://api.netlify.com/api/v1/accounts", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
const accountId = res3[0].id;
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accountId
|
||||
};
|
||||
};
|
||||
|
||||
const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeGithubResponse = (
|
||||
await request.get(IntegrationUrls.GITHUB_TOKEN_URL, {
|
||||
params: {
|
||||
client_id: appCfg.CLIENT_ID_GITHUB,
|
||||
client_secret: appCfg.CLIENT_SECRET_GITHUB,
|
||||
code,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/github/oauth2/callback`
|
||||
},
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: null,
|
||||
accessExpiresAt: null
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Gitlab
|
||||
* code-token exchange
|
||||
*/
|
||||
const exchangeCodeGitlab = async ({ code, url }: { code: string; url?: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_GITLAB || !appCfg.CLIENT_SECRET_GITLAB) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeGitlabResponse = (
|
||||
await request.post(
|
||||
url ? `${url}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: appCfg.CLIENT_ID_GITLAB,
|
||||
client_secret: appCfg.CLIENT_SECRET_GITLAB,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
|
||||
* code-token exchange
|
||||
*/
|
||||
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const res: ExchangeCodeBitBucketResponse = (
|
||||
await request.post(
|
||||
IntegrationUrls.BITBUCKET_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: appCfg.CLIENT_ID_BITBUCKET,
|
||||
client_secret: appCfg.CLIENT_SECRET_BITBUCKET,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/bitbucket/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
type TExchangeReturn = {
|
||||
accessToken: string;
|
||||
refreshToken?: string | null;
|
||||
accessExpiresAt?: Date | null;
|
||||
url?: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||
* code-token exchange for integration named [integration]
|
||||
*/
|
||||
export const exchangeCode = async ({
|
||||
integration,
|
||||
code,
|
||||
url
|
||||
}: {
|
||||
integration: string;
|
||||
code: string;
|
||||
url?: string;
|
||||
}): Promise<TExchangeReturn> => {
|
||||
switch (integration) {
|
||||
case Integrations.GCP_SECRET_MANAGER:
|
||||
return exchangeCodeGCP({
|
||||
code
|
||||
});
|
||||
case Integrations.AZURE_KEY_VAULT:
|
||||
return exchangeCodeAzure({
|
||||
code
|
||||
});
|
||||
case Integrations.HEROKU:
|
||||
return exchangeCodeHeroku({
|
||||
code
|
||||
});
|
||||
case Integrations.VERCEL:
|
||||
return exchangeCodeVercel({
|
||||
code
|
||||
});
|
||||
case Integrations.NETLIFY:
|
||||
return exchangeCodeNetlify({
|
||||
code
|
||||
});
|
||||
case Integrations.GITHUB:
|
||||
return exchangeCodeGithub({
|
||||
code
|
||||
});
|
||||
case Integrations.GITLAB:
|
||||
return exchangeCodeGitlab({
|
||||
code,
|
||||
url
|
||||
});
|
||||
case Integrations.BITBUCKET:
|
||||
return exchangeCodeBitBucket({
|
||||
code
|
||||
});
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown integration" });
|
||||
}
|
||||
};
|
||||
|
||||
type RefreshTokenAzureResponse = {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: 4871;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
type RefreshTokenHerokuResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
type RefreshTokenGitLabResponse = {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
type RefreshTokenBitBucketResponse = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scopes: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
type ServiceAccountAccessTokenGCPSecretManagerResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
type RefreshTokenGCPSecretManagerResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* Azure integration
|
||||
*/
|
||||
const exchangeRefreshAzure = async ({ refreshToken }: { refreshToken: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
|
||||
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
||||
IntegrationUrls.AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: appCfg.CLIENT_ID_AZURE,
|
||||
scope: "openid offline_access",
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret: appCfg.CLIENT_SECRET_AZURE
|
||||
})
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* Heroku integration
|
||||
*/
|
||||
const exchangeRefreshHeroku = async ({ refreshToken }: { refreshToken: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_SECRET_HEROKU) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenHerokuResponse;
|
||||
} = await request.post(
|
||||
IntegrationUrls.HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_secret: appCfg.CLIENT_SECRET_HEROKU
|
||||
})
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* GitLab integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for GitLab
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshGitLab = async ({
|
||||
refreshToken,
|
||||
url
|
||||
}: {
|
||||
url?: string | null;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_ID_GITLAB || !appCfg.CLIENT_SECRET_GITLAB) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenGitLabResponse;
|
||||
} = await request.post(
|
||||
url ? `${url}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: appCfg.CLIENT_ID_GITLAB,
|
||||
client_secret: appCfg.CLIENT_SECRET_GITLAB,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* BitBucket integration
|
||||
*/
|
||||
const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string }) => {
|
||||
const accessExpiresAt = new Date();
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenBitBucketResponse;
|
||||
} = await request.post(
|
||||
IntegrationUrls.BITBUCKET_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: appCfg.CLIENT_ID_BITBUCKET,
|
||||
client_secret: appCfg.CLIENT_SECRET_BITBUCKET,
|
||||
redirect_uri: `${appCfg.SITE_URL}/integrations/bitbucket/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* GCP Secret Manager integration
|
||||
*/
|
||||
const exchangeRefreshGCPSecretManager = async ({
|
||||
refreshToken,
|
||||
metadata = {}
|
||||
}: {
|
||||
metadata?: Record<string, string>;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
const accessExpiresAt = new Date();
|
||||
|
||||
if (metadata?.authMethod === "serviceAccount") {
|
||||
const serviceAccount = JSON.parse(refreshToken);
|
||||
|
||||
const payload = {
|
||||
iss: serviceAccount.client_email,
|
||||
aud: serviceAccount.token_uri,
|
||||
scope: IntegrationUrls.GCP_CLOUD_PLATFORM_SCOPE,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 3600
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" });
|
||||
|
||||
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } =
|
||||
await request.post(
|
||||
IntegrationUrls.GCP_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
assertion: token
|
||||
}).toString(),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken,
|
||||
accessExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER || !appCfg.CLIENT_ID_GCP_SECRET_MANAGER) {
|
||||
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||
}
|
||||
const { data }: { data: RefreshTokenGCPSecretManagerResponse } = await request.post(
|
||||
IntegrationUrls.GCP_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
|
||||
client_secret: appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token"
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken,
|
||||
accessExpiresAt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||
*/
|
||||
export const exchangeRefresh = async (
|
||||
integration: string,
|
||||
refreshToken: string,
|
||||
url?: string | null,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}> => {
|
||||
switch (integration) {
|
||||
case Integrations.AZURE_KEY_VAULT:
|
||||
return exchangeRefreshAzure({
|
||||
refreshToken
|
||||
});
|
||||
case Integrations.HEROKU:
|
||||
return exchangeRefreshHeroku({
|
||||
refreshToken
|
||||
});
|
||||
case Integrations.GITLAB:
|
||||
return exchangeRefreshGitLab({
|
||||
refreshToken,
|
||||
url
|
||||
});
|
||||
case Integrations.BITBUCKET:
|
||||
return exchangeRefreshBitBucket({
|
||||
refreshToken
|
||||
});
|
||||
case Integrations.GCP_SECRET_MANAGER:
|
||||
return exchangeRefreshGCPSecretManager({
|
||||
refreshToken,
|
||||
metadata
|
||||
});
|
||||
default:
|
||||
throw new Error("Failed to exchange token for incompatible integration");
|
||||
}
|
||||
};
|
||||
63
backend-pg/src/services/integration/integration-dal.ts
Normal file
63
backend-pg/src/services/integration/integration-dal.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName,TIntegrations } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TIntegrationDalFactory = ReturnType<typeof integrationDalFactory>;
|
||||
|
||||
export const integrationDalFactory = (db: TDbClient) => {
|
||||
const integrationOrm = ormify(db, TableName.Integration);
|
||||
|
||||
const integrationFindQuery = (tx: Knex, filter: Partial<TIntegrations>) =>
|
||||
tx(TableName.Integration)
|
||||
.where(filter)
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(selectAllTableCols(TableName.Integration));
|
||||
|
||||
const find = async (filter: Partial<TIntegrations>, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await integrationFindQuery(tx || db, filter);
|
||||
return docs.map(({ envId, envSlug, envName, ...el }) => ({
|
||||
...el,
|
||||
environment: {
|
||||
id: envId,
|
||||
slug: envSlug,
|
||||
name: envName
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id integrations" });
|
||||
}
|
||||
};
|
||||
|
||||
const findOne = async (filter: Partial<TIntegrations>, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await integrationFindQuery(tx || db, filter).first();
|
||||
if (!doc) return;
|
||||
|
||||
const { envName: name, envSlug: slug, envId: id, ...el } = doc;
|
||||
return { ...el, environment: { id, name, slug } };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one integrations" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await integrationFindQuery(tx || db, { id }).first();
|
||||
if (!doc) return;
|
||||
|
||||
const { envName: name, envSlug: slug, envId, ...el } = doc;
|
||||
return { ...el, environment: { id: envId, name, slug } };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id integrations" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...integrationOrm, find, findOne, findById };
|
||||
};
|
||||
160
backend-pg/src/services/integration/integration-service.ts
Normal file
160
backend-pg/src/services/integration/integration-service.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TIntegrationAuthDalFactory } from "../integration-auth/integration-auth-dal";
|
||||
import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TIntegrationDalFactory } from "./integration-dal";
|
||||
import {
|
||||
TCreateIntegrationDTO,
|
||||
TDeleteIntegrationDTO,
|
||||
TUpdateIntegrationDTO
|
||||
} from "./integration-types";
|
||||
|
||||
type TIntegrationServiceFactoryDep = {
|
||||
integrationDal: TIntegrationDalFactory;
|
||||
integrationAuthDal: TIntegrationAuthDalFactory;
|
||||
folderDal: Pick<TSecretFolderDalFactory, "findBySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
|
||||
|
||||
export const integrationServiceFactory = ({
|
||||
integrationDal,
|
||||
integrationAuthDal,
|
||||
folderDal,
|
||||
permissionService
|
||||
}: TIntegrationServiceFactoryDep) => {
|
||||
const createIntegration = async ({
|
||||
app,
|
||||
actor,
|
||||
path,
|
||||
appId,
|
||||
owner,
|
||||
scope,
|
||||
actorId,
|
||||
region,
|
||||
isActive,
|
||||
metadata,
|
||||
secretPath,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
integrationAuthId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId
|
||||
}: TCreateIntegrationDTO) => {
|
||||
const integrationAuth = await integrationAuthDal.findById(integrationAuthId);
|
||||
if (!integrationAuth) throw new BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integrationAuth.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const folder = await folderDal.findBySecretPath(
|
||||
integrationAuth.projectId,
|
||||
sourceEnvironment,
|
||||
secretPath
|
||||
);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||
|
||||
const integration = await integrationDal.create({
|
||||
envId: folder.envId,
|
||||
secretPath,
|
||||
isActive,
|
||||
integrationAuthId,
|
||||
targetEnvironmentId,
|
||||
targetEnvironment,
|
||||
targetServiceId,
|
||||
targetService,
|
||||
metadata,
|
||||
region,
|
||||
scope,
|
||||
owner,
|
||||
appId,
|
||||
path,
|
||||
app,
|
||||
integration: integrationAuth.integration
|
||||
});
|
||||
|
||||
// TODO(akhilmhdh-pg): audit log
|
||||
return integration;
|
||||
};
|
||||
|
||||
const updateIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
targetEnvironment,
|
||||
app,
|
||||
id,
|
||||
appId,
|
||||
owner,
|
||||
isActive,
|
||||
environment,
|
||||
secretPath
|
||||
}: TUpdateIntegrationDTO) => {
|
||||
const integration = await integrationDal.findById(id);
|
||||
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const folder = await folderDal.findBySecretPath(integration.projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||
|
||||
const updatedIntegration = await integrationDal.updateById(id, {
|
||||
envId: folder.envId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
secretPath
|
||||
});
|
||||
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const deleteIntegration = async ({ actorId, id, actor }: TDeleteIntegrationDTO) => {
|
||||
const integration = await integrationDal.findById(id);
|
||||
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const deletedIntegration = await integrationDal.deleteById(id);
|
||||
return deletedIntegration;
|
||||
};
|
||||
|
||||
return {
|
||||
createIntegration,
|
||||
updateIntegration,
|
||||
deleteIntegration
|
||||
};
|
||||
};
|
||||
41
backend-pg/src/services/integration/integration-types.ts
Normal file
41
backend-pg/src/services/integration/integration-types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateIntegrationDTO = {
|
||||
secretPath: string;
|
||||
integrationAuthId: string;
|
||||
app?: string;
|
||||
isActive: boolean;
|
||||
appId?: string;
|
||||
sourceEnvironment: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
owner?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
scope?: string;
|
||||
metadata?: {
|
||||
secretPrefix?: string;
|
||||
secretSuffix?: string;
|
||||
secretGCPLabel?: {
|
||||
labelName: string;
|
||||
labelValue: string;
|
||||
};
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIntegrationDTO = {
|
||||
id: string;
|
||||
app: string;
|
||||
appId: string;
|
||||
isActive?: boolean;
|
||||
secretPath: string;
|
||||
targetEnvironment: string;
|
||||
owner: string;
|
||||
environment: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
35
backend-pg/src/services/project-bot/project-bot-dal.ts
Normal file
35
backend-pg/src/services/project-bot/project-bot-dal.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName,TProjectBots } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectBotDalFactory = ReturnType<typeof projectBotDalFactory>;
|
||||
|
||||
export const projectBotDalFactory = (db: TDbClient) => {
|
||||
const projectBotOrm = ormify(db, TableName.ProjectBot);
|
||||
|
||||
const findOne = async (filter: Partial<TProjectBots>, tx?: Knex) => {
|
||||
try {
|
||||
const bot = await (tx || db)(TableName.ProjectBot)
|
||||
.where(filter)
|
||||
.join(TableName.Users, `${TableName.ProjectBot}.senderId`, `${TableName.Users}.id`)
|
||||
.join(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.ProjectBot))
|
||||
.select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey).as("senderPubKey"))
|
||||
.first();
|
||||
if (!bot) return bot;
|
||||
const { senderPubKey, ...el } = bot;
|
||||
return { ...el, sender: { publicKey: senderPubKey } };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find on project bot" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...projectBotOrm, findOne };
|
||||
};
|
||||
183
backend-pg/src/services/project-bot/project-bot-service.ts
Normal file
183
backend-pg/src/services/project-bot/project-bot-service.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
} from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TProjectBotDalFactory } from "./project-bot-dal";
|
||||
import { TSetActiveStateDTO } from "./project-bot-types";
|
||||
|
||||
type TProjectBotServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectBotDal: TProjectBotDalFactory;
|
||||
};
|
||||
|
||||
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
||||
|
||||
export const projectBotServiceFactory = ({
|
||||
projectBotDal,
|
||||
permissionService
|
||||
}: TProjectBotServiceFactoryDep) => {
|
||||
const getBotKey = async (projectId: string) => {
|
||||
const appCfg = getConfig();
|
||||
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
||||
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
||||
|
||||
const bot = await projectBotDal.findOne({ projectId });
|
||||
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
||||
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
||||
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||
throw new BadRequestError({ message: "Encryption key missing" });
|
||||
|
||||
if (rootEncryptionKey && bot.keyEncoding === SecretKeyEncoding.BASE64) {
|
||||
const privateKeyBot = decryptSymmetric({
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
key: rootEncryptionKey
|
||||
});
|
||||
return decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: privateKeyBot,
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
}
|
||||
if (encryptionKey && bot.keyEncoding === SecretKeyEncoding.UTF8) {
|
||||
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
return decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: privateKeyBot,
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Failed to obtain bot copy of workspace key needed for operation"
|
||||
});
|
||||
};
|
||||
|
||||
const findBotByProjectId = async ({ actorId, actor, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
const appCfg = getConfig();
|
||||
|
||||
const bot = await projectBotDal.transaction(async (tx) => {
|
||||
const doc = await projectBotDal.findOne({ projectId }, tx);
|
||||
if (doc) return doc;
|
||||
|
||||
const { publicKey, privateKey } = generateAsymmetricKeyPair();
|
||||
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
||||
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
||||
return projectBotDal.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
projectId,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.BASE64
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
if (appCfg.ENCRYPTION_KEY) {
|
||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(
|
||||
privateKey,
|
||||
appCfg.ENCRYPTION_KEY
|
||||
);
|
||||
return projectBotDal.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
projectId,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
|
||||
});
|
||||
return bot;
|
||||
};
|
||||
|
||||
const setBotActiveState = async ({
|
||||
actor,
|
||||
botId,
|
||||
botKey,
|
||||
actorId,
|
||||
isActive
|
||||
}: TSetActiveStateDTO) => {
|
||||
const bot = await projectBotDal.findOne({ id: botId });
|
||||
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
bot.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
if (isActive) {
|
||||
if (!botKey?.nonce || !botKey?.encryptionKey) {
|
||||
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
||||
}
|
||||
const doc = await projectBotDal.updateById(botId, {
|
||||
isActive: true,
|
||||
encryptedProjectKey: botKey.encryptionKey,
|
||||
encryptedProjectKeyNonce: botKey.nonce,
|
||||
senderId: actorId
|
||||
});
|
||||
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
|
||||
return doc;
|
||||
}
|
||||
|
||||
const doc = await projectBotDal.updateById(botId, {
|
||||
isActive: false,
|
||||
encryptedProjectKey: null,
|
||||
encryptedProjectKeyNonce: null
|
||||
});
|
||||
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
|
||||
return doc;
|
||||
};
|
||||
|
||||
return {
|
||||
findBotByProjectId,
|
||||
setBotActiveState,
|
||||
getBotKey
|
||||
};
|
||||
};
|
||||
10
backend-pg/src/services/project-bot/project-bot-types.ts
Normal file
10
backend-pg/src/services/project-bot/project-bot-types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TSetActiveStateDTO = {
|
||||
isActive: boolean;
|
||||
botKey?: {
|
||||
nonce?: string;
|
||||
encryptionKey?: string;
|
||||
};
|
||||
botId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -37,7 +37,7 @@ const sqlFindFolderByPathQuery = (
|
||||
depth: 1,
|
||||
path: db.raw("'/'")
|
||||
})
|
||||
.select(selectAllTableCols(db, TableName.SecretFolder))
|
||||
.select(selectAllTableCols(TableName.SecretFolder))
|
||||
.from(TableName.SecretFolder)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
@@ -60,7 +60,7 @@ const sqlFindFolderByPathQuery = (
|
||||
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
|
||||
)
|
||||
})
|
||||
.select(selectAllTableCols(db, TableName.SecretFolder))
|
||||
.select(selectAllTableCols(TableName.SecretFolder))
|
||||
.whereRaw(
|
||||
`depth = array_position(ARRAY[${pathSegments
|
||||
.map(() => "?")
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDalFactory } from "./secret-folder-dal";
|
||||
import { TSecretFolderDalFactory } from "./secret-folder-dal";
|
||||
import {
|
||||
TCreateFolderDTO,
|
||||
TDeleteFolderDTO,
|
||||
|
||||
Reference in New Issue
Block a user