feat(infisical-pg): completed integration, integration auth and project bot api migration

This commit is contained in:
Akhil Mohan
2023-12-25 12:32:32 +05:30
parent 010963a80c
commit 771bec6d6d
36 changed files with 5245 additions and 9 deletions

57
backend-pg/folder.ts Normal file
View 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();

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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,

View 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);
}

View 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);
}

View File

@@ -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";

View 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>>;

View 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>>;

View File

@@ -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"
}

View 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>>;

View File

@@ -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) }));

View File

@@ -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;

View File

@@ -6,6 +6,7 @@ export {
decryptSymmetric128BitHexKeyUTF8,
encryptAsymmetric,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8
encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair
} from "./encryption";
export { generateSrpServerKey, srpCheckClientProof } from "./srp";

View File

@@ -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"];

View File

@@ -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", {

View 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 };
}
});
};

View File

@@ -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" });
};

View 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 };
}
});
};

View 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
};

File diff suppressed because it is too large Load Diff

View File

@@ -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;
};

File diff suppressed because it is too large Load Diff

View File

@@ -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;
};

View 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;
};

View 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" });
}
};

View 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");
}
};

View 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 };
};

View 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
};
};

View 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">;

View 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 };
};

View 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
};
};

View 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">;

View File

@@ -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(() => "?")

View File

@@ -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,