From 17bbdbe7bb08a9b467974cbbb151b0302c736072 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 11 Jun 2025 20:06:45 -0300 Subject: [PATCH] feat(secret-sync): Add Azure Devops PR suggestions --- .../app-connection/app-connection-fns.ts | 2 +- .../azure-devops/azure-devops-enums.ts | 2 +- .../azure-devops/azure-devops-fns.ts | 26 +++++++++---------- .../azure-devops/azure-devops-schemas.ts | 10 +++---- .../azure-devops/azure-devops-service.ts | 6 ++--- .../azure-devops/azure-devops-sync-fns.ts | 10 +++---- .../app-connections/azure-devops.mdx | 16 ++++++------ .../secret-syncs/azure-devops.mdx | 26 +++++++++---------- frontend/src/helpers/appConnections.ts | 2 +- .../types/azure-devops-connection.ts | 6 ++--- .../AzureDevOpsConnectionForm.tsx | 20 +++++++------- .../OauthCallbackPage/OauthCallbackPage.tsx | 2 +- 12 files changed, 64 insertions(+), 64 deletions(-) diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 8cd6bb2bd..1aba18560 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -225,7 +225,6 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case TerraformCloudConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: - case AzureDevOpsConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -234,6 +233,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case WindmillConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: + case AzureDevOpsConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-enums.ts b/backend/src/services/app-connection/azure-devops/azure-devops-enums.ts index c5965943e..f724a0b4a 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-enums.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-enums.ts @@ -1,4 +1,4 @@ export enum AzureDevOpsConnectionMethod { OAuth = "oauth", - ApiToken = "api-token" + AccessToken = "access-token" } diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts index 1b9131b22..b5aa1f9af 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts @@ -30,13 +30,13 @@ export const getAzureDevopsConnectionListItem = () => { app: AppConnection.AzureDevOps as const, methods: Object.values(AzureDevOpsConnectionMethod) as [ AzureDevOpsConnectionMethod.OAuth, - AzureDevOpsConnectionMethod.ApiToken + AzureDevOpsConnectionMethod.AccessToken ], oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; -export const getAzureDevopsConnectionAccessToken = async ( +export const getAzureDevopsConnection = async ( connectionId: string, appConnectionDAL: Pick, kmsService: Pick @@ -104,12 +104,12 @@ export const getAzureDevopsConnectionAccessToken = async ( return data.access_token; - case AzureDevOpsConnectionMethod.ApiToken: - if (!("apiKey" in credentials)) { + case AzureDevOpsConnectionMethod.AccessToken: + if (!("accessToken" in credentials)) { throw new BadRequestError({ message: "Invalid API token credentials" }); } // For API token, return the basic auth token directly - return credentials.apiKey as string; + return credentials.accessToken; default: throw new BadRequestError({ message: `Unsupported connection method` }); @@ -188,17 +188,17 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev expiresAt: Date.now() + tokenResp.data.expires_in * 1000 }; - case AzureDevOpsConnectionMethod.ApiToken: - const apiTokenCredentials = inputCredentials as { apiKey: string; orgName?: string }; + case AzureDevOpsConnectionMethod.AccessToken: + const apiTokenCredentials = inputCredentials as { accessToken: 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`, + `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(apiTokenCredentials.orgName)}/_apis/projects?api-version=7.2-preview.2&$top=1`, { headers: { - Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.apiKey}`).toString("base64")}` + Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.accessToken}`).toString("base64")}` } } ); @@ -210,7 +210,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev } return { - apiKey: apiTokenCredentials.apiKey, + accessToken: apiTokenCredentials.accessToken, orgName: apiTokenCredentials.orgName }; } @@ -219,7 +219,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev `https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1`, { headers: { - Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.apiKey}`).toString("base64")}` + Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.accessToken}`).toString("base64")}` } } ); @@ -230,7 +230,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev 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")}` + Authorization: `Basic ${Buffer.from(`:${apiTokenCredentials.accessToken}`).toString("base64")}` } }); organizations = orgsResponse.data.value || []; @@ -239,7 +239,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev } return { - apiKey: apiTokenCredentials.apiKey, + accessToken: apiTokenCredentials.accessToken, userDisplayName: profileResponse.data.displayName, organizations: organizations.map((org) => ({ accountId: org.accountId, diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-schemas.ts b/backend/src/services/app-connection/azure-devops/azure-devops-schemas.ts index 60d2f6922..9d3735fcd 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-schemas.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-schemas.ts @@ -29,12 +29,12 @@ export const AzureDevOpsConnectionOAuthOutputCredentialsSchema = z.object({ }); export const AzureDevOpsConnectionApiTokenInputCredentialsSchema = z.object({ - apiKey: z.string().trim().min(1, "API Key required"), + accessToken: z.string().trim().min(1, "Access Token required"), orgName: z.string().trim().min(1, "Organization name required") }); export const AzureDevOpsConnectionApiTokenOutputCredentialsSchema = z.object({ - apiKey: z.string(), + accessToken: z.string(), orgName: z.string() }); @@ -49,7 +49,7 @@ export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUni }), z.object({ method: z - .literal(AzureDevOpsConnectionMethod.ApiToken) + .literal(AzureDevOpsConnectionMethod.AccessToken) .describe(AppConnections.CREATE(AppConnection.AzureDevOps).method), credentials: AzureDevOpsConnectionApiTokenInputCredentialsSchema.describe( AppConnections.CREATE(AppConnection.AzureDevOps).credentials @@ -82,7 +82,7 @@ export const AzureDevOpsConnectionSchema = z.intersection( credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema }), z.object({ - method: z.literal(AzureDevOpsConnectionMethod.ApiToken), + method: z.literal(AzureDevOpsConnectionMethod.AccessToken), credentials: AzureDevOpsConnectionApiTokenOutputCredentialsSchema }) ]) @@ -97,7 +97,7 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method }) }), BaseAzureDevOpsConnectionSchema.extend({ - method: z.literal(AzureDevOpsConnectionMethod.ApiToken), + method: z.literal(AzureDevOpsConnectionMethod.AccessToken), credentials: AzureDevOpsConnectionApiTokenOutputCredentialsSchema.pick({ orgName: true }) diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-service.ts b/backend/src/services/app-connection/azure-devops/azure-devops-service.ts index a79a3e7ea..2048bfeac 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-service.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-service.ts @@ -11,7 +11,7 @@ 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 { getAzureDevopsConnection } from "./azure-devops-fns"; import { TAzureDevOpsConnection } from "./azure-devops-types"; type TGetAppConnectionFunc = ( @@ -45,7 +45,7 @@ const getAuthHeaders = (appConnection: TAzureDevOpsConnection, accessToken: stri Authorization: `Bearer ${accessToken}`, Accept: "application/json" }; - case AzureDevOpsConnectionMethod.ApiToken: + case AzureDevOpsConnectionMethod.AccessToken: // For API token, create Basic auth header const basicAuthToken = Buffer.from(`user:${accessToken}`).toString("base64"); return { @@ -62,7 +62,7 @@ const listAzureDevOpsProjects = async ( appConnectionDAL: Pick, kmsService: Pick ): Promise => { - const accessToken = await getAzureDevopsConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService); + const accessToken = await getAzureDevopsConnection(appConnection.id, appConnectionDAL, kmsService); // Both OAuth and API Token methods use organization name from credentials const credentials = appConnection.credentials as { orgName: string }; diff --git a/backend/src/services/secret-sync/azure-devops/azure-devops-sync-fns.ts b/backend/src/services/secret-sync/azure-devops/azure-devops-sync-fns.ts index 3a2921fbd..f849ba501 100644 --- a/backend/src/services/secret-sync/azure-devops/azure-devops-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-devops/azure-devops-sync-fns.ts @@ -2,7 +2,7 @@ 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 { getAzureDevopsConnection } 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"; @@ -44,7 +44,7 @@ export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureD }); } - const accessToken = await getAzureDevopsConnectionAccessToken( + const accessToken = await getAzureDevopsConnection( secretSync.connectionId, appConnectionDAL, kmsService @@ -115,7 +115,7 @@ export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureD 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`; + const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups?api-version=7.1`; await request.post( url, @@ -143,7 +143,7 @@ export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureD } ); } else { - const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`; + const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`; await request.put( url, @@ -186,7 +186,7 @@ export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureD 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`; + const deleteUrl = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups/${groupId}?projectIds=${secretSync.destinationConfig.devopsProjectId}&api-version=7.1`; await request.delete(deleteUrl, { headers: { diff --git a/docs/integrations/app-connections/azure-devops.mdx b/docs/integrations/app-connections/azure-devops.mdx index 1eaa8e82b..8fcc25427 100644 --- a/docs/integrations/app-connections/azure-devops.mdx +++ b/docs/integrations/app-connections/azure-devops.mdx @@ -3,10 +3,10 @@ title: "Azure DevOps Connection" description: "Learn how to configure an Azure DevOps Connection for Infisical." --- -Infisical currently supports two methods for connecting to Azure DevOps, which are OAuth and Azure DevOps API Token. +Infisical currently supports two methods for connecting to Azure DevOps, which are OAuth and Azure DevOps Personal Access Token. - - Using the Azure DevOps OAuth connection on a self-hosted instance of Infisical requires configuring an application in Azure + + Using the Azure DevOps OAuth connection on a self-hosted instance of Infisical requires configuring an application in Azure and registering your instance with it. **Prerequisites:** @@ -67,7 +67,7 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a #### Create a new Azure DevOps personal access token (PAT) - You'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps. + When using the Azure DevOps Access Token connection you'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps. ![integrations](../../images/integrations/azure-devops/overview-page.png) @@ -100,10 +100,10 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a - + - Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step. Also fill in the organization name of the Azure DevOps organization you want to connect to. + Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous [step](#azure-oauth-on-a-self-hosted-instance). Also fill in the organization name of the Azure DevOps organization you want to connect to. ![Fill in Connection Details](/images/app-connections/azure/devops/fill-in-connection-details-oauth.png) @@ -117,10 +117,10 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a - + - Fill in the **API Key** field with the API key you obtained in the previous step. And the organization name of the Azure DevOps organization you want to connect to. + Fill in the **Access Token** field with the Access Token you obtained in the previous step. And the organization name of the Azure DevOps organization you want to connect to. ![Fill in Connection Details](/images/app-connections/azure/devops/fill-in-connection-details-token.png) diff --git a/docs/integrations/secret-syncs/azure-devops.mdx b/docs/integrations/secret-syncs/azure-devops.mdx index 751c0040f..2f99fe128 100644 --- a/docs/integrations/secret-syncs/azure-devops.mdx +++ b/docs/integrations/secret-syncs/azure-devops.mdx @@ -113,28 +113,28 @@ description: "Learn how to configure a Azure DevOps Sync for Infisical." "lastRemoveMessage": null, "lastRemovedAt": null, "syncOptions": { - "initialSyncBehavior": "overwrite-destination", - "keySchema": "PIPELINE_${secretKey}", - "disableSecretDeletion": true + "initialSyncBehavior": "overwrite-destination", + "keySchema": "PIPELINE_${secretKey}", + "disableSecretDeletion": true }, "connection": { - "app": "azure-devops", - "name": "Production DevOps Organization", - "id": "8b92f5cc-3g77-5e80-6666-6ff57069385d" + "app": "azure-devops", + "name": "Production DevOps Organization", + "id": "8b92f5cc-3g77-5e80-6666-6ff57069385d" }, "environment": { - "slug": "production", - "name": "Production Environment", - "id": "4f16j9gg-7k11-9i23-2222-2jj91403729h" + "slug": "production", + "name": "Production Environment", + "id": "4f16j9gg-7k11-9i23-2222-2jj91403729h" }, "folder": { - "id": "5a71e8dd-2f66-4d70-7777-7cc46958274c", - "path": "/devops/pipeline-secrets" + "id": "5a71e8dd-2f66-4d70-7777-7cc46958274c", + "path": "/devops/pipeline-secrets" }, "destination": "azure-devops", "destinationConfig": { - "devopsProjectId": "12345678-90ab-cdef-1234-567890abcdef", - "devopsProjectName": "example-project" + "devopsProjectId": "12345678-90ab-cdef-1234-567890abcdef", + "devopsProjectName": "example-project" } } } diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 320c44bd6..0fca6aa95 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -105,7 +105,6 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case TerraformCloudConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: - case AzureDevOpsConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -113,6 +112,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Username & Password", icon: faLock }; case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: + case AzureDevOpsConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: diff --git a/frontend/src/hooks/api/appConnections/types/azure-devops-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-devops-connection.ts index 1c6da31f8..ef30936e6 100644 --- a/frontend/src/hooks/api/appConnections/types/azure-devops-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/azure-devops-connection.ts @@ -3,7 +3,7 @@ import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-con export enum AzureDevOpsConnectionMethod { OAuth = "oauth", - ApiToken = "api-token" + AccessToken = "access-token" } export type TAzureDevOpsConnection = TRootAppConnection & { @@ -18,9 +18,9 @@ export type TAzureDevOpsConnection = TRootAppConnection & { }; } | { - method: AzureDevOpsConnectionMethod.ApiToken; + method: AzureDevOpsConnectionMethod.AccessToken; credentials: { - apiKey: string; + accessToken: string; orgName: string; }; } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx index 6c5358f87..2bab181ba 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureDevOpsConnectionForm.tsx @@ -42,9 +42,9 @@ const oauthSchema = baseSchema.extend({ }); const apiTokenSchema = baseSchema.extend({ - method: z.literal(AzureDevOpsConnectionMethod.ApiToken), + method: z.literal(AzureDevOpsConnectionMethod.AccessToken), credentials: z.object({ - apiKey: z.string().trim().min(1, "API Key is required"), + accessToken: z.string().trim().min(1, "Access Token is required"), orgName: z.string().trim().min(1, "Organization name is required") }) }); @@ -81,13 +81,13 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial ); break; - case AzureDevOpsConnectionMethod.ApiToken: + case AzureDevOpsConnectionMethod.AccessToken: onSubmit(formData); break; @@ -231,16 +231,16 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => )} {/* API Token-specific fields */} - {selectedMethod === AzureDevOpsConnectionMethod.ApiToken && ( + {selectedMethod === AzureDevOpsConnectionMethod.AccessToken && ( <> ( ["credentials"]; type ApiTokenCredentials = Extract< TAzureDevOpsConnection, - { method: AzureDevOpsConnectionMethod.ApiToken } + { method: AzureDevOpsConnectionMethod.AccessToken } >["credentials"]; type AzureDevOpsFormData = BaseFormData &