Compare commits
52 Commits
ssh-certs
...
ssh-cli-do
Author | SHA1 | Date | |
---|---|---|---|
e9564f5231 | |||
05cdca9202 | |||
5ab0c66dee | |||
f5a0641671 | |||
2843818395 | |||
2357f3bc1f | |||
cde813aafb | |||
bbc8091d44 | |||
ce5e591457 | |||
5ae74f9761 | |||
eef331bbd1 | |||
d5c2e9236a | |||
13eef7e524 | |||
f97f98b2c3 | |||
3fa84c578c | |||
c22ed04733 | |||
64fac1979f | |||
2d60f389c2 | |||
7798e5a2ad | |||
ed78227725 | |||
89848a2f5c | |||
1936f7cc4f | |||
058475fc3f | |||
ee4eb7f84b | |||
8122433f5c | |||
62968c5e43 | |||
f3cf1a3f50 | |||
b4b417658f | |||
e70ca57510 | |||
06f321e4bf | |||
3c3fcd0db8 | |||
21eb2bed7e | |||
31a21a432d | |||
7eb05afe2a | |||
0b54948b15 | |||
39e598e408 | |||
b735618601 | |||
3a5e862def | |||
d1c4a9c75a | |||
5532844ee7 | |||
dd5aab973f | |||
ced12baf5d | |||
7db1e62654 | |||
0ab3ae442e | |||
ed9472efc8 | |||
e094844601 | |||
e761b49964 | |||
765280eef6 | |||
7947e73569 | |||
52ce90846a | |||
be36827392 | |||
097512c691 |
17
.env.example
@ -88,3 +88,20 @@ PLAIN_WISH_LABEL_IDS=
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
||||
|
||||
# App Connections
|
||||
|
||||
# aws assume-role
|
||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID=
|
||||
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
# github oauth
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
|
||||
|
||||
#github app
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
||||
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID=
|
2
backend/src/@types/fastify.d.ts
vendored
@ -36,6 +36,7 @@ import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-cert
|
||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
@ -208,6 +209,7 @@ declare module "fastify" {
|
||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||
projectTemplate: TProjectTemplateServiceFactory;
|
||||
totp: TTotpServiceFactory;
|
||||
appConnection: TAppConnectionServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
6
backend/src/@types/knex.d.ts
vendored
@ -363,6 +363,7 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
|
||||
import {
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
@ -886,5 +887,10 @@ declare module "knex/types/tables" {
|
||||
TProjectSplitBackfillIdsInsert,
|
||||
TProjectSplitBackfillIdsUpdate
|
||||
>;
|
||||
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
|
||||
TAppConnections,
|
||||
TAppConnectionsInsert,
|
||||
TAppConnectionsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
28
backend/src/db/migrations/20241218181018_app-connection.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.AppConnection))) {
|
||||
await knex.schema.createTable(TableName.AppConnection, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name", 32).notNullable();
|
||||
t.string("description");
|
||||
t.string("app").notNullable();
|
||||
t.string("method").notNullable();
|
||||
t.binary("encryptedCredentials").notNullable();
|
||||
t.integer("version").defaultTo(1).notNullable();
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.AppConnection);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AppConnection);
|
||||
await dropOnUpdateTrigger(knex, TableName.AppConnection);
|
||||
}
|
27
backend/src/db/schemas/app-connections.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// 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 { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AppConnectionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
app: z.string(),
|
||||
method: z.string(),
|
||||
encryptedCredentials: zodBuffer,
|
||||
version: z.number().default(1),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||
export type TAppConnectionsInsert = Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>;
|
||||
export type TAppConnectionsUpdate = Partial<Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>>;
|
@ -129,7 +129,8 @@ export enum TableName {
|
||||
KmsKeyVersion = "kms_key_versions",
|
||||
WorkflowIntegrations = "workflow_integrations",
|
||||
SlackIntegrations = "slack_integrations",
|
||||
ProjectSlackConfigs = "project_slack_configs"
|
||||
ProjectSlackConfigs = "project_slack_configs",
|
||||
AppConnection = "app_connections"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
),
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
description: z.string().trim().nullish(),
|
||||
permissions: z.any().array()
|
||||
}),
|
||||
response: {
|
||||
@ -95,7 +95,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
)
|
||||
.optional(),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
description: z.string().trim().nullish(),
|
||||
permissions: z.any().array().optional()
|
||||
}),
|
||||
response: {
|
||||
|
@ -39,7 +39,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
)
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
@ -95,7 +95,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||
.optional(),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
}),
|
||||
response: {
|
||||
|
@ -36,7 +36,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
)
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
@ -91,7 +91,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
}),
|
||||
response: {
|
||||
|
@ -6,6 +6,8 @@ import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-a
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
@ -222,7 +224,12 @@ export enum EventType {
|
||||
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
||||
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
||||
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||
APPLY_PROJECT_TEMPLATE = "apply-project-template"
|
||||
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
||||
GET_APP_CONNECTIONS = "get-app-connections",
|
||||
GET_APP_CONNECTION = "get-app-connection",
|
||||
CREATE_APP_CONNECTION = "create-app-connection",
|
||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||
DELETE_APP_CONNECTION = "delete-app-connection"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -1867,6 +1874,39 @@ interface ApplyProjectTemplateEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAppConnectionsEvent {
|
||||
type: EventType.GET_APP_CONNECTIONS;
|
||||
metadata: {
|
||||
app?: AppConnection;
|
||||
count: number;
|
||||
connectionIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAppConnectionEvent {
|
||||
type: EventType.GET_APP_CONNECTION;
|
||||
metadata: {
|
||||
connectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateAppConnectionEvent {
|
||||
type: EventType.CREATE_APP_CONNECTION;
|
||||
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
|
||||
}
|
||||
|
||||
interface UpdateAppConnectionEvent {
|
||||
type: EventType.UPDATE_APP_CONNECTION;
|
||||
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
|
||||
}
|
||||
|
||||
interface DeleteAppConnectionEvent {
|
||||
type: EventType.DELETE_APP_CONNECTION;
|
||||
metadata: {
|
||||
connectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -2038,4 +2078,9 @@ export type Event =
|
||||
| CreateProjectTemplateEvent
|
||||
| UpdateProjectTemplateEvent
|
||||
| DeleteProjectTemplateEvent
|
||||
| ApplyProjectTemplateEvent;
|
||||
| ApplyProjectTemplateEvent
|
||||
| GetAppConnectionsEvent
|
||||
| GetAppConnectionEvent
|
||||
| CreateAppConnectionEvent
|
||||
| UpdateAppConnectionEvent
|
||||
| DeleteAppConnectionEvent;
|
||||
|
@ -49,7 +49,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
},
|
||||
pkiEst: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false
|
||||
projectTemplates: false,
|
||||
appConnections: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -67,6 +67,7 @@ export type TFeatureSet = {
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: false;
|
||||
appConnections: false; // TODO: remove once live
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -27,7 +27,8 @@ export enum OrgPermissionSubjects {
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates"
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@ -46,6 +47,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
@ -123,6 +125,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return rules;
|
||||
@ -153,6 +160,8 @@ const buildMemberPermission = () => {
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||
import {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
|
||||
export const GROUPS = {
|
||||
CREATE: {
|
||||
name: "The name of the group to create.",
|
||||
@ -1605,3 +1608,34 @@ export const ProjectTemplates = {
|
||||
templateId: "The ID of the project template to be deleted."
|
||||
}
|
||||
};
|
||||
|
||||
export const AppConnections = {
|
||||
GET_BY_ID: (app: AppConnection) => ({
|
||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||
}),
|
||||
GET_BY_NAME: (app: AppConnection) => ({
|
||||
connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||
}),
|
||||
CREATE: (app: AppConnection) => {
|
||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||
return {
|
||||
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||
description: `An optional description for the ${appName} Connection.`,
|
||||
credentials: `The credentials used to connect with ${appName}.`,
|
||||
method: `The method used to authenticate with ${appName}.`
|
||||
};
|
||||
},
|
||||
UPDATE: (app: AppConnection) => {
|
||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||
return {
|
||||
connectionId: `The ID of the ${appName} Connection to be updated.`,
|
||||
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||
description: `The updated description of the ${appName} Connection.`,
|
||||
credentials: `The credentials used to connect with ${appName}.`,
|
||||
method: `The method used to authenticate with ${appName}.`
|
||||
};
|
||||
},
|
||||
DELETE: (app: AppConnection) => ({
|
||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
|
||||
})
|
||||
};
|
||||
|
@ -180,7 +180,24 @@ const envSchema = z
|
||||
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||
|
||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false")
|
||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
|
||||
// aws
|
||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()),
|
||||
|
||||
// github oauth
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// github app
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional())
|
||||
})
|
||||
// To ensure that basic encryption is always possible.
|
||||
.refine(
|
||||
|
@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
|
||||
if (str.startsWith("/")) return str;
|
||||
return `/${str}`;
|
||||
};
|
||||
|
||||
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
||||
|
@ -43,6 +43,8 @@ export type RequiredKeys<T> = {
|
||||
|
||||
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||
|
||||
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||
|
||||
export enum EnforcementLevel {
|
||||
Hard = "hard",
|
||||
Soft = "soft"
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||
import opentelemetry from "@opentelemetry/api";
|
||||
import fastifyPlugin from "fastify-plugin";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
BadRequestError,
|
||||
DatabaseError,
|
||||
@ -35,8 +37,30 @@ enum HttpStatusCodes {
|
||||
}
|
||||
|
||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||
const errorHistogram = apiMeter.createHistogram("API_errors", {
|
||||
description: "API errors by type, status code, and name",
|
||||
unit: "1"
|
||||
});
|
||||
|
||||
server.setErrorHandler((error, req, res) => {
|
||||
req.log.error(error);
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
const { method } = req;
|
||||
const route = req.routerPath;
|
||||
const errorType =
|
||||
error instanceof jwt.JsonWebTokenError ? "TokenError" : error.constructor.name || "UnknownError";
|
||||
|
||||
errorHistogram.record(1, {
|
||||
route,
|
||||
method,
|
||||
type: errorType,
|
||||
name: error.name
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof BadRequestError) {
|
||||
void res
|
||||
.status(HttpStatusCodes.BadRequest)
|
||||
@ -52,13 +76,20 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
message: error.message,
|
||||
error: error.name
|
||||
});
|
||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
||||
} else if (error instanceof DatabaseError) {
|
||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||
reqId: req.id,
|
||||
statusCode: HttpStatusCodes.InternalServerError,
|
||||
message: "Something went wrong",
|
||||
error: error.name
|
||||
});
|
||||
} else if (error instanceof InternalServerError) {
|
||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||
reqId: req.id,
|
||||
statusCode: HttpStatusCodes.InternalServerError,
|
||||
message: error.message ?? "Something went wrong",
|
||||
error: error.name
|
||||
});
|
||||
} else if (error instanceof GatewayTimeoutError) {
|
||||
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
||||
reqId: req.id,
|
||||
|
@ -91,6 +91,8 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||
@ -314,6 +316,7 @@ export const registerRoutes = async (
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
const trustedIpDAL = trustedIpDALFactory(db);
|
||||
const telemetryDAL = telemetryDALFactory(db);
|
||||
const appConnectionDAL = appConnectionDALFactory(db);
|
||||
|
||||
// ee db layer ops
|
||||
const permissionDAL = permissionDALFactory(db);
|
||||
@ -1352,6 +1355,13 @@ export const registerRoutes = async (
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
const appConnectionService = appConnectionServiceFactory({
|
||||
appConnectionDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
|
||||
// setup the communication with license key server
|
||||
@ -1448,7 +1458,8 @@ export const registerRoutes = async (
|
||||
migration: migrationService,
|
||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||
projectTemplate: projectTemplateService,
|
||||
totp: totpService
|
||||
totp: totpService,
|
||||
appConnection: appConnectionService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -0,0 +1,74 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAwsConnectionSchema.options,
|
||||
...SanitizedGitHubConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AwsConnectionListItemSchema,
|
||||
GitHubConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List the available App Connection Options.",
|
||||
response: {
|
||||
200: z.object({
|
||||
appConnectionOptions: AppConnectionOptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: () => {
|
||||
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
|
||||
return { appConnectionOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List all the App Connections for the current organization.",
|
||||
response: {
|
||||
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_APP_CONNECTIONS,
|
||||
metadata: {
|
||||
count: appConnections.length,
|
||||
connectionIds: appConnections.map((connection) => connection.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnections };
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,274 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { startsWithVowel } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
|
||||
server,
|
||||
app,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
responseSchema
|
||||
}: {
|
||||
app: AppConnection;
|
||||
server: FastifyZodProvider;
|
||||
createSchema: z.ZodType<{
|
||||
name: string;
|
||||
method: I["method"];
|
||||
credentials: I["credentials"];
|
||||
description?: string | null;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `List the ${appName} Connections for the current organization.`,
|
||||
response: {
|
||||
200: z.object({ appConnections: responseSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_APP_CONNECTIONS,
|
||||
metadata: {
|
||||
app,
|
||||
count: appConnections.length,
|
||||
connectionIds: appConnections.map((connection) => connection.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnections };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:connectionId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the specified ${appName} Connection by ID.`,
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ appConnection: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const appConnection = (await server.services.appConnection.findAppConnectionById(
|
||||
app,
|
||||
connectionId,
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_APP_CONNECTION,
|
||||
metadata: {
|
||||
connectionId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnection };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/name/:connectionName`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the specified ${appName} Connection by name.`,
|
||||
params: z.object({
|
||||
connectionName: z
|
||||
.string()
|
||||
.min(0, "Connection name required")
|
||||
.describe(AppConnections.GET_BY_NAME(app).connectionName)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ appConnection: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { connectionName } = req.params;
|
||||
|
||||
const appConnection = (await server.services.appConnection.findAppConnectionByName(
|
||||
app,
|
||||
connectionName,
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_APP_CONNECTION,
|
||||
metadata: {
|
||||
connectionId: appConnection.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnection };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Create ${
|
||||
startsWithVowel(appName) ? "an" : "a"
|
||||
} ${appName} Connection for the current organization.`,
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: z.object({ appConnection: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, method, credentials, description } = req.body;
|
||||
|
||||
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||
{ name, method, app, credentials, description },
|
||||
req.permission
|
||||
)) as TAppConnection;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_APP_CONNECTION,
|
||||
metadata: {
|
||||
name,
|
||||
method,
|
||||
app,
|
||||
connectionId: appConnection.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnection };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:connectionId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Update the specified ${appName} Connection.`,
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid().describe(AppConnections.UPDATE(app).connectionId)
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: z.object({ appConnection: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, credentials, description } = req.body;
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||
{ name, credentials, connectionId, description },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_APP_CONNECTION,
|
||||
metadata: {
|
||||
name,
|
||||
description,
|
||||
credentialsUpdated: Boolean(credentials),
|
||||
connectionId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnection };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: `/:connectionId`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Delete the specified ${appName} Connection.`,
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ appConnection: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const appConnection = (await server.services.appConnection.deleteAppConnection(
|
||||
app,
|
||||
connectionId,
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_APP_CONNECTION,
|
||||
metadata: {
|
||||
connectionId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { appConnection };
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateAwsConnectionSchema,
|
||||
SanitizedAwsConnectionSchema,
|
||||
UpdateAwsConnectionSchema
|
||||
} from "@app/services/app-connection/aws";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.AWS,
|
||||
server,
|
||||
responseSchema: SanitizedAwsConnectionSchema,
|
||||
createSchema: CreateAwsConnectionSchema,
|
||||
updateSchema: UpdateAwsConnectionSchema
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateGitHubConnectionSchema,
|
||||
SanitizedGitHubConnectionSchema,
|
||||
UpdateGitHubConnectionSchema
|
||||
} from "@app/services/app-connection/github";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.GitHub,
|
||||
server,
|
||||
responseSchema: SanitizedGitHubConnectionSchema,
|
||||
createSchema: CreateGitHubConnectionSchema,
|
||||
updateSchema: UpdateGitHubConnectionSchema
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from "./app-connection-router";
|
||||
export * from "./apps";
|
@ -1,3 +1,4 @@
|
||||
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers";
|
||||
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
||||
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||
|
||||
@ -110,4 +111,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
||||
|
||||
await server.register(
|
||||
async (appConnectionsRouter) => {
|
||||
await appConnectionsRouter.register(registerAppConnectionRouter);
|
||||
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) {
|
||||
await appConnectionsRouter.register(router, { prefix: `/${app}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/app-connections" }
|
||||
);
|
||||
};
|
||||
|
11
backend/src/services/app-connection/app-connection-dal.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
||||
|
||||
export const appConnectionDALFactory = (db: TDbClient) => {
|
||||
const appConnectionOrm = ormify(db, TableName.AppConnection);
|
||||
|
||||
return { ...appConnectionOrm };
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum AppConnection {
|
||||
GitHub = "github",
|
||||
AWS = "aws"
|
||||
}
|
92
backend/src/services/app-connection/app-connection-fns.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
getAwsAppConnectionListItem,
|
||||
validateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
getGitHubConnectionListItem,
|
||||
GitHubConnectionMethod,
|
||||
validateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
export const encryptAppConnectionCredentials = async ({
|
||||
orgId,
|
||||
credentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
credentials: TAppConnection["credentials"];
|
||||
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials))
|
||||
});
|
||||
|
||||
return encryptedCredentialsBlob;
|
||||
};
|
||||
|
||||
export const decryptAppConnectionCredentials = async ({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedCredentials: Buffer;
|
||||
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedCredentials
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
appConnection: TAppConnectionConfig
|
||||
): Promise<TAppConnection["credentials"]> => {
|
||||
const { app } = appConnection;
|
||||
switch (app) {
|
||||
case AppConnection.AWS: {
|
||||
return validateAwsConnectionCredentials(appConnection);
|
||||
}
|
||||
case AppConnection.GitHub:
|
||||
return validateGitHubConnectionCredentials(appConnection);
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection ${app}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return "GitHub App";
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return "OAuth";
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
return "Access Key";
|
||||
case AwsConnectionMethod.AssumeRole:
|
||||
return "Assume Role";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
|
||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AWS]: "AWS",
|
||||
[AppConnection.GitHub]: "GitHub"
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
|
||||
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||
encryptedCredentials: true,
|
||||
app: true,
|
||||
method: true
|
||||
});
|
||||
|
||||
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.CREATE(app).description)
|
||||
});
|
||||
|
||||
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.UPDATE(app).description)
|
||||
});
|
360
backend/src/services/app-connection/app-connection-service.ts
Normal file
@ -0,0 +1,360 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
decryptAppConnectionCredentials,
|
||||
encryptAppConnectionCredentials,
|
||||
getAppConnectionMethodName,
|
||||
listAppConnectionOptions,
|
||||
validateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TCreateAppConnectionDTO,
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
|
||||
};
|
||||
|
||||
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
appConnectionDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
licenseService
|
||||
}: TAppConnectionServiceFactoryDep) => {
|
||||
// app connections are disabled for public until launch
|
||||
const checkAppServicesAvailability = async (orgId: string) => {
|
||||
const subscription = await licenseService.getPlan(orgId);
|
||||
|
||||
if (!subscription.appConnections) throw new BadRequestError({ message: "App Connections are not available yet." });
|
||||
};
|
||||
|
||||
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
||||
await checkAppServicesAvailability(actor.orgId);
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
const appConnections = await appConnectionDAL.find(
|
||||
app
|
||||
? { orgId: actor.orgId, app }
|
||||
: {
|
||||
orgId: actor.orgId
|
||||
}
|
||||
);
|
||||
|
||||
return Promise.all(
|
||||
appConnections
|
||||
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||
.map(async ({ encryptedCredentials, ...connection }) => {
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
encryptedCredentials,
|
||||
kmsService,
|
||||
orgId: connection.orgId
|
||||
});
|
||||
|
||||
return {
|
||||
...connection,
|
||||
credentials
|
||||
} as TAppConnection;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const findAppConnectionById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||
await checkAppServicesAvailability(actor.orgId);
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||
|
||||
return {
|
||||
...appConnection,
|
||||
credentials: await decryptAppConnectionCredentials({
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
})
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
||||
await checkAppServicesAvailability(actor.orgId);
|
||||
|
||||
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId });
|
||||
|
||||
if (!appConnection)
|
||||
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
||||
|
||||
return {
|
||||
...appConnection,
|
||||
credentials: await decryptAppConnectionCredentials({
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
})
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
const createAppConnection = async (
|
||||
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
await checkAppServicesAvailability(actor.orgId);
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||
const isConflictingName = Boolean(
|
||||
await appConnectionDAL.findOne(
|
||||
{
|
||||
name: params.name,
|
||||
orgId: actor.orgId
|
||||
},
|
||||
tx
|
||||
)
|
||||
);
|
||||
|
||||
if (isConflictingName)
|
||||
throw new BadRequestError({
|
||||
message: `An App Connection with the name "${params.name}" already exists`
|
||||
});
|
||||
|
||||
const validatedCredentials = await validateAppConnectionCredentials({
|
||||
app,
|
||||
credentials,
|
||||
method,
|
||||
orgId: actor.orgId
|
||||
} as TAppConnectionConfig);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const connection = await appConnectionDAL.create(
|
||||
{
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
method,
|
||||
app,
|
||||
...params
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return {
|
||||
...connection,
|
||||
credentials: validatedCredentials
|
||||
};
|
||||
});
|
||||
|
||||
return appConnection;
|
||||
};
|
||||
|
||||
const updateAppConnection = async (
|
||||
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
await checkAppServicesAvailability(actor.orgId);
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||
if (params.name && appConnection.name !== params.name) {
|
||||
const isConflictingName = Boolean(
|
||||
await appConnectionDAL.findOne(
|
||||
{
|
||||
name: params.name,
|
||||
orgId: appConnection.orgId
|
||||
},
|
||||
tx
|
||||
)
|
||||
);
|
||||
|
||||
if (isConflictingName)
|
||||
throw new BadRequestError({
|
||||
message: `An App Connection with the name "${params.name}" already exists`
|
||||
});
|
||||
}
|
||||
|
||||
let encryptedCredentials: undefined | Buffer;
|
||||
|
||||
if (credentials) {
|
||||
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||
|
||||
if (
|
||||
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||
method,
|
||||
credentials
|
||||
}).success
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message: `Invalid credential format for ${
|
||||
APP_CONNECTION_NAME_MAP[app]
|
||||
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||
});
|
||||
|
||||
const validatedCredentials = await validateAppConnectionCredentials({
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials,
|
||||
method
|
||||
} as TAppConnectionConfig);
|
||||
|
||||
if (!validatedCredentials)
|
||||
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||
|
||||
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
const updatedConnection = await appConnectionDAL.updateById(
|
||||
connectionId,
|
||||
{
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
...params
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return updatedConnection;
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedAppConnection,
|
||||
credentials: await decryptAppConnectionCredentials({
|
||||
encryptedCredentials: updatedAppConnection.encryptedCredentials,
|
||||
orgId: updatedAppConnection.orgId,
|
||||
kmsService
|
||||
})
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||
await checkAppServicesAvailability(actor.orgId);
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||
|
||||
// TODO: specify delete error message if due to existing dependencies
|
||||
|
||||
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
|
||||
|
||||
return {
|
||||
...deletedAppConnection,
|
||||
credentials: await decryptAppConnectionCredentials({
|
||||
encryptedCredentials: deletedAppConnection.encryptedCredentials,
|
||||
orgId: deletedAppConnection.orgId,
|
||||
kmsService
|
||||
})
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
return {
|
||||
listAppConnectionOptions,
|
||||
listAppConnectionsByOrg,
|
||||
findAppConnectionById,
|
||||
findAppConnectionByName,
|
||||
createAppConnection,
|
||||
updateAppConnection,
|
||||
deleteAppConnection
|
||||
};
|
||||
};
|
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
TAwsConnectionInput,
|
||||
TValidateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
TGitHubConnection,
|
||||
TGitHubConnectionConfig,
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
|
||||
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
||||
|
||||
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
"credentials" | "method" | "name" | "app" | "description"
|
||||
>;
|
||||
|
||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentials =
|
||||
| TValidateAwsConnectionCredentials
|
||||
| TValidateGitHubConnectionCredentials;
|
@ -0,0 +1,4 @@
|
||||
export enum AwsConnectionMethod {
|
||||
AssumeRole = "assume-role",
|
||||
AccessKey = "access-key"
|
||||
}
|
105
backend/src/services/app-connection/aws/aws-connection-fns.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import AWS from "aws-sdk";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||
import { TAwsConnectionConfig } from "./aws-connection-types";
|
||||
|
||||
export const getAwsAppConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
|
||||
|
||||
return {
|
||||
name: "AWS" as const,
|
||||
app: AppConnection.AWS as const,
|
||||
methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey],
|
||||
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
|
||||
};
|
||||
};
|
||||
|
||||
export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = "us-east-1") => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
let accessKeyId: string;
|
||||
let secretAccessKey: string;
|
||||
let sessionToken: undefined | string;
|
||||
|
||||
const { method, credentials, orgId } = appConnection;
|
||||
|
||||
switch (method) {
|
||||
case AwsConnectionMethod.AssumeRole: {
|
||||
const client = new STSClient({
|
||||
region,
|
||||
credentials:
|
||||
appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID && appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
: undefined // if hosting on AWS
|
||||
});
|
||||
|
||||
const command = new AssumeRoleCommand({
|
||||
RoleArn: credentials.roleArn,
|
||||
RoleSessionName: `infisical-app-connection-${randomUUID()}`,
|
||||
DurationSeconds: 900, // 15 mins
|
||||
ExternalId: orgId
|
||||
});
|
||||
|
||||
const assumeRes = await client.send(command);
|
||||
|
||||
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
|
||||
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
||||
}
|
||||
|
||||
accessKeyId = assumeRes.Credentials.AccessKeyId;
|
||||
secretAccessKey = assumeRes.Credentials.SecretAccessKey;
|
||||
sessionToken = assumeRes.Credentials?.SessionToken;
|
||||
break;
|
||||
}
|
||||
case AwsConnectionMethod.AccessKey: {
|
||||
accessKeyId = credentials.accessKeyId;
|
||||
secretAccessKey = credentials.secretAccessKey;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new InternalServerError({ message: `Unsupported AWS connection method: ${method}` });
|
||||
}
|
||||
|
||||
return new AWS.Config({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
||||
const awsConfig = await getAwsConnectionConfig(appConnection);
|
||||
const sts = new AWS.STS(awsConfig);
|
||||
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
|
||||
|
||||
try {
|
||||
resp = await sts.getCallerIdentity().promise();
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
});
|
||||
}
|
||||
|
||||
if (resp.$response.httpResponse.statusCode !== 200)
|
||||
throw new InternalServerError({
|
||||
message: `Unable to validate credentials: ${
|
||||
resp.$response.error?.message ??
|
||||
`AWS responded with a status code of ${resp.$response.httpResponse.statusCode}. Verify credentials and try again.`
|
||||
}`
|
||||
});
|
||||
|
||||
return appConnection.credentials;
|
||||
};
|
@ -0,0 +1,82 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||
|
||||
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
|
||||
roleArn: z.string().trim().min(1, "Role ARN required")
|
||||
});
|
||||
|
||||
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
|
||||
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
|
||||
});
|
||||
|
||||
const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) });
|
||||
|
||||
export const AwsConnectionSchema = z.intersection(
|
||||
BaseAwsConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||
credentials: AwsConnectionAssumeRoleCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||
credentials: AwsConnectionAccessTokenCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAwsConnectionSchema.extend({
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||
credentials: AwsConnectionAssumeRoleCredentialsSchema.omit({ roleArn: true })
|
||||
}),
|
||||
BaseAwsConnectionSchema.extend({
|
||||
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||
credentials: AwsConnectionAccessTokenCredentialsSchema.omit({ secretAccessKey: true })
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AWS).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAwsConnectionSchema = ValidateAwsConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.AWS)
|
||||
);
|
||||
|
||||
export const UpdateAwsConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
|
||||
|
||||
export const AwsConnectionListItemSchema = z.object({
|
||||
name: z.literal("AWS"),
|
||||
app: z.literal(AppConnection.AWS),
|
||||
// the below is preferable but currently breaks mintlify
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(AwsConnectionMethod).array(),
|
||||
accessKeyId: z.string().optional()
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
AwsConnectionSchema,
|
||||
CreateAwsConnectionSchema,
|
||||
ValidateAwsConnectionCredentialsSchema
|
||||
} from "./aws-connection-schemas";
|
||||
|
||||
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
|
||||
|
||||
export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
|
||||
app: AppConnection.AWS;
|
||||
};
|
||||
|
||||
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
|
||||
|
||||
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
4
backend/src/services/app-connection/aws/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./aws-connection-enums";
|
||||
export * from "./aws-connection-fns";
|
||||
export * from "./aws-connection-schemas";
|
||||
export * from "./aws-connection-types";
|
@ -0,0 +1,4 @@
|
||||
export enum GitHubConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
App = "github-app"
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
import { TGitHubConnectionConfig } from "./github-connection-types";
|
||||
|
||||
export const getGitHubConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||
|
||||
return {
|
||||
name: "GitHub" as const,
|
||||
app: AppConnection.GitHub as const,
|
||||
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
||||
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||
};
|
||||
};
|
||||
|
||||
type TokenRespData = {
|
||||
access_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
||||
const { credentials, method } = config;
|
||||
|
||||
const {
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET,
|
||||
SITE_URL
|
||||
} = getConfig();
|
||||
|
||||
const { clientId, clientSecret } =
|
||||
method === GitHubConnectionMethod.App
|
||||
? {
|
||||
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
}
|
||||
: // oauth
|
||||
{
|
||||
clientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
clientSecret: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET
|
||||
};
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new InternalServerError({
|
||||
message: `GitHub ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<TokenRespData>;
|
||||
|
||||
try {
|
||||
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
|
||||
params: {
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code: credentials.code,
|
||||
redirect_uri: `${SITE_URL}/app-connections/github/oauth/callback`
|
||||
},
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenResp.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
|
||||
});
|
||||
}
|
||||
|
||||
if (method === GitHubConnectionMethod.App) {
|
||||
const installationsResp = await request.get<{
|
||||
installations: {
|
||||
id: number;
|
||||
account: {
|
||||
login: string;
|
||||
};
|
||||
}[];
|
||||
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${tokenResp.data.access_token}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
const matchingInstallation = installationsResp.data.installations.find(
|
||||
(installation) => installation.id === +credentials.installationId
|
||||
);
|
||||
|
||||
if (!matchingInstallation) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User does not have access to the provided installation"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return {
|
||||
// access token not needed for GitHub App
|
||||
installationId: credentials.installationId
|
||||
};
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return {
|
||||
accessToken: tokenResp.data.access_token
|
||||
};
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,93 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
|
||||
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required")
|
||||
});
|
||||
|
||||
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "GitHub App code required"),
|
||||
installationId: z.string().min(1, "GitHub App Installation ID required")
|
||||
});
|
||||
|
||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
accessToken: z.string()
|
||||
});
|
||||
|
||||
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
||||
installationId: z.string()
|
||||
});
|
||||
|
||||
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||
credentials: GitHubConnectionAppInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
|
||||
);
|
||||
|
||||
export const UpdateGitHubConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
|
||||
|
||||
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
||||
|
||||
export const GitHubAppConnectionSchema = z.intersection(
|
||||
BaseGitHubConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.App),
|
||||
credentials: GitHubConnectionAppOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseGitHubConnectionSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.App),
|
||||
credentials: GitHubConnectionAppOutputCredentialsSchema.omit({ installationId: true })
|
||||
}),
|
||||
BaseGitHubConnectionSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema.omit({ accessToken: true })
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionListItemSchema = z.object({
|
||||
name: z.literal("GitHub"),
|
||||
app: z.literal(AppConnection.GitHub),
|
||||
// the below is preferable but currently breaks mintlify
|
||||
// methods: z.tuple([z.literal(GitHubConnectionMethod.GitHubApp), z.literal(GitHubConnectionMethod.OAuth)]),
|
||||
methods: z.nativeEnum(GitHubConnectionMethod).array(),
|
||||
oauthClientId: z.string().optional(),
|
||||
appClientSlug: z.string().optional()
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateGitHubConnectionSchema,
|
||||
GitHubAppConnectionSchema,
|
||||
ValidateGitHubConnectionCredentialsSchema
|
||||
} from "./github-connection-schemas";
|
||||
|
||||
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
|
||||
|
||||
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
|
||||
app: AppConnection.GitHub;
|
||||
};
|
||||
|
||||
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||
|
||||
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
4
backend/src/services/app-connection/github/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./github-connection-enums";
|
||||
export * from "./github-connection-fns";
|
||||
export * from "./github-connection-schemas";
|
||||
export * from "./github-connection-types";
|
@ -1397,14 +1397,24 @@ const syncSecretsHeroku = async ({
|
||||
* Sync/push [secrets] to Vercel project named [integration.app]
|
||||
*/
|
||||
const syncSecretsVercel = async ({
|
||||
createManySecretsRawFn,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
secrets: infisicalSecrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
|
||||
integration: TIntegrations & {
|
||||
projectId: string;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
secretPath: string;
|
||||
};
|
||||
integrationAuth: TIntegrationAuths;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
interface VercelSecret {
|
||||
@ -1477,80 +1487,119 @@ const syncSecretsVercel = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const updateSecrets: VercelSecret[] = [];
|
||||
const deleteSecrets: VercelSecret[] = [];
|
||||
const newSecrets: VercelSecret[] = [];
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: secret has been created
|
||||
newSecrets.push({
|
||||
key,
|
||||
value: secrets[key].value,
|
||||
type: "encrypted",
|
||||
target: [integration.targetEnvironment as string],
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
}
|
||||
: {})
|
||||
});
|
||||
// Default to overwrite target for old integrations that doesn't have a initial sync behavior set.
|
||||
if (!metadata.initialSyncBehavior) {
|
||||
metadata.initialSyncBehavior = IntegrationInitialSyncBehavior.OVERWRITE_TARGET;
|
||||
}
|
||||
|
||||
const secretsToAddToInfisical: { [key: string]: VercelSecret } = {};
|
||||
|
||||
Object.keys(res).forEach((vercelKey) => {
|
||||
if (!integration.lastUsed) {
|
||||
// first time using integration
|
||||
// -> apply initial sync behavior
|
||||
switch (metadata.initialSyncBehavior) {
|
||||
// Override all the secrets in Vercel
|
||||
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
|
||||
if (!(vercelKey in infisicalSecrets)) infisicalSecrets[vercelKey] = null;
|
||||
break;
|
||||
}
|
||||
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
|
||||
// if the vercel secret is not in infisical, we need to add it to infisical
|
||||
if (!(vercelKey in infisicalSecrets)) {
|
||||
infisicalSecrets[vercelKey] = {
|
||||
value: res[vercelKey].value
|
||||
};
|
||||
secretsToAddToInfisical[vercelKey] = res[vercelKey];
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`);
|
||||
}
|
||||
}
|
||||
} else if (!(vercelKey in infisicalSecrets)) {
|
||||
infisicalSecrets[vercelKey] = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to update and delete
|
||||
Object.keys(res).forEach((key) => {
|
||||
if (key in secrets) {
|
||||
if (res[key].value !== secrets[key].value) {
|
||||
// case: secret value has changed
|
||||
updateSecrets.push({
|
||||
id: res[key].id,
|
||||
key,
|
||||
value: secrets[key].value,
|
||||
type: res[key].type,
|
||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||
? [...res[key].target]
|
||||
: [...res[key].target, integration.targetEnvironment as string],
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
}
|
||||
: {})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: secret has been deleted
|
||||
deleteSecrets.push({
|
||||
id: res[key].id,
|
||||
key,
|
||||
value: res[key].value,
|
||||
type: "encrypted", // value doesn't matter
|
||||
target: [integration.targetEnvironment as string],
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
}
|
||||
: {})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await request.post(`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
if (Object.keys(secretsToAddToInfisical).length) {
|
||||
await createManySecretsRawFn({
|
||||
projectId: integration.projectId,
|
||||
environment: integration.environment.slug,
|
||||
path: integration.secretPath,
|
||||
secrets: Object.keys(secretsToAddToInfisical).map((key) => ({
|
||||
secretName: key,
|
||||
secretValue: secretsToAddToInfisical[key].value,
|
||||
type: SecretType.Shared,
|
||||
secretComment: ""
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
for await (const secret of updateSecrets) {
|
||||
if (secret.type !== "sensitive") {
|
||||
const { id, ...updatedSecret } = secret;
|
||||
await request.patch(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, updatedSecret, {
|
||||
// update and create logic
|
||||
for await (const key of Object.keys(infisicalSecrets)) {
|
||||
if (!(key in res) || infisicalSecrets[key]?.value !== res[key].value) {
|
||||
// if the key is not in the vercel res, we need to create it
|
||||
if (!(key in res)) {
|
||||
await request.post(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||
{
|
||||
key,
|
||||
value: infisicalSecrets[key]?.value,
|
||||
type: "encrypted",
|
||||
target: [integration.targetEnvironment as string],
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
}
|
||||
: {})
|
||||
},
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Else if the key already exists and its not sensitive, we need to update it
|
||||
} else if (res[key].type !== "sensitive") {
|
||||
await request.patch(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`,
|
||||
{
|
||||
key,
|
||||
value: infisicalSecrets[key]?.value,
|
||||
type: res[key].type,
|
||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||
? [...res[key].target]
|
||||
: [...res[key].target, integration.targetEnvironment as string],
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
}
|
||||
: {})
|
||||
},
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete logic
|
||||
for await (const key of Object.keys(res)) {
|
||||
if (infisicalSecrets[key] === null) {
|
||||
// case: delete secret
|
||||
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@ -1559,16 +1608,6 @@ const syncSecretsVercel = async ({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const secret of deleteSecrets) {
|
||||
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -4471,7 +4510,8 @@ export const syncIntegrationSecrets = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
createManySecretsRawFn
|
||||
});
|
||||
break;
|
||||
case Integrations.NETLIFY:
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import opentelemetry from "@opentelemetry/api";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import {
|
||||
@ -158,6 +159,12 @@ export const secretQueueFactory = ({
|
||||
projectUserMembershipRoleDAL,
|
||||
projectKeyDAL
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||
description: "Integration secret sync errors",
|
||||
unit: "1"
|
||||
});
|
||||
|
||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||
const appCfg = getConfig();
|
||||
await queueService.stopRepeatableJob(
|
||||
@ -933,6 +940,19 @@ export const secretQueueFactory = ({
|
||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
errorHistogram.record(1, {
|
||||
version: 1,
|
||||
integration: integration.integration,
|
||||
integrationId: integration.id,
|
||||
type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
name: err instanceof Error ? err.name : undefined,
|
||||
projectId: integration.projectId
|
||||
});
|
||||
}
|
||||
|
||||
const message =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
(err instanceof AxiosError
|
||||
|
12
cli/go.mod
@ -10,7 +10,7 @@ require (
|
||||
github.com/fatih/semgroup v1.2.0
|
||||
github.com/gitleaks/go-gitdiff v0.8.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/infisical/go-sdk v0.4.3
|
||||
github.com/infisical/go-sdk v0.4.7
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
@ -23,8 +23,8 @@ require (
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/term v0.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@ -93,9 +93,9 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
|
24
cli/go.sum
@ -265,8 +265,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.4.3 h1:O5ZJ2eCBAZDE9PIAfBPq9Utb2CgQKrhmj9R0oFTRu4U=
|
||||
github.com/infisical/go-sdk v0.4.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
|
||||
github.com/infisical/go-sdk v0.4.7 h1:+cxIdDfciMh0Syxbxbqjhvz9/ShnN1equ2zqlVQYGtw=
|
||||
github.com/infisical/go-sdk v0.4.7/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@ -453,8 +453,8 @@ golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -564,8 +564,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -620,16 +620,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -643,8 +643,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
609
cli/packages/cmd/ssh.go
Normal file
@ -0,0 +1,609 @@
|
||||
/*
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
infisicalSdkUtil "github.com/infisical/go-sdk/packages/util"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
)
|
||||
|
||||
var sshCmd = &cobra.Command{
|
||||
Example: `infisical ssh`,
|
||||
Short: "Used to issue SSH credentials",
|
||||
Use: "ssh",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
var sshIssueCredentialsCmd = &cobra.Command{
|
||||
Example: `ssh issue-credentials`,
|
||||
Short: "Used to issue SSH credentials against a certificate template",
|
||||
Use: "issue-credentials",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: issueCredentials,
|
||||
}
|
||||
|
||||
var sshSignKeyCmd = &cobra.Command{
|
||||
Example: `ssh sign-key`,
|
||||
Short: "Used to sign a SSH public key against a certificate template",
|
||||
Use: "sign-key",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: signKey,
|
||||
}
|
||||
|
||||
var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{
|
||||
infisicalSdkUtil.RSA2048: "id_rsa_2048",
|
||||
infisicalSdkUtil.RSA4096: "id_rsa_4096",
|
||||
infisicalSdkUtil.ECDSAP256: "id_ecdsa_p256",
|
||||
infisicalSdkUtil.ECDSAP384: "id_ecdsa_p384",
|
||||
}
|
||||
|
||||
func isValidKeyAlgorithm(algo infisicalSdkUtil.CertKeyAlgorithm) bool {
|
||||
_, exists := algoToFileName[algo]
|
||||
return exists
|
||||
}
|
||||
|
||||
func isValidCertType(certType infisicalSdkUtil.SshCertType) bool {
|
||||
switch certType {
|
||||
case infisicalSdkUtil.UserCert, infisicalSdkUtil.HostCert:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func writeToFile(filePath string, content string, perm os.FileMode) error {
|
||||
// Ensure the directory exists
|
||||
dir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
// Write the content to the file
|
||||
err := os.WriteFile(filePath, []byte(content), perm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addCredentialsToAgent(privateKeyContent, certContent string) error {
|
||||
// Parse the private key
|
||||
privateKey, err := ssh.ParseRawPrivateKey([]byte(privateKeyContent))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
// Parse the certificate
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certContent))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
cert, ok := pubKey.(*ssh.Certificate)
|
||||
if !ok {
|
||||
return fmt.Errorf("parsed key is not a certificate")
|
||||
}
|
||||
// Calculate LifetimeSecs based on certificate's valid-to time
|
||||
validUntil := time.Unix(int64(cert.ValidBefore), 0)
|
||||
now := time.Now()
|
||||
|
||||
// Handle ValidBefore as either a timestamp or an enumeration
|
||||
// SSH certificates use ValidBefore as a timestamp unless set to 0 or ~0
|
||||
if cert.ValidBefore == ssh.CertTimeInfinity {
|
||||
// If certificate never expires, set default lifetime to 1 year (can adjust as needed)
|
||||
validUntil = now.Add(365 * 24 * time.Hour)
|
||||
}
|
||||
|
||||
// Calculate the duration until expiration
|
||||
lifetime := validUntil.Sub(now)
|
||||
if lifetime <= 0 {
|
||||
return fmt.Errorf("certificate is already expired")
|
||||
}
|
||||
|
||||
// Convert duration to seconds
|
||||
lifetimeSecs := uint32(lifetime.Seconds())
|
||||
|
||||
// Connect to the SSH agent
|
||||
socket := os.Getenv("SSH_AUTH_SOCK")
|
||||
if socket == "" {
|
||||
return fmt.Errorf("SSH_AUTH_SOCK not set")
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to SSH agent: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
agentClient := agent.NewClient(conn)
|
||||
|
||||
// Add the key with certificate to the agent
|
||||
err = agentClient.Add(agent.AddedKey{
|
||||
PrivateKey: privateKey,
|
||||
Certificate: cert,
|
||||
Comment: "Added via Infisical CLI",
|
||||
LifetimeSecs: lifetimeSecs,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add key to agent: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func issueCredentials(cmd *cobra.Command, args []string) {
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
if certificateTemplateId == "" {
|
||||
util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag")
|
||||
}
|
||||
|
||||
principalsStr, err := cmd.Flags().GetString("principals")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
// Check if the input string is empty before splitting
|
||||
if principalsStr == "" {
|
||||
util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty")
|
||||
}
|
||||
|
||||
// Convert the comma-delimited string into a slice of strings
|
||||
principals := strings.Split(principalsStr, ",")
|
||||
for i, principal := range principals {
|
||||
principals[i] = strings.TrimSpace(principal)
|
||||
}
|
||||
|
||||
keyAlgorithm, err := cmd.Flags().GetString("keyAlgorithm")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse keyAlgorithm flag")
|
||||
}
|
||||
|
||||
if !isValidKeyAlgorithm(infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)) {
|
||||
util.HandleError(fmt.Errorf("invalid keyAlgorithm: %s", keyAlgorithm),
|
||||
"Valid values: RSA_2048, RSA_4096, EC_prime256v1, EC_secp384r1")
|
||||
}
|
||||
|
||||
certType, err := cmd.Flags().GetString("certType")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) {
|
||||
util.HandleError(fmt.Errorf("invalid certType: %s", certType),
|
||||
"Valid values: user, host")
|
||||
}
|
||||
|
||||
ttl, err := cmd.Flags().GetString("ttl")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
keyId, err := cmd.Flags().GetString("keyId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
outFilePath, err := cmd.Flags().GetString("outFilePath")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
addToAgent, err := cmd.Flags().GetBool("addToAgent")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse addToAgent flag")
|
||||
}
|
||||
|
||||
if outFilePath == "" && addToAgent == false {
|
||||
util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command")
|
||||
}
|
||||
|
||||
var (
|
||||
outputDir string
|
||||
privateKeyPath string
|
||||
publicKeyPath string
|
||||
signedKeyPath string
|
||||
)
|
||||
|
||||
if outFilePath != "" {
|
||||
// Expand ~ to home directory if present
|
||||
if strings.HasPrefix(outFilePath, "~") {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to resolve home directory")
|
||||
}
|
||||
outFilePath = strings.Replace(outFilePath, "~", homeDir, 1)
|
||||
}
|
||||
|
||||
// Check if outFilePath ends with "-cert.pub"
|
||||
if strings.HasSuffix(outFilePath, "-cert.pub") {
|
||||
// Treat outFilePath as the signed key path
|
||||
signedKeyPath = outFilePath
|
||||
|
||||
// Derive the base name by removing "-cert.pub"
|
||||
baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub")
|
||||
|
||||
// Set the output directory
|
||||
outputDir = filepath.Dir(outFilePath)
|
||||
|
||||
// Define private and public key paths
|
||||
privateKeyPath = filepath.Join(outputDir, baseName)
|
||||
publicKeyPath = filepath.Join(outputDir, baseName+".pub")
|
||||
} else {
|
||||
// Treat outFilePath as a directory
|
||||
outputDir = outFilePath
|
||||
|
||||
// Check if the directory exists; if not, create it
|
||||
info, err := os.Stat(outputDir)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(outputDir, 0755)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to create output directory")
|
||||
}
|
||||
} else if err != nil {
|
||||
util.HandleError(err, "Failed to access output directory")
|
||||
} else if !info.IsDir() {
|
||||
util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define file names based on key algorithm
|
||||
fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]
|
||||
|
||||
// Define file paths
|
||||
privateKeyPath = filepath.Join(outputDir, fileName)
|
||||
publicKeyPath = filepath.Join(outputDir, fileName+".pub")
|
||||
signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub")
|
||||
|
||||
// If outFilePath ends with "-cert.pub", ensure the signedKeyPath is set
|
||||
if strings.HasSuffix(outFilePath, "-cert.pub") {
|
||||
// Ensure the signedKeyPath was set
|
||||
if signedKeyPath == "" {
|
||||
util.HandleError(fmt.Errorf("signedKeyPath is not set correctly"), "Internal error")
|
||||
}
|
||||
} else {
|
||||
// Ensure all paths are set
|
||||
if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" {
|
||||
util.HandleError(fmt.Errorf("file paths are not set correctly"), "Internal error")
|
||||
}
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
creds, err := infisicalClient.Ssh().IssueCredentials(infisicalSdk.IssueSshCredsOptions{
|
||||
CertificateTemplateID: certificateTemplateId,
|
||||
Principals: principals,
|
||||
KeyAlgorithm: infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm),
|
||||
CertType: infisicalSdkUtil.SshCertType(certType),
|
||||
TTL: ttl,
|
||||
KeyID: keyId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to issue SSH credentials")
|
||||
}
|
||||
|
||||
if outFilePath != "" {
|
||||
// If signedKeyPath wasn't set in the directory scenario, set it now
|
||||
if signedKeyPath == "" {
|
||||
fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]
|
||||
signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub")
|
||||
}
|
||||
|
||||
if privateKeyPath == "" {
|
||||
privateKeyPath = filepath.Join(outputDir, algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)])
|
||||
}
|
||||
err = writeToFile(privateKeyPath, creds.PrivateKey, 0600)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to write Private Key to file")
|
||||
}
|
||||
|
||||
if publicKeyPath == "" {
|
||||
publicKeyPath = privateKeyPath + ".pub"
|
||||
}
|
||||
err = writeToFile(publicKeyPath, creds.PublicKey, 0644)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to write Public Key to file")
|
||||
}
|
||||
|
||||
err = writeToFile(signedKeyPath, creds.SignedKey, 0644)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to write Signed Key to file")
|
||||
}
|
||||
|
||||
fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
|
||||
}
|
||||
|
||||
// Add SSH credentials to the SSH agent if needed
|
||||
if addToAgent {
|
||||
// Call the helper function to handle add-to-agent flow
|
||||
err := addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to add keys to SSH agent")
|
||||
} else {
|
||||
fmt.Println("The SSH key and certificate have been successfully added to your ssh-agent.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signKey(cmd *cobra.Command, args []string) {
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
infisicalToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
if certificateTemplateId == "" {
|
||||
util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag")
|
||||
}
|
||||
|
||||
publicKey, err := cmd.Flags().GetString("publicKey")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
publicKeyFilePath, err := cmd.Flags().GetString("publicKeyFilePath")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if publicKey == "" && publicKeyFilePath == "" {
|
||||
util.HandleError(fmt.Errorf("either --publicKey or --publicKeyFilePath must be provided"), "Invalid input")
|
||||
}
|
||||
|
||||
if publicKey != "" && publicKeyFilePath != "" {
|
||||
util.HandleError(fmt.Errorf("only one of --publicKey or --publicKeyFile can be provided"), "Invalid input")
|
||||
}
|
||||
|
||||
if publicKeyFilePath != "" {
|
||||
if strings.HasPrefix(publicKeyFilePath, "~") {
|
||||
// Expand the tilde (~) to the user's home directory
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to resolve home directory")
|
||||
}
|
||||
publicKeyFilePath = strings.Replace(publicKeyFilePath, "~", homeDir, 1)
|
||||
}
|
||||
|
||||
// Ensure the file has a .pub extension
|
||||
if !strings.HasSuffix(publicKeyFilePath, ".pub") {
|
||||
util.HandleError(fmt.Errorf("public key file must have a .pub extension"), "Invalid input")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(publicKeyFilePath)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to read public key file")
|
||||
}
|
||||
|
||||
publicKey = strings.TrimSpace(string(content))
|
||||
}
|
||||
|
||||
if strings.TrimSpace(publicKey) == "" {
|
||||
util.HandleError(fmt.Errorf("Public key is empty"), "Invalid input")
|
||||
}
|
||||
|
||||
principalsStr, err := cmd.Flags().GetString("principals")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
// Check if the input string is empty before splitting
|
||||
if principalsStr == "" {
|
||||
util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty")
|
||||
}
|
||||
|
||||
// Convert the comma-delimited string into a slice of strings
|
||||
principals := strings.Split(principalsStr, ",")
|
||||
for i, principal := range principals {
|
||||
principals[i] = strings.TrimSpace(principal)
|
||||
}
|
||||
|
||||
certType, err := cmd.Flags().GetString("certType")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) {
|
||||
util.HandleError(fmt.Errorf("invalid certType: %s", certType),
|
||||
"Valid values: user, host")
|
||||
}
|
||||
|
||||
ttl, err := cmd.Flags().GetString("ttl")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
keyId, err := cmd.Flags().GetString("keyId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
outFilePath, err := cmd.Flags().GetString("outFilePath")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
var (
|
||||
outputDir string
|
||||
signedKeyPath string
|
||||
)
|
||||
|
||||
if outFilePath == "" {
|
||||
// Use current working directory
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to get current working directory")
|
||||
}
|
||||
|
||||
// check if public key path exists
|
||||
if publicKeyFilePath == "" {
|
||||
util.PrintErrorMessageAndExit("--outFilePath must be specified when --publicKeyFilePath is not provided")
|
||||
}
|
||||
|
||||
outputDir = filepath.Dir(publicKeyFilePath)
|
||||
// Derive the base name by removing "-cert.pub"
|
||||
baseName := strings.TrimSuffix(filepath.Base(publicKeyFilePath), ".pub")
|
||||
signedKeyPath = filepath.Join(outputDir, baseName+"-cert.pub")
|
||||
} else {
|
||||
// Expand ~ to home directory if present
|
||||
if strings.HasPrefix(outFilePath, "~") {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to resolve home directory")
|
||||
}
|
||||
outFilePath = strings.Replace(outFilePath, "~", homeDir, 1)
|
||||
}
|
||||
|
||||
// Check if outFilePath ends with "-cert.pub"
|
||||
if !strings.HasSuffix(outFilePath, "-cert.pub") {
|
||||
util.PrintErrorMessageAndExit("--outFilePath must end with -cert.pub")
|
||||
}
|
||||
|
||||
// Extract the directory from outFilePath
|
||||
outputDir = filepath.Dir(outFilePath)
|
||||
|
||||
// Validate the output directory
|
||||
info, err := os.Stat(outputDir)
|
||||
if os.IsNotExist(err) {
|
||||
// Directory does not exist; attempt to create it
|
||||
err = os.MkdirAll(outputDir, 0755)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to create output directory")
|
||||
}
|
||||
} else if err != nil {
|
||||
// Other errors accessing the directory
|
||||
util.HandleError(err, "Failed to access output directory")
|
||||
} else if !info.IsDir() {
|
||||
// Path exists but is not a directory
|
||||
util.PrintErrorMessageAndExit("The provided --outFilePath's directory is not valid")
|
||||
}
|
||||
|
||||
signedKeyPath = outFilePath
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
creds, err := infisicalClient.Ssh().SignKey(infisicalSdk.SignSshPublicKeyOptions{
|
||||
CertificateTemplateID: certificateTemplateId,
|
||||
PublicKey: publicKey,
|
||||
Principals: principals,
|
||||
CertType: infisicalSdkUtil.SshCertType(certType),
|
||||
TTL: ttl,
|
||||
KeyID: keyId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to sign SSH public key")
|
||||
}
|
||||
|
||||
err = writeToFile(signedKeyPath, creds.SignedKey, 0644)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to write Signed Key to file")
|
||||
}
|
||||
|
||||
fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
|
||||
}
|
||||
|
||||
func init() {
|
||||
sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token")
|
||||
sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for")
|
||||
sshSignKeyCmd.Flags().String("publicKey", "", "The public key to sign")
|
||||
sshSignKeyCmd.Flags().String("publicKeyFilePath", "", "The file path to the public key file to sign")
|
||||
sshSignKeyCmd.Flags().String("outFilePath", "", "The path to write the SSH certificate to such as ~/.ssh/id_rsa-cert.pub. If not provided, the credentials will be saved to the directory of the specified public key file path or the current working directory")
|
||||
sshSignKeyCmd.Flags().String("principals", "", "The principals that the certificate should be signed for")
|
||||
sshSignKeyCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type for the created certificate")
|
||||
sshSignKeyCmd.Flags().String("ttl", "", "The ttl for the created certificate")
|
||||
sshSignKeyCmd.Flags().String("keyId", "", "The keyId that the created certificate should have")
|
||||
sshCmd.AddCommand(sshSignKeyCmd)
|
||||
|
||||
sshIssueCredentialsCmd.Flags().String("token", "", "Issue SSH credentials using machine identity access token")
|
||||
sshIssueCredentialsCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue SSH credentials for")
|
||||
sshIssueCredentialsCmd.Flags().String("principals", "", "The principals to issue SSH credentials for")
|
||||
sshIssueCredentialsCmd.Flags().String("keyAlgorithm", string(infisicalSdkUtil.RSA2048), "The key algorithm to issue SSH credentials for")
|
||||
sshIssueCredentialsCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type to issue SSH credentials for")
|
||||
sshIssueCredentialsCmd.Flags().String("ttl", "", "The ttl to issue SSH credentials for")
|
||||
sshIssueCredentialsCmd.Flags().String("keyId", "", "The keyId to issue SSH credentials for")
|
||||
sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory")
|
||||
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
||||
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
||||
rootCmd.AddCommand(sshCmd)
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/aws"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [AWS Connections](/integrations/app-connections/aws) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/aws/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/aws/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/aws/name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/aws"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/aws/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [AWS Connections](/integrations/app-connections/aws) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/github"
|
||||
---
|
||||
|
||||
<Note>
|
||||
GitHub Connections must be created through the Infisical UI.
|
||||
Check out the configuration docs for [GitHub Connections](/integrations/app-connections/github) for a step-by-step
|
||||
guide.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/github/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/github/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/github/name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/github"
|
||||
---
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/github/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
GitHub Connections must be updated through the Infisical UI.
|
||||
Check out the configuration docs for [GitHub Connections](/integrations/app-connections/github) for a step-by-step
|
||||
guide.
|
||||
</Note>
|
4
docs/api-reference/endpoints/app-connections/list.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections"
|
||||
---
|
4
docs/api-reference/endpoints/app-connections/options.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Options"
|
||||
openapi: "GET /api/v1/app-connections/options"
|
||||
---
|
116
docs/cli/commands/ssh.mdx
Normal file
@ -0,0 +1,116 @@
|
||||
---
|
||||
title: "infisical ssh"
|
||||
description: "Generate SSH credentials with the CLI"
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
[Infisical SSH](/documentation/platform/ssh) lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure.
|
||||
|
||||
This command enables you to obtain SSH credentials used to access a remote host; we recommend using the `issue-credentials` sub-command to generate dynamic SSH credentials for each SSH session.
|
||||
|
||||
### Sub-commands
|
||||
|
||||
<Accordion title="infisical ssh issue-credentials">
|
||||
This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template.
|
||||
|
||||
We recommend using the `--addToAgent` flag to automatically load issued SSH credentials to the SSH agent.
|
||||
|
||||
```bash
|
||||
$ infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<principals> --addToAgent
|
||||
```
|
||||
|
||||
### Flags
|
||||
<Accordion title="--certificateTemplateId">
|
||||
The ID of the SSH certificate template to issue SSH credentials for.
|
||||
</Accordion>
|
||||
<Accordion title="--principals">
|
||||
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
|
||||
</Accordion>
|
||||
<Accordion title="--addToAgent">
|
||||
Whether to add issued SSH credentials to the SSH agent.
|
||||
|
||||
Default value: `false`
|
||||
|
||||
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
|
||||
</Accordion>
|
||||
<Accordion title="--outFilePath">
|
||||
The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be saved to the current working directory where the command is run.
|
||||
|
||||
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
|
||||
</Accordion>
|
||||
<Accordion title="--keyAlgorithm">
|
||||
The key algorithm to issue SSH credentials for.
|
||||
|
||||
Default value: `RSA_2048`
|
||||
|
||||
Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`.
|
||||
</Accordion>
|
||||
<Accordion title="--certType">
|
||||
The certificate type to issue SSH credentials for.
|
||||
|
||||
Default value: `user`
|
||||
|
||||
Available options: `user` or `host`
|
||||
</Accordion>
|
||||
<Accordion title="--ttl">
|
||||
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
|
||||
|
||||
Defaults to the Default TTL value set in the certificate template.
|
||||
</Accordion>
|
||||
<Accordion title="--keyId">
|
||||
A custom Key ID to issue SSH credentials for.
|
||||
|
||||
Defaults to the autogenerated Key ID by Infisical.
|
||||
</Accordion>
|
||||
<Accordion title="--token">
|
||||
An authenticated token to use to issue SSH credentials.
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical ssh sign-key">
|
||||
This command is used to sign an existing SSH public key against a certificate template; the command outputs the corresponding signed SSH certificate.
|
||||
|
||||
```bash
|
||||
$ infisical ssh sign-key --certificateTemplateId=<certificate-template-id> --publicKey=<public-key> --principals=<principals> --outFilePath=<out-file-path>
|
||||
```
|
||||
<Accordion title="--certificateTemplateId">
|
||||
The ID of the SSH certificate template to issue the SSH certificate for.
|
||||
</Accordion>
|
||||
<Accordion title="--publicKey">
|
||||
The public key to sign.
|
||||
|
||||
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
|
||||
</Accordion>
|
||||
<Accordion title="--publicKeyFilePath">
|
||||
The path to the public key file to sign.
|
||||
|
||||
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
|
||||
</Accordion>
|
||||
<Accordion title="--principals">
|
||||
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
|
||||
</Accordion>
|
||||
<Accordion title="--outFilePath">
|
||||
The path to write the SSH certificate to such as `~/.ssh/id_rsa-cert.pub`; the specified file must have the `.pub` extension. If not provided, the credentials will be saved to the directory of the specified `--publicKeyFilePath` or the current working directory where the command is run.
|
||||
</Accordion>
|
||||
<Accordion title="--certType">
|
||||
The certificate type to issue SSH credentials for.
|
||||
|
||||
Default value: `user`
|
||||
|
||||
Available options: `user` or `host`
|
||||
</Accordion>
|
||||
<Accordion title="--ttl">
|
||||
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
|
||||
|
||||
Defaults to the Default TTL value set in the certificate template.
|
||||
</Accordion>
|
||||
<Accordion title="--keyId">
|
||||
A custom Key ID to issue SSH credentials for.
|
||||
|
||||
Defaults to the autogenerated Key ID by Infisical.
|
||||
</Accordion>
|
||||
<Accordion title="--token">
|
||||
An authenticated token to use to issue SSH credentials.
|
||||
</Accordion>
|
||||
</Accordion>
|
@ -6,7 +6,7 @@ description: "Learn how to generate SSH credentials to provide secure and centra
|
||||
|
||||
## Concept
|
||||
|
||||
Infisical can be used to issue SSH certificates to clients to provide short-lived, secure SSH access to infrastructure;
|
||||
Infisical can be used to issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure;
|
||||
this improves on many limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management,
|
||||
unauthorized access, and SSH key sprawl.
|
||||
|
||||
@ -159,7 +159,19 @@ as part of the SSH operation.
|
||||
|
||||
## Guide to Using Infisical SSH to Access a Host
|
||||
|
||||
We show how to obtain a SSH certificate (and optionally a new SSH key pair) for a client to access a host via CLI:
|
||||
We show how to obtain a SSH certificate and use it for a client to access a host via CLI:
|
||||
|
||||
<Note>
|
||||
The subsequent guide assumes the following prerequisites:
|
||||
|
||||
- SSH Agent is running: The `ssh-agent` must be actively running on the host machine.
|
||||
- OpenSSH is installed: The system should have OpenSSH installed; this includes
|
||||
both the `ssh` client and `ssh-agent`.
|
||||
- `SSH_AUTH_SOCK` environment variable
|
||||
is set; the `SSH_AUTH_SOCK` variable should point to the UNIX socket that
|
||||
`ssh-agent` uses for communication.
|
||||
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
<Step title="Authenticate with Infisical">
|
||||
@ -169,74 +181,30 @@ infisical login
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Obtain a SSH certificate (and optionally new key-pair)">
|
||||
Depending on the use-case, a client may either request a SSH certificate along with a new SSH key pair or obtain a SSH certificate for an existing SSH key pair to access a host.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Using New Key Pair (Recommended)">
|
||||
If you wish to obtain a new SSH key pair in conjunction with the SSH certificate, then you can use the `infisical ssh issue-credentials` command.
|
||||
|
||||
```bash
|
||||
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username>
|
||||
```
|
||||
|
||||
The following flags may be relevant:
|
||||
|
||||
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
|
||||
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
|
||||
- `outFilePath` (optional): The path to the file to write the SSH certificate to.
|
||||
|
||||
<Note>
|
||||
If `outFilePath` is not specified, the SSH certificate will be written to the current working directory where the command is run.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Using Existing Key Pair">
|
||||
If you have an existing SSH key pair, then you can use the `infisical ssh sign-key` command with either
|
||||
the `--publicKey` flag or the `--publicKeyFilePath` flag to obtain a SSH certificate corresponding to
|
||||
the existing credential.
|
||||
|
||||
```bash
|
||||
infisical ssh sign-key --publicKeyFilePath=<public-key-file-path> --certificateTemplateId=<certificate-template-id> --principals=<username>
|
||||
```
|
||||
|
||||
The following flags may be relevant:
|
||||
|
||||
- `publicKey`: The public key to sign.
|
||||
- `publicKeyFilePath`: The path to the public key file to sign.
|
||||
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
|
||||
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
|
||||
- `outFilePath` (optional): The path to the file to write the SSH certificate to.
|
||||
|
||||
<Note>
|
||||
If `outFilePath` is not specified but `publicKeyFilePath` is then the SSH certificate will be written to the directory of the public key file; if the public key file is called `id_rsa.pub`, then the file containing the SSH certificate will be called `id_rsa-cert.pub`.
|
||||
|
||||
Otherwise, if `outFilePath` is not specified, the SSH certificate will be written to the current working directory where the command is run.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
</Step>
|
||||
<Step title="SSH into the host">
|
||||
Once you have obtained the SSH certificate, you can use it to SSH into the desired host.
|
||||
|
||||
<Step title="Obtain a SSH certificate and load it into the SSH agent">
|
||||
Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent.
|
||||
```bash
|
||||
ssh -i /path/to/private_key.pem \
|
||||
-o CertificateFile=/path/to/ssh-cert.pub \
|
||||
username@hostname
|
||||
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username> --addToAgent
|
||||
```
|
||||
|
||||
<Note>
|
||||
We recommend setting up aliases so you can more easily SSH into the desired host.
|
||||
Here's some guidance on each flag:
|
||||
|
||||
For example, you may set up an SSH alias using the SSH client configuration file (usually `~/.ssh/config`), defining a host alias including the file path to the issued SSH credential(s).
|
||||
</Note>
|
||||
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
|
||||
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
|
||||
|
||||
For fuller documentation on commands and flags supported by the Infisical CLI for SSH, refer to the docs [here](/cli/commands/ssh).
|
||||
|
||||
</Step>
|
||||
<Step title="SSH into the host">
|
||||
Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent.
|
||||
|
||||
```bash
|
||||
ssh username@hostname
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
Note that the above workflow can be executed via API or other client methods
|
||||
such as SDK.
|
||||
</Note>
|
||||
</Note>
|
BIN
docs/images/app-connections/aws/access-key-connection.png
Normal file
After Width: | Height: | Size: 957 KiB |
BIN
docs/images/app-connections/aws/assume-role-connection.png
Normal file
After Width: | Height: | Size: 957 KiB |
BIN
docs/images/app-connections/aws/create-access-key-method.png
Normal file
After Width: | Height: | Size: 598 KiB |
BIN
docs/images/app-connections/aws/create-assume-role-method.png
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
docs/images/app-connections/aws/parameter-store-permissions.png
Normal file
After Width: | Height: | Size: 306 KiB |
BIN
docs/images/app-connections/aws/secrets-manager-permissions.png
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
docs/images/app-connections/aws/select-aws-connection.png
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
docs/images/app-connections/general/add-connection.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/app-connections/github/create-github-app-method.png
Normal file
After Width: | Height: | Size: 586 KiB |
BIN
docs/images/app-connections/github/create-oauth-method.png
Normal file
After Width: | Height: | Size: 580 KiB |
BIN
docs/images/app-connections/github/github-app-connection.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/app-connections/github/install-github-app.png
Normal file
After Width: | Height: | Size: 352 KiB |
BIN
docs/images/app-connections/github/oauth-connection.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/app-connections/github/select-github-connection.png
Normal file
After Width: | Height: | Size: 562 KiB |
354
docs/integrations/app-connections/aws.mdx
Normal file
@ -0,0 +1,354 @@
|
||||
---
|
||||
title: "AWS Connection"
|
||||
description: "Learn how to configure an AWS Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical supports two methods for connecting to AWS.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Assume Role (Recommended)">
|
||||
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role.
|
||||
|
||||
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
|
||||
|
||||
The following steps are for instances not deployed on AWS:
|
||||
<Steps>
|
||||
<Step title="Create an IAM User">
|
||||
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
|
||||
</Step>
|
||||
<Step title="Create an Inline Policy">
|
||||
Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowAssumeAnyRole",
|
||||
"Effect": "Allow",
|
||||
"Action": "sts:AssumeRole",
|
||||
"Resource": "arn:aws:iam::*:role/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Obtain the IAM User Credentials">
|
||||
Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**.
|
||||
|
||||

|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Set Up Connection Keys">
|
||||
1. Set the access key as **INF_APP_CONNECTION_AWS_CLIENT_ID**.
|
||||
2. Set the secret key as **INF_APP_CONNECTION_AWS_CLIENT_SECRET**.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the Managing User IAM Role for Infisical">
|
||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||

|
||||
|
||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
|
||||
</Step>
|
||||
|
||||
<Step title="Add Required Permissions for the IAM Role">
|
||||
Depending on your use case, add one or more of the following policies to your IAM Role:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Secrets Sync">
|
||||
<AccordionGroup>
|
||||
<Accordion title="AWS Secrets Manager">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||
|
||||

|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowSecretsManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListKeys",
|
||||
"kms:ListAliases",
|
||||
"kms:Encrypt",
|
||||
"kms:Decrypt"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="AWS Parameter Store">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||
|
||||

|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowSSMAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ssm:PutParameter",
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParameters",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step title="Copy the AWS IAM Role ARN">
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Setup AWS Connection in Infisical">
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to the App Connections tab on the Organization Settings page.
|
||||

|
||||
|
||||
2. Select the **AWS Connection** option.
|
||||

|
||||
|
||||
3. Select the **Assume Role** method option and provide the **AWS IAM Role ARN** obtained from the previous step and press **Connect to AWS**.
|
||||

|
||||
|
||||
4. Your **AWS Connection** is now available for use.
|
||||

|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create an AWS Connection, make an API request to the [Create AWS
|
||||
Connection](/api-reference/endpoints/app-connections/aws/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/app-connections/aws \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-aws-connection",
|
||||
"method": "assume-role",
|
||||
"credentials": {
|
||||
"roleArn": "...",
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"appConnection": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-aws-connection",
|
||||
"version": 123,
|
||||
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-11-07T05:31:56Z",
|
||||
"updatedAt": "2023-11-07T05:31:56Z",
|
||||
"app": "aws",
|
||||
"method": "assume-role",
|
||||
"credentials": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Access Key">
|
||||
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the Managing User IAM Role for Infisical">
|
||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||

|
||||
|
||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
|
||||
</Step>
|
||||
|
||||
<Step title="Add Required Permissions for the IAM Role">
|
||||
Depending on your use case, add one or more of the following policies to your IAM Role:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Secrets Sync">
|
||||
<AccordionGroup>
|
||||
<Accordion title="AWS Secrets Manager">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||
|
||||

|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowSecretsManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListKeys",
|
||||
"kms:ListAliases",
|
||||
"kms:Encrypt",
|
||||
"kms:Decrypt"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="AWS Parameter Store">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||
|
||||

|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowSSMAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ssm:PutParameter",
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParameters",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Obtain Access Key ID and Secret Access Key">
|
||||
Retrieve an AWS **Access Key ID** and a **Secret Key** for your IAM user in **IAM > Users > User > Security credentials > Access keys**.
|
||||
|
||||

|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Setup AWS Connection in Infisical">
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to the App Connections tab on the Organization Settings page.
|
||||

|
||||
|
||||
2. Select the **AWS Connection** option.
|
||||

|
||||
|
||||
3. Select the **Access Key** method option and provide the **Access Key ID** and **Secret Key** obtained from the previous step and press **Connect to AWS**.
|
||||

|
||||
|
||||
4. Your **AWS Connection** is now available for use.
|
||||

|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create an AWS Connection, make an API request to the [Create AWS
|
||||
Connection](/api-reference/endpoints/app-connections/aws/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/app-connections/aws \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-aws-connection",
|
||||
"method": "access-key",
|
||||
"credentials": {
|
||||
"accessKeyId": "...",
|
||||
"secretKey": "..."
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"appConnection": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-aws-connection",
|
||||
"version": 123,
|
||||
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-11-07T05:31:56Z",
|
||||
"updatedAt": "2023-11-07T05:31:56Z",
|
||||
"app": "aws",
|
||||
"method": "access-key",
|
||||
"credentials": {
|
||||
"accessKeyId": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
169
docs/integrations/app-connections/github.mdx
Normal file
@ -0,0 +1,169 @@
|
||||
---
|
||||
title: "GitHub Connection"
|
||||
description: "Learn how to configure a GitHub Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical supports two methods for connecting to GitHub.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="GitHub App (Recommended)">
|
||||
Infisical will use a GitHub App with finely grained permissions to connect to GitHub.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the GitHub integration with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub
|
||||
and registering your instance with it.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an application on GitHub">
|
||||
Navigate to the GitHub app settings [here](https://github.com/settings/apps). Click **New GitHub App**.
|
||||
|
||||

|
||||
|
||||
Give the application a name, a homepage URL (your self-hosted domain i.e. `https://your-domain.com`), and a callback URL (i.e. `https://your-domain.com/app-connections/github/oauth/callback`).
|
||||
|
||||

|
||||
|
||||
Enable request user authorization during app installation.
|
||||

|
||||
|
||||
Disable webhook by unchecking the Active checkbox.
|
||||

|
||||
|
||||
Set the repository permissions as follows: Metadata: Read-only, Secrets: Read and write, Environments: Read and write, Actions: Read.
|
||||

|
||||
|
||||
Similarly, set the organization permissions as follows: Secrets: Read and write.
|
||||

|
||||
|
||||
Create the Github application.
|
||||

|
||||
|
||||
<Note>
|
||||
If you have a GitHub organization, you can create an application under it
|
||||
in your organization Settings > Developer settings > GitHub Apps > New GitHub App.
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Add your application credentials to Infisical">
|
||||
Generate a new **Client Secret** for your GitHub application.
|
||||

|
||||
|
||||
Generate a new **Private Key** for your Github application.
|
||||

|
||||
|
||||
Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key.
|
||||

|
||||
|
||||
Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application:
|
||||
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SLUG`: The **Slug** of your GitHub application. This is the one found in the URL.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_APP_ID`: The **App ID** of your GitHub application.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_PRIVATE_KEY`: The **Private Key** of your GitHub application.
|
||||
|
||||
Once added, restart your Infisical instance and use the GitHub integration via app authentication.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup GitHub Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the App Connections">
|
||||
Navigate to the **App Connections** tab on the **Organization Settings** page.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GitHub Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
Select the **GitHub App** method and click **Connect to GitHub**.
|
||||

|
||||
</Step>
|
||||
<Step title="Install GitHub App">
|
||||
You will then be redirected to the GitHub app installation page.
|
||||
|
||||
Install and authorize the GitHub application. This will redirect you back to Infisical's App Connections page.
|
||||

|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **GitHub Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="OAuth">
|
||||
Infisical will use an OAuth App to connect to GitHub.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
|
||||
and registering your instance with it.
|
||||
<Steps>
|
||||
<Step title="Create an OAuth application in GitHub">
|
||||
Navigate to your user Settings > Developer settings > OAuth Apps to create a new GitHub OAuth application.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com`
|
||||
and the **Authorization callback URL** to `https://your-domain.com/app-connections/github/oauth/callback`.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
If you have a GitHub organization, you can create an OAuth application under it
|
||||
in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App.
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Add your OAuth application credentials to Infisical">
|
||||
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
|
||||
|
||||

|
||||
|
||||
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
|
||||
|
||||
- `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID`: The **Client ID** of your GitHub OAuth application.
|
||||
- `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET`: The **Client Secret** of your GitHub OAuth application.
|
||||
|
||||
Once added, restart your Infisical instance and use the GitHub integration.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup GitHub Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the App Connections">
|
||||
Navigate to the **App Connections** tab on the **Organization Settings** page.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GitHub Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
Select the **OAuth** method and click **Connect to GitHub**.
|
||||

|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to the GitHub to grant Infisical access to your GitHub account (organization and repo privileges).
|
||||
Once granted, you will redirect you back to Infisical's App Connections page.
|
||||

|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **GitHub Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
77
docs/integrations/app-connections/overview.mdx
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
sidebarTitle: "Overview"
|
||||
description: "Learn how to manage and configure third-party app connections with Infisical."
|
||||
---
|
||||
|
||||
App Connections enable your organization to integrate Infisical with third-party services in a secure and versatile way.
|
||||
|
||||
## Concept
|
||||
|
||||
App Connections are an organization-level resource used to establish connections with third-party applications
|
||||
that can be used across Infisical projects. Example use cases include syncing secrets, generating dynamic secrets, and more.
|
||||
|
||||
<br />
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'flowchart': {'curve': 'linear'} } }%%
|
||||
graph TD
|
||||
A[AWS]
|
||||
B[AWS Connection]
|
||||
C[Project 1 Secret Sync]
|
||||
D[Project 2 Secret Sync]
|
||||
E[Project 3 Generate Dynamic Secret]
|
||||
|
||||
B --> A
|
||||
C --> B
|
||||
D --> B
|
||||
E --> B
|
||||
|
||||
classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black
|
||||
classDef aws fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px
|
||||
classDef project fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
|
||||
classDef connection fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
|
||||
|
||||
class A aws
|
||||
class B connection
|
||||
class C,D,E project
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Workflow
|
||||
|
||||
App Connections require initial setup in both your third-party application and Infisical. Follow these steps to establish a secure connection:
|
||||
|
||||
<Note>
|
||||
For step-by-step guides specific to each application, refer to the App Connections section in the Navigation Bar.
|
||||
</Note>
|
||||
|
||||
1. <strong>Create Access Entity:</strong> If necessary, create an entity such as a service account or role within the third-party application you want to connect to. Be sure
|
||||
to limit the access of this entity to the minimal permission set required to perform the operations you need. For example:
|
||||
- For secret syncing: Read/write permissions to specific secret stores
|
||||
- For dynamic secrets: Permissions to create temporary credentials
|
||||
|
||||
<Tip>
|
||||
Whenever possible, Infisical encourages creating a designated service account for your App Connection to limit the scope of permissions based on your use-case.
|
||||
</Tip>
|
||||
|
||||
2. <strong>Generate Authentication Credentials:</strong> Obtain the required credentials from your third-party application. These can vary between applications and might be:
|
||||
- an API key or access token
|
||||
- A client ID and secret pair
|
||||
- other credentials, etc.
|
||||
|
||||
3. <strong>Create App Connection:</strong> Configure the connection in Infisical using your generated credentials through either the UI or API.
|
||||
|
||||
<Info>
|
||||
Some App Connections can only be created via the UI such as connections using OAuth.
|
||||
</Info>
|
||||
|
||||
4. <strong>Utilize the Connection:</strong> Use your App Connection for various features across Infisical such as our Secrets Sync by selecting it via the dropdown menu
|
||||
in the UI or by passing the associated `connectionId` when generating resources via the API.
|
||||
|
||||
<Note>
|
||||
Infisical is continuously expanding its third-party application support. If your desired application isn't listed,
|
||||
you can still use previous methods of connecting to it such as our Native Integrations.
|
||||
</Note>
|
@ -831,9 +831,9 @@ type: Opaque
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Apply the Infisical CRD to your cluster
|
||||
### Apply the InfisicalSecret CRD to your cluster
|
||||
|
||||
Once you have configured the Infisical CRD with the required fields, you can apply it to your cluster.
|
||||
Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster.
|
||||
After applying, you should notice that the managed secret has been created in the desired namespace your specified.
|
||||
|
||||
```
|
||||
@ -1032,12 +1032,12 @@ stringData:
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
## Auto redeployment
|
||||
### Auto redeployment
|
||||
|
||||
Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed.
|
||||
To address this, we added functionality to automatically redeploy your deployment when its managed secret updates.
|
||||
|
||||
### Enabling auto redeploy
|
||||
#### Enabling auto redeploy
|
||||
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
|
||||
|
||||
@ -1080,6 +1080,419 @@ spec:
|
||||
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
|
||||
Then, for each deployment that has this annotation present, a rolling update will be triggered.
|
||||
</Info>
|
||||
|
||||
## Push Secrets to Infisical
|
||||
|
||||
|
||||
### Example usage
|
||||
|
||||
Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical.
|
||||
|
||||
After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster.
|
||||
|
||||
Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD.
|
||||
|
||||
```bash
|
||||
kubectl apply -f source-secret.yaml
|
||||
```
|
||||
|
||||
After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD.
|
||||
|
||||
```bash
|
||||
kubectl apply -f infisical-push-secret.yaml
|
||||
```
|
||||
|
||||
After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical.
|
||||
|
||||
```yaml infisical-push-secret.yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalPushSecret
|
||||
metadata:
|
||||
name: infisical-push-secret-demo
|
||||
spec:
|
||||
resyncInterval: 1m
|
||||
hostAPI: https://app.infisical.com/api
|
||||
|
||||
# Optional, defaults to no replacement.
|
||||
updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync.
|
||||
|
||||
# Optional, defaults to no deletion.
|
||||
deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted.
|
||||
|
||||
destination:
|
||||
projectId: <project-id>
|
||||
environmentSlug: <env-slug>
|
||||
secretsPath: <secret-path>
|
||||
|
||||
push:
|
||||
secret:
|
||||
secretName: push-secret-demo # Secret CRD
|
||||
secretNamespace: default
|
||||
|
||||
# Only have one authentication method defined or you are likely to run into authentication issues.
|
||||
# Remove all except one authentication method.
|
||||
authentication:
|
||||
awsIamAuth:
|
||||
identityId: <machine-identity-id>
|
||||
azureAuth:
|
||||
identityId: <machine-identity-id>
|
||||
gcpIamAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
|
||||
gcpIdTokenAuth:
|
||||
identityId: <machine-identity-id>
|
||||
kubernetesAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountRef:
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
universalAuth:
|
||||
credentialsRef:
|
||||
secretName: <secret-name> # universal-auth-credentials
|
||||
secretNamespace: <secret-namespace> # default
|
||||
```
|
||||
|
||||
```yaml source-secret.yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: push-secret-demo
|
||||
namespace: default
|
||||
stringData: # can also be "data", but needs to be base64 encoded
|
||||
API_KEY: some-api-key
|
||||
DATABASE_URL: postgres://127.0.0.1:5432
|
||||
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
|
||||
```
|
||||
|
||||
### InfisicalPushSecret CRD properties
|
||||
|
||||
<Accordion title="hostAPI">
|
||||
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
|
||||
` https://your-self-hosted-instace.com/api`
|
||||
|
||||
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
|
||||
<Accordion title="Advanced use case">
|
||||
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
|
||||
To achieve this, use the following address for the hostAPI field:
|
||||
|
||||
``` bash
|
||||
http://<backend-svc-name>.<namespace>.svc.cluster.local:4000/api
|
||||
```
|
||||
|
||||
Make sure to replace `<backend-svc-name>` and `<namespace>` with the appropriate values for your backend service and namespace.
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="resyncInterval">
|
||||
|
||||
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
|
||||
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
|
||||
The following units are supported:
|
||||
- `s` for seconds (must be at least 5 seconds)
|
||||
- `m` for minutes
|
||||
- `h` for hours
|
||||
- `d` for days
|
||||
- `w` for weeks
|
||||
|
||||
The default value is `1m` (1 minute).
|
||||
|
||||
Valid intervals examples:
|
||||
```yaml
|
||||
resyncInterval: 5s # 10 seconds
|
||||
resyncInterval: 10s # 10 seconds
|
||||
resyncInterval: 5m # 5 minutes
|
||||
resyncInterval: 1h # 1 hour
|
||||
resyncInterval: 1d # 1 day
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="updatePolicy">
|
||||
|
||||
The field is optional and will default to `None` if not defined.
|
||||
|
||||
The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical.
|
||||
|
||||
Valid values are `None` and `Replace`.
|
||||
|
||||
Behavior of each policy:
|
||||
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
|
||||
- `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
updatePolicy: Replace
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="deletionPolicy">
|
||||
|
||||
This field is optional and will default to `None` if not defined.
|
||||
|
||||
The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted.
|
||||
|
||||
Valid values are `None` and `Delete`.
|
||||
|
||||
Behavior of each policy:
|
||||
- `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted.
|
||||
- `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
deletionPolicy: Delete
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="destination">
|
||||
The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
destination:
|
||||
projectId: <project-id>
|
||||
environmentSlug: <env-slug>
|
||||
secretsPath: <secrets-path>
|
||||
```
|
||||
|
||||
<Accordion title="destination.projectId">
|
||||
The project ID where you want to create the secrets in Infisical.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="destination.environmentSlug">
|
||||
The environment slug where you want to create the secrets in Infisical.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="destination.secretsPath">
|
||||
The path where you want to create the secrets in Infisical. The root path is `/`.
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="push">
|
||||
The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below.
|
||||
|
||||
|
||||
|
||||
<Accordion title="secret">
|
||||
The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`.
|
||||
|
||||
|
||||
|
||||
Example usage of the `push.secret` field:
|
||||
|
||||
```yaml infisical-push-secret.yaml
|
||||
push:
|
||||
secret:
|
||||
secretName: push-secret-demo
|
||||
secretNamespace: default
|
||||
```
|
||||
|
||||
```yaml push-secret-demo.yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: push-secret-demo
|
||||
namespace: default
|
||||
# Pass in the secrets you wish to push to Infisical
|
||||
stringData:
|
||||
API_KEY: some-api-key
|
||||
DATABASE_URL: postgres://127.0.0.1:5432
|
||||
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="authentication">
|
||||
|
||||
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
|
||||
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
|
||||
|
||||
|
||||
<Accordion title="universalAuth">
|
||||
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
|
||||
[Read more about Universal Auth](/documentation/platform/identities/universal-auth).
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
- `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token.
|
||||
- `credentialsRef.secretName`: The name of the Kubernetes secret.
|
||||
- `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
# infisical-push-secret.yaml
|
||||
spec:
|
||||
universalAuth:
|
||||
credentialsRef:
|
||||
secretName: <secret-name>
|
||||
secretNamespace: <secret-namespace>
|
||||
```
|
||||
|
||||
```yaml
|
||||
# machine-identity-credentials.yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: universal-auth-credentials
|
||||
type: Opaque
|
||||
stringData:
|
||||
clientId: <machine-identity-client-id>
|
||||
clientSecret: <machine-identity-client-secret>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="kubernetesAuth">
|
||||
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
|
||||
[Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth).
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
|
||||
- `serviceAccountRef.name`: The name of the service account.
|
||||
- `serviceAccountRef.namespace`: The namespace of the service account.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubernetesAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountRef:
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="awsIamAuth">
|
||||
The AWS IAM machine identity authentication method is used to authenticate with Infisical.
|
||||
[Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth).
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
authentication:
|
||||
awsIamAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="azureAuth">
|
||||
The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/azure-auth).
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
authentication:
|
||||
azureAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="gcpIamAuth">
|
||||
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
|
||||
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
gcpIamAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="gcpIdTokenAuth">
|
||||
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
gcpIdTokenAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
<Accordion title="tls">
|
||||
This block defines the TLS settings to use for connecting to the Infisical
|
||||
instance.
|
||||
|
||||
Fields:
|
||||
<Accordion title="caRef">
|
||||
This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
|
||||
|
||||
Valid fields:
|
||||
- `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
|
||||
- `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
|
||||
- `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
caRef:
|
||||
secretName: custom-ca-certificate
|
||||
secretNamespace: default
|
||||
key: ca.crt
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
### Applying the InfisicalPushSecret CRD to your cluster
|
||||
|
||||
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.
|
||||
After applying, you should notice that the secrets have been pushed to Infisical.
|
||||
|
||||
```bash
|
||||
kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field
|
||||
kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself
|
||||
```
|
||||
|
||||
### Connecting to instances with private/self-signed certificate
|
||||
|
||||
To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalPushSecret` CRD
|
||||
to point to a CA certificate stored in a Kubernetes secret resource.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 30s
|
||||
tls:
|
||||
caRef:
|
||||
secretName: custom-ca-certificate
|
||||
secretNamespace: default
|
||||
key: ca.crt
|
||||
authentication:
|
||||
# ...
|
||||
```
|
||||
|
||||
|
||||
## Global configuration
|
||||
|
||||
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
|
||||
|
@ -320,6 +320,7 @@
|
||||
"cli/commands/run",
|
||||
"cli/commands/secrets",
|
||||
"cli/commands/dynamic-secrets",
|
||||
"cli/commands/ssh",
|
||||
"cli/commands/export",
|
||||
"cli/commands/token",
|
||||
"cli/commands/service-token",
|
||||
@ -341,6 +342,14 @@
|
||||
"cli/faq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "App Connections",
|
||||
"pages": [
|
||||
"integrations/app-connections/overview",
|
||||
"integrations/app-connections/aws",
|
||||
"integrations/app-connections/github"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Infrastructure Integrations",
|
||||
"pages": [
|
||||
@ -757,6 +766,33 @@
|
||||
"api-reference/endpoints/identity-specific-privilege/list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "App Connections",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/list",
|
||||
"api-reference/endpoints/app-connections/options",
|
||||
{ "group": "AWS",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/aws/list",
|
||||
"api-reference/endpoints/app-connections/aws/get-by-id",
|
||||
"api-reference/endpoints/app-connections/aws/get-by-name",
|
||||
"api-reference/endpoints/app-connections/aws/create",
|
||||
"api-reference/endpoints/app-connections/aws/update",
|
||||
"api-reference/endpoints/app-connections/aws/delete"
|
||||
]
|
||||
},
|
||||
{ "group": "GitHub",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/github/list",
|
||||
"api-reference/endpoints/app-connections/github/get-by-id",
|
||||
"api-reference/endpoints/app-connections/github/get-by-name",
|
||||
"api-reference/endpoints/app-connections/github/create",
|
||||
"api-reference/endpoints/app-connections/github/update",
|
||||
"api-reference/endpoints/app-connections/github/delete"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
|
@ -418,7 +418,53 @@ When set, all visits to the Infisical login page will automatically redirect use
|
||||
information.
|
||||
</Accordion>
|
||||
|
||||
## Native secret integrations
|
||||
## App Connections
|
||||
|
||||
You can configure third-party app connections for re-use across Infisical Projects.
|
||||
|
||||
<Accordion title="AWS Assume Role Connection">
|
||||
<ParamField query="INF_APP_CONNECTION_AWS_ACCESS_KEY_ID" type="string" default="none" optional>
|
||||
The AWS IAM User access key ID for assuming roles
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY" type="string" default="none" optional>
|
||||
The AWS IAM User secret key for assuming roles
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitHub App Connection">
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_APP_ID" type="string" default="none" optional>
|
||||
The ID of the GitHub App
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_APP_SLUG" type="string" default="none" optional>
|
||||
The slug of the GitHub App
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID" type="string" default="none" optional>
|
||||
The client ID for the GitHub App
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET" type="string" default="none" optional>
|
||||
The client secret for the GitHub App
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY" type="string" default="none" optional>
|
||||
The private key for the GitHub App
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitHub OAuth Connection">
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID" type="string" default="none" optional>
|
||||
The OAuth2 client ID for GitHub OAuth Connection
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET" type="string" default="none" optional>
|
||||
The OAuth2 client secret for GitHub OAuth Connection
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
## Native Secret Integrations
|
||||
|
||||
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.
|
||||
|
||||
@ -492,7 +538,7 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="AWS">
|
||||
<Accordion title="AWS Integration">
|
||||
<ParamField query="CLIENT_ID_AWS_INTEGRATION" type="string" default="none" optional>
|
||||
The AWS IAM User access key for assuming roles.
|
||||
</ParamField>
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 47 KiB |
@ -3,6 +3,7 @@ import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
|
||||
enum Region {
|
||||
US = "us",
|
||||
@ -79,10 +80,7 @@ export const RegionSelect = () => {
|
||||
};
|
||||
|
||||
const shouldDisplay =
|
||||
window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://us.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com") ||
|
||||
window.location.origin.includes("http://localhost:8080");
|
||||
isInfisicalCloud() || window.location.origin.includes("http://localhost:8080");
|
||||
|
||||
// only display region select for cloud
|
||||
if (!shouldDisplay) return null;
|
||||
|
@ -44,7 +44,7 @@ export const FormLabel = ({
|
||||
)}
|
||||
{tooltipText && (
|
||||
<Tooltip content={tooltipText} className={tooltipClassName}>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="1x" className="ml-2" />
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Label.Root>
|
||||
|
@ -23,7 +23,8 @@ export enum OrgPermissionSubjects {
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates"
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@ -47,6 +48,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections];
|
||||
|
||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||
|
29
frontend/src/helpers/appConnections.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
TAppConnection
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
||||
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
||||
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return { name: "GitHub App", icon: faGithub };
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return { name: "OAuth", icon: faPassport };
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
return { name: "Access Key", icon: faKey };
|
||||
case AwsConnectionMethod.AssumeRole:
|
||||
return { name: "Assume Role", icon: faUser };
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
};
|
4
frontend/src/helpers/platform.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const isInfisicalCloud = () =>
|
||||
window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://us.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com");
|
4
frontend/src/hooks/api/appConnections/enums.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum AppConnection {
|
||||
AWS = "aws",
|
||||
GitHub = "github"
|
||||
}
|
3
frontend/src/hooks/api/appConnections/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
58
frontend/src/hooks/api/appConnections/mutations.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections/queries";
|
||||
import {
|
||||
TAppConnectionResponse,
|
||||
TCreateAppConnectionDTO,
|
||||
TDeleteAppConnectionDTO,
|
||||
TUpdateAppConnectionDTO
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
|
||||
export const useCreateAppConnection = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ app, ...params }: TCreateAppConnectionDTO) => {
|
||||
const { data } = await apiRequest.post<TAppConnectionResponse>(
|
||||
`/api/v1/app-connections/${app}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.appConnection;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(appConnectionKeys.list())
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateAppConnection = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ connectionId, app, ...params }: TUpdateAppConnectionDTO) => {
|
||||
const { data } = await apiRequest.patch<TAppConnectionResponse>(
|
||||
`/api/v1/app-connections/${app}/${connectionId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.appConnection;
|
||||
},
|
||||
onSuccess: (_, { connectionId, app }) => {
|
||||
queryClient.invalidateQueries(appConnectionKeys.list());
|
||||
queryClient.invalidateQueries(appConnectionKeys.byId(app, connectionId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteAppConnection = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ connectionId, app }: TDeleteAppConnectionDTO) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/app-connections/${app}/${connectionId}`);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { connectionId, app }) => {
|
||||
queryClient.invalidateQueries(appConnectionKeys.list());
|
||||
queryClient.invalidateQueries(appConnectionKeys.byId(app, connectionId));
|
||||
}
|
||||
});
|
||||
};
|
136
frontend/src/hooks/api/appConnections/queries.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useMemo } from "react";
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionMap,
|
||||
TAppConnectionOptions,
|
||||
TGetAppConnection,
|
||||
TListAppConnections
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
import {
|
||||
TAppConnectionOption,
|
||||
TAppConnectionOptionMap
|
||||
} from "@app/hooks/api/appConnections/types/app-options";
|
||||
|
||||
export const appConnectionKeys = {
|
||||
all: ["app-connection"] as const,
|
||||
options: () => [...appConnectionKeys.all, "options"] as const,
|
||||
list: () => [...appConnectionKeys.all, "list"] as const,
|
||||
listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
|
||||
byId: (app: AppConnection, templateId: string) =>
|
||||
[...appConnectionKeys.all, app, "by-id", templateId] as const
|
||||
};
|
||||
|
||||
export const useAppConnectionOptions = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAppConnectionOption[],
|
||||
unknown,
|
||||
TAppConnectionOption[],
|
||||
ReturnType<typeof appConnectionKeys.options>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: appConnectionKeys.options(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TAppConnectionOptions>(
|
||||
"/api/v1/app-connections/options"
|
||||
);
|
||||
|
||||
return data.appConnectionOptions;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
|
||||
const { data: options = [], isLoading } = useAppConnectionOptions();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
option: (options.find((opt) => opt.app === app) as TAppConnectionOptionMap[T]) ?? {},
|
||||
isLoading
|
||||
}),
|
||||
[options, app]
|
||||
);
|
||||
};
|
||||
|
||||
export const useListAppConnections = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAppConnection[],
|
||||
unknown,
|
||||
TAppConnection[],
|
||||
ReturnType<typeof appConnectionKeys.list>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: appConnectionKeys.list(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListAppConnections<TAppConnection>>(
|
||||
"/api/v1/app-connections"
|
||||
);
|
||||
|
||||
return data.appConnections;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListAppConnectionsByApp = <T extends AppConnection>(
|
||||
app: T,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAppConnectionMap[T][],
|
||||
unknown,
|
||||
TAppConnectionMap[T][],
|
||||
ReturnType<typeof appConnectionKeys.listByApp>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: appConnectionKeys.listByApp(app),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
|
||||
`/api/v1/app-connections/${app}`
|
||||
);
|
||||
|
||||
return data.appConnections;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAppConnectionById = <T extends AppConnection>(
|
||||
app: T,
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAppConnectionMap[T],
|
||||
unknown,
|
||||
TAppConnectionMap[T],
|
||||
ReturnType<typeof appConnectionKeys.byId>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: appConnectionKeys.byId(app, connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
|
||||
`/api/v1/app-connections/${app}/${connectionId}`
|
||||
);
|
||||
|
||||
return data.appConnection;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
24
frontend/src/hooks/api/appConnections/types/app-options.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
export type TAppConnectionOptionBase = {
|
||||
name: string;
|
||||
methods: string[];
|
||||
};
|
||||
|
||||
export type TAwsConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.AWS;
|
||||
accessKeyId?: string;
|
||||
};
|
||||
|
||||
export type TGitHubConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.GitHub;
|
||||
oauthClientId?: string;
|
||||
appClientSlug?: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
[AppConnection.GitHub]: TGitHubConnectionOption;
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum AwsConnectionMethod {
|
||||
AssumeRole = "assume-role",
|
||||
AccessKey = "access-key"
|
||||
}
|
||||
|
||||
export type TAwsConnection = TRootAppConnection & { app: AppConnection.AWS } & (
|
||||
| {
|
||||
method: AwsConnectionMethod.AccessKey;
|
||||
credentials: {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: AwsConnectionMethod.AssumeRole;
|
||||
credentials: {
|
||||
roleArn: string;
|
||||
};
|
||||
}
|
||||
);
|
@ -0,0 +1,23 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum GitHubConnectionMethod {
|
||||
App = "github-app",
|
||||
OAuth = "oauth"
|
||||
}
|
||||
|
||||
export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub } & (
|
||||
| {
|
||||
method: GitHubConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: GitHubConnectionMethod.App;
|
||||
credentials: {
|
||||
code: string;
|
||||
installationId: string;
|
||||
};
|
||||
}
|
||||
);
|
36
frontend/src/hooks/api/appConnections/types/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options";
|
||||
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
|
||||
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
|
||||
|
||||
export * from "./aws-connection";
|
||||
export * from "./github-connection";
|
||||
|
||||
export type TAppConnection = TAwsConnection | TGitHubConnection;
|
||||
|
||||
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
|
||||
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
|
||||
export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] };
|
||||
export type TAppConnectionResponse = { appConnection: TAppConnection };
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnection,
|
||||
"name" | "credentials" | "method" | "app" | "description"
|
||||
>;
|
||||
|
||||
export type TUpdateAppConnectionDTO = Partial<
|
||||
Pick<TAppConnection, "name" | "credentials" | "description">
|
||||
> & {
|
||||
connectionId: string;
|
||||
app: AppConnection;
|
||||
};
|
||||
|
||||
export type TDeleteAppConnectionDTO = {
|
||||
app: AppConnection;
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionMap = {
|
||||
[AppConnection.AWS]: TAwsConnection;
|
||||
[AppConnection.GitHub]: TGitHubConnection;
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
export type TRootAppConnection = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
version: number;
|
||||
orgId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|