diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 32829a1d7..23f783792 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1718,36 +1718,40 @@ export const SecretSyncs = { SYNC_OPTIONS: (destination: SecretSync) => { const destinationName = SECRET_SYNC_NAME_MAP[destination]; return { - INITIAL_SYNC_BEHAVIOR: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, - PREPEND_PREFIX: `Optionally prepend a prefix to your secrets' keys when syncing to ${destinationName}.`, - APPEND_SUFFIX: `Optionally append a suffix to your secrets' keys when syncing to ${destinationName}.` + initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.` }; }, DESTINATION_CONFIG: { AWS_PARAMETER_STORE: { - REGION: "The AWS region to sync secrets to.", - PATH: "The Parameter Store path to sync secrets to." + region: "The AWS region to sync secrets to.", + path: "The Parameter Store path to sync secrets to." }, AWS_SECRETS_MANAGER: { - REGION: "The AWS region to sync secrets to.", - MAPPING_BEHAVIOR: - "How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.", - SECRET_NAME: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one." + region: "The AWS region to sync secrets to.", + mappingBehavior: "How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.", + secretName: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one." }, GITHUB: { - ORG: "The name of the GitHub organization.", - OWNER: "The name of the GitHub account owner of the repository.", - REPO: "The name of the GitHub repository.", - ENV: "The name of the GitHub environment." + scope: "The GitHub scope that secrets should be synced to", + org: "The name of the GitHub organization.", + owner: "The name of the GitHub account owner of the repository.", + repo: "The name of the GitHub repository.", + env: "The name of the GitHub environment." }, AZURE_KEY_VAULT: { - VAULT_BASE_URL: - "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/" + vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/" }, AZURE_APP_CONFIGURATION: { - CONFIGURATION_URL: + configurationUrl: "The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/", - LABEL: "An optional label to assign to secrets created in Azure App Configuration." + label: "An optional label to assign to secrets created in Azure App Configuration." + }, + GCP: { + scope: "The Google project scope that secrets should be synced to.", + projectId: "The ID of the Google project secrets should be synced to." + }, + DATABRICKS: { + scope: "The Databricks secret scope that secrets should be synced to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index c9c49eeb8..964a57a13 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -12,6 +12,10 @@ import { AzureKeyVaultConnectionListItemSchema, SanitizedAzureKeyVaultConnectionSchema } from "@app/services/app-connection/azure-key-vault"; +import { + DatabricksConnectionListItemSchema, + SanitizedDatabricksConnectionSchema +} from "@app/services/app-connection/databricks"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -22,7 +26,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedGitHubConnectionSchema.options, ...SanitizedGcpConnectionSchema.options, ...SanitizedAzureKeyVaultConnectionSchema.options, - ...SanitizedAzureAppConfigurationConnectionSchema.options + ...SanitizedAzureAppConfigurationConnectionSchema.options, + ...SanitizedDatabricksConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -30,7 +35,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ GitHubConnectionListItemSchema, GcpConnectionListItemSchema, AzureKeyVaultConnectionListItemSchema, - AzureAppConfigurationConnectionListItemSchema + AzureAppConfigurationConnectionListItemSchema, + DatabricksConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts new file mode 100644 index 000000000..7fdb7f3a2 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +import { readLimit } 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 { + CreateDatabricksConnectionSchema, + SanitizedDatabricksConnectionSchema, + UpdateDatabricksConnectionSchema +} from "@app/services/app-connection/databricks"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDatabricksConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Databricks, + server, + sanitizedResponseSchema: SanitizedDatabricksConnectionSchema, + createSchema: CreateDatabricksConnectionSchema, + updateSchema: UpdateDatabricksConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/secret-scopes`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + secretScopes: z.object({ name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const secretScopes = await server.services.appConnection.databricks.listSecretScopes( + connectionId, + req.permission + ); + + return { secretScopes }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts index 9c33f3fad..8444b0cd6 100644 --- a/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts @@ -41,7 +41,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { connectionId } = req.params; @@ -67,7 +67,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { connectionId } = req.params; @@ -97,7 +97,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { connectionId } = req.params; const { repo, owner } = req.query; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 91eadc942..e86c37753 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -3,6 +3,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; +import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -14,5 +15,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Databricks, + server, + responseSchema: DatabricksSyncSchema, + createSchema: CreateDatabricksSyncSchema, + updateSchema: UpdateDatabricksSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 1b9592c7c..f94ef30c0 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -1,3 +1,4 @@ +import { registerDatabricksSyncRouter } from "@app/server/routes/v1/secret-sync-routers/databricks-sync-router"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; @@ -15,5 +16,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 6a8e2aa97..ce116a80a 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -1,6 +1,7 @@ export enum AppConnection { GitHub = "github", AWS = "aws", + Databricks = "databricks", GCP = "gcp", AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration" diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 645890b3b..c66fe52c4 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -5,12 +5,17 @@ import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/ap import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types"; import { AwsConnectionMethod, - getAwsAppConnectionListItem, + getAwsConnectionListItem, validateAwsConnectionCredentials } from "@app/services/app-connection/aws"; +import { + DatabricksConnectionMethod, + getDatabricksConnectionListItem, + validateDatabricksConnectionCredentials +} from "@app/services/app-connection/databricks"; import { GcpConnectionMethod, - getGcpAppConnectionListItem, + getGcpConnectionListItem, validateGcpConnectionCredentials } from "@app/services/app-connection/gcp"; import { @@ -33,11 +38,12 @@ import { export const listAppConnectionOptions = () => { return [ - getAwsAppConnectionListItem(), + getAwsConnectionListItem(), getGitHubConnectionListItem(), - getGcpAppConnectionListItem(), + getGcpConnectionListItem(), getAzureKeyVaultConnectionListItem(), - getAzureAppConfigurationConnectionListItem() + getAzureAppConfigurationConnectionListItem(), + getDatabricksConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -90,6 +96,8 @@ export const validateAppConnectionCredentials = async ( switch (app) { case AppConnection.AWS: return validateAwsConnectionCredentials(appConnection); + case AppConnection.Databricks: + return validateDatabricksConnectionCredentials(appConnection); case AppConnection.GitHub: return validateGitHubConnectionCredentials(appConnection); case AppConnection.GCP: @@ -118,6 +126,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Assume Role"; case GcpConnectionMethod.ServiceAccountImpersonation: return "Service Account Impersonation"; + case DatabricksConnectionMethod.ServicePrincipal: + return "Service Principal"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 78fde3127..8a045efe4 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -5,5 +5,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.GitHub]: "GitHub", [AppConnection.GCP]: "GCP", [AppConnection.AzureKeyVault]: "Azure Key Vault", - [AppConnection.AzureAppConfiguration]: "Azure App Configuration" + [AppConnection.AzureAppConfiguration]: "Azure App Configuration", + [AppConnection.Databricks]: "Databricks" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index b5397b915..7ae8b6377 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -23,6 +23,8 @@ import { TValidateAppConnectionCredentials } from "@app/services/app-connection/app-connection-types"; import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws"; +import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks"; +import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service"; import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github"; import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -46,7 +48,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { +export const getAwsConnectionListItem = () => { const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig(); return { diff --git a/backend/src/services/app-connection/databricks/databricks-connection-enums.ts b/backend/src/services/app-connection/databricks/databricks-connection-enums.ts new file mode 100644 index 000000000..b65161e45 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-enums.ts @@ -0,0 +1,3 @@ +export enum DatabricksConnectionMethod { + ServicePrincipal = "service-principal" +} diff --git a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts new file mode 100644 index 000000000..a12fe290c --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -0,0 +1,92 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { DatabricksConnectionMethod } from "./databricks-connection-enums"; +import { + TAuthorizeDatabricksConnection, + TDatabricksConnection, + TDatabricksConnectionConfig +} from "./databricks-connection-types"; + +export const getDatabricksConnectionListItem = () => { + return { + name: "Databricks" as const, + app: AppConnection.Databricks as const, + methods: Object.values(DatabricksConnectionMethod) as [DatabricksConnectionMethod.ServicePrincipal] + }; +}; + +const authorizeDatabricksConnection = async ({ + clientId, + clientSecret, + workspaceUrl +}: Pick) => { + const { data } = await request.post( + `${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`, + "grant_type=client_credentials&scope=all-apis", + { + auth: { + username: clientId, + password: clientSecret + }, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() }; +}; + +export const getDatabricksConnectionAccessToken = async ( + { id, orgId, credentials }: TDatabricksConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { clientSecret, clientId, workspaceUrl, accessToken, expiresAt } = credentials; + + // get new token if less than 10 minutes from expiry + if (Date.now() < expiresAt - 10_000) { + return accessToken; + } + + const authData = await authorizeDatabricksConnection({ clientId, clientSecret, workspaceUrl }); + + const updatedCredentials: TDatabricksConnection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateDatabricksConnectionCredentials = async (appConnection: TDatabricksConnectionConfig) => { + const { credentials } = appConnection; + + try { + const { accessToken, expiresAt } = await authorizeDatabricksConnection(appConnection.credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection - verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts new file mode 100644 index 000000000..af1a75127 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts @@ -0,0 +1,77 @@ +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 { DatabricksConnectionMethod } from "./databricks-connection-enums"; + +export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client Secret required"), + workspaceUrl: z.string().trim().url().min(1, "Workspace URL required") +}); + +export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(DatabricksConnectionServicePrincipalInputCredentialsSchema); + +const BaseDatabricksConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Databricks) }); + +export const DatabricksConnectionSchema = z.intersection( + BaseDatabricksConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema + }) + ]) +); + +export const SanitizedDatabricksConnectionSchema = z.discriminatedUnion("method", [ + BaseDatabricksConnectionSchema.extend({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema.pick({ + clientId: true, + workspaceUrl: true + }) + }) +]); + +export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(DatabricksConnectionMethod.ServicePrincipal) + .describe(AppConnections?.CREATE(AppConnection.Databricks).method), + credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Databricks).credentials + ) + }) +]); + +export const CreateDatabricksConnectionSchema = ValidateDatabricksConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Databricks) +); + +export const UpdateDatabricksConnectionSchema = z + .object({ + credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Databricks).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks)); + +export const DatabricksConnectionListItemSchema = z.object({ + name: z.literal("Databricks"), + app: z.literal(AppConnection.Databricks), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(DatabricksConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/databricks/databricks-connection-service.ts b/backend/src/services/app-connection/databricks/databricks-connection-service.ts new file mode 100644 index 000000000..37b88705a --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-service.ts @@ -0,0 +1,60 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks/databricks-connection-fns"; +import { + TDatabricksConnection, + TDatabricksListSecretScopesResponse +} from "@app/services/app-connection/databricks/databricks-connection-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listDatabricksSecretScopes = async ( + appConnection: TDatabricksConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { + credentials: { workspaceUrl } + } = appConnection; + + const accessToken = await getDatabricksConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { data } = await request.get( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/scopes/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // not present in response if no scopes exists + return data.scopes ?? []; +}; + +export const databricksConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listSecretScopes = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Databricks, connectionId, actor); + + const secretScopes = await listDatabricksSecretScopes(appConnection, appConnectionDAL, kmsService); + + return secretScopes; + }; + + return { + listSecretScopes + }; +}; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-types.ts b/backend/src/services/app-connection/databricks/databricks-connection-types.ts new file mode 100644 index 000000000..e45610b16 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-types.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + CreateDatabricksConnectionSchema, + DatabricksConnectionSchema, + ValidateDatabricksConnectionCredentialsSchema +} from "./databricks-connection-schemas"; + +export type TDatabricksConnection = z.infer; + +export type TDatabricksConnectionInput = z.infer & { + app: AppConnection.Databricks; +}; + +export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema; + +export type TDatabricksConnectionConfig = DiscriminativePick< + TDatabricksConnection, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TAuthorizeDatabricksConnection = { + access_token: string; + scope: string; + token_type: string; + expires_in: number; +}; + +export type TDatabricksListSecretScopesResponse = { + scopes?: { name: string; backend_type: string; keyvault_metadata: { resource_id: string; dns_name: string } }[]; +}; diff --git a/backend/src/services/app-connection/databricks/index.ts b/backend/src/services/app-connection/databricks/index.ts new file mode 100644 index 000000000..844000af4 --- /dev/null +++ b/backend/src/services/app-connection/databricks/index.ts @@ -0,0 +1,4 @@ +export * from "./databricks-connection-enums"; +export * from "./databricks-connection-fns"; +export * from "./databricks-connection-schemas"; +export * from "./databricks-connection-types"; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts index 3c4abb6d9..8f54735e4 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -17,7 +17,7 @@ import { TGcpConnectionConfig } from "./gcp-connection-types"; -export const getGcpAppConnectionListItem = () => { +export const getGcpConnectionListItem = () => { return { name: "GCP" as const, app: AppConnection.GCP as const, diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts index e89096baa..8b0765388 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts @@ -10,14 +10,14 @@ import { } from "@app/services/secret-sync/secret-sync-schemas"; const AwsParameterStoreSyncDestinationConfigSchema = z.object({ - region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.REGION), + region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region), path: z .string() .trim() .min(1, "Parameter Store Path required") .max(2048, "Cannot exceed 2048 characters") .regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format') - .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path) }); export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({ diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts index b345b1cd2..6de014502 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts @@ -15,12 +15,12 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z z.object({ mappingBehavior: z .literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) - .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior) }), z.object({ mappingBehavior: z .literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne) - .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR), + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior), secretName: z .string() .regex( @@ -29,12 +29,12 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z ) .min(1, "Secret name is required") .max(256, "Secret name cannot exceed 256 characters") - .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.SECRET_NAME) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName) }) ]) .and( z.object({ - region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.REGION) + region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.region) }) ); diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts index 2d2f9d129..8c0587599 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -11,7 +11,7 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types"; -type TAzureAppConfigurationSecretSyncFactoryDeps = { +type TAzureAppConfigurationSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; }; @@ -22,10 +22,10 @@ interface AzureAppConfigKeyValue { label?: string; } -export const azureAppConfigurationSecretSyncFactory = ({ +export const azureAppConfigurationSyncFactory = ({ kmsService, appConnectionDAL -}: TAzureAppConfigurationSecretSyncFactoryDeps) => { +}: TAzureAppConfigurationSyncFactoryDeps) => { const $getCompleteAzureAppConfigValues = async (accessToken: string, baseURL: string, url: string) => { let result: AzureAppConfigKeyValue[] = []; let currentUrl = url; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts index b11d67858..c39581fda 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts @@ -14,8 +14,8 @@ const AzureAppConfigurationSyncDestinationConfigSchema = z.object({ configurationUrl: z .string() .min(1, "App Configuration URL required") - .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.CONFIGURATION_URL), - label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.LABEL) + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.configurationUrl), + label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.label) }); const AzureAppConfigurationSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts index e4254074d..21fef297a 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -10,15 +10,12 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { SecretSyncError } from "../secret-sync-errors"; import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types"; -type TAzureKeyVaultSecretSyncFactoryDeps = { +type TAzureKeyVaultSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; }; -export const azureKeyVaultSecretSyncFactory = ({ - kmsService, - appConnectionDAL -}: TAzureKeyVaultSecretSyncFactoryDeps) => { +export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultSyncFactoryDeps) => { const $getAzureKeyVaultSecrets = async (accessToken: string, vaultBaseUrl: string) => { const paginateAzureKeyVaultSecrets = async () => { let result: GetAzureKeyVaultSecret[] = []; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts index 40b476744..d528f531e 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts @@ -15,7 +15,7 @@ const AzureKeyVaultSyncDestinationConfigSchema = z.object({ .string() .url("Invalid vault base URL format") .min(1, "Vault base URL required") - .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.VAULT_BASE_URL) + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.vaultBaseUrl) }); const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts b/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts new file mode 100644 index 000000000..b4ee51a04 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts @@ -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 DATABRICKS_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Databricks", + destination: SecretSync.Databricks, + connection: AppConnection.Databricks, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts new file mode 100644 index 000000000..2d450b2b1 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -0,0 +1,164 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TDatabricksDeleteSecret, + TDatabricksListSecretKeys, + TDatabricksListSecretKeysResponse, + TDatabricksPutSecret, + TDatabricksSyncWithCredentials +} from "@app/services/secret-sync/databricks/databricks-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; + +import { TSecretMap } from "../secret-sync-types"; + +type TDatabricksSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const DATABRICKS_SCOPE_SECRET_LIMIT = 1000; + +const listDatabricksSecrets = async ({ workspaceUrl, scope, accessToken }: TDatabricksListSecretKeys) => { + const { data } = await request.get( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/list`, + { + params: { + scope + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // not present in response if no secrets exist in scope + return data.secrets ?? []; +}; +const putDatabricksSecret = async ({ workspaceUrl, scope, key, value, accessToken }: TDatabricksPutSecret) => + request.post( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/put`, + { + scope, + key, + string_value: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +const deleteDatabricksSecrets = async ({ workspaceUrl, scope, key, accessToken }: TDatabricksDeleteSecret) => + request.post( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/delete`, + { + scope, + key + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabricksSecretSyncFactoryDeps) => { + const syncSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => { + if (Object.keys(secretSync).length > DATABRICKS_SCOPE_SECRET_LIMIT) { + throw new Error( + `Databricks does not support storing more than ${DATABRICKS_SCOPE_SECRET_LIMIT} secrets per scope.` + ); + } + + const { + destinationConfig: { scope }, + connection + } = secretSync; + + const { workspaceUrl } = connection.credentials; + + const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + try { + await putDatabricksSecret({ + key, + value, + workspaceUrl, + scope, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + const databricksSecretKeys = await listDatabricksSecrets({ + workspaceUrl, + scope, + accessToken + }); + + for await (const secret of databricksSecretKeys) { + if (!(secret.key in secretMap)) { + await deleteDatabricksSecrets({ + key: secret.key, + workspaceUrl, + scope, + accessToken + }); + } + } + }; + + const removeSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { scope }, + connection + } = secretSync; + + const { workspaceUrl } = connection.credentials; + + const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService); + + const databricksSecretKeys = await listDatabricksSecrets({ + workspaceUrl, + scope, + accessToken + }); + + for await (const secret of databricksSecretKeys) { + if (secret.key in secretMap) { + await deleteDatabricksSecrets({ + key: secret.key, + workspaceUrl, + scope, + accessToken + }); + } + } + }; + + const getSecrets = async (secretSync: TDatabricksSyncWithCredentials) => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts b/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts new file mode 100644 index 000000000..c0f148244 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts @@ -0,0 +1,43 @@ +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 DatabricksSyncDestinationConfigSchema = z.object({ + scope: z.string().trim().min(1, "Databricks scope required").describe(SecretSyncs.DESTINATION_CONFIG.DATABRICKS.scope) +}); + +const DatabricksSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const DatabricksSyncSchema = BaseSecretSyncSchema(SecretSync.Databricks, DatabricksSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Databricks), + destinationConfig: DatabricksSyncDestinationConfigSchema +}); + +export const CreateDatabricksSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Databricks, + DatabricksSyncOptionsConfig +).extend({ + destinationConfig: DatabricksSyncDestinationConfigSchema +}); + +export const UpdateDatabricksSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Databricks, + DatabricksSyncOptionsConfig +).extend({ + destinationConfig: DatabricksSyncDestinationConfigSchema.optional() +}); + +export const DatabricksSyncListItemSchema = z.object({ + name: z.literal("Databricks"), + connection: z.literal(AppConnection.Databricks), + destination: z.literal(SecretSync.Databricks), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-types.ts b/backend/src/services/secret-sync/databricks/databricks-sync-types.ts new file mode 100644 index 000000000..41ec06d85 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-types.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { TDatabricksConnection } from "@app/services/app-connection/databricks"; + +import { + CreateDatabricksSyncSchema, + DatabricksSyncListItemSchema, + DatabricksSyncSchema +} from "./databricks-sync-schemas"; + +export type TDatabricksSync = z.infer; + +export type TDatabricksSyncInput = z.infer; + +export type TDatabricksSyncListItem = z.infer; + +export type TDatabricksSyncWithCredentials = TDatabricksSync & { + connection: TDatabricksConnection; +}; + +export type TDatabricksListSecretKeysResponse = { + secrets?: { key: string; last_updated_timestamp: number }[]; +}; + +type TBaseDatabricksSecretRequest = { + scope: string; + workspaceUrl: string; + accessToken: string; +}; + +export type TDatabricksListSecretKeys = TBaseDatabricksSecretRequest; + +export type TDatabricksPutSecret = { + key: string; + value?: string; +} & TBaseDatabricksSecretRequest; + +export type TDatabricksDeleteSecret = { + key: string; +} & TBaseDatabricksSecretRequest; diff --git a/backend/src/services/secret-sync/databricks/index.ts b/backend/src/services/secret-sync/databricks/index.ts new file mode 100644 index 000000000..5b4dec07d --- /dev/null +++ b/backend/src/services/secret-sync/databricks/index.ts @@ -0,0 +1,4 @@ +export * from "./databricks-sync-constants"; +export * from "./databricks-sync-fns"; +export * from "./databricks-sync-schemas"; +export * from "./databricks-sync-types"; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts index b0516d166..0643c431a 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -1,5 +1,6 @@ import z from "zod"; +import { SecretSyncs } from "@app/lib/api-docs"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { BaseSecretSyncSchema, @@ -14,8 +15,8 @@ import { GcpSyncScope } from "./gcp-sync-enums"; const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; const GcpSyncDestinationConfigSchema = z.object({ - scope: z.literal(GcpSyncScope.Global), - projectId: z.string().min(1, "Project ID is required") + scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) }); export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({ diff --git a/backend/src/services/secret-sync/github/github-sync-schemas.ts b/backend/src/services/secret-sync/github/github-sync-schemas.ts index 37a294a1b..76bbc63a7 100644 --- a/backend/src/services/secret-sync/github/github-sync-schemas.ts +++ b/backend/src/services/secret-sync/github/github-sync-schemas.ts @@ -14,21 +14,21 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types" const GitHubSyncDestinationConfigSchema = z .discriminatedUnion("scope", [ z.object({ - scope: z.literal(GitHubSyncScope.Organization), - org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.ORG), + scope: z.literal(GitHubSyncScope.Organization).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.org), visibility: z.nativeEnum(GitHubSyncVisibility), selectedRepositoryIds: z.number().array().optional() }), z.object({ - scope: z.literal(GitHubSyncScope.Repository), - owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.OWNER), - repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.REPO) + scope: z.literal(GitHubSyncScope.Repository).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner), + repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo) }), z.object({ - scope: z.literal(GitHubSyncScope.RepositoryEnvironment), - owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.OWNER), - repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.REPO), - env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.ENV) + scope: z.literal(GitHubSyncScope.RepositoryEnvironment).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner), + repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo), + env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.env) }) ]) .superRefine((options, ctx) => { diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 0bee11d95..743b900bc 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -4,7 +4,8 @@ export enum SecretSync { GitHub = "github", GCPSecretManager = "gcp-secret-manager", AzureKeyVault = "azure-key-vault", - AzureAppConfiguration = "azure-app-configuration" + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index c39ceed54..597c7fc01 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -8,6 +8,7 @@ import { AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, AwsSecretsManagerSyncFns } from "@app/services/secret-sync/aws-secrets-manager"; +import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks"; import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; @@ -19,11 +20,8 @@ import { import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; -import { - AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, - azureAppConfigurationSecretSyncFactory -} from "./azure-app-configuration"; -import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSecretSyncFactory } from "./azure-key-vault"; +import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; +import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -33,7 +31,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION, [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION, [SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION, - [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION + [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, + [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -41,7 +40,7 @@ export const listSecretSyncOptions = () => { }; type TSyncSecretDeps = { - appConnectionDAL: Pick; + appConnectionDAL: Pick; kmsService: Pick; }; @@ -103,12 +102,17 @@ export const SecretSyncFns = { case SecretSync.GCPSecretManager: return GcpSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.AzureKeyVault: - return azureKeyVaultSecretSyncFactory({ + return azureKeyVaultSyncFactory({ appConnectionDAL, kmsService }).syncSecrets(secretSync, secretMap); case SecretSync.AzureAppConfiguration: - return azureAppConfigurationSecretSyncFactory({ + return azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.Databricks: + return databricksSyncFactory({ appConnectionDAL, kmsService }).syncSecrets(secretSync, secretMap); @@ -137,17 +141,22 @@ export const SecretSyncFns = { secretMap = await GcpSyncFns.getSecrets(secretSync); break; case SecretSync.AzureKeyVault: - secretMap = await azureKeyVaultSecretSyncFactory({ + secretMap = await azureKeyVaultSyncFactory({ appConnectionDAL, kmsService }).getSecrets(secretSync); break; case SecretSync.AzureAppConfiguration: - secretMap = await azureAppConfigurationSecretSyncFactory({ + secretMap = await azureAppConfigurationSyncFactory({ appConnectionDAL, kmsService }).getSecrets(secretSync); break; + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -174,12 +183,17 @@ export const SecretSyncFns = { case SecretSync.GCPSecretManager: return GcpSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.AzureKeyVault: - return azureKeyVaultSecretSyncFactory({ + return azureKeyVaultSyncFactory({ appConnectionDAL, kmsService }).removeSecrets(secretSync, secretMap); case SecretSync.AzureAppConfiguration: - return azureAppConfigurationSecretSyncFactory({ + return azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.Databricks: + return databricksSyncFactory({ appConnectionDAL, kmsService }).removeSecrets(secretSync, secretMap); diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 33a87fbef..92546706d 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -7,7 +7,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.GitHub]: "GitHub", [SecretSync.GCPSecretManager]: "GCP Secret Manager", [SecretSync.AzureKeyVault]: "Azure Key Vault", - [SecretSync.AzureAppConfiguration]: "Azure App Configuration" + [SecretSync.AzureAppConfiguration]: "Azure App Configuration", + [SecretSync.Databricks]: "Databricks" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -16,5 +17,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.GitHub]: AppConnection.GitHub, [SecretSync.GCPSecretManager]: AppConnection.GCP, [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, - [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, + [SecretSync.Databricks]: AppConnection.Databricks }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index e822a20b7..808930b15 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -64,7 +64,7 @@ export type TSecretSyncQueueFactory = ReturnType; type TSecretSyncQueueFactoryDep = { queueService: Pick; kmsService: Pick; - appConnectionDAL: Pick; + appConnectionDAL: Pick; keyStore: Pick; folderDAL: TSecretFolderDALFactory; secretV2BridgeDAL: Pick< diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 9821c4e1d..89bdc4375 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -13,7 +13,7 @@ const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = initialSyncBehavior: (options.canImportSecrets ? z.nativeEnum(SecretSyncInitialSyncBehavior) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) - ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR) + ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior) // prependPrefix: z // .string() // .trim() diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index bf43ae927..2c6d3a830 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -8,6 +8,12 @@ import { TAwsSecretsManagerSyncListItem, TAwsSecretsManagerSyncWithCredentials } from "@app/services/secret-sync/aws-secrets-manager"; +import { + TDatabricksSync, + TDatabricksSyncInput, + TDatabricksSyncListItem, + TDatabricksSyncWithCredentials +} from "@app/services/secret-sync/databricks"; import { TGitHubSync, TGitHubSyncInput, @@ -43,7 +49,8 @@ export type TSecretSync = | TGitHubSync | TGcpSync | TAzureKeyVaultSync - | TAzureAppConfigurationSync; + | TAzureAppConfigurationSync + | TDatabricksSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -51,7 +58,8 @@ export type TSecretSyncWithCredentials = | TGitHubSyncWithCredentials | TGcpSyncWithCredentials | TAzureKeyVaultSyncWithCredentials - | TAzureAppConfigurationSyncWithCredentials; + | TAzureAppConfigurationSyncWithCredentials + | TDatabricksSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -59,7 +67,8 @@ export type TSecretSyncInput = | TGitHubSyncInput | TGcpSyncInput | TAzureKeyVaultSyncInput - | TAzureAppConfigurationSyncInput; + | TAzureAppConfigurationSyncInput + | TDatabricksSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -67,7 +76,8 @@ export type TSecretSyncListItem = | TGitHubSyncListItem | TGcpSyncListItem | TAzureKeyVaultSyncListItem - | TAzureAppConfigurationSyncListItem; + | TAzureAppConfigurationSyncListItem + | TDatabricksSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/app-connections/databricks/available.mdx b/docs/api-reference/endpoints/app-connections/databricks/available.mdx new file mode 100644 index 000000000..6c277f702 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/databricks/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/create.mdx b/docs/api-reference/endpoints/app-connections/databricks/create.mdx new file mode 100644 index 000000000..5361acb4b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/databricks" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/delete.mdx b/docs/api-reference/endpoints/app-connections/databricks/delete.mdx new file mode 100644 index 000000000..fba97fb14 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx new file mode 100644 index 000000000..1f861328c --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx new file mode 100644 index 000000000..f89c8a8d7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/databricks/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/list.mdx b/docs/api-reference/endpoints/app-connections/databricks/list.mdx new file mode 100644 index 000000000..1449b5166 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/databricks" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/update.mdx b/docs/api-reference/endpoints/app-connections/databricks/update.mdx new file mode 100644 index 000000000..69ddbf617 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx new file mode 100644 index 000000000..ba91528f5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/databricks" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx new file mode 100644 index 000000000..862681613 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx new file mode 100644 index 000000000..7cf8fed53 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx new file mode 100644 index 000000000..fe2d239ff --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/databricks/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx new file mode 100644 index 000000000..dd705408f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/databricks" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx new file mode 100644 index 000000000..5e4e69fce --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/databricks/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx new file mode 100644 index 000000000..002fea158 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/databricks/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx new file mode 100644 index 000000000..4a43311a3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/images/app-connections/databricks/add-service-principal.png b/docs/images/app-connections/databricks/add-service-principal.png new file mode 100644 index 000000000..0d695582d Binary files /dev/null and b/docs/images/app-connections/databricks/add-service-principal.png differ diff --git a/docs/images/app-connections/databricks/create-databricks-service-principal-method.png b/docs/images/app-connections/databricks/create-databricks-service-principal-method.png new file mode 100644 index 000000000..b8dd38bf1 Binary files /dev/null and b/docs/images/app-connections/databricks/create-databricks-service-principal-method.png differ diff --git a/docs/images/app-connections/databricks/create-service-principal.png b/docs/images/app-connections/databricks/create-service-principal.png new file mode 100644 index 000000000..145931442 Binary files /dev/null and b/docs/images/app-connections/databricks/create-service-principal.png differ diff --git a/docs/images/app-connections/databricks/databricks-service-principal-connection.png b/docs/images/app-connections/databricks/databricks-service-principal-connection.png new file mode 100644 index 000000000..6b632553c Binary files /dev/null and b/docs/images/app-connections/databricks/databricks-service-principal-connection.png differ diff --git a/docs/images/app-connections/databricks/manage-service-principals.png b/docs/images/app-connections/databricks/manage-service-principals.png new file mode 100644 index 000000000..f76d2400d Binary files /dev/null and b/docs/images/app-connections/databricks/manage-service-principals.png differ diff --git a/docs/images/app-connections/databricks/select-databricks-connection.png b/docs/images/app-connections/databricks/select-databricks-connection.png new file mode 100644 index 000000000..41a21e6f4 Binary files /dev/null and b/docs/images/app-connections/databricks/select-databricks-connection.png differ diff --git a/docs/images/app-connections/databricks/service-principal-ids.png b/docs/images/app-connections/databricks/service-principal-ids.png new file mode 100644 index 000000000..2748c13e8 Binary files /dev/null and b/docs/images/app-connections/databricks/service-principal-ids.png differ diff --git a/docs/images/app-connections/databricks/service-principal-secrets.png b/docs/images/app-connections/databricks/service-principal-secrets.png new file mode 100644 index 000000000..e1e0b053e Binary files /dev/null and b/docs/images/app-connections/databricks/service-principal-secrets.png differ diff --git a/docs/images/app-connections/databricks/workspace-settings.png b/docs/images/app-connections/databricks/workspace-settings.png new file mode 100644 index 000000000..5c3ec54a6 Binary files /dev/null and b/docs/images/app-connections/databricks/workspace-settings.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-created.png b/docs/images/secret-syncs/databricks/databricks-created.png new file mode 100644 index 000000000..829c5196c Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-created.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-destination.png b/docs/images/secret-syncs/databricks/databricks-destination.png new file mode 100644 index 000000000..8b76f8f65 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-destination.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-details.png b/docs/images/secret-syncs/databricks/databricks-details.png new file mode 100644 index 000000000..71630f158 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-details.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-options.png b/docs/images/secret-syncs/databricks/databricks-options.png new file mode 100644 index 000000000..76890d183 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-options.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-review.png b/docs/images/secret-syncs/databricks/databricks-review.png new file mode 100644 index 000000000..e7ff5f6c1 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-review.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-source.png b/docs/images/secret-syncs/databricks/databricks-source.png new file mode 100644 index 000000000..74e9e3535 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-source.png differ diff --git a/docs/images/secret-syncs/databricks/select-databricks-option.png b/docs/images/secret-syncs/databricks/select-databricks-option.png new file mode 100644 index 000000000..26f2fd4bd Binary files /dev/null and b/docs/images/secret-syncs/databricks/select-databricks-option.png differ diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx index 0d481986d..959a1812a 100644 --- a/docs/integrations/app-connections/azure-app-configuration.mdx +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -62,7 +62,7 @@ Infisical currently only supports one method for connecting to Azure, which is O ## Setup Azure Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx index 705401708..f73dab834 100644 --- a/docs/integrations/app-connections/azure-key-vault.mdx +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -61,7 +61,7 @@ Infisical currently only supports one method for connecting to Azure, which is O ## Setup Azure Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/databricks.mdx b/docs/integrations/app-connections/databricks.mdx new file mode 100644 index 000000000..38d125ea6 --- /dev/null +++ b/docs/integrations/app-connections/databricks.mdx @@ -0,0 +1,64 @@ +--- +title: "Databricks Connection" +description: "Learn how to configure a Databricks Connection for Infisical." +--- + +Infisical supports the use of [service principals](https://docs.databricks.com/en/admin/users-groups/service-principals.html) to connect with your Databricks workspaces. + +## Configure a Service Principal for Infisical + + + + Navigate to your Databricks Workspace **Settings** via the dropdown in the top right. + ![Workspace Settings Page](/images/app-connections/databricks/workspace-settings.png) + + + Under the **Identity & Access** tab, click the **Manage** button in the **Service Principals** section. + + ![Manage Service Principals](/images/app-connections/databricks/manage-service-principals.png) + + + Click the **Add Service Principal** button. + + ![Add Service Principal](/images/app-connections/databricks/add-service-principal.png) + + + Select the **Add New** option and create a service principal for Infisical. + + ![Create Service Principal](/images/app-connections/databricks/create-service-principal.png) + + + Click on your new service principal, select the **Secrets** tab and click the **Generate Secret** button. + + ![Generate Secret](/images/app-connections/databricks/service-principal-secrets.png) + + + Copy your service principal **Secret** and **Client ID** for use in the following steps. + + ![Generate Secret](/images/app-connections/databricks/service-principal-ids.png) + + + +## Setup Databricks Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Databricks Connection** option from the connection options modal. + ![Select Databricks + Connection](/images/app-connections/databricks/select-databricks-connection.png) + + + Select the **Service Principal** method, add your **workspace URL** and **service principal credentials**, then click **Connect to + Databricks**. ![Connect via Databricks + service principal](/images/app-connections/databricks/create-databricks-service-principal-method.png) + + + Your **Databricks Connection** is now available for use. ![Databricks Service Principal + Connection](/images/app-connections/databricks/databricks-service-principal-connection.png) + + diff --git a/docs/integrations/app-connections/gcp.mdx b/docs/integrations/app-connections/gcp.mdx index d75d2ae12..129c26c2a 100644 --- a/docs/integrations/app-connections/gcp.mdx +++ b/docs/integrations/app-connections/gcp.mdx @@ -81,7 +81,7 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/ ## Setup GCP Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 1b97d3162..2f8caffed 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -69,7 +69,7 @@ Infisical supports two methods for connecting to GitHub. ## Setup GitHub Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -135,7 +135,7 @@ Infisical supports two methods for connecting to GitHub. ## Setup GitHub Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx new file mode 100644 index 000000000..8f305ecd6 --- /dev/null +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -0,0 +1,140 @@ +--- +title: "Databricks Sync" +description: "Learn how to configure a Databricks Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Databricks Connection](/integrations/app-connections/databricks) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Databricks** option. + ![Select Databricks](/images/secret-syncs/databricks/select-databricks-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/databricks/databricks-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/databricks/databricks-destination.png) + + - **Databricks Connection**: The Databricks Connection to authenticate with. + - **Scope**: The Databricks secret scope to sync secrets to. + + + You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope. + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/databricks/databricks-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Databricks does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Databricks Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/databricks/databricks-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Databricks Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/databricks/databricks-review.png) + + 8. If enabled, your Databricks Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/databricks/databricks-created.png) + + + + To create an **Databricks Sync**, make an API request to the [Create Databricks Sync](/api-reference/endpoints/secret-syncs/databricks/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/databricks \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-databricks-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "my-scope" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-databricks-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "databricks", + "name": "my-databricks-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "databricks", + "destinationConfig": { + "scope": "my-scope" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 4b55a687a..babb9be1a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -392,10 +392,11 @@ "group": "Connections", "pages": [ "integrations/app-connections/aws", - "integrations/app-connections/github", - "integrations/app-connections/gcp", + "integrations/app-connections/azure-app-configuration", "integrations/app-connections/azure-key-vault", - "integrations/app-connections/azure-app-configuration" + "integrations/app-connections/databricks", + "integrations/app-connections/gcp", + "integrations/app-connections/github" ] } ] @@ -409,10 +410,11 @@ "pages": [ "integrations/secret-syncs/aws-parameter-store", "integrations/secret-syncs/aws-secrets-manager", - "integrations/secret-syncs/github", - "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/azure-app-configuration", "integrations/secret-syncs/azure-key-vault", - "integrations/secret-syncs/azure-app-configuration" + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github" ] } ] @@ -825,27 +827,15 @@ ] }, { - "group": "GitHub", + "group": "Azure App Configuration", "pages": [ - "api-reference/endpoints/app-connections/github/list", - "api-reference/endpoints/app-connections/github/available", - "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": "GCP", - "pages": [ - "api-reference/endpoints/app-connections/gcp/list", - "api-reference/endpoints/app-connections/gcp/available", - "api-reference/endpoints/app-connections/gcp/get-by-id", - "api-reference/endpoints/app-connections/gcp/get-by-name", - "api-reference/endpoints/app-connections/gcp/create", - "api-reference/endpoints/app-connections/gcp/update", - "api-reference/endpoints/app-connections/gcp/delete" + "api-reference/endpoints/app-connections/azure-app-configuration/list", + "api-reference/endpoints/app-connections/azure-app-configuration/available", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", + "api-reference/endpoints/app-connections/azure-app-configuration/create", + "api-reference/endpoints/app-connections/azure-app-configuration/update", + "api-reference/endpoints/app-connections/azure-app-configuration/delete" ] }, { @@ -861,15 +851,39 @@ ] }, { - "group": "Azure App Configuration", + "group": "Databricks", "pages": [ - "api-reference/endpoints/app-connections/azure-app-configuration/list", - "api-reference/endpoints/app-connections/azure-app-configuration/available", - "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", - "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", - "api-reference/endpoints/app-connections/azure-app-configuration/create", - "api-reference/endpoints/app-connections/azure-app-configuration/update", - "api-reference/endpoints/app-connections/azure-app-configuration/delete" + "api-reference/endpoints/app-connections/databricks/list", + "api-reference/endpoints/app-connections/databricks/available", + "api-reference/endpoints/app-connections/databricks/get-by-id", + "api-reference/endpoints/app-connections/databricks/get-by-name", + "api-reference/endpoints/app-connections/databricks/create", + "api-reference/endpoints/app-connections/databricks/update", + "api-reference/endpoints/app-connections/databricks/delete" + ] + }, + { + "group": "GCP", + "pages": [ + "api-reference/endpoints/app-connections/gcp/list", + "api-reference/endpoints/app-connections/gcp/available", + "api-reference/endpoints/app-connections/gcp/get-by-id", + "api-reference/endpoints/app-connections/gcp/get-by-name", + "api-reference/endpoints/app-connections/gcp/create", + "api-reference/endpoints/app-connections/gcp/update", + "api-reference/endpoints/app-connections/gcp/delete" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/app-connections/github/list", + "api-reference/endpoints/app-connections/github/available", + "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" ] } ] @@ -908,30 +922,17 @@ ] }, { - "group": "GitHub", + "group": "Azure App Configuration", "pages": [ - "api-reference/endpoints/secret-syncs/github/list", - "api-reference/endpoints/secret-syncs/github/get-by-id", - "api-reference/endpoints/secret-syncs/github/get-by-name", - "api-reference/endpoints/secret-syncs/github/create", - "api-reference/endpoints/secret-syncs/github/update", - "api-reference/endpoints/secret-syncs/github/delete", - "api-reference/endpoints/secret-syncs/github/sync-secrets", - "api-reference/endpoints/secret-syncs/github/remove-secrets" - ] - }, - { - "group": "GCP Secret Manager", - "pages": [ - "api-reference/endpoints/secret-syncs/gcp-secret-manager/list", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/create", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/update", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/delete", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", - "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" + "api-reference/endpoints/secret-syncs/azure-app-configuration/list", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", + "api-reference/endpoints/secret-syncs/azure-app-configuration/create", + "api-reference/endpoints/secret-syncs/azure-app-configuration/update", + "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", + "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" ] }, { @@ -949,20 +950,45 @@ ] }, { - "group": "Azure App Configuration", + "group": "Databricks", "pages": [ - "api-reference/endpoints/secret-syncs/azure-app-configuration/list", - "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", - "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", - "api-reference/endpoints/secret-syncs/azure-app-configuration/create", - "api-reference/endpoints/secret-syncs/azure-app-configuration/update", - "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", - "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", - "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", - "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" + "api-reference/endpoints/secret-syncs/databricks/list", + "api-reference/endpoints/secret-syncs/databricks/get-by-id", + "api-reference/endpoints/secret-syncs/databricks/get-by-name", + "api-reference/endpoints/secret-syncs/databricks/create", + "api-reference/endpoints/secret-syncs/databricks/update", + "api-reference/endpoints/secret-syncs/databricks/delete", + "api-reference/endpoints/secret-syncs/databricks/sync-secrets", + "api-reference/endpoints/secret-syncs/databricks/remove-secrets" + ] + }, + { + "group": "GCP Secret Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/gcp-secret-manager/list", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/create", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/update", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/delete", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-syncs/github/list", + "api-reference/endpoints/secret-syncs/github/get-by-id", + "api-reference/endpoints/secret-syncs/github/get-by-name", + "api-reference/endpoints/secret-syncs/github/create", + "api-reference/endpoints/secret-syncs/github/update", + "api-reference/endpoints/secret-syncs/github/delete", + "api-reference/endpoints/secret-syncs/github/sync-secrets", + "api-reference/endpoints/secret-syncs/github/remove-secrets" ] } - ] }, { diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx new file mode 100644 index 000000000..9b76e3005 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx @@ -0,0 +1,72 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { + TDatabricksSecretScope, + useDatabricksConnectionListSecretScopes +} from "@app/hooks/api/appConnections/databricks"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const DatabricksSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Databricks } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: secretScopes = [], isPending: isSecretScopesPending } = + useDatabricksConnectionListSecretScopes(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.scope", ""); + }} + /> + ( + +
+ Don't see the secret scope you're looking for?{" "} + +
+ + } + > + scope.name === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.name ?? null) + } + options={secretScopes} + placeholder="Select a secret scope..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.name} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 23724f7b5..7953f30d8 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -1,5 +1,6 @@ import { useFormContext } from "react-hook-form"; +import { DatabricksSyncFields } from "@app/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; @@ -28,6 +29,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.AzureAppConfiguration: return ; + case SecretSync.Databricks: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields.tsx new file mode 100644 index 000000000..76de5ca54 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields.tsx @@ -0,0 +1,12 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DatabricksSyncReviewFields = () => { + const { watch } = useFormContext(); + const scope = watch("destinationConfig.scope"); + + return {scope}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index a772b603b..61408cf93 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -4,6 +4,7 @@ import { useFormContext } from "react-hook-form"; import { SecretSyncLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields"; +import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields"; import { Badge } from "@app/components/v2"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs"; @@ -54,6 +55,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.AzureAppConfiguration: DestinationFieldsComponent = ; break; + case SecretSync.Databricks: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts new file mode 100644 index 000000000..0a8f5e723 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DatabricksSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.Databricks), + destinationConfig: z.object({ + scope: z.string().trim().min(1, "Databricks scope required") + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 28fe232d3..f7d11dba4 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema"; +import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema"; import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; import { slugSchema } from "@app/lib/schemas"; @@ -39,7 +40,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ GitHubSyncDestinationSchema, GcpSyncDestinationSchema, AzureKeyVaultSyncDestinationSchema, - AzureAppConfigurationSyncDestinationSchema + AzureAppConfigurationSyncDestinationSchema, + DatabricksSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index ece5fe906..016a86264 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -10,6 +10,7 @@ import { GitHubConnectionMethod, TAppConnection } from "@app/hooks/api/appConnections/types"; +import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection"; export const APP_CONNECTION_MAP: Record = { [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, @@ -22,7 +23,8 @@ export const APP_CONNECTION_MAP: Record { @@ -39,6 +41,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Assume Role", icon: faUser }; case GcpConnectionMethod.ServiceAccountImpersonation: return { name: "Service Account Impersonation", icon: faUser }; + case DatabricksConnectionMethod.ServicePrincipal: + return { name: "Service Principal", icon: faUser }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 104a45ef4..b119983a0 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -14,6 +14,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.GitHub]: AppConnection.GitHub, [SecretSync.GCPSecretManager]: AppConnection.GCP, [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, - [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, + [SecretSync.Databricks]: AppConnection.Databricks }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/databricks/index.ts b/frontend/src/hooks/api/appConnections/databricks/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/databricks/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/databricks/queries.tsx b/frontend/src/hooks/api/appConnections/databricks/queries.tsx new file mode 100644 index 000000000..14f84cf9c --- /dev/null +++ b/frontend/src/hooks/api/appConnections/databricks/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TDatabricksConnectionListSecretScopesResponse, TDatabricksSecretScope } from "./types"; + +const databricksConnectionKeys = { + all: [...appConnectionKeys.all, "databricks"] as const, + listSecretScopes: (connectionId: string) => + [...databricksConnectionKeys.all, "workspace-scopes", connectionId] as const +}; + +export const useDatabricksConnectionListSecretScopes = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TDatabricksSecretScope[], + unknown, + TDatabricksSecretScope[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: databricksConnectionKeys.listSecretScopes(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/databricks/${connectionId}/secret-scopes` + ); + + return data.secretScopes; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/databricks/types.ts b/frontend/src/hooks/api/appConnections/databricks/types.ts new file mode 100644 index 000000000..1a2c0cae1 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/databricks/types.ts @@ -0,0 +1,7 @@ +export type TDatabricksSecretScope = { + name: string; +}; + +export type TDatabricksConnectionListSecretScopesResponse = { + secretScopes: TDatabricksSecretScope[]; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 46e853cb4..97fcf9be6 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -3,5 +3,6 @@ export enum AppConnection { GitHub = "github", GCP = "gcp", AzureKeyVault = "azure-key-vault", - AzureAppConfiguration = "azure-app-configuration" + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index b4aeb29d5..e42593c94 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -30,7 +30,17 @@ export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & oauthClientId?: string; }; -export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption; +export type TDatabricksConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Databricks; +}; + +export type TAppConnectionOption = + | TAwsConnectionOption + | TGitHubConnectionOption + | TGcpConnectionOption + | TAzureAppConfigurationConnectionOption + | TAzureKeyVaultConnectionOption + | TDatabricksConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -38,4 +48,5 @@ export type TAppConnectionOptionMap = { [AppConnection.GCP]: TGcpConnectionOption; [AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption; [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption; + [AppConnection.Databricks]: TDatabricksConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/databricks-connection.ts b/frontend/src/hooks/api/appConnections/types/databricks-connection.ts new file mode 100644 index 000000000..56774f9dd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/databricks-connection.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "../enums"; +import { TRootAppConnection } from "./root-connection"; + +export enum DatabricksConnectionMethod { + ServicePrincipal = "service-principal" +} + +export type TDatabricksConnection = TRootAppConnection & { app: AppConnection.Databricks } & { + method: DatabricksConnectionMethod.ServicePrincipal; + credentials: { + workspaceUrl: string; + clientId: string; + clientSecret: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index b3005a2df..a2a1b6792 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -1,6 +1,7 @@ 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 { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection"; import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection"; import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; @@ -18,7 +19,8 @@ export type TAppConnection = | TGitHubConnection | TGcpConnection | TAzureKeyVaultConnection - | TAzureAppConfigurationConnection; + | TAzureAppConfigurationConnection + | TDatabricksConnection; export type TAvailableAppConnection = Pick; @@ -51,4 +53,5 @@ export type TAppConnectionMap = { [AppConnection.GCP]: TGcpConnection; [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; + [AppConnection.Databricks]: TDatabricksConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index e141693b5..c942c388d 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -4,7 +4,8 @@ export enum SecretSync { GitHub = "github", GCPSecretManager = "gcp-secret-manager", AzureKeyVault = "azure-key-vault", - AzureAppConfiguration = "azure-app-configuration" + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/databricks-sync.ts b/frontend/src/hooks/api/secretSyncs/types/databricks-sync.ts new file mode 100644 index 000000000..982bcc059 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/databricks-sync.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TDatabricksSync = TRootSecretSync & { + destination: SecretSync.Databricks; + destinationConfig: { + scope: string; + }; + connection: { + app: AppConnection.Databricks; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 51f60810c..e0dbe3b1d 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -1,5 +1,6 @@ import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs"; import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync"; +import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync"; import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync"; import { DiscriminativePick } from "@app/types"; @@ -20,7 +21,8 @@ export type TSecretSync = | TGitHubSync | TGcpSync | TAzureKeyVaultSync - | TAzureAppConfigurationSync; + | TAzureAppConfigurationSync + | TDatabricksSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx index 29fef31ec..c84b20885 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx @@ -12,6 +12,7 @@ import { AppConnectionHeader } from "../AppConnectionHeader"; import { AwsConnectionForm } from "./AwsConnectionForm"; import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm"; import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; +import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -59,6 +60,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.AzureAppConfiguration: return ; + case AppConnection.Databricks: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -102,6 +105,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.AzureAppConfiguration: return ; + case AppConnection.Databricks: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/DatabricksConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/DatabricksConnectionForm.tsx new file mode 100644 index 000000000..997609d1b --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/DatabricksConnectionForm.tsx @@ -0,0 +1,166 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + DatabricksConnectionMethod, + TDatabricksConnection +} from "@app/hooks/api/appConnections/types/databricks-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TDatabricksConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Databricks) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: z.object({ + workspaceUrl: z.string().url().trim().min(1, "Workspace URL required"), + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client secret required") + }) + }) +]); + +type FormData = z.infer; + +export const DatabricksConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Databricks, + method: DatabricksConnectionMethod.ServicePrincipal + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx index 285144875..5db13f714 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx @@ -23,7 +23,7 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => { } return ( -
+
{appConnectionOptions?.map((option) => (