mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 10:29:52 +00:00
Merge remote-tracking branch 'origin/main' into feat/gitlabSecretSync
This commit is contained in:
@@ -26,7 +26,8 @@ export enum AppConnection {
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab"
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -51,6 +51,11 @@ import {
|
||||
validateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
validateCloudflareConnectionCredentials
|
||||
} from "./cloudflare/cloudflare-connection-fns";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
@@ -130,7 +135,8 @@ export const listAppConnectionOptions = () => {
|
||||
getHerokuConnectionListItem(),
|
||||
getRenderConnectionListItem(),
|
||||
getFlyioConnectionListItem(),
|
||||
getGitLabConnectionListItem()
|
||||
getGitLabConnectionListItem(),
|
||||
getCloudflareConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -209,7 +215,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -245,6 +252,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case TerraformCloudConnectionMethod.ApiToken:
|
||||
case VercelConnectionMethod.ApiToken:
|
||||
case OnePassConnectionMethod.ApiToken:
|
||||
case CloudflareConnectionMethod.APIToken:
|
||||
return "API Token";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -323,7 +331,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Heroku]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Render]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -28,7 +28,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Heroku]: "Heroku",
|
||||
[AppConnection.Render]: "Render",
|
||||
[AppConnection.Flyio]: "Fly.io",
|
||||
[AppConnection.GitLab]: "GitLab"
|
||||
[AppConnection.GitLab]: "GitLab",
|
||||
[AppConnection.Cloudflare]: "Cloudflare"
|
||||
};
|
||||
|
||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -59,5 +60,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Heroku]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Render]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-servic
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
|
||||
@@ -116,7 +118,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
|
||||
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -525,6 +528,7 @@ export const appConnectionServiceFactory = ({
|
||||
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
render: renderConnectionService(connectAppConnectionById),
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -62,6 +62,12 @@ import {
|
||||
TCamundaConnectionInput,
|
||||
TValidateCamundaConnectionCredentialsSchema
|
||||
} from "./camunda";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
TCloudflareConnectionInput,
|
||||
TValidateCloudflareConnectionCredentialsSchema
|
||||
} from "./cloudflare/cloudflare-connection-types";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
@@ -189,6 +195,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TRenderConnection
|
||||
| TFlyioConnection
|
||||
| TGitLabConnection
|
||||
| TCloudflareConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -224,6 +231,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TRenderConnectionInput
|
||||
| TFlyioConnectionInput
|
||||
| TGitLabConnectionInput
|
||||
| TCloudflareConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -266,7 +274,8 @@ export type TAppConnectionConfig =
|
||||
| THerokuConnectionConfig
|
||||
| TRenderConnectionConfig
|
||||
| TFlyioConnectionConfig
|
||||
| TGitLabConnectionConfig;
|
||||
| TGitLabConnectionConfig
|
||||
| TCloudflareConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -296,7 +305,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateHerokuConnectionCredentialsSchema
|
||||
| TValidateRenderConnectionCredentialsSchema
|
||||
| TValidateFlyioConnectionCredentialsSchema
|
||||
| TValidateGitLabConnectionCredentialsSchema;
|
||||
| TValidateGitLabConnectionCredentialsSchema
|
||||
| TValidateCloudflareConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum CloudflareConnectionMethod {
|
||||
APIToken = "api-token"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
TCloudflarePagesProject
|
||||
} from "./cloudflare-connection-types";
|
||||
|
||||
export const getCloudflareConnectionListItem = () => {
|
||||
return {
|
||||
name: "Cloudflare" as const,
|
||||
app: AppConnection.Cloudflare as const,
|
||||
methods: Object.values(CloudflareConnectionMethod) as [CloudflareConnectionMethod.APIToken]
|
||||
};
|
||||
};
|
||||
|
||||
export const listCloudflarePagesProjects = async (
|
||||
appConnection: TCloudflareConnection
|
||||
): Promise<TCloudflarePagesProject[]> => {
|
||||
const {
|
||||
credentials: { apiToken, accountId }
|
||||
} = appConnection;
|
||||
|
||||
const { data } = await request.get<{ result: { name: string; id: string }[] }>(
|
||||
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.result.map((a) => ({
|
||||
name: a.name,
|
||||
id: a.id
|
||||
}));
|
||||
};
|
||||
|
||||
export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
|
||||
const { apiToken, accountId } = config.credentials;
|
||||
|
||||
try {
|
||||
const resp = await request.get(`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (resp.data === null) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: Invalid API token provided."
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to validate credentials: ${error.response?.data?.errors?.[0]?.message || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
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 { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
|
||||
const accountIdCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
]);
|
||||
|
||||
export const CloudflareConnectionApiTokenCredentialsSchema = z.object({
|
||||
accountId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Account ID required")
|
||||
.max(256, "Account ID cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) => accountIdCharacterValidator(val),
|
||||
"Account ID can only contain alphanumeric characters, underscores, and hyphens"
|
||||
),
|
||||
apiToken: z.string().trim().min(1, "API token required").max(256, "API token cannot exceed 256 characters")
|
||||
});
|
||||
|
||||
const BaseCloudflareConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Cloudflare) });
|
||||
|
||||
export const CloudflareConnectionSchema = BaseCloudflareConnectionSchema.extend({
|
||||
method: z.literal(CloudflareConnectionMethod.APIToken),
|
||||
credentials: CloudflareConnectionApiTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedCloudflareConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseCloudflareConnectionSchema.extend({
|
||||
method: z.literal(CloudflareConnectionMethod.APIToken),
|
||||
credentials: CloudflareConnectionApiTokenCredentialsSchema.pick({ accountId: true })
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateCloudflareConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(CloudflareConnectionMethod.APIToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.Cloudflare).method),
|
||||
credentials: CloudflareConnectionApiTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Cloudflare).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateCloudflareConnectionSchema = ValidateCloudflareConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Cloudflare)
|
||||
);
|
||||
|
||||
export const UpdateCloudflareConnectionSchema = z
|
||||
.object({
|
||||
credentials: CloudflareConnectionApiTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Cloudflare).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Cloudflare));
|
||||
|
||||
export const CloudflareConnectionListItemSchema = z.object({
|
||||
name: z.literal("Cloudflare"),
|
||||
app: z.literal(AppConnection.Cloudflare),
|
||||
methods: z.nativeEnum(CloudflareConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listCloudflarePagesProjects } from "./cloudflare-connection-fns";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TCloudflareConnection>;
|
||||
|
||||
export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listPagesProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||
try {
|
||||
const projects = await listCloudflarePagesProjects(appConnection);
|
||||
|
||||
return projects;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list Cloudflare Pages projects for Cloudflare connection");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listPagesProjects
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CloudflareConnectionSchema,
|
||||
CreateCloudflareConnectionSchema,
|
||||
ValidateCloudflareConnectionCredentialsSchema
|
||||
} from "./cloudflare-connection-schema";
|
||||
|
||||
export type TCloudflareConnection = z.infer<typeof CloudflareConnectionSchema>;
|
||||
|
||||
export type TCloudflareConnectionInput = z.infer<typeof CreateCloudflareConnectionSchema> & {
|
||||
app: AppConnection.Cloudflare;
|
||||
};
|
||||
|
||||
export type TValidateCloudflareConnectionCredentialsSchema = typeof ValidateCloudflareConnectionCredentialsSchema;
|
||||
|
||||
export type TCloudflareConnectionConfig = DiscriminativePick<
|
||||
TCloudflareConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCloudflarePagesProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ 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 { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
@@ -14,13 +15,14 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
|
||||
|
||||
export const getGitHubConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
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
|
||||
appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,23 +32,24 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
|
||||
const { method, credentials } = appConnection;
|
||||
|
||||
let client: Octokit;
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) {
|
||||
if (!appId || !appPrivateKey) {
|
||||
throw new InternalServerError({
|
||||
message: `GitHub ${getAppConnectionMethodName(method).replace(
|
||||
"GitHub",
|
||||
""
|
||||
)} environment variables have not been configured`
|
||||
message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
client = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID,
|
||||
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY,
|
||||
appId,
|
||||
privateKey: appPrivateKey,
|
||||
installationId: credentials.installationId
|
||||
}
|
||||
});
|
||||
@@ -154,6 +157,8 @@ type TokenRespData = {
|
||||
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
||||
const { credentials, method } = config;
|
||||
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
const {
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||
@@ -165,8 +170,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
||||
const { clientId, clientSecret } =
|
||||
method === GitHubConnectionMethod.App
|
||||
? {
|
||||
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
}
|
||||
: // oauth
|
||||
{
|
||||
|
||||
@@ -84,6 +84,8 @@ export enum IntegrationUrls {
|
||||
QOVERY_API_URL = "https://api.qovery.com",
|
||||
TERRAFORM_CLOUD_API_URL = "https://app.terraform.io",
|
||||
CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com",
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
||||
CLOUDFLARE_API_URL = "https://api.cloudflare.com",
|
||||
// eslint-disable-next-line
|
||||
CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com",
|
||||
BITBUCKET_API_URL = "https://api.bitbucket.org",
|
||||
|
||||
@@ -42,7 +42,7 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { expandInternalCa } from "../certificate-authority/certificate-authority-fns";
|
||||
@@ -82,6 +82,7 @@ import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } f
|
||||
import { TProjectQueueFactory } from "./project-queue";
|
||||
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
||||
import {
|
||||
ProjectFilterType,
|
||||
TCreateProjectDTO,
|
||||
TDeleteProjectDTO,
|
||||
TDeleteProjectWorkflowIntegration,
|
||||
@@ -866,6 +867,39 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const extractProjectIdFromSlug = async ({
|
||||
projectSlug,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: {
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actor: ActorType;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
if (projectId) return projectId;
|
||||
if (!projectSlug) throw new BadRequestError({ message: "You must provide projectSlug or workspaceId" });
|
||||
const project = await getAProject({
|
||||
filter: {
|
||||
type: ProjectFilterType.SLUG,
|
||||
orgId: actorOrgId,
|
||||
slug: projectSlug
|
||||
},
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
if (!project) throw new NotFoundError({ message: `No project found with slug ${projectSlug}` });
|
||||
return project.id;
|
||||
};
|
||||
|
||||
const getProjectUpgradeStatus = async ({
|
||||
projectId,
|
||||
actor,
|
||||
@@ -2006,6 +2040,7 @@ export const projectServiceFactory = ({
|
||||
getProjectSshConfig,
|
||||
updateProjectSshConfig,
|
||||
requestProjectAccess,
|
||||
searchProjects
|
||||
searchProjects,
|
||||
extractProjectIdFromSlug
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||
@@ -83,36 +84,75 @@ export const secretFolderServiceFactory = ({
|
||||
// that is this request must be idempotent
|
||||
// so we do a tricky move. we try to find the to be created folder path if that is exactly match return that
|
||||
// else we get some path before that then we will start creating remaining folder
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateFolder(env.id, env.projectId)]);
|
||||
|
||||
const pathWithFolder = path.join(secretPath, name);
|
||||
const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx);
|
||||
// no folder found is not possible root should be their
|
||||
|
||||
if (!parentFolder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${pathWithFolder}' in environment with slug '${environment}' not found`
|
||||
message: `Parent folder for path '${pathWithFolder}' not found`
|
||||
});
|
||||
}
|
||||
// exact folder
|
||||
if (parentFolder.path === pathWithFolder) return parentFolder;
|
||||
|
||||
let parentFolderId = parentFolder.id;
|
||||
// check if the exact folder already exists
|
||||
const existingFolder = await folderDAL.findOne(
|
||||
{
|
||||
envId: env.id,
|
||||
parentId: parentFolder.id,
|
||||
name,
|
||||
isReserved: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (existingFolder) {
|
||||
return existingFolder;
|
||||
}
|
||||
|
||||
// exact folder case
|
||||
if (parentFolder.path === pathWithFolder) {
|
||||
return parentFolder;
|
||||
}
|
||||
|
||||
let currentParentId = parentFolder.id;
|
||||
|
||||
// build the full path we need by processing each segment
|
||||
if (parentFolder.path !== secretPath) {
|
||||
// this is upsert folder in a path
|
||||
// we are not taking snapshots of this because
|
||||
// snapshot will be removed from automatic for all commits to user click or cron based
|
||||
const missingSegment = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
|
||||
if (missingSegment.length) {
|
||||
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegment.map((segment) => {
|
||||
const missingSegments = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
|
||||
|
||||
const newFolders: TSecretFoldersInsert[] = [];
|
||||
|
||||
// process each segment sequentially
|
||||
for await (const segment of missingSegments) {
|
||||
const existingSegment = await folderDAL.findOne(
|
||||
{
|
||||
name: segment,
|
||||
parentId: currentParentId,
|
||||
envId: env.id,
|
||||
isReserved: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (existingSegment) {
|
||||
// use existing folder and update the path / parent
|
||||
currentParentId = existingSegment.id;
|
||||
} else {
|
||||
const newFolder = {
|
||||
name: segment,
|
||||
parentId: parentFolderId,
|
||||
parentId: currentParentId,
|
||||
id: uuidv4(),
|
||||
envId: env.id,
|
||||
version: 1
|
||||
};
|
||||
parentFolderId = newFolder.id;
|
||||
return newFolder;
|
||||
});
|
||||
parentFolderId = newFolders.at(-1)?.id as string;
|
||||
|
||||
currentParentId = newFolder.id;
|
||||
newFolders.push(newFolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (newFolders.length) {
|
||||
const docs = await folderDAL.insertMany(newFolders, tx);
|
||||
const folderVersions = await folderVersionDAL.insertMany(
|
||||
docs.map((doc) => ({
|
||||
@@ -133,7 +173,7 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: parentFolderId,
|
||||
folderId: currentParentId,
|
||||
changes: folderVersions.map((fv) => ({
|
||||
type: CommitType.ADD,
|
||||
folderVersionId: fv.id
|
||||
@@ -145,9 +185,10 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
|
||||
const doc = await folderDAL.create(
|
||||
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
||||
{ name, envId: env.id, version: 1, parentId: currentParentId, description },
|
||||
tx
|
||||
);
|
||||
|
||||
const folderVersion = await folderVersionDAL.create(
|
||||
{
|
||||
name: doc.name,
|
||||
@@ -158,6 +199,7 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
@@ -167,7 +209,7 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: parentFolderId,
|
||||
folderId: doc.id,
|
||||
changes: [
|
||||
{
|
||||
type: CommitType.ADD,
|
||||
@@ -177,6 +219,7 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return doc;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const CLOUDFLARE_PAGES_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Cloudflare Pages",
|
||||
destination: SecretSync.CloudflarePages,
|
||||
connection: AppConnection.Cloudflare,
|
||||
canImportSecrets: false
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
|
||||
import { TCloudflarePagesSyncWithCredentials } from "./cloudflare-pages-types";
|
||||
|
||||
const getProjectEnvironmentSecrets = async (secretSync: TCloudflarePagesSyncWithCredentials) => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken, accountId }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const secrets = (
|
||||
await request.get<{
|
||||
result: {
|
||||
deployment_configs: Record<
|
||||
string,
|
||||
{
|
||||
env_vars: Record<string, { type: "plain_text" | "secret_text"; value: string }>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
}>(
|
||||
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data.result.deployment_configs[destinationConfig.environment].env_vars;
|
||||
|
||||
return Object.entries(secrets ?? {}).map(([key, envVar]) => ({
|
||||
key,
|
||||
value: envVar.value
|
||||
}));
|
||||
};
|
||||
|
||||
export const CloudflarePagesSyncFns = {
|
||||
syncSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken, accountId }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
// Create/update secret entries
|
||||
let secretEntries: [string, object | null][] = Object.entries(secretMap).map(([key, val]) => [
|
||||
key,
|
||||
{ type: "secret_text", value: val.value }
|
||||
]);
|
||||
|
||||
// Handle deletions if not disabled
|
||||
if (!secretSync.syncOptions.disableSecretDeletion) {
|
||||
const existingSecrets = await getProjectEnvironmentSecrets(secretSync);
|
||||
const toDeleteKeys = existingSecrets
|
||||
.filter(
|
||||
(secret) =>
|
||||
matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
|
||||
!secretMap[secret.key]
|
||||
)
|
||||
.map((secret) => secret.key);
|
||||
|
||||
const toDeleteEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
|
||||
secretEntries = [...secretEntries, ...toDeleteEntries];
|
||||
}
|
||||
|
||||
const data = {
|
||||
deployment_configs: {
|
||||
[destinationConfig.environment]: {
|
||||
env_vars: Object.fromEntries(secretEntries)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await request.patch(
|
||||
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials): Promise<TSecretMap> => {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
},
|
||||
|
||||
removeSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken, accountId }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const secrets = await getProjectEnvironmentSecrets(secretSync);
|
||||
const toDeleteKeys = secrets
|
||||
.filter(
|
||||
(secret) =>
|
||||
matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
|
||||
secret.key in secretMap
|
||||
)
|
||||
.map((secret) => secret.key);
|
||||
|
||||
if (toDeleteKeys.length === 0) return;
|
||||
|
||||
const secretEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
|
||||
|
||||
const data = {
|
||||
deployment_configs: {
|
||||
[destinationConfig.environment]: {
|
||||
env_vars: Object.fromEntries(secretEntries)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await request.patch(
|
||||
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const CloudflarePagesSyncDestinationConfigSchema = z.object({
|
||||
projectName: z
|
||||
.string()
|
||||
.min(1, "Project name is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_PAGES.projectName),
|
||||
environment: z
|
||||
.string()
|
||||
.min(1, "Environment is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_PAGES.environment)
|
||||
});
|
||||
|
||||
const CloudflarePagesSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
||||
export const CloudflarePagesSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.CloudflarePages,
|
||||
CloudflarePagesSyncOptionsConfig
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.CloudflarePages),
|
||||
destinationConfig: CloudflarePagesSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateCloudflarePagesSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.CloudflarePages,
|
||||
CloudflarePagesSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: CloudflarePagesSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateCloudflarePagesSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.CloudflarePages,
|
||||
CloudflarePagesSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: CloudflarePagesSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const CloudflarePagesSyncListItemSchema = z.object({
|
||||
name: z.literal("Cloudflare Pages"),
|
||||
connection: z.literal(AppConnection.Cloudflare),
|
||||
destination: z.literal(SecretSync.CloudflarePages),
|
||||
canImportSecrets: z.literal(false)
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema,
|
||||
CreateCloudflarePagesSyncSchema
|
||||
} from "./cloudflare-pages-schema";
|
||||
|
||||
export type TCloudflarePagesSyncListItem = z.infer<typeof CloudflarePagesSyncListItemSchema>;
|
||||
|
||||
export type TCloudflarePagesSync = z.infer<typeof CloudflarePagesSyncSchema>;
|
||||
|
||||
export type TCloudflarePagesSyncInput = z.infer<typeof CreateCloudflarePagesSyncSchema>;
|
||||
|
||||
export type TCloudflarePagesSyncWithCredentials = TCloudflarePagesSync & {
|
||||
connection: TCloudflareConnection;
|
||||
};
|
||||
@@ -19,7 +19,8 @@ export enum SecretSync {
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab"
|
||||
GitLab = "gitlab",
|
||||
CloudflarePages = "cloudflare-pages"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -29,6 +29,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact
|
||||
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
|
||||
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
|
||||
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
|
||||
import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
|
||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
@@ -65,7 +67,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
|
||||
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION
|
||||
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
|
||||
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -231,6 +234,8 @@ export const SecretSyncFns = {
|
||||
return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.GitLab:
|
||||
return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
|
||||
case SecretSync.CloudflarePages:
|
||||
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -320,6 +325,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.GitLab:
|
||||
secretMap = await GitLabSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.CloudflarePages:
|
||||
secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -395,6 +403,8 @@ export const SecretSyncFns = {
|
||||
return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.GitLab:
|
||||
return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
|
||||
case SecretSync.CloudflarePages:
|
||||
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -22,7 +22,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Heroku]: "Heroku",
|
||||
[SecretSync.Render]: "Render",
|
||||
[SecretSync.Flyio]: "Fly.io",
|
||||
[SecretSync.GitLab]: "GitLab"
|
||||
[SecretSync.GitLab]: "GitLab",
|
||||
[SecretSync.CloudflarePages]: "Cloudflare Pages"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -46,7 +47,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Heroku]: AppConnection.Heroku,
|
||||
[SecretSync.Render]: AppConnection.Render,
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.GitLab]: AppConnection.GitLab
|
||||
[SecretSync.GitLab]: AppConnection.GitLab,
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
@@ -70,5 +72,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Render]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.GitLab]: SecretSyncPlanType.Regular
|
||||
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -72,6 +72,12 @@ import {
|
||||
TAzureKeyVaultSyncListItem,
|
||||
TAzureKeyVaultSyncWithCredentials
|
||||
} from "./azure-key-vault";
|
||||
import {
|
||||
TCloudflarePagesSync,
|
||||
TCloudflarePagesSyncInput,
|
||||
TCloudflarePagesSyncListItem,
|
||||
TCloudflarePagesSyncWithCredentials
|
||||
} from "./cloudflare-pages/cloudflare-pages-types";
|
||||
import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
|
||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||
import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab";
|
||||
@@ -129,7 +135,8 @@ export type TSecretSync =
|
||||
| THerokuSync
|
||||
| TRenderSync
|
||||
| TFlyioSync
|
||||
| TGitLabSync;
|
||||
| TGitLabSync
|
||||
| TCloudflarePagesSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -152,7 +159,8 @@ export type TSecretSyncWithCredentials =
|
||||
| THerokuSyncWithCredentials
|
||||
| TRenderSyncWithCredentials
|
||||
| TFlyioSyncWithCredentials
|
||||
| TGitLabSyncWithCredentials;
|
||||
| TGitLabSyncWithCredentials
|
||||
| TCloudflarePagesSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -175,7 +183,8 @@ export type TSecretSyncInput =
|
||||
| THerokuSyncInput
|
||||
| TRenderSyncInput
|
||||
| TFlyioSyncInput
|
||||
| TGitLabSyncInput;
|
||||
| TGitLabSyncInput
|
||||
| TCloudflarePagesSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -198,7 +207,8 @@ export type TSecretSyncListItem =
|
||||
| THerokuSyncListItem
|
||||
| TRenderSyncListItem
|
||||
| TFlyioSyncListItem
|
||||
| TGitLabSyncListItem;
|
||||
| TGitLabSyncListItem
|
||||
| TCloudflarePagesSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
@@ -1543,9 +1543,8 @@ export const secretServiceFactory = ({
|
||||
actor,
|
||||
environment,
|
||||
viewSecretValue,
|
||||
projectId: workspaceId,
|
||||
projectId,
|
||||
expandSecretReferences,
|
||||
projectSlug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@@ -1553,7 +1552,6 @@ export const secretServiceFactory = ({
|
||||
includeImports,
|
||||
version
|
||||
}: TGetASecretRawDTO) => {
|
||||
const projectId = workspaceId || (await projectDAL.findProjectBySlug(projectSlug as string, actorOrgId)).id;
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const secret = await secretV2BridgeService.getSecretByName({
|
||||
|
||||
@@ -229,8 +229,7 @@ export type TGetASecretRawDTO = {
|
||||
type: "shared" | "personal";
|
||||
includeImports?: boolean;
|
||||
version?: number;
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
projectId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetASecretByIdRawDTO = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { CronJob } from "cron";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||
@@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
TAdminBootstrapInstanceDTO,
|
||||
TAdminGetIdentitiesDTO,
|
||||
TAdminGetUsersDTO,
|
||||
TAdminIntegrationConfig,
|
||||
TAdminSignUpDTO,
|
||||
TGetOrganizationsDTO
|
||||
} from "./super-admin-types";
|
||||
@@ -70,6 +73,31 @@ export let getServerCfg: () => Promise<
|
||||
}
|
||||
>;
|
||||
|
||||
let adminIntegrationsConfig: TAdminIntegrationConfig = {
|
||||
slack: {
|
||||
clientSecret: "",
|
||||
clientId: ""
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: "",
|
||||
clientSecret: "",
|
||||
botId: ""
|
||||
},
|
||||
gitHubAppConnection: {
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
appSlug: "",
|
||||
appId: "",
|
||||
privateKey: ""
|
||||
}
|
||||
};
|
||||
|
||||
Object.freeze(adminIntegrationsConfig);
|
||||
|
||||
export const getInstanceIntegrationsConfig = () => {
|
||||
return adminIntegrationsConfig;
|
||||
};
|
||||
|
||||
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
|
||||
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
|
||||
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
@@ -138,6 +166,74 @@ export const superAdminServiceFactory = ({
|
||||
return serverCfg;
|
||||
};
|
||||
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionClientId = serverCfg.encryptedGitHubAppConnectionClientId
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionClientId).toString()
|
||||
: "";
|
||||
const gitHubAppConnectionClientSecret = serverCfg.encryptedGitHubAppConnectionClientSecret
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionAppSlug = serverCfg.encryptedGitHubAppConnectionSlug
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionSlug).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionAppId = serverCfg.encryptedGitHubAppConnectionId
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionId).toString()
|
||||
: "";
|
||||
const gitHubAppConnectionAppPrivateKey = serverCfg.encryptedGitHubAppConnectionPrivateKey
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
},
|
||||
gitHubAppConnection: {
|
||||
clientId: gitHubAppConnectionClientId,
|
||||
clientSecret: gitHubAppConnectionClientSecret,
|
||||
appSlug: gitHubAppConnectionAppSlug,
|
||||
appId: gitHubAppConnectionAppId,
|
||||
privateKey: gitHubAppConnectionAppPrivateKey
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const $syncAdminIntegrationConfig = async () => {
|
||||
const config = await getAdminIntegrationsConfig();
|
||||
Object.freeze(config);
|
||||
adminIntegrationsConfig = config;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (
|
||||
data: TSuperAdminUpdate & {
|
||||
slackClientId?: string;
|
||||
@@ -145,6 +241,11 @@ export const superAdminServiceFactory = ({
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
gitHubAppConnectionClientId?: string;
|
||||
gitHubAppConnectionClientSecret?: string;
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
},
|
||||
userId: string
|
||||
) => {
|
||||
@@ -236,10 +337,51 @@ export const superAdminServiceFactory = ({
|
||||
updatedData.microsoftTeamsBotId = undefined;
|
||||
microsoftTeamsSettingsUpdated = true;
|
||||
}
|
||||
|
||||
let gitHubAppConnectionSettingsUpdated = false;
|
||||
if (data.gitHubAppConnectionClientId !== undefined) {
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientId));
|
||||
updatedData.encryptedGitHubAppConnectionClientId = encryptedClientId;
|
||||
updatedData.gitHubAppConnectionClientId = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionClientSecret !== undefined) {
|
||||
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientSecret));
|
||||
updatedData.encryptedGitHubAppConnectionClientSecret = encryptedClientSecret;
|
||||
updatedData.gitHubAppConnectionClientSecret = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionSlug !== undefined) {
|
||||
const encryptedAppSlug = encryptWithRoot(Buffer.from(data.gitHubAppConnectionSlug));
|
||||
updatedData.encryptedGitHubAppConnectionSlug = encryptedAppSlug;
|
||||
updatedData.gitHubAppConnectionSlug = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionId !== undefined) {
|
||||
const encryptedAppId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionId));
|
||||
updatedData.encryptedGitHubAppConnectionId = encryptedAppId;
|
||||
updatedData.gitHubAppConnectionId = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionPrivateKey !== undefined) {
|
||||
const encryptedAppPrivateKey = encryptWithRoot(Buffer.from(data.gitHubAppConnectionPrivateKey));
|
||||
updatedData.encryptedGitHubAppConnectionPrivateKey = encryptedAppPrivateKey;
|
||||
updatedData.gitHubAppConnectionPrivateKey = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||
|
||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||
|
||||
if (gitHubAppConnectionSettingsUpdated) {
|
||||
await $syncAdminIntegrationConfig();
|
||||
}
|
||||
|
||||
if (
|
||||
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
|
||||
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
|
||||
@@ -593,43 +735,6 @@ export const superAdminServiceFactory = ({
|
||||
await userDAL.updateById(userId, { superAdmin: true });
|
||||
};
|
||||
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getConfiguredEncryptionStrategies = async () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -696,6 +801,19 @@ export const superAdminServiceFactory = ({
|
||||
return (await keyStore.getItem("invalidating-cache")) !== null;
|
||||
};
|
||||
|
||||
const initializeAdminIntegrationConfigSync = async () => {
|
||||
logger.info("Setting up background sync process for admin integrations config");
|
||||
|
||||
// initial sync upon startup
|
||||
await $syncAdminIntegrationConfig();
|
||||
|
||||
// sync admin integrations config every 5 minutes
|
||||
const job = new CronJob("*/5 * * * *", $syncAdminIntegrationConfig);
|
||||
job.start();
|
||||
|
||||
return job;
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
@@ -714,6 +832,7 @@ export const superAdminServiceFactory = ({
|
||||
checkIfInvalidatingCache,
|
||||
getOrganizations,
|
||||
deleteOrganization,
|
||||
deleteOrganizationMembership
|
||||
deleteOrganizationMembership,
|
||||
initializeAdminIntegrationConfigSync
|
||||
};
|
||||
};
|
||||
|
||||
@@ -55,3 +55,22 @@ export enum CacheType {
|
||||
ALL = "all",
|
||||
SECRETS = "secrets"
|
||||
}
|
||||
|
||||
export type TAdminIntegrationConfig = {
|
||||
slack: {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
};
|
||||
microsoftTeams: {
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
botId: string;
|
||||
};
|
||||
gitHubAppConnection: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
appSlug: string;
|
||||
appId: string;
|
||||
privateKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user