mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
feat(secret-sync): Add Azure Devops Secret Sync
This commit is contained in:
@@ -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",
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureDevOps = "azure-devops",
|
||||
Databricks = "databricks",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password";
|
||||
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 { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||
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.GCPSecretManager]: GCP_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.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||
@@ -152,6 +154,11 @@ export const SecretSyncFns = {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.AzureDevOps:
|
||||
return azureDevOpsSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Databricks:
|
||||
return databricksSyncFactory({
|
||||
appConnectionDAL,
|
||||
@@ -214,6 +221,12 @@ export const SecretSyncFns = {
|
||||
kmsService
|
||||
}).getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.AzureDevOps:
|
||||
secretMap = await azureDevOpsSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Databricks:
|
||||
return databricksSyncFactory({
|
||||
appConnectionDAL,
|
||||
@@ -283,6 +296,11 @@ export const SecretSyncFns = {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.AzureDevOps:
|
||||
return azureDevOpsSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).removeSecrets(secretSync);
|
||||
case SecretSync.Databricks:
|
||||
return databricksSyncFactory({
|
||||
appConnectionDAL,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
|
||||
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[SecretSync.AzureDevOps]: "Azure DevOps",
|
||||
[SecretSync.Databricks]: "Databricks",
|
||||
[SecretSync.Humanitec]: "Humanitec",
|
||||
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
||||
@@ -27,6 +28,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||
[SecretSync.AzureDevOps]: AppConnection.AzureDevOps,
|
||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||
@@ -46,6 +48,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.AzureDevOps]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Databricks]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Humanitec]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.TerraformCloud]: SecretSyncPlanType.Regular,
|
||||
|
||||
@@ -60,6 +60,12 @@ import {
|
||||
TAzureAppConfigurationSyncListItem,
|
||||
TAzureAppConfigurationSyncWithCredentials
|
||||
} from "./azure-app-configuration";
|
||||
import {
|
||||
TAzureDevOpsSync,
|
||||
TAzureDevOpsSyncInput,
|
||||
TAzureDevOpsSyncListItem,
|
||||
TAzureDevOpsSyncWithCredentials
|
||||
} from "./azure-devops";
|
||||
import {
|
||||
TAzureKeyVaultSync,
|
||||
TAzureKeyVaultSyncInput,
|
||||
@@ -100,6 +106,7 @@ export type TSecretSync =
|
||||
| TGcpSync
|
||||
| TAzureKeyVaultSync
|
||||
| TAzureAppConfigurationSync
|
||||
| TAzureDevOpsSync
|
||||
| TDatabricksSync
|
||||
| THumanitecSync
|
||||
| TTerraformCloudSync
|
||||
@@ -118,6 +125,7 @@ export type TSecretSyncWithCredentials =
|
||||
| TGcpSyncWithCredentials
|
||||
| TAzureKeyVaultSyncWithCredentials
|
||||
| TAzureAppConfigurationSyncWithCredentials
|
||||
| TAzureDevOpsSyncWithCredentials
|
||||
| TDatabricksSyncWithCredentials
|
||||
| THumanitecSyncWithCredentials
|
||||
| TTerraformCloudSyncWithCredentials
|
||||
@@ -136,6 +144,7 @@ export type TSecretSyncInput =
|
||||
| TGcpSyncInput
|
||||
| TAzureKeyVaultSyncInput
|
||||
| TAzureAppConfigurationSyncInput
|
||||
| TAzureDevOpsSyncInput
|
||||
| TDatabricksSyncInput
|
||||
| THumanitecSyncInput
|
||||
| TTerraformCloudSyncInput
|
||||
@@ -154,6 +163,7 @@ export type TSecretSyncListItem =
|
||||
| TGcpSyncListItem
|
||||
| TAzureKeyVaultSyncListItem
|
||||
| TAzureAppConfigurationSyncListItem
|
||||
| TAzureDevOpsSyncListItem
|
||||
| TDatabricksSyncListItem
|
||||
| THumanitecSyncListItem
|
||||
| TTerraformCloudSyncListItem
|
||||
|
||||
Reference in New Issue
Block a user