mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
Merge pull request #3736 from Infisical/feat/azureDevopsSecretSync
Feat/azure devops secret sync
This commit is contained in:
@@ -7,6 +7,7 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureClientSecrets = "azure-client-secrets",
|
||||
AzureDevOps = "azure-devops",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Vercel = "vercel",
|
||||
|
||||
@@ -39,6 +39,11 @@ import {
|
||||
getAzureClientSecretsConnectionListItem,
|
||||
validateAzureClientSecretsConnectionCredentials
|
||||
} from "./azure-client-secrets";
|
||||
import { AzureDevOpsConnectionMethod } from "./azure-devops/azure-devops-enums";
|
||||
import {
|
||||
getAzureDevopsConnectionListItem,
|
||||
validateAzureDevOpsConnectionCredentials
|
||||
} from "./azure-devops/azure-devops-fns";
|
||||
import {
|
||||
AzureKeyVaultConnectionMethod,
|
||||
getAzureKeyVaultConnectionListItem,
|
||||
@@ -98,6 +103,7 @@ export const listAppConnectionOptions = () => {
|
||||
getGcpConnectionListItem(),
|
||||
getAzureKeyVaultConnectionListItem(),
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getAzureDevopsConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getTerraformCloudConnectionListItem(),
|
||||
@@ -173,6 +179,7 @@ export const validateAppConnectionCredentials = async (
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureClientSecrets]:
|
||||
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
@@ -201,6 +208,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
case AzureDevOpsConnectionMethod.OAuth:
|
||||
return "OAuth";
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
case OCIConnectionMethod.AccessKey:
|
||||
@@ -225,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";
|
||||
@@ -270,6 +279,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: 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.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||
[AppConnection.AzureDevOps]: "Azure DevOps",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||
@@ -33,6 +34,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
||||
|
||||
@@ -41,6 +41,8 @@ import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||
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 { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
@@ -84,6 +86,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||
@@ -498,6 +501,7 @@ export const appConnectionServiceFactory = ({
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
|
||||
@@ -39,6 +39,12 @@ import {
|
||||
TAzureClientSecretsConnectionInput,
|
||||
TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||
} from "./azure-client-secrets";
|
||||
import {
|
||||
TAzureDevOpsConnection,
|
||||
TAzureDevOpsConnectionConfig,
|
||||
TAzureDevOpsConnectionInput,
|
||||
TValidateAzureDevOpsConnectionCredentialsSchema
|
||||
} from "./azure-devops/azure-devops-types";
|
||||
import {
|
||||
TAzureKeyVaultConnection,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
@@ -132,6 +138,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TAzureDevOpsConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
@@ -161,6 +168,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TGcpConnectionInput
|
||||
| TAzureKeyVaultConnectionInput
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TAzureDevOpsConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TTerraformCloudConnectionInput
|
||||
@@ -197,6 +205,7 @@ export type TAppConnectionConfig =
|
||||
| TGcpConnectionConfig
|
||||
| TAzureKeyVaultConnectionConfig
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TAzureDevOpsConnectionConfig
|
||||
| TAzureClientSecretsConnectionConfig
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
@@ -220,6 +229,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||
| TValidateAzureDevOpsConnectionCredentialsSchema
|
||||
| TValidateDatabricksConnectionCredentialsSchema
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum AzureDevOpsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-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.AccessToken
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||
};
|
||||
};
|
||||
|
||||
export const getAzureDevopsConnection = 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.AccessToken:
|
||||
if (!("accessToken" in credentials)) {
|
||||
throw new BadRequestError({ message: "Invalid API token credentials" });
|
||||
}
|
||||
// For access token, return the basic auth token directly
|
||||
return credentials.accessToken;
|
||||
|
||||
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.AccessToken:
|
||||
const accessTokenCredentials = inputCredentials as { accessToken: string; orgName?: string };
|
||||
|
||||
try {
|
||||
if (accessTokenCredentials.orgName) {
|
||||
// Validate against specific organization
|
||||
const response = await request.get(
|
||||
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(accessTokenCredentials.orgName)}/_apis/projects?api-version=7.2-preview.2&$top=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate connection: ${response.status}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: accessTokenCredentials.accessToken,
|
||||
orgName: accessTokenCredentials.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(`:${accessTokenCredentials.accessToken}`).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(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
|
||||
}
|
||||
});
|
||||
organizations = orgsResponse.data.value || [];
|
||||
} catch (orgError) {
|
||||
logger.warn(orgError, "Could not fetch organizations automatically:");
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: accessTokenCredentials.accessToken,
|
||||
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 = accessTokenCredentials.orgName
|
||||
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
`Failed to validate access token for organization '${accessTokenCredentials.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 AzureDevOpsConnectionAccessTokenInputCredentialsSchema = z.object({
|
||||
accessToken: z.string().trim().min(1, "Access Token required"),
|
||||
orgName: z.string().trim().min(1, "Organization name required")
|
||||
});
|
||||
|
||||
export const AzureDevOpsConnectionAccessTokenOutputCredentialsSchema = z.object({
|
||||
accessToken: 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.AccessToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||
credentials: AzureDevOpsConnectionAccessTokenInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAzureDevOpsConnectionSchema = ValidateAzureDevOpsConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureDevOps)
|
||||
);
|
||||
|
||||
export const UpdateAzureDevOpsConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([AzureDevOpsConnectionOAuthInputCredentialsSchema, AzureDevOpsConnectionAccessTokenInputCredentialsSchema])
|
||||
.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.AccessToken),
|
||||
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
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.AccessToken),
|
||||
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.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 { getAzureDevopsConnection } 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.AccessToken:
|
||||
// For access 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 getAzureDevopsConnection(appConnection.id, appConnectionDAL, kmsService);
|
||||
|
||||
// Both OAuth and access 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}/${encodeURIComponent(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,54 @@
|
||||
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[];
|
||||
}
|
||||
@@ -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,233 @@
|
||||
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 { 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";
|
||||
|
||||
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 getAzureDevopsConnection(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}/${encodeURIComponent(orgName)}/${encodeURIComponent(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}/${encodeURIComponent(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}/${encodeURIComponent(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}/${encodeURIComponent(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,
|
||||
@@ -182,6 +184,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,
|
||||
@@ -244,6 +251,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,
|
||||
@@ -315,6 +328,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