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
@@ -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";
@@ -1,4 +1,4 @@
export enum AzureDevOpsConnectionMethod {
OAuth = "oauth",
ApiToken = "api-token"
AccessToken = "access-token"
}
@@ -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,
@@ -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
})
@@ -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 };
@@ -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: {