mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
feat(secret-sync): Add Azure Devops Secret Sync
This commit is contained in:
@@ -2156,6 +2156,11 @@ export const AppConnections = {
|
|||||||
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
||||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
||||||
},
|
},
|
||||||
|
AZURE_DEVOPS: {
|
||||||
|
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||||
|
tenantId: "The Tenant ID to use to connect with Azure DevOps.",
|
||||||
|
orgName: "The Organization name to use to connect with Azure DevOps."
|
||||||
|
},
|
||||||
OCI: {
|
OCI: {
|
||||||
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
||||||
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
|
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
|
||||||
@@ -2270,6 +2275,10 @@ export const SecretSyncs = {
|
|||||||
"The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/",
|
"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."
|
||||||
},
|
},
|
||||||
|
AZURE_DEVOPS: {
|
||||||
|
devopsProjectId: "The ID of the Azure DevOps project to sync secrets to.",
|
||||||
|
devopsProjectName: "The name of the Azure DevOps project to sync secrets to."
|
||||||
|
},
|
||||||
GCP: {
|
GCP: {
|
||||||
scope: "The Google project scope that secrets should be synced to.",
|
scope: "The Google project scope that secrets should be synced to.",
|
||||||
projectId: "The ID of the Google project secrets should be synced to."
|
projectId: "The ID of the Google project secrets should be synced to."
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
AzureClientSecretsConnectionListItemSchema,
|
AzureClientSecretsConnectionListItemSchema,
|
||||||
SanitizedAzureClientSecretsConnectionSchema
|
SanitizedAzureClientSecretsConnectionSchema
|
||||||
} from "@app/services/app-connection/azure-client-secrets";
|
} from "@app/services/app-connection/azure-client-secrets";
|
||||||
|
import {
|
||||||
|
AzureDevOpsConnectionListItemSchema,
|
||||||
|
SanitizedAzureDevOpsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
|
||||||
import {
|
import {
|
||||||
AzureKeyVaultConnectionListItemSchema,
|
AzureKeyVaultConnectionListItemSchema,
|
||||||
SanitizedAzureKeyVaultConnectionSchema
|
SanitizedAzureKeyVaultConnectionSchema
|
||||||
@@ -75,6 +79,7 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedGcpConnectionSchema.options,
|
...SanitizedGcpConnectionSchema.options,
|
||||||
...SanitizedAzureKeyVaultConnectionSchema.options,
|
...SanitizedAzureKeyVaultConnectionSchema.options,
|
||||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||||
|
...SanitizedAzureDevOpsConnectionSchema.options,
|
||||||
...SanitizedDatabricksConnectionSchema.options,
|
...SanitizedDatabricksConnectionSchema.options,
|
||||||
...SanitizedHumanitecConnectionSchema.options,
|
...SanitizedHumanitecConnectionSchema.options,
|
||||||
...SanitizedTerraformCloudConnectionSchema.options,
|
...SanitizedTerraformCloudConnectionSchema.options,
|
||||||
@@ -100,6 +105,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
GcpConnectionListItemSchema,
|
GcpConnectionListItemSchema,
|
||||||
AzureKeyVaultConnectionListItemSchema,
|
AzureKeyVaultConnectionListItemSchema,
|
||||||
AzureAppConfigurationConnectionListItemSchema,
|
AzureAppConfigurationConnectionListItemSchema,
|
||||||
|
AzureDevOpsConnectionListItemSchema,
|
||||||
DatabricksConnectionListItemSchema,
|
DatabricksConnectionListItemSchema,
|
||||||
HumanitecConnectionListItemSchema,
|
HumanitecConnectionListItemSchema,
|
||||||
TerraformCloudConnectionListItemSchema,
|
TerraformCloudConnectionListItemSchema,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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 {
|
||||||
|
CreateAzureDevOpsConnectionSchema,
|
||||||
|
SanitizedAzureDevOpsConnectionSchema,
|
||||||
|
UpdateAzureDevOpsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAzureDevOpsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.AzureDevOps,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedAzureDevOpsConnectionSchema,
|
||||||
|
createSchema: CreateAzureDevOpsConnectionSchema,
|
||||||
|
updateSchema: UpdateAzureDevOpsConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/projects`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projects: z.object({ name: z.string(), id: z.string(), appId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const projects = await server.services.appConnection.azureDevOps.listProjects(connectionId, req.permission);
|
||||||
|
|
||||||
|
return { projects };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
|||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||||
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
||||||
|
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
|
||||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
@@ -34,6 +35,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
||||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||||
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
||||||
|
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
|
||||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
AzureDevOpsSyncSchema,
|
||||||
|
CreateAzureDevOpsSyncSchema,
|
||||||
|
UpdateAzureDevOpsSyncSchema
|
||||||
|
} from "@app/services/secret-sync/azure-devops";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerAzureDevOpsSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.AzureDevOps,
|
||||||
|
server,
|
||||||
|
responseSchema: AzureDevOpsSyncSchema,
|
||||||
|
createSchema: CreateAzureDevOpsSyncSchema,
|
||||||
|
updateSchema: UpdateAzureDevOpsSyncSchema
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import { registerOnePassSyncRouter } from "./1password-sync-router";
|
|||||||
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||||
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
||||||
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
|
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
|
||||||
|
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
|
||||||
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
||||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||||
@@ -26,6 +27,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.GCPSecretManager]: registerGcpSyncRouter,
|
[SecretSync.GCPSecretManager]: registerGcpSyncRouter,
|
||||||
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
||||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||||
|
[SecretSync.AzureDevOps]: registerAzureDevOpsSyncRouter,
|
||||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
AzureAppConfigurationSyncListItemSchema,
|
AzureAppConfigurationSyncListItemSchema,
|
||||||
AzureAppConfigurationSyncSchema
|
AzureAppConfigurationSyncSchema
|
||||||
} from "@app/services/secret-sync/azure-app-configuration";
|
} from "@app/services/secret-sync/azure-app-configuration";
|
||||||
|
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
|
||||||
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
|
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
|
||||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||||
@@ -38,6 +39,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
GcpSyncSchema,
|
GcpSyncSchema,
|
||||||
AzureKeyVaultSyncSchema,
|
AzureKeyVaultSyncSchema,
|
||||||
AzureAppConfigurationSyncSchema,
|
AzureAppConfigurationSyncSchema,
|
||||||
|
AzureDevOpsSyncSchema,
|
||||||
DatabricksSyncSchema,
|
DatabricksSyncSchema,
|
||||||
HumanitecSyncSchema,
|
HumanitecSyncSchema,
|
||||||
TerraformCloudSyncSchema,
|
TerraformCloudSyncSchema,
|
||||||
@@ -57,6 +59,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
GcpSyncListItemSchema,
|
GcpSyncListItemSchema,
|
||||||
AzureKeyVaultSyncListItemSchema,
|
AzureKeyVaultSyncListItemSchema,
|
||||||
AzureAppConfigurationSyncListItemSchema,
|
AzureAppConfigurationSyncListItemSchema,
|
||||||
|
AzureDevOpsSyncListItemSchema,
|
||||||
DatabricksSyncListItemSchema,
|
DatabricksSyncListItemSchema,
|
||||||
HumanitecSyncListItemSchema,
|
HumanitecSyncListItemSchema,
|
||||||
TerraformCloudSyncListItemSchema,
|
TerraformCloudSyncListItemSchema,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export enum AppConnection {
|
|||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
AzureClientSecrets = "azure-client-secrets",
|
AzureClientSecrets = "azure-client-secrets",
|
||||||
|
AzureDevOps = "azure-devops",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ import {
|
|||||||
getAzureClientSecretsConnectionListItem,
|
getAzureClientSecretsConnectionListItem,
|
||||||
validateAzureClientSecretsConnectionCredentials
|
validateAzureClientSecretsConnectionCredentials
|
||||||
} from "./azure-client-secrets";
|
} from "./azure-client-secrets";
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops/azure-devops-enums";
|
||||||
|
import {
|
||||||
|
getAzureDevopsConnectionListItem,
|
||||||
|
validateAzureDevOpsConnectionCredentials
|
||||||
|
} from "./azure-devops/azure-devops-fns";
|
||||||
import {
|
import {
|
||||||
AzureKeyVaultConnectionMethod,
|
AzureKeyVaultConnectionMethod,
|
||||||
getAzureKeyVaultConnectionListItem,
|
getAzureKeyVaultConnectionListItem,
|
||||||
@@ -98,6 +103,7 @@ export const listAppConnectionOptions = () => {
|
|||||||
getGcpConnectionListItem(),
|
getGcpConnectionListItem(),
|
||||||
getAzureKeyVaultConnectionListItem(),
|
getAzureKeyVaultConnectionListItem(),
|
||||||
getAzureAppConfigurationConnectionListItem(),
|
getAzureAppConfigurationConnectionListItem(),
|
||||||
|
getAzureDevopsConnectionListItem(),
|
||||||
getDatabricksConnectionListItem(),
|
getDatabricksConnectionListItem(),
|
||||||
getHumanitecConnectionListItem(),
|
getHumanitecConnectionListItem(),
|
||||||
getTerraformCloudConnectionListItem(),
|
getTerraformCloudConnectionListItem(),
|
||||||
@@ -173,6 +179,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.AzureClientSecrets]:
|
[AppConnection.AzureClientSecrets]:
|
||||||
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
@@ -201,6 +208,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
case GitHubConnectionMethod.OAuth:
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
return "OAuth";
|
return "OAuth";
|
||||||
case AwsConnectionMethod.AccessKey:
|
case AwsConnectionMethod.AccessKey:
|
||||||
case OCIConnectionMethod.AccessKey:
|
case OCIConnectionMethod.AccessKey:
|
||||||
@@ -217,6 +225,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case TerraformCloudConnectionMethod.ApiToken:
|
case TerraformCloudConnectionMethod.ApiToken:
|
||||||
case VercelConnectionMethod.ApiToken:
|
case VercelConnectionMethod.ApiToken:
|
||||||
case OnePassConnectionMethod.ApiToken:
|
case OnePassConnectionMethod.ApiToken:
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
return "API Token";
|
return "API Token";
|
||||||
case PostgresConnectionMethod.UsernameAndPassword:
|
case PostgresConnectionMethod.UsernameAndPassword:
|
||||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||||
@@ -270,6 +279,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
||||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||||
|
[AppConnection.AzureDevOps]: "Azure DevOps",
|
||||||
[AppConnection.Databricks]: "Databricks",
|
[AppConnection.Databricks]: "Databricks",
|
||||||
[AppConnection.Humanitec]: "Humanitec",
|
[AppConnection.Humanitec]: "Humanitec",
|
||||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||||
@@ -33,6 +34,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
|||||||
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
|
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
||||||
|
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import { awsConnectionService } from "./aws/aws-connection-service";
|
|||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||||
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
||||||
|
import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas";
|
||||||
|
import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service";
|
||||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||||
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||||
@@ -84,6 +86,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||||
|
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
|
||||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||||
@@ -498,6 +501,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
vercel: vercelConnectionService(connectAppConnectionById),
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||||
windmill: windmillConnectionService(connectAppConnectionById),
|
windmill: windmillConnectionService(connectAppConnectionById),
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ import {
|
|||||||
TAzureClientSecretsConnectionInput,
|
TAzureClientSecretsConnectionInput,
|
||||||
TValidateAzureClientSecretsConnectionCredentialsSchema
|
TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
} from "./azure-client-secrets";
|
} from "./azure-client-secrets";
|
||||||
|
import {
|
||||||
|
TAzureDevOpsConnection,
|
||||||
|
TAzureDevOpsConnectionConfig,
|
||||||
|
TAzureDevOpsConnectionInput,
|
||||||
|
TValidateAzureDevOpsConnectionCredentialsSchema
|
||||||
|
} from "./azure-devops/azure-devops-types";
|
||||||
import {
|
import {
|
||||||
TAzureKeyVaultConnection,
|
TAzureKeyVaultConnection,
|
||||||
TAzureKeyVaultConnectionConfig,
|
TAzureKeyVaultConnectionConfig,
|
||||||
@@ -132,6 +138,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TGcpConnection
|
| TGcpConnection
|
||||||
| TAzureKeyVaultConnection
|
| TAzureKeyVaultConnection
|
||||||
| TAzureAppConfigurationConnection
|
| TAzureAppConfigurationConnection
|
||||||
|
| TAzureDevOpsConnection
|
||||||
| TDatabricksConnection
|
| TDatabricksConnection
|
||||||
| THumanitecConnection
|
| THumanitecConnection
|
||||||
| TTerraformCloudConnection
|
| TTerraformCloudConnection
|
||||||
@@ -161,6 +168,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TGcpConnectionInput
|
| TGcpConnectionInput
|
||||||
| TAzureKeyVaultConnectionInput
|
| TAzureKeyVaultConnectionInput
|
||||||
| TAzureAppConfigurationConnectionInput
|
| TAzureAppConfigurationConnectionInput
|
||||||
|
| TAzureDevOpsConnectionInput
|
||||||
| TDatabricksConnectionInput
|
| TDatabricksConnectionInput
|
||||||
| THumanitecConnectionInput
|
| THumanitecConnectionInput
|
||||||
| TTerraformCloudConnectionInput
|
| TTerraformCloudConnectionInput
|
||||||
@@ -197,6 +205,7 @@ export type TAppConnectionConfig =
|
|||||||
| TGcpConnectionConfig
|
| TGcpConnectionConfig
|
||||||
| TAzureKeyVaultConnectionConfig
|
| TAzureKeyVaultConnectionConfig
|
||||||
| TAzureAppConfigurationConnectionConfig
|
| TAzureAppConfigurationConnectionConfig
|
||||||
|
| TAzureDevOpsConnectionConfig
|
||||||
| TAzureClientSecretsConnectionConfig
|
| TAzureClientSecretsConnectionConfig
|
||||||
| TDatabricksConnectionConfig
|
| TDatabricksConnectionConfig
|
||||||
| THumanitecConnectionConfig
|
| THumanitecConnectionConfig
|
||||||
@@ -220,6 +229,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||||
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
|
| TValidateAzureDevOpsConnectionCredentialsSchema
|
||||||
| TValidateDatabricksConnectionCredentialsSchema
|
| TValidateDatabricksConnectionCredentialsSchema
|
||||||
| TValidateHumanitecConnectionCredentialsSchema
|
| TValidateHumanitecConnectionCredentialsSchema
|
||||||
| TValidatePostgresConnectionCredentialsSchema
|
| TValidatePostgresConnectionCredentialsSchema
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum AzureDevOpsConnectionMethod {
|
||||||
|
OAuth = "oauth",
|
||||||
|
ApiToken = "api-token"
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import {
|
||||||
|
decryptAppConnectionCredentials,
|
||||||
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName
|
||||||
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||||
|
import {
|
||||||
|
ExchangeCodeAzureResponse,
|
||||||
|
TAzureDevOpsConnectionConfig,
|
||||||
|
TAzureDevOpsConnectionCredentials
|
||||||
|
} from "./azure-devops-types";
|
||||||
|
|
||||||
|
export const getAzureDevopsConnectionListItem = () => {
|
||||||
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Azure DevOps" as const,
|
||||||
|
app: AppConnection.AzureDevOps as const,
|
||||||
|
methods: Object.values(AzureDevOpsConnectionMethod) as [
|
||||||
|
AzureDevOpsConnectionMethod.OAuth,
|
||||||
|
AzureDevOpsConnectionMethod.ApiToken
|
||||||
|
],
|
||||||
|
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAzureDevopsConnectionAccessToken = async (
|
||||||
|
connectionId: string,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConnection.app !== AppConnection.AzureDevOps) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Connection with ID '${connectionId}' is not an Azure DevOps connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = (await decryptAppConnectionCredentials({
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials
|
||||||
|
})) as TAzureDevOpsConnectionCredentials;
|
||||||
|
|
||||||
|
// Handle different connection methods
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Azure environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("refreshToken" in credentials)) {
|
||||||
|
throw new BadRequestError({ message: "Invalid OAuth credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { refreshToken, tenantId } = credentials;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||||
|
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
refresh_token: refreshToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCredentials = {
|
||||||
|
...credentials,
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresAt: currentTime + data.expires_in * 1000,
|
||||||
|
refreshToken: data.refresh_token
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
|
if (!("apiKey" in credentials)) {
|
||||||
|
throw new BadRequestError({ message: "Invalid API token credentials" });
|
||||||
|
}
|
||||||
|
// For API token, return the basic auth token directly
|
||||||
|
return credentials.apiKey as string;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Unsupported connection method` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => {
|
||||||
|
const { credentials: inputCredentials, method } = config;
|
||||||
|
|
||||||
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
if (!SITE_URL) {
|
||||||
|
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||||
|
let tokenError: AxiosError | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oauthCredentials = inputCredentials as { code: string; tenantId: string };
|
||||||
|
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: oauthCredentials.code,
|
||||||
|
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||||
|
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
tokenError = e;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection: verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenError) {
|
||||||
|
if (tokenError instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResp) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Failed to get access token: Token was empty with no error`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthCredentials = inputCredentials as { code: string; tenantId: string; orgName: string };
|
||||||
|
return {
|
||||||
|
tenantId: oauthCredentials.tenantId,
|
||||||
|
orgName: oauthCredentials.orgName,
|
||||||
|
accessToken: tokenResp.data.access_token,
|
||||||
|
refreshToken: tokenResp.data.refresh_token,
|
||||||
|
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
|
const apiTokenCredentials = inputCredentials as { apiKey: string; orgName?: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (apiTokenCredentials.orgName) {
|
||||||
|
// Validate against specific organization
|
||||||
|
const response = await request.get(
|
||||||
|
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${apiTokenCredentials.orgName}/_apis/projects?api-version=7.2-preview.2&$top=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.apiKey}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to validate connection: ${response.status}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: apiTokenCredentials.apiKey,
|
||||||
|
orgName: apiTokenCredentials.orgName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Validate via profile and discover organizations
|
||||||
|
const profileResponse = await request.get<{ displayName: string }>(
|
||||||
|
`https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.apiKey}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let organizations: Array<{ accountId: string; accountName: string; accountUri: string }> = [];
|
||||||
|
try {
|
||||||
|
const orgsResponse = await request.get<{
|
||||||
|
value: Array<{ accountId: string; accountName: string; accountUri: string }>;
|
||||||
|
}>(`https://app.vssps.visualstudio.com/_apis/accounts?api-version=7.1`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.apiKey}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
organizations = orgsResponse.data.value || [];
|
||||||
|
} catch (orgError) {
|
||||||
|
logger.warn(orgError, "Could not fetch organizations automatically:");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: apiTokenCredentials.apiKey,
|
||||||
|
userDisplayName: profileResponse.data.displayName,
|
||||||
|
organizations: organizations.map((org) => ({
|
||||||
|
accountId: org.accountId,
|
||||||
|
accountName: org.accountName,
|
||||||
|
accountUri: org.accountUri
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
const errorMessage = apiTokenCredentials.orgName
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
`Failed to validate API token for organization '${apiTokenCredentials.orgName}': ${error.response?.data?.message || error.message}`
|
||||||
|
: `Invalid Azure DevOps Personal Access Token: ${error.response?.status === 401 ? "Token is invalid or expired" : error.message}`;
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: errorMessage });
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate Azure DevOps token`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unhandled Azure connection method: ${method as AzureDevOpsConnectionMethod}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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 { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionOAuthInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.code),
|
||||||
|
tenantId: z.string().trim().min(1, "Tenant ID required").describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.tenantId),
|
||||||
|
orgName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Organization name required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.orgName)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionOAuthOutputCredentialsSchema = z.object({
|
||||||
|
tenantId: z.string(),
|
||||||
|
orgName: z.string(),
|
||||||
|
accessToken: z.string(),
|
||||||
|
refreshToken: z.string(),
|
||||||
|
expiresAt: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionApiTokenInputCredentialsSchema = z.object({
|
||||||
|
apiKey: z.string().trim().min(1, "API Key required"),
|
||||||
|
orgName: z.string().trim().min(1, "Organization name required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionApiTokenOutputCredentialsSchema = z.object({
|
||||||
|
apiKey: z.string(),
|
||||||
|
orgName: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureDevOpsConnectionMethod.OAuth)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||||
|
credentials: AzureDevOpsConnectionOAuthInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureDevOpsConnectionMethod.ApiToken)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||||
|
credentials: AzureDevOpsConnectionApiTokenInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateAzureDevOpsConnectionSchema = ValidateAzureDevOpsConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureDevOps)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAzureDevOpsConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([AzureDevOpsConnectionOAuthInputCredentialsSchema, AzureDevOpsConnectionApiTokenInputCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AzureDevOps).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureDevOps));
|
||||||
|
|
||||||
|
const BaseAzureDevOpsConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.AzureDevOps)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionSchema = z.intersection(
|
||||||
|
BaseAzureDevOpsConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
|
||||||
|
credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.ApiToken),
|
||||||
|
credentials: AzureDevOpsConnectionApiTokenOutputCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseAzureDevOpsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
|
||||||
|
credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema.pick({
|
||||||
|
tenantId: true,
|
||||||
|
orgName: true
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
BaseAzureDevOpsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.ApiToken),
|
||||||
|
credentials: AzureDevOpsConnectionApiTokenOutputCredentialsSchema.pick({
|
||||||
|
orgName: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("Azure DevOps"),
|
||||||
|
app: z.literal(AppConnection.AzureDevOps),
|
||||||
|
methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(),
|
||||||
|
oauthClientId: z.string().optional()
|
||||||
|
});
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
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 { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||||
|
import { getAzureDevopsConnectionAccessToken } from "./azure-devops-fns";
|
||||||
|
import { TAzureDevOpsConnection } from "./azure-devops-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TAzureDevOpsConnection>;
|
||||||
|
|
||||||
|
type TAzureDevOpsProject = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
state?: string;
|
||||||
|
visibility?: string;
|
||||||
|
lastUpdateTime?: string;
|
||||||
|
revision?: number;
|
||||||
|
abbreviation?: string;
|
||||||
|
defaultTeamImageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TAzureDevOpsProjectsResponse = {
|
||||||
|
count: number;
|
||||||
|
value: TAzureDevOpsProject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthHeaders = (appConnection: TAzureDevOpsConnection, accessToken: string) => {
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
};
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
|
// For API token, create Basic auth header
|
||||||
|
const basicAuthToken = Buffer.from(`user:${accessToken}`).toString("base64");
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${basicAuthToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unsupported connection method" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAzureDevOpsProjects = async (
|
||||||
|
appConnection: TAzureDevOpsConnection,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
): Promise<TAzureDevOpsProject[]> => {
|
||||||
|
const accessToken = await getAzureDevopsConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
// Both OAuth and API Token methods use organization name from credentials
|
||||||
|
const credentials = appConnection.credentials as { orgName: string };
|
||||||
|
const { orgName } = credentials;
|
||||||
|
|
||||||
|
// Use the standard Azure DevOps Projects API endpoint
|
||||||
|
// This endpoint returns only projects that the authenticated user has access to
|
||||||
|
const devOpsEndpoint = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.1`;
|
||||||
|
try {
|
||||||
|
const { data } = await request.get<TAzureDevOpsProjectsResponse>(devOpsEndpoint, {
|
||||||
|
headers: getAuthHeaders(appConnection, accessToken)
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.value || [];
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
// Provide more specific error messages based on the response
|
||||||
|
if (error?.response?.status === 401) {
|
||||||
|
throw new Error(
|
||||||
|
`Authentication failed for Azure DevOps organization: ${orgName}. Please check your credentials and ensure the token has the required scopes (vso.project or vso.profile).`
|
||||||
|
);
|
||||||
|
} else if (error?.response?.status === 403) {
|
||||||
|
throw new Error(
|
||||||
|
`Access denied to Azure DevOps organization: ${orgName}. Please ensure the user has access to the organization.`
|
||||||
|
);
|
||||||
|
} else if (error?.response?.status === 404) {
|
||||||
|
throw new Error(`Azure DevOps organization not found: ${orgName}. Please verify the organization name.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const azureDevOpsConnectionService = (
|
||||||
|
getAppConnection: TGetAppConnectionFunc,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.AzureDevOps, connectionId, actor);
|
||||||
|
|
||||||
|
const projects = await listAzureDevOpsProjects(appConnection, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
return projects.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
appId: project.id,
|
||||||
|
description: project.description,
|
||||||
|
url: project.url,
|
||||||
|
state: project.state,
|
||||||
|
visibility: project.visibility,
|
||||||
|
lastUpdateTime: project.lastUpdateTime,
|
||||||
|
revision: project.revision,
|
||||||
|
abbreviation: project.abbreviation,
|
||||||
|
defaultTeamImageUrl: project.defaultTeamImageUrl
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listProjects
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
AzureDevOpsConnectionOAuthOutputCredentialsSchema,
|
||||||
|
AzureDevOpsConnectionSchema,
|
||||||
|
CreateAzureDevOpsConnectionSchema,
|
||||||
|
ValidateAzureDevOpsConnectionCredentialsSchema
|
||||||
|
} from "./azure-devops-schemas";
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnection = z.infer<typeof AzureDevOpsConnectionSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionInput = z.infer<typeof CreateAzureDevOpsConnectionSchema> & {
|
||||||
|
app: AppConnection.AzureDevOps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateAzureDevOpsConnectionCredentialsSchema = typeof ValidateAzureDevOpsConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionConfig = DiscriminativePick<
|
||||||
|
TAzureDevOpsConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionCredentials = z.infer<typeof AzureDevOpsConnectionOAuthOutputCredentialsSchema>;
|
||||||
|
|
||||||
|
export interface ExchangeCodeAzureResponse {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureRegisteredApp {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
createdDateTime: string;
|
||||||
|
identifierUris?: string[];
|
||||||
|
signInAudience?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureListRegisteredAppsResponse {
|
||||||
|
"@odata.context": string;
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
value: TAzureRegisteredApp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureDevOps {
|
||||||
|
keyId: string;
|
||||||
|
displayName?: string;
|
||||||
|
startDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
secretText?: string;
|
||||||
|
}
|
||||||
@@ -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 AZURE_DEVOPS_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "Azure DevOps",
|
||||||
|
destination: SecretSync.AzureDevOps,
|
||||||
|
connection: AppConnection.AzureDevOps,
|
||||||
|
canImportSecrets: false
|
||||||
|
};
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { AzureDevOpsConnectionMethod } from "@app/services/app-connection/azure-devops/azure-devops-enums";
|
||||||
|
import { getAzureDevopsConnectionAccessToken } from "@app/services/app-connection/azure-devops/azure-devops-fns";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TAzureDevOpsSyncWithCredentials } from "./azure-devops-sync-types";
|
||||||
|
|
||||||
|
type TAzureDevOpsSyncFactoryDeps = {
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AzureDevOpsVariableGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
variables: Record<string, { value: string; isSecret: boolean }>;
|
||||||
|
variableGroupProjectReferences: Array<{
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
projectReference: { id: string; name: string };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AzureDevOpsVariableGroupList {
|
||||||
|
count: number;
|
||||||
|
value: AzureDevOpsVariableGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureDevOpsSyncFactoryDeps) => {
|
||||||
|
const getConnectionAuth = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
|
||||||
|
const { credentials } = secretSync.connection;
|
||||||
|
const isOAuth = secretSync.connection.method === AzureDevOpsConnectionMethod.OAuth;
|
||||||
|
|
||||||
|
const { orgName } = credentials;
|
||||||
|
if (!orgName) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Azure DevOps: organization name is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = await getAzureDevopsConnectionAccessToken(
|
||||||
|
secretSync.connectionId,
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accessToken, orgName, isOAuth };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthHeader = (accessToken: string, isOAuth: boolean) => {
|
||||||
|
if (isOAuth) {
|
||||||
|
return `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
const basicAuth = Buffer.from(`:${accessToken}`).toString("base64");
|
||||||
|
return `Basic ${basicAuth}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $getEnvGroupId = async (
|
||||||
|
accessToken: string,
|
||||||
|
orgName: string,
|
||||||
|
projectId: string,
|
||||||
|
environmentName: string,
|
||||||
|
isOAuth: boolean
|
||||||
|
) => {
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/${projectId}/_apis/distributedtask/variablegroups?api-version=7.1`;
|
||||||
|
const response = await request.get<AzureDevOpsVariableGroupList>(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const group of response.data.value) {
|
||||||
|
if (group.name === environmentName) {
|
||||||
|
return { groupId: group.id.toString(), groupName: group.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { groupId: "", groupName: "" };
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
if (!secretSync.destinationConfig.devopsProjectId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Azure DevOps: project ID is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secretSync.environment?.name) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Azure DevOps: environment name is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
|
||||||
|
|
||||||
|
const { groupId, groupName } = await $getEnvGroupId(
|
||||||
|
accessToken,
|
||||||
|
orgName,
|
||||||
|
secretSync.destinationConfig.devopsProjectId,
|
||||||
|
secretSync.environment.name,
|
||||||
|
isOAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
const variables: Record<string, { value: string; isSecret: boolean }> = {};
|
||||||
|
for (const [key, secret] of Object.entries(secretMap)) {
|
||||||
|
if (secret?.value !== undefined) {
|
||||||
|
variables[key] = { value: secret.value, isSecret: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupId) {
|
||||||
|
// Create new variable group - API endpoint is organization-level
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups?api-version=7.1`;
|
||||||
|
|
||||||
|
await request.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
name: secretSync.environment.name,
|
||||||
|
description: secretSync.environment.name,
|
||||||
|
type: "Vsts",
|
||||||
|
variables,
|
||||||
|
variableGroupProjectReferences: [
|
||||||
|
{
|
||||||
|
description: secretSync.environment.name,
|
||||||
|
name: secretSync.environment.name,
|
||||||
|
projectReference: {
|
||||||
|
id: secretSync.destinationConfig.devopsProjectId,
|
||||||
|
name: secretSync.destinationConfig.devopsProjectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth),
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`;
|
||||||
|
|
||||||
|
await request.put(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
name: groupName,
|
||||||
|
description: groupName,
|
||||||
|
type: "Vsts",
|
||||||
|
variables,
|
||||||
|
variableGroupProjectReferences: [
|
||||||
|
{
|
||||||
|
description: groupName,
|
||||||
|
name: groupName,
|
||||||
|
projectReference: {
|
||||||
|
id: secretSync.destinationConfig.devopsProjectId,
|
||||||
|
name: secretSync.destinationConfig.devopsProjectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth),
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
|
||||||
|
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
|
||||||
|
|
||||||
|
const { groupId } = await $getEnvGroupId(
|
||||||
|
accessToken,
|
||||||
|
orgName,
|
||||||
|
secretSync.destinationConfig.devopsProjectId,
|
||||||
|
secretSync.environment?.name || "",
|
||||||
|
isOAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
// Delete the variable group entirely using the DELETE API
|
||||||
|
const deleteUrl = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups/${groupId}?projectIds=${secretSync.destinationConfig.devopsProjectId}&api-version=7.1`;
|
||||||
|
|
||||||
|
await request.delete(deleteUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
|
||||||
|
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
|
||||||
|
|
||||||
|
const { groupId } = await $getEnvGroupId(
|
||||||
|
accessToken,
|
||||||
|
orgName,
|
||||||
|
secretSync.destinationConfig.devopsProjectId,
|
||||||
|
secretSync.environment?.name || "",
|
||||||
|
isOAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretMap: TSecretMap = {};
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`;
|
||||||
|
const response = await request.get<AzureDevOpsVariableGroup>(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.data?.variables) {
|
||||||
|
Object.entries(response.data.variables).forEach(([key, variable]) => {
|
||||||
|
secretMap[key] = {
|
||||||
|
value: variable.value || ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncSecrets,
|
||||||
|
removeSecrets,
|
||||||
|
getSecrets
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncDestinationConfigSchema = z.object({
|
||||||
|
devopsProjectId: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Project ID required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"),
|
||||||
|
devopsProjectName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Project name required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name")
|
||||||
|
});
|
||||||
|
|
||||||
|
const AzureDevOpsSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncSchema = BaseSecretSyncSchema(SecretSync.AzureDevOps, AzureDevOpsSyncOptionsConfig).extend({
|
||||||
|
destination: z.literal(SecretSync.AzureDevOps),
|
||||||
|
destinationConfig: AzureDevOpsSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAzureDevOpsSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AzureDevOps,
|
||||||
|
AzureDevOpsSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AzureDevOpsSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAzureDevOpsSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AzureDevOps,
|
||||||
|
AzureDevOpsSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AzureDevOpsSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("Azure DevOps"),
|
||||||
|
connection: z.literal(AppConnection.AzureDevOps),
|
||||||
|
destination: z.literal(SecretSync.AzureDevOps),
|
||||||
|
canImportSecrets: z.literal(false)
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAzureDevOpsConnection } from "@app/services/app-connection/azure-devops/azure-devops-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AzureDevOpsSyncDestinationConfigSchema,
|
||||||
|
AzureDevOpsSyncListItemSchema,
|
||||||
|
AzureDevOpsSyncSchema,
|
||||||
|
CreateAzureDevOpsSyncSchema
|
||||||
|
} from "./azure-devops-sync-schemas";
|
||||||
|
|
||||||
|
export type TAzureDevOpsSync = z.infer<typeof AzureDevOpsSyncSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncInput = z.infer<typeof CreateAzureDevOpsSyncSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncListItem = z.infer<typeof AzureDevOpsSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncDestinationConfig = z.infer<typeof AzureDevOpsSyncDestinationConfigSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncWithCredentials = TAzureDevOpsSync & {
|
||||||
|
connection: TAzureDevOpsConnection;
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./azure-devops-sync-constants";
|
||||||
|
export * from "./azure-devops-sync-fns";
|
||||||
|
export * from "./azure-devops-sync-schemas";
|
||||||
|
export * from "./azure-devops-sync-types";
|
||||||
@@ -5,6 +5,7 @@ export enum SecretSync {
|
|||||||
GCPSecretManager = "gcp-secret-manager",
|
GCPSecretManager = "gcp-secret-manager",
|
||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
|
AzureDevOps = "azure-devops",
|
||||||
Databricks = "databricks",
|
Databricks = "databricks",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password";
|
import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password";
|
||||||
import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
||||||
|
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
|
||||||
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
||||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||||
@@ -45,6 +46,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
|||||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
||||||
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION,
|
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION,
|
||||||
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
|
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
|
||||||
|
[SecretSync.AzureDevOps]: AZURE_DEVOPS_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,
|
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||||
@@ -152,6 +154,11 @@ export const SecretSyncFns = {
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}).syncSecrets(secretSync, schemaSecretMap);
|
}).syncSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return azureDevOpsSyncFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
}).syncSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return databricksSyncFactory({
|
return databricksSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -214,6 +221,12 @@ export const SecretSyncFns = {
|
|||||||
kmsService
|
kmsService
|
||||||
}).getSecrets(secretSync);
|
}).getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
secretMap = await azureDevOpsSyncFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
}).getSecrets(secretSync);
|
||||||
|
break;
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return databricksSyncFactory({
|
return databricksSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -283,6 +296,11 @@ export const SecretSyncFns = {
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}).removeSecrets(secretSync, schemaSecretMap);
|
}).removeSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return azureDevOpsSyncFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
}).removeSecrets(secretSync);
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return databricksSyncFactory({
|
return databricksSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
|||||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
|
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
|
||||||
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
||||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
|
[SecretSync.AzureDevOps]: "Azure DevOps",
|
||||||
[SecretSync.Databricks]: "Databricks",
|
[SecretSync.Databricks]: "Databricks",
|
||||||
[SecretSync.Humanitec]: "Humanitec",
|
[SecretSync.Humanitec]: "Humanitec",
|
||||||
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
||||||
@@ -27,6 +28,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
||||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||||
|
[SecretSync.AzureDevOps]: AppConnection.AzureDevOps,
|
||||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||||
@@ -46,6 +48,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
|||||||
[SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular,
|
[SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular,
|
[SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular,
|
[SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular,
|
||||||
|
[SecretSync.AzureDevOps]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Databricks]: SecretSyncPlanType.Regular,
|
[SecretSync.Databricks]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Humanitec]: SecretSyncPlanType.Regular,
|
[SecretSync.Humanitec]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.TerraformCloud]: SecretSyncPlanType.Regular,
|
[SecretSync.TerraformCloud]: SecretSyncPlanType.Regular,
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ import {
|
|||||||
TAzureAppConfigurationSyncListItem,
|
TAzureAppConfigurationSyncListItem,
|
||||||
TAzureAppConfigurationSyncWithCredentials
|
TAzureAppConfigurationSyncWithCredentials
|
||||||
} from "./azure-app-configuration";
|
} from "./azure-app-configuration";
|
||||||
|
import {
|
||||||
|
TAzureDevOpsSync,
|
||||||
|
TAzureDevOpsSyncInput,
|
||||||
|
TAzureDevOpsSyncListItem,
|
||||||
|
TAzureDevOpsSyncWithCredentials
|
||||||
|
} from "./azure-devops";
|
||||||
import {
|
import {
|
||||||
TAzureKeyVaultSync,
|
TAzureKeyVaultSync,
|
||||||
TAzureKeyVaultSyncInput,
|
TAzureKeyVaultSyncInput,
|
||||||
@@ -100,6 +106,7 @@ export type TSecretSync =
|
|||||||
| TGcpSync
|
| TGcpSync
|
||||||
| TAzureKeyVaultSync
|
| TAzureKeyVaultSync
|
||||||
| TAzureAppConfigurationSync
|
| TAzureAppConfigurationSync
|
||||||
|
| TAzureDevOpsSync
|
||||||
| TDatabricksSync
|
| TDatabricksSync
|
||||||
| THumanitecSync
|
| THumanitecSync
|
||||||
| TTerraformCloudSync
|
| TTerraformCloudSync
|
||||||
@@ -118,6 +125,7 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TGcpSyncWithCredentials
|
| TGcpSyncWithCredentials
|
||||||
| TAzureKeyVaultSyncWithCredentials
|
| TAzureKeyVaultSyncWithCredentials
|
||||||
| TAzureAppConfigurationSyncWithCredentials
|
| TAzureAppConfigurationSyncWithCredentials
|
||||||
|
| TAzureDevOpsSyncWithCredentials
|
||||||
| TDatabricksSyncWithCredentials
|
| TDatabricksSyncWithCredentials
|
||||||
| THumanitecSyncWithCredentials
|
| THumanitecSyncWithCredentials
|
||||||
| TTerraformCloudSyncWithCredentials
|
| TTerraformCloudSyncWithCredentials
|
||||||
@@ -136,6 +144,7 @@ export type TSecretSyncInput =
|
|||||||
| TGcpSyncInput
|
| TGcpSyncInput
|
||||||
| TAzureKeyVaultSyncInput
|
| TAzureKeyVaultSyncInput
|
||||||
| TAzureAppConfigurationSyncInput
|
| TAzureAppConfigurationSyncInput
|
||||||
|
| TAzureDevOpsSyncInput
|
||||||
| TDatabricksSyncInput
|
| TDatabricksSyncInput
|
||||||
| THumanitecSyncInput
|
| THumanitecSyncInput
|
||||||
| TTerraformCloudSyncInput
|
| TTerraformCloudSyncInput
|
||||||
@@ -154,6 +163,7 @@ export type TSecretSyncListItem =
|
|||||||
| TGcpSyncListItem
|
| TGcpSyncListItem
|
||||||
| TAzureKeyVaultSyncListItem
|
| TAzureKeyVaultSyncListItem
|
||||||
| TAzureAppConfigurationSyncListItem
|
| TAzureAppConfigurationSyncListItem
|
||||||
|
| TAzureDevOpsSyncListItem
|
||||||
| TDatabricksSyncListItem
|
| TDatabricksSyncListItem
|
||||||
| THumanitecSyncListItem
|
| THumanitecSyncListItem
|
||||||
| TTerraformCloudSyncListItem
|
| TTerraformCloudSyncListItem
|
||||||
|
|||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
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 { useGetAzureDevOpsProjects } from "@app/hooks/api/appConnections/azure";
|
||||||
|
import { AzureDevOpsProject } from "@app/hooks/api/appConnections/azure/types";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncFields = () => {
|
||||||
|
const { control, setValue } = useFormContext<
|
||||||
|
TSecretSyncForm & { destination: SecretSync.AzureDevOps }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const connectionId = useWatch({ name: "connection.id", control });
|
||||||
|
|
||||||
|
const { data: { projects } = { projects: [] }, isLoading: isProjectsLoading } =
|
||||||
|
useGetAzureDevOpsProjects(connectionId, {
|
||||||
|
enabled: Boolean(connectionId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SecretSyncConnectionField
|
||||||
|
onChange={() => {
|
||||||
|
setValue("destinationConfig.devopsProjectId", "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="destinationConfig.devopsProjectId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Project"
|
||||||
|
helperText={
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content="Ensure the project exists in the connection's Azure DevOps instance URL."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span>Don't see the project you're looking for?</span>{" "}
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isLoading={isProjectsLoading && Boolean(connectionId)}
|
||||||
|
isDisabled={!connectionId}
|
||||||
|
value={projects?.find((v) => v.appId === value) ?? null}
|
||||||
|
onChange={(option) => {
|
||||||
|
onChange((option as SingleValue<AzureDevOpsProject>)?.appId ?? null);
|
||||||
|
setValue(
|
||||||
|
"destinationConfig.devopsProjectName",
|
||||||
|
(option as SingleValue<AzureDevOpsProject>)?.name ?? ""
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={projects}
|
||||||
|
placeholder="Select a project..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+3
@@ -7,6 +7,7 @@ import { OnePassSyncFields } from "./1PasswordSyncFields";
|
|||||||
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
||||||
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
|
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
|
||||||
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
|
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
|
||||||
|
import { AzureDevOpsSyncFields } from "./AzureDevopsSyncFields";
|
||||||
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
||||||
import { CamundaSyncFields } from "./CamundaSyncFields";
|
import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||||
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
||||||
@@ -38,6 +39,8 @@ export const SecretSyncDestinationFields = () => {
|
|||||||
return <AzureKeyVaultSyncFields />;
|
return <AzureKeyVaultSyncFields />;
|
||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
return <AzureAppConfigurationSyncFields />;
|
return <AzureAppConfigurationSyncFields />;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return <AzureDevOpsSyncFields />;
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return <DatabricksSyncFields />;
|
return <DatabricksSyncFields />;
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
|
|||||||
+1
@@ -41,6 +41,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
case SecretSync.TerraformCloud:
|
case SecretSync.TerraformCloud:
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncReviewFields = () => {
|
||||||
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.AzureDevOps }>();
|
||||||
|
const devopsProjectId = watch("destinationConfig.devopsProjectId");
|
||||||
|
const devopsProjectName = watch("destinationConfig.devopsProjectName");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Project ID">{devopsProjectId}</GenericFieldLabel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
@@ -16,6 +16,7 @@ import {
|
|||||||
AwsSecretsManagerSyncReviewFields
|
AwsSecretsManagerSyncReviewFields
|
||||||
} from "./AwsSecretsManagerSyncReviewFields";
|
} from "./AwsSecretsManagerSyncReviewFields";
|
||||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||||
|
import { AzureDevOpsSyncReviewFields } from "./AzureDevopsSyncReviewFields";
|
||||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||||
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||||
@@ -70,6 +71,9 @@ export const SecretSyncReviewFields = () => {
|
|||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
|
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
DestinationFieldsComponent = <AzureDevOpsSyncReviewFields />;
|
||||||
|
break;
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
DestinationFieldsComponent = <DatabricksSyncReviewFields />;
|
DestinationFieldsComponent = <DatabricksSyncReviewFields />;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
|
destination: z.literal(SecretSync.AzureDevOps),
|
||||||
|
destinationConfig: z.object({
|
||||||
|
devopsProjectId: z.string().trim().min(1, { message: "Azure DevOps Project ID is required" }),
|
||||||
|
devopsProjectName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Azure DevOps Project Name is required" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -4,6 +4,7 @@ import { OnePassSyncDestinationSchema } from "./1password-sync-destination-schem
|
|||||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||||
import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sync-destination-schema";
|
import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sync-destination-schema";
|
||||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||||
|
import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema";
|
||||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||||
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
||||||
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
||||||
@@ -24,6 +25,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
|||||||
GcpSyncDestinationSchema,
|
GcpSyncDestinationSchema,
|
||||||
AzureKeyVaultSyncDestinationSchema,
|
AzureKeyVaultSyncDestinationSchema,
|
||||||
AzureAppConfigurationSyncDestinationSchema,
|
AzureAppConfigurationSyncDestinationSchema,
|
||||||
|
AzureDevOpsSyncDestinationSchema,
|
||||||
DatabricksSyncDestinationSchema,
|
DatabricksSyncDestinationSchema,
|
||||||
HumanitecSyncDestinationSchema,
|
HumanitecSyncDestinationSchema,
|
||||||
TerraformCloudSyncDestinationSchema,
|
TerraformCloudSyncDestinationSchema,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
AwsConnectionMethod,
|
AwsConnectionMethod,
|
||||||
AzureAppConfigurationConnectionMethod,
|
AzureAppConfigurationConnectionMethod,
|
||||||
AzureClientSecretsConnectionMethod,
|
AzureClientSecretsConnectionMethod,
|
||||||
|
AzureDevOpsConnectionMethod,
|
||||||
AzureKeyVaultConnectionMethod,
|
AzureKeyVaultConnectionMethod,
|
||||||
CamundaConnectionMethod,
|
CamundaConnectionMethod,
|
||||||
DatabricksConnectionMethod,
|
DatabricksConnectionMethod,
|
||||||
@@ -60,6 +61,7 @@ export const APP_CONNECTION_MAP: Record<
|
|||||||
name: "Azure Client Secrets",
|
name: "Azure Client Secrets",
|
||||||
image: "Microsoft Azure.png"
|
image: "Microsoft Azure.png"
|
||||||
},
|
},
|
||||||
|
[AppConnection.AzureDevOps]: { name: "Azure DevOps", image: "Microsoft Azure.png" },
|
||||||
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
||||||
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
||||||
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
|
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
|
||||||
@@ -85,6 +87,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case AzureKeyVaultConnectionMethod.OAuth:
|
case AzureKeyVaultConnectionMethod.OAuth:
|
||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
case GitHubConnectionMethod.OAuth:
|
case GitHubConnectionMethod.OAuth:
|
||||||
return { name: "OAuth", icon: faPassport };
|
return { name: "OAuth", icon: faPassport };
|
||||||
case AwsConnectionMethod.AccessKey:
|
case AwsConnectionMethod.AccessKey:
|
||||||
@@ -102,6 +105,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case TerraformCloudConnectionMethod.ApiToken:
|
case TerraformCloudConnectionMethod.ApiToken:
|
||||||
case VercelConnectionMethod.ApiToken:
|
case VercelConnectionMethod.ApiToken:
|
||||||
case OnePassConnectionMethod.ApiToken:
|
case OnePassConnectionMethod.ApiToken:
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
return { name: "API Token", icon: faKey };
|
return { name: "API Token", icon: faKey };
|
||||||
case PostgresConnectionMethod.UsernameAndPassword:
|
case PostgresConnectionMethod.UsernameAndPassword:
|
||||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
|||||||
name: "Azure App Configuration",
|
name: "Azure App Configuration",
|
||||||
image: "Microsoft Azure.png"
|
image: "Microsoft Azure.png"
|
||||||
},
|
},
|
||||||
|
[SecretSync.AzureDevOps]: {
|
||||||
|
name: "Azure DevOps",
|
||||||
|
image: "Microsoft Azure.png"
|
||||||
|
},
|
||||||
[SecretSync.Databricks]: {
|
[SecretSync.Databricks]: {
|
||||||
name: "Databricks",
|
name: "Databricks",
|
||||||
image: "Databricks.png"
|
image: "Databricks.png"
|
||||||
@@ -65,6 +69,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
||||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||||
|
[SecretSync.AzureDevOps]: AppConnection.AzureDevOps,
|
||||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { appConnectionKeys } from "../queries";
|
import { appConnectionKeys } from "../queries";
|
||||||
import { TAzureClient } from "./types";
|
import { AzureDevOpsProjectsResponse, TAzureClient } from "./types";
|
||||||
|
|
||||||
const azureConnectionKeys = {
|
const azureConnectionKeys = {
|
||||||
all: [...appConnectionKeys.all, "azure"] as const,
|
all: [...appConnectionKeys.all, "azure"] as const,
|
||||||
listClients: (connectionId: string) =>
|
listClients: (connectionId: string) =>
|
||||||
[...azureConnectionKeys.all, "clients", connectionId] as const
|
[...azureConnectionKeys.all, "clients", connectionId] as const,
|
||||||
|
listDevopsProjects: (connectionId: string) =>
|
||||||
|
[...azureConnectionKeys.all, "devops-projects", connectionId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAzureConnectionListClients = (
|
export const useAzureConnectionListClients = (
|
||||||
@@ -35,3 +37,29 @@ export const useAzureConnectionListClients = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchAzureDevOpsProjects = async (
|
||||||
|
connectionId: string
|
||||||
|
): Promise<AzureDevOpsProjectsResponse> => {
|
||||||
|
if (!connectionId) {
|
||||||
|
throw new Error("Connection ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await apiRequest.get<AzureDevOpsProjectsResponse>(
|
||||||
|
`/api/v1/app-connections/azure-devops/${connectionId}/projects`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAzureDevOpsProjects = (
|
||||||
|
connectionId: string,
|
||||||
|
options?: Omit<UseQueryOptions<AzureDevOpsProjectsResponse, Error>, "queryKey" | "queryFn">
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: azureConnectionKeys.listDevopsProjects(connectionId),
|
||||||
|
queryFn: () => fetchAzureDevOpsProjects(connectionId),
|
||||||
|
enabled: Boolean(connectionId),
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,3 +3,13 @@ export type TAzureClient = {
|
|||||||
appId: string;
|
appId: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AzureDevOpsProject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
appId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AzureDevOpsProjectsResponse {
|
||||||
|
projects: AzureDevOpsProject[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum AppConnection {
|
|||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
AzureClientSecrets = "azure-client-secrets",
|
AzureClientSecrets = "azure-client-secrets",
|
||||||
|
AzureDevOps = "azure-devops",
|
||||||
Databricks = "databricks",
|
Databricks = "databricks",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export type TDatabricksConnectionOption = TAppConnectionOptionBase & {
|
|||||||
app: AppConnection.Databricks;
|
app: AppConnection.Databricks;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.AzureDevOps;
|
||||||
|
oauthClientId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type THumanitecConnectionOption = TAppConnectionOptionBase & {
|
export type THumanitecConnectionOption = TAppConnectionOptionBase & {
|
||||||
app: AppConnection.Humanitec;
|
app: AppConnection.Humanitec;
|
||||||
};
|
};
|
||||||
@@ -108,6 +113,7 @@ export type TAppConnectionOption =
|
|||||||
| TAzureAppConfigurationConnectionOption
|
| TAzureAppConfigurationConnectionOption
|
||||||
| TAzureKeyVaultConnectionOption
|
| TAzureKeyVaultConnectionOption
|
||||||
| TAzureClientSecretsConnectionOption
|
| TAzureClientSecretsConnectionOption
|
||||||
|
| TAzureDevOpsConnectionOption
|
||||||
| TDatabricksConnectionOption
|
| TDatabricksConnectionOption
|
||||||
| THumanitecConnectionOption
|
| THumanitecConnectionOption
|
||||||
| TTerraformCloudConnectionOption
|
| TTerraformCloudConnectionOption
|
||||||
@@ -131,6 +137,7 @@ export type TAppConnectionOptionMap = {
|
|||||||
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
|
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
|
||||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
|
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
|
||||||
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnectionOption;
|
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnectionOption;
|
||||||
|
[AppConnection.AzureDevOps]: TAzureDevOpsConnectionOption;
|
||||||
[AppConnection.Databricks]: TDatabricksConnectionOption;
|
[AppConnection.Databricks]: TDatabricksConnectionOption;
|
||||||
[AppConnection.Humanitec]: THumanitecConnectionOption;
|
[AppConnection.Humanitec]: THumanitecConnectionOption;
|
||||||
[AppConnection.TerraformCloud]: TTerraformCloudConnectionOption;
|
[AppConnection.TerraformCloud]: TTerraformCloudConnectionOption;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
|
export enum AzureDevOpsConnectionMethod {
|
||||||
|
OAuth = "oauth",
|
||||||
|
ApiToken = "api-token"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnection = TRootAppConnection & {
|
||||||
|
app: AppConnection.AzureDevOps;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
method: AzureDevOpsConnectionMethod.OAuth;
|
||||||
|
credentials: {
|
||||||
|
code: string;
|
||||||
|
tenantId: string;
|
||||||
|
orgName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: AzureDevOpsConnectionMethod.ApiToken;
|
||||||
|
credentials: {
|
||||||
|
apiKey: string;
|
||||||
|
orgName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -5,6 +5,7 @@ import { TAuth0Connection } from "./auth0-connection";
|
|||||||
import { TAwsConnection } from "./aws-connection";
|
import { TAwsConnection } from "./aws-connection";
|
||||||
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
|
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
|
||||||
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
|
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
|
||||||
|
import { TAzureDevOpsConnection } from "./azure-devops-connection";
|
||||||
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
||||||
import { TCamundaConnection } from "./camunda-connection";
|
import { TCamundaConnection } from "./camunda-connection";
|
||||||
import { TDatabricksConnection } from "./databricks-connection";
|
import { TDatabricksConnection } from "./databricks-connection";
|
||||||
@@ -28,6 +29,7 @@ export * from "./auth0-connection";
|
|||||||
export * from "./aws-connection";
|
export * from "./aws-connection";
|
||||||
export * from "./azure-app-configuration-connection";
|
export * from "./azure-app-configuration-connection";
|
||||||
export * from "./azure-client-secrets-connection";
|
export * from "./azure-client-secrets-connection";
|
||||||
|
export * from "./azure-devops-connection";
|
||||||
export * from "./azure-key-vault-connection";
|
export * from "./azure-key-vault-connection";
|
||||||
export * from "./camunda-connection";
|
export * from "./camunda-connection";
|
||||||
export * from "./databricks-connection";
|
export * from "./databricks-connection";
|
||||||
@@ -54,6 +56,7 @@ export type TAppConnection =
|
|||||||
| TAzureKeyVaultConnection
|
| TAzureKeyVaultConnection
|
||||||
| TAzureAppConfigurationConnection
|
| TAzureAppConfigurationConnection
|
||||||
| TAzureClientSecretsConnection
|
| TAzureClientSecretsConnection
|
||||||
|
| TAzureDevOpsConnection
|
||||||
| TDatabricksConnection
|
| TDatabricksConnection
|
||||||
| THumanitecConnection
|
| THumanitecConnection
|
||||||
| TTerraformCloudConnection
|
| TTerraformCloudConnection
|
||||||
@@ -103,6 +106,7 @@ export type TAppConnectionMap = {
|
|||||||
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
|
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
|
||||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
|
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
|
||||||
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
|
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
|
||||||
|
[AppConnection.AzureDevOps]: TAzureDevOpsConnection;
|
||||||
[AppConnection.Databricks]: TDatabricksConnection;
|
[AppConnection.Databricks]: TDatabricksConnection;
|
||||||
[AppConnection.Humanitec]: THumanitecConnection;
|
[AppConnection.Humanitec]: THumanitecConnection;
|
||||||
[AppConnection.TerraformCloud]: TTerraformCloudConnection;
|
[AppConnection.TerraformCloud]: TTerraformCloudConnection;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum SecretSync {
|
|||||||
GCPSecretManager = "gcp-secret-manager",
|
GCPSecretManager = "gcp-secret-manager",
|
||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
|
AzureDevOps = "azure-devops",
|
||||||
Databricks = "databricks",
|
Databricks = "databricks",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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 TAzureDevOpsSync = TRootSecretSync & {
|
||||||
|
destination: SecretSync.AzureDevOps;
|
||||||
|
destinationConfig: {
|
||||||
|
devopsProjectId: string;
|
||||||
|
devopsProjectName: string;
|
||||||
|
};
|
||||||
|
connection: {
|
||||||
|
app: AppConnection.AzureDevOps;
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { TOnePassSync } from "./1password-sync";
|
|||||||
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
|
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
|
||||||
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
|
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
|
||||||
import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
|
import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
|
||||||
|
import { TAzureDevOpsSync } from "./azure-devops-sync";
|
||||||
import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
|
import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
|
||||||
import { TCamundaSync } from "./camunda-sync";
|
import { TCamundaSync } from "./camunda-sync";
|
||||||
import { TDatabricksSync } from "./databricks-sync";
|
import { TDatabricksSync } from "./databricks-sync";
|
||||||
@@ -32,6 +33,7 @@ export type TSecretSync =
|
|||||||
| TGcpSync
|
| TGcpSync
|
||||||
| TAzureKeyVaultSync
|
| TAzureKeyVaultSync
|
||||||
| TAzureAppConfigurationSync
|
| TAzureAppConfigurationSync
|
||||||
|
| TAzureDevOpsSync
|
||||||
| TDatabricksSync
|
| TDatabricksSync
|
||||||
| THumanitecSync
|
| THumanitecSync
|
||||||
| TTerraformCloudSync
|
| TTerraformCloudSync
|
||||||
|
|||||||
+5
@@ -14,6 +14,7 @@ import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
|
|||||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||||
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
|
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
|
||||||
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
||||||
|
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
|
||||||
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||||
@@ -99,6 +100,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
|||||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm />;
|
return <AzureClientSecretsConnectionForm />;
|
||||||
|
case AppConnection.AzureDevOps:
|
||||||
|
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
return <WindmillConnectionForm onSubmit={onSubmit} />;
|
return <WindmillConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Auth0:
|
case AppConnection.Auth0:
|
||||||
@@ -179,6 +182,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
||||||
|
case AppConnection.AzureDevOps:
|
||||||
|
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Auth0:
|
case AppConnection.Auth0:
|
||||||
|
|||||||
+291
@@ -0,0 +1,291 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
import {
|
||||||
|
AzureDevOpsConnectionMethod,
|
||||||
|
TAzureDevOpsConnection,
|
||||||
|
useGetAppConnectionOption
|
||||||
|
} from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
genericAppConnectionFieldsSchema,
|
||||||
|
GenericAppConnectionsFields
|
||||||
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
|
type ApiTokenForm = z.infer<typeof apiTokenSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appConnection?: TAzureDevOpsConnection;
|
||||||
|
onSubmit: (formData: ApiTokenForm) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base schema with common fields
|
||||||
|
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
|
app: z.literal(AppConnection.AzureDevOps),
|
||||||
|
method: z.nativeEnum(AzureDevOpsConnectionMethod)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method-specific schemas
|
||||||
|
const oauthSchema = baseSchema.extend({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
|
||||||
|
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||||
|
orgName: z.string().trim().min(1, "Organization name is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiTokenSchema = baseSchema.extend({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.ApiToken),
|
||||||
|
credentials: z.object({
|
||||||
|
apiKey: z.string().trim().min(1, "API Key is required"),
|
||||||
|
orgName: z.string().trim().min(1, "Organization name is required")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Union schema
|
||||||
|
const formSchema = z.discriminatedUnion("method", [oauthSchema, apiTokenSchema]);
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormData> => {
|
||||||
|
if (!appConnection) {
|
||||||
|
return {
|
||||||
|
app: AppConnection.AzureDevOps,
|
||||||
|
method: AzureDevOpsConnectionMethod.OAuth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
name: appConnection.name,
|
||||||
|
description: appConnection.description,
|
||||||
|
app: appConnection.app,
|
||||||
|
method: appConnection.method
|
||||||
|
};
|
||||||
|
const { credentials } = appConnection;
|
||||||
|
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
if ("tenantId" in credentials && "orgName" in credentials) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
method: AzureDevOpsConnectionMethod.OAuth,
|
||||||
|
tenantId: credentials.tenantId,
|
||||||
|
orgName: credentials.orgName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
|
if ("apiKey" in credentials && "orgName" in credentials) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
method: AzureDevOpsConnectionMethod.ApiToken,
|
||||||
|
credentials: {
|
||||||
|
apiKey: credentials.apiKey,
|
||||||
|
orgName: credentials.orgName
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||||
|
const isUpdate = Boolean(appConnection);
|
||||||
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
option: { oauthClientId },
|
||||||
|
isLoading
|
||||||
|
} = useGetAppConnectionOption(AppConnection.AzureDevOps);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: getDefaultValues(appConnection)
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
|
const onSubmitHandler = async (formData: FormData) => {
|
||||||
|
switch (formData.method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
setIsRedirecting(true);
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorage.setItem(
|
||||||
|
"azureDevOpsConnectionFormData",
|
||||||
|
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||||
|
);
|
||||||
|
|
||||||
|
window.location.assign(
|
||||||
|
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-devops`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AzureDevOpsConnectionMethod.ApiToken:
|
||||||
|
onSubmit(formData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMissingConfig = selectedMethod === AzureDevOpsConnectionMethod.OAuth && !oauthClientId;
|
||||||
|
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmitHandler)} className="space-y-6">
|
||||||
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="method"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText={`The method you would like to use to connect with ${
|
||||||
|
APP_CONNECTION_MAP[AppConnection.AzureDevOps].name
|
||||||
|
}. This field cannot be changed after creation.`}
|
||||||
|
errorText={
|
||||||
|
!isLoading && isMissingConfig
|
||||||
|
? `Environment variables have not been configured. ${
|
||||||
|
isInfisicalCloud()
|
||||||
|
? "Please contact Infisical."
|
||||||
|
: `See documentation to configure Azure ${methodDetails.name} Connections.`
|
||||||
|
}`
|
||||||
|
: error?.message
|
||||||
|
}
|
||||||
|
isError={Boolean(error?.message) || isMissingConfig}
|
||||||
|
label="Method"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={isUpdate}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
{Object.values(AzureDevOpsConnectionMethod).map((method) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={method} key={method}>
|
||||||
|
{getAppConnectionMethodDetails(method).name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* OAuth-specific fields */}
|
||||||
|
{selectedMethod === AzureDevOpsConnectionMethod.OAuth && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="tenantId"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="The Directory (tenant) ID."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Tenant ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="orgName"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Your Azure DevOps organization name."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Organization Name"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="myorganization" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Token-specific fields */}
|
||||||
|
{selectedMethod === AzureDevOpsConnectionMethod.ApiToken && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="credentials.apiKey"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Personal Access Token from Azure DevOps."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="API Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your Personal Access Token"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="credentials.orgName"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Your Azure DevOps organization name."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Organization Name"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="myorganization" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting || isRedirecting}
|
||||||
|
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Reconnect to Azure" : "Connect to Azure"}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+76
-1
@@ -8,10 +8,12 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
|||||||
import {
|
import {
|
||||||
AzureAppConfigurationConnectionMethod,
|
AzureAppConfigurationConnectionMethod,
|
||||||
AzureClientSecretsConnectionMethod,
|
AzureClientSecretsConnectionMethod,
|
||||||
|
AzureDevOpsConnectionMethod,
|
||||||
AzureKeyVaultConnectionMethod,
|
AzureKeyVaultConnectionMethod,
|
||||||
GitHubConnectionMethod,
|
GitHubConnectionMethod,
|
||||||
TAzureAppConfigurationConnection,
|
TAzureAppConfigurationConnection,
|
||||||
TAzureClientSecretsConnection,
|
TAzureClientSecretsConnection,
|
||||||
|
TAzureDevOpsConnection,
|
||||||
TAzureKeyVaultConnection,
|
TAzureKeyVaultConnection,
|
||||||
TGitHubConnection,
|
TGitHubConnection,
|
||||||
TGitHubRadarConnection,
|
TGitHubRadarConnection,
|
||||||
@@ -42,6 +44,19 @@ type AzureClientSecretsFormData = BaseFormData &
|
|||||||
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
|
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
|
||||||
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
|
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
|
||||||
|
|
||||||
|
type OAuthCredentials = Extract<
|
||||||
|
TAzureDevOpsConnection,
|
||||||
|
{ method: AzureDevOpsConnectionMethod.OAuth }
|
||||||
|
>["credentials"];
|
||||||
|
type ApiTokenCredentials = Extract<
|
||||||
|
TAzureDevOpsConnection,
|
||||||
|
{ method: AzureDevOpsConnectionMethod.ApiToken }
|
||||||
|
>["credentials"];
|
||||||
|
|
||||||
|
type AzureDevOpsFormData = BaseFormData &
|
||||||
|
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
|
||||||
|
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<ApiTokenCredentials, "orgName">);
|
||||||
|
|
||||||
type FormDataMap = {
|
type FormDataMap = {
|
||||||
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
||||||
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
|
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
|
||||||
@@ -52,6 +67,9 @@ type FormDataMap = {
|
|||||||
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
|
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
|
||||||
app: AppConnection.AzureClientSecrets;
|
app: AppConnection.AzureClientSecrets;
|
||||||
};
|
};
|
||||||
|
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
|
||||||
|
app: AppConnection.AzureDevOps;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
||||||
@@ -59,7 +77,8 @@ const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
|||||||
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
|
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
|
||||||
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
||||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
||||||
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData"
|
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
|
||||||
|
[AppConnection.AzureDevOps]: "azureDevOpsConnectionFormData"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OAuthCallbackPage = () => {
|
export const OAuthCallbackPage = () => {
|
||||||
@@ -258,6 +277,60 @@ export const OAuthCallbackPage = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleAzureDevOps = useCallback(async () => {
|
||||||
|
const formData = getFormData(AppConnection.AzureDevOps);
|
||||||
|
if (formData === null) return null;
|
||||||
|
|
||||||
|
clearState(AppConnection.AzureDevOps);
|
||||||
|
|
||||||
|
const { connectionId, name, description, returnUrl } = formData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!("tenantId" in formData)) {
|
||||||
|
throw new Error("Expected OAuth form data but got API token data");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
await updateAppConnection.mutateAsync({
|
||||||
|
app: AppConnection.AzureDevOps,
|
||||||
|
connectionId,
|
||||||
|
credentials: {
|
||||||
|
code: code as string,
|
||||||
|
tenantId: formData.tenantId as string,
|
||||||
|
orgName: formData.orgName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createAppConnection.mutateAsync({
|
||||||
|
app: AppConnection.AzureDevOps,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
method: AzureDevOpsConnectionMethod.OAuth,
|
||||||
|
credentials: {
|
||||||
|
code: code as string,
|
||||||
|
tenantId: formData.tenantId as string,
|
||||||
|
orgName: formData.orgName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
createNotification({
|
||||||
|
title: `Failed to ${connectionId ? "update" : "add"} Azure DevOps Connection`,
|
||||||
|
text: err?.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: returnUrl ?? "/organization/app-connections"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionId,
|
||||||
|
returnUrl,
|
||||||
|
appConnectionName: formData.app
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleGithub = useCallback(async () => {
|
const handleGithub = useCallback(async () => {
|
||||||
const formData = getFormData(AppConnection.GitHub);
|
const formData = getFormData(AppConnection.GitHub);
|
||||||
if (formData === null) return null;
|
if (formData === null) return null;
|
||||||
@@ -396,6 +469,8 @@ export const OAuthCallbackPage = () => {
|
|||||||
data = await handleAzureAppConfiguration();
|
data = await handleAzureAppConfiguration();
|
||||||
} else if (appConnection === AppConnection.AzureClientSecrets) {
|
} else if (appConnection === AppConnection.AzureClientSecrets) {
|
||||||
data = await handleAzureClientSecrets();
|
data = await handleAzureClientSecrets();
|
||||||
|
} else if (appConnection === AppConnection.AzureDevOps) {
|
||||||
|
data = await handleAzureDevOps();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { TAzureDevOpsSync } from "@app/hooks/api/secretSyncs/types/azure-devops-sync";
|
||||||
|
|
||||||
|
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||||
|
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretSync: TAzureDevOpsSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncDestinationCol = ({ secretSync }: Props) => {
|
||||||
|
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||||
|
|
||||||
|
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||||
|
};
|
||||||
+3
@@ -4,6 +4,7 @@ import { OnePassSyncDestinationCol } from "./1PasswordSyncDestinationCol";
|
|||||||
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
||||||
import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol";
|
import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol";
|
||||||
import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol";
|
import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol";
|
||||||
|
import { AzureDevOpsSyncDestinationCol } from "./AzureDevopsDestinationCol";
|
||||||
import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol";
|
import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol";
|
||||||
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
||||||
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
||||||
@@ -55,6 +56,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
|||||||
return <OCIVaultSyncDestinationCol secretSync={secretSync} />;
|
return <OCIVaultSyncDestinationCol secretSync={secretSync} />;
|
||||||
case SecretSync.OnePass:
|
case SecretSync.OnePass:
|
||||||
return <OnePassSyncDestinationCol secretSync={secretSync} />;
|
return <OnePassSyncDestinationCol secretSync={secretSync} />;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return <AzureDevOpsSyncDestinationCol secretSync={secretSync} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||||
|
|||||||
+3
@@ -110,6 +110,9 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
|||||||
primaryText = destinationConfig.vaultId;
|
primaryText = destinationConfig.vaultId;
|
||||||
secondaryText = "Vault ID";
|
secondaryText = "Vault ID";
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
primaryText = destinationConfig.devopsProjectName;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
|
import { TAzureDevOpsSync } from "@app/hooks/api/secretSyncs/types/azure-devops-sync";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretSync: TAzureDevOpsSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncDestinationSection = ({ secretSync }: Props) => {
|
||||||
|
const {
|
||||||
|
destinationConfig: { devopsProjectName }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
return <GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>;
|
||||||
|
};
|
||||||
+4
@@ -14,6 +14,7 @@ import { OnePassSyncDestinationSection } from "./1PasswordSyncDestinationSection
|
|||||||
import { AwsParameterStoreSyncDestinationSection } from "./AwsParameterStoreSyncDestinationSection";
|
import { AwsParameterStoreSyncDestinationSection } from "./AwsParameterStoreSyncDestinationSection";
|
||||||
import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSyncDestinationSection";
|
import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSyncDestinationSection";
|
||||||
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
|
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
|
||||||
|
import { AzureDevOpsSyncDestinationSection } from "./AzureDevopsSyncDestinationSection";
|
||||||
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
|
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
|
||||||
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
||||||
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
||||||
@@ -89,6 +90,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
|||||||
case SecretSync.OnePass:
|
case SecretSync.OnePass:
|
||||||
DestinationComponents = <OnePassSyncDestinationSection secretSync={secretSync} />;
|
DestinationComponents = <OnePassSyncDestinationSection secretSync={secretSync} />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
DestinationComponents = <AzureDevOpsSyncDestinationSection secretSync={secretSync} />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -41,6 +41,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
|||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
case SecretSync.TerraformCloud:
|
case SecretSync.TerraformCloud:
|
||||||
|
|||||||
Reference in New Issue
Block a user