feat(secret-sync): Add Azure Devops PR suggestions

This commit is contained in:
carlosmonastyrski
2025-06-11 20:06:45 -03:00
parent 427de068d5
commit 17bbdbe7bb
12 changed files with 64 additions and 64 deletions

View File

@@ -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";

View File

@@ -1,4 +1,4 @@
export enum AzureDevOpsConnectionMethod {
OAuth = "oauth",
ApiToken = "api-token"
AccessToken = "access-token"
}

View File

@@ -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<TAppConnectionDALFactory, "findById" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
@@ -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,

View File

@@ -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
})

View File

@@ -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<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<TAzureDevOpsProject[]> => {
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 };

View File

@@ -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: {

View File

@@ -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.
<Accordion title="Self-Hosted Instance">
Using the Azure DevOps OAuth connection on a self-hosted instance of Infisical requires configuring an application in Azure
<Accordion title="Azure OAuth on a Self-Hosted Instance">
Using the Azure DevOps <b>OAuth connection</b> 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
<Accordion title="Azure DevOps personal access token (PAT)">
#### 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 <b>Access Token connection</b> you'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps.
<Steps>
<Step title="Navigate to 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
<Step title="Create Connection">
<Tabs>
<Tab title="Infisical UI">
<Tab title="OAuth">
<Steps>
<Step title="Fill in Connection Details">
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)
<Tip>
@@ -117,10 +117,10 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
</Step>
</Steps>
</Tab>
<Tab title="API">
<Tab title="Access Token">
<Steps>
<Step title="Fill in Connection Details">
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)
<Tip>

View File

@@ -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"
}
}
}

View File

@@ -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:

View File

@@ -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;
};
}

View File

@@ -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<FormD
};
}
break;
case AzureDevOpsConnectionMethod.ApiToken:
if ("apiKey" in credentials && "orgName" in credentials) {
case AzureDevOpsConnectionMethod.AccessToken:
if ("accessToken" in credentials && "orgName" in credentials) {
return {
...base,
method: AzureDevOpsConnectionMethod.ApiToken,
method: AzureDevOpsConnectionMethod.AccessToken,
credentials: {
apiKey: credentials.apiKey,
accessToken: credentials.accessToken,
orgName: credentials.orgName
}
};
@@ -139,7 +139,7 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
);
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 && (
<>
<Controller
name="credentials.apiKey"
name="credentials.accessToken"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Personal Access Token from Azure DevOps."
isError={Boolean(error?.message)}
label="API Key"
label="Access Token"
errorText={error?.message}
>
<Input

View File

@@ -50,7 +50,7 @@ type OAuthCredentials = Extract<
>["credentials"];
type ApiTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.ApiToken }
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
type AzureDevOpsFormData = BaseFormData &