From 326764dd410b4a9fb7312e481e9d6bf50b103e0e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 17:13:41 +0400 Subject: [PATCH 1/7] Feat: Azure DevOps Integration --- .../dynamic-secret/dynamic-secret-service.ts | 18 +++ .../src/ee/services/license/licence-fns.ts | 2 +- .../integration-auth/integration-app-list.ts | 26 +++ .../integration-auth-types.ts | 11 ++ .../integration-auth/integration-list.ts | 13 +- .../integration-sync-secret.ts | 138 ++++++++++++++-- frontend/public/data/frequentConstants.ts | 3 +- .../integrations/azure-devops/authorize.tsx | 76 +++++++++ .../integrations/azure-devops/create.tsx | 150 ++++++++++++++++++ .../IntegrationPage.utils.tsx | 3 + 10 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 frontend/src/pages/integrations/azure-devops/authorize.tsx create mode 100644 frontend/src/pages/integrations/azure-devops/create.tsx diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 1aef3cc86..e560c837d 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -98,6 +98,23 @@ export const dynamicSecretServiceFactory = ({ if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); + + console.log("YES THIS IS WHERE WE ARE"); + + console.log({ + type: provider.type, + version: 1, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, + maxTTL, + defaultTTL, + folderId: folder.id, + name + }); + const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, @@ -111,6 +128,7 @@ export const dynamicSecretServiceFactory = ({ folderId: folder.id, name }); + console.log("IT WORKED"); return dynamicSecretCfg; }; diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index bd40f75cb..b7dc3d694 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 9cb0d822c..4fd8264f5 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -1030,6 +1030,26 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => { return apps; }; +const getAppsAzureDevOps = async ({ accessToken, orgId }: { accessToken: string; orgId: string }) => { + console.log({ accessToken, orgId }); + const res = ( + await request.get<{ count: number; value: Record[] }>( + `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgId}/_apis/projects?api-version=7.2-preview.2`, + { + headers: { + Authorization: `Basic ${accessToken}` + } + } + ) + ).data; + const apps = res.value.map((a) => ({ + name: a.name, + appId: a.id + })); + + return apps; +}; + export const getApps = async ({ integration, accessToken, @@ -1184,6 +1204,12 @@ export const getApps = async ({ accessToken }); + case Integrations.AZURE_DEVOPS: + return getAppsAzureDevOps({ + accessToken, + orgId: teamId as string // small hack to pass orgId as teamId + }); + default: throw new BadRequestError({ message: "integration not found" }); } diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 5d1bfc18f..44b6d4c4c 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -1,3 +1,4 @@ +import { TIntegrations } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TGetIntegrationAuthDTO = { @@ -163,3 +164,13 @@ export type TTeamCityBuildConfig = { href: string; webUrl: string; }; + +export type TIntegrationsWithEnvironment = TIntegrations & { + environment?: + | { + id?: string | null | undefined; + name?: string | null | undefined; + } + | null + | undefined; +}; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index edc426327..46c76c5cf 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -31,7 +31,8 @@ export enum Integrations { CLOUD_66 = "cloud-66", NORTHFLANK = "northflank", HASURA_CLOUD = "hasura-cloud", - RUNDECK = "rundeck" + RUNDECK = "rundeck", + AZURE_DEVOPS = "azure-devops" } export enum IntegrationType { @@ -88,6 +89,7 @@ export enum IntegrationUrls { CLOUD_66_API_URL = "https://app.cloud66.com/api", NORTHFLANK_API_URL = "https://api.northflank.com", HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql", + AZURE_DEVOPS_API_URL = "https://dev.azure.com", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, @@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => { type: "pat", clientId: "", docsLink: "" + }, + { + name: "Azure Devops", + slug: "azure-devops", + image: "Microsoft Azure.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" } ]; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index b1f7d4cb8..ba22178d5 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -26,7 +26,7 @@ import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; import { z } from "zod"; -import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; +import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; @@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/ import { TIntegrationDALFactory } from "../integration/integration-dal"; import { IntegrationMetadataSchema } from "../integration/integration-schema"; +import { TIntegrationsWithEnvironment } from "./integration-auth-types"; import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, @@ -275,8 +276,8 @@ const syncSecretsAzureKeyVault = async ({ }; secrets: Record; accessToken: string; - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; }) => { interface GetAzureKeyVaultSecret { id: string; // secret URI @@ -966,8 +967,8 @@ const syncSecretsHeroku = async ({ secrets, accessToken }: { - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integration: TIntegrations & { projectId: string; environment: { @@ -2075,6 +2076,116 @@ const syncSecretsTravisCI = async ({ } }; +/** + * Sync/push [secrets] to GitLab repo with name [integration.app] + */ +const syncSecretsAzureDevops = async ({ + integrationAuth, + integration, + secrets, + accessToken +}: { + integrationAuth: TIntegrationAuths; + integration: TIntegrationsWithEnvironment; + secrets: Record; + accessToken: string; +}) => { + if (!integration.appId || !integration.app) { + throw new Error("Azure DevOps: orgId and projectId are required"); + } + if (!integration.environment || !integration.environment.name) { + throw new Error("Azure DevOps: environment is required"); + } + const headers = { + Authorization: `Basic ${accessToken}` + }; + const azureDevopsApiUrl = integrationAuth.url ? `${integrationAuth.url}` : IntegrationUrls.AZURE_DEVOPS_API_URL; + + const getEnvGroupId = async (orgId: string, project: string, env: string) => { + let groupId; + const url: string | null = + `${azureDevopsApiUrl}/${orgId}/${project}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`; + + const response = await request.get(url, { headers }); + for (const group of response.data.value) { + const groupName = group.name; + if (groupName === env) { + groupId = group.id; + return { groupId, groupName }; + } + } + return { groupId: "", groupName: "" }; + }; + + const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name); + + const variables: Record = {}; + for await (const key of Object.keys(secrets)) { + variables[key] = { value: secrets[key].value }; + } + + if (!groupId) { + // create new variable group if not present + const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`; + const config = { + method: "POST", + url, + data: { + name: integration.environment.name, + description: integration.environment.name, + type: "Vsts", + owner: "Library", + variables, + variableGroupProjectReferences: [ + { + name: integration.environment.name, + projectReference: { + name: integration.appId + } + } + ] + }, + headers: { + headers + } + }; + + const res = await request.post(url, config.data, config.headers); + if (res.status !== 200) { + throw new Error(`Azure DevOps: Failed to create variable group: ${res.statusText}`); + } + } else { + // sync variables for pre-existing variable group + const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.2-preview.2`; + const config = { + method: "PUT", + url, + data: { + name: groupName, + description: groupName, + type: "Vsts", + owner: "Library", + variables, + variableGroupProjectReferences: [ + { + name: groupName, + projectReference: { + name: integration.appId + } + } + ] + }, + headers: { + headers + } + }; + const res = await request.put(url, config.data, config.headers); + if (res.status !== 200) { + throw new Error(`Azure DevOps: Failed to update variable group: ${res.statusText}`); + } + } +}; + /** * Sync/push [secrets] to GitLab repo with name [integration.app] */ @@ -2527,8 +2638,8 @@ const syncSecretsTerraformCloud = async ({ accessToken, integrationDAL }: { - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integration: TIntegrations & { projectId: string; environment: { @@ -3675,8 +3786,8 @@ export const syncIntegrationSecrets = async ({ appendices, projectId }: { - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integrationDAL: Pick; integration: TIntegrations & { projectId: string; @@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({ updateManySecretsRawFn }); break; + + case Integrations.AZURE_DEVOPS: + await syncSecretsAzureDevops({ + integrationAuth, + integration, + secrets, + accessToken + }); + break; case Integrations.AWS_PARAMETER_STORE: response = await syncSecretsAWSParameterStore({ integration, diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index cf90ef659..1fd7e7789 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -33,7 +33,8 @@ const integrationSlugNameMapping: Mapping = { windmill: "Windmill", "gcp-secret-manager": "GCP Secret Manager", "hasura-cloud": "Hasura Cloud", - rundeck: "Rundeck" + rundeck: "Rundeck", + "azure-devops": "Azure DevOps" }; const envMapping: Mapping = { diff --git a/frontend/src/pages/integrations/azure-devops/authorize.tsx b/frontend/src/pages/integrations/azure-devops/authorize.tsx new file mode 100644 index 000000000..8dbb2e946 --- /dev/null +++ b/frontend/src/pages/integrations/azure-devops/authorize.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { useSaveIntegrationAccessToken } from "@app/hooks/api"; + +import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; + +export default function AzureDevopsCreateIntegrationPage() { + const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + + const [apiKey, setApiKey] = useState(""); + const [orgId, setOrgId] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + if (apiKey.length === 0) { + setApiKeyErrorText("API Key cannot be blank"); + return; + } + + setIsLoading(true); + + localStorage.setItem("azure-devops-org-id", orgId); + + const integrationAuth = await mutateAsync({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "azure-devops", + accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username + }); + + setIsLoading(false); + + router.push(`/integrations/azure-devops/create?integrationAuthId=${integrationAuth.id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + AzureDevops Integration + + setApiKey(e.target.value)} /> + + + setOrgId(e.target.value)} /> + + + + +
+ ); +} + +AzureDevopsCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/azure-devops/create.tsx b/frontend/src/pages/integrations/azure-devops/create.tsx new file mode 100644 index 000000000..59b9720d8 --- /dev/null +++ b/frontend/src/pages/integrations/azure-devops/create.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { useCreateIntegration } from "@app/hooks/api"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; + +export default function AzureDevopsCreateIntegrationPage() { + const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "", + teamId: localStorage.getItem("azure-devops-org-id") ?? "" + }); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [targetApp, setTargetApp] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetApp(integrationAuthApps[0].name); + } else { + setTargetApp("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?.id) return; + + setIsLoading(true); + + await mutateAsync({ + integrationAuthId: integrationAuth?.id, + isActive: true, + app: localStorage.getItem("azure-devops-org-id") || "", + appId: targetApp, + sourceEnvironment: selectedSourceEnvironment, + secretPath + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( +
+ + AzureDevops Integration + + + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + + + +
+ ) : ( +
+ ); +} + +AzureDevopsCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index 1aee4c656..69d603f20 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -131,6 +131,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "rundeck": link = `${window.location.origin}/integrations/rundeck/authorize`; break; + case "azure-devops": + link = `${window.location.origin}/integrations/azure-devops/authorize`; + break; default: break; } From c0fd0a56f39ed7a3af4ae428597731a59108759c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 17:37:59 +0400 Subject: [PATCH 2/7] Update integration-list.ts --- backend/src/services/integration-auth/integration-list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 46c76c5cf..b91654474 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -382,7 +382,7 @@ export const getIntegrationOptions = async () => { docsLink: "" }, { - name: "Azure Devops", + name: "Azure DevOps", slug: "azure-devops", image: "Microsoft Azure.png", isAvailable: true, From 8b80622d2f58aa2b3882fad280aeae7c0a8051c3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 17:38:27 +0400 Subject: [PATCH 3/7] Cleanup --- .../dynamic-secret/dynamic-secret-service.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index e560c837d..ea08b212a 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -99,22 +99,6 @@ export const dynamicSecretServiceFactory = ({ const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); - console.log("YES THIS IS WHERE WE ARE"); - - console.log({ - type: provider.type, - version: 1, - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, - maxTTL, - defaultTTL, - folderId: folder.id, - name - }); - const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, @@ -128,7 +112,6 @@ export const dynamicSecretServiceFactory = ({ folderId: folder.id, name }); - console.log("IT WORKED"); return dynamicSecretCfg; }; From a18015b1e5fa38e8e37bd10a7df73434f4069680 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 19:35:34 +0400 Subject: [PATCH 4/7] Fix: Use unique parameter for passing devops org name Used to be teamId, now it's azureDevopsOrgName. --- .../server/routes/v1/integration-auth-router.ts | 1 + .../integration-auth/integration-app-list.ts | 9 +++++---- .../integration-auth/integration-auth-service.ts | 2 ++ .../integration-auth/integration-auth-types.ts | 1 + frontend/src/hooks/api/integrationAuth/queries.tsx | 9 +++++++++ .../pages/integrations/azure-devops/authorize.tsx | 14 +++++++++----- .../src/pages/integrations/azure-devops/create.tsx | 4 ++-- 7 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 963b7101c..4baa39f76 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }), querystring: z.object({ teamId: z.string().trim().optional(), + azureDevOpsOrgName: z.string().trim().optional(), workspaceSlug: z.string().trim().optional() }), response: { diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 4fd8264f5..bd7c19228 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -1030,11 +1030,10 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => { return apps; }; -const getAppsAzureDevOps = async ({ accessToken, orgId }: { accessToken: string; orgId: string }) => { - console.log({ accessToken, orgId }); +const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: string; orgName: string }) => { const res = ( await request.get<{ count: number; value: Record[] }>( - `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgId}/_apis/projects?api-version=7.2-preview.2`, + `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.2-preview.2`, { headers: { Authorization: `Basic ${accessToken}` @@ -1055,6 +1054,7 @@ export const getApps = async ({ accessToken, accessId, teamId, + azureDevOpsOrgName, workspaceSlug, url }: { @@ -1062,6 +1062,7 @@ export const getApps = async ({ accessToken: string; accessId?: string; teamId?: string | null; + azureDevOpsOrgName?: string | null; workspaceSlug?: string; url?: string | null; }): Promise => { @@ -1207,7 +1208,7 @@ export const getApps = async ({ case Integrations.AZURE_DEVOPS: return getAppsAzureDevOps({ accessToken, - orgId: teamId as string // small hack to pass orgId as teamId + orgName: azureDevOpsOrgName as string }); default: diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index e16f6bb77..7b201e6ea 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({ actorOrgId, actorAuthMethod, teamId, + azureDevOpsOrgName, id, workspaceSlug }: TIntegrationAuthAppsDTO) => { @@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({ accessToken, accessId, teamId, + azureDevOpsOrgName, workspaceSlug, url: integrationAuth.url }); diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 44b6d4c4c..af390297a 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -29,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & { export type TIntegrationAuthAppsDTO = { id: string; teamId?: string; + azureDevOpsOrgName?: string; workspaceSlug?: string; } & Omit; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 45ac90a84..efb5ad7fb 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -120,16 +120,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => { const fetchIntegrationAuthApps = async ({ integrationAuthId, teamId, + azureDevOpsOrgName, workspaceSlug }: { integrationAuthId: string; teamId?: string; + azureDevOpsOrgName?: string; workspaceSlug?: string; }) => { const params: Record = {}; if (teamId) { params.teamId = teamId; } + if (azureDevOpsOrgName) { + params.azureDevOpsOrgName = azureDevOpsOrgName; + } + if (workspaceSlug) { params.workspaceSlug = workspaceSlug; } @@ -452,10 +458,12 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthApps = ({ integrationAuthId, teamId, + azureDevOpsOrgName, workspaceSlug }: { integrationAuthId: string; teamId?: string; + azureDevOpsOrgName?: string; workspaceSlug?: string; }) => { return useQuery({ @@ -464,6 +472,7 @@ export const useGetIntegrationAuthApps = ({ fetchIntegrationAuthApps({ integrationAuthId, teamId, + azureDevOpsOrgName, workspaceSlug }), enabled: true diff --git a/frontend/src/pages/integrations/azure-devops/authorize.tsx b/frontend/src/pages/integrations/azure-devops/authorize.tsx index 8dbb2e946..2f4d17628 100644 --- a/frontend/src/pages/integrations/azure-devops/authorize.tsx +++ b/frontend/src/pages/integrations/azure-devops/authorize.tsx @@ -10,7 +10,7 @@ export default function AzureDevopsCreateIntegrationPage() { const { mutateAsync } = useSaveIntegrationAccessToken(); const [apiKey, setApiKey] = useState(""); - const [orgId, setOrgId] = useState(""); + const [devopsOrgName, setDevopsOrgName] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -24,7 +24,7 @@ export default function AzureDevopsCreateIntegrationPage() { setIsLoading(true); - localStorage.setItem("azure-devops-org-id", orgId); + localStorage.setItem("azure-devops-org-name", devopsOrgName); const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), @@ -52,12 +52,16 @@ export default function AzureDevopsCreateIntegrationPage() { setApiKey(e.target.value)} /> - setOrgId(e.target.value)} /> + setDevopsOrgName(e.target.value)} + />
diff --git a/frontend/src/pages/integrations/azure-devops/create.tsx b/frontend/src/pages/integrations/azure-devops/create.tsx index 59b9720d8..d34636106 100644 --- a/frontend/src/pages/integrations/azure-devops/create.tsx +++ b/frontend/src/pages/integrations/azure-devops/create.tsx @@ -29,7 +29,7 @@ export default function AzureDevopsCreateIntegrationPage() { const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "", - teamId: localStorage.getItem("azure-devops-org-id") ?? "" + azureDevOpsOrgName: localStorage.getItem("azure-devops-org-name") ?? "" }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); @@ -63,7 +63,7 @@ export default function AzureDevopsCreateIntegrationPage() { await mutateAsync({ integrationAuthId: integrationAuth?.id, isActive: true, - app: localStorage.getItem("azure-devops-org-id") || "", + app: localStorage.getItem("azure-devops-org-name") || "", appId: targetApp, sourceEnvironment: selectedSourceEnvironment, secretPath From df8f2cf9ab27c9851d68f6235a16eb39cd7e61b8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 19:35:51 +0400 Subject: [PATCH 5/7] Update integration-sync-secret.ts --- .../src/services/integration-auth/integration-sync-secret.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index ba22178d5..d1dabcd0e 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -2120,7 +2120,7 @@ const syncSecretsAzureDevops = async ({ const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name); const variables: Record = {}; - for await (const key of Object.keys(secrets)) { + for (const key of Object.keys(secrets)) { variables[key] = { value: secrets[key].value }; } From 2a043afe11e146df46530b5cfd66482ffcb98f7f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 19:42:27 +0400 Subject: [PATCH 6/7] Cleanup --- backend/src/ee/services/license/licence-fns.ts | 2 +- frontend/src/pages/integrations/azure-devops/authorize.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index b7dc3d694..bd40f75cb 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: true, + dynamicSecret: false, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, diff --git a/frontend/src/pages/integrations/azure-devops/authorize.tsx b/frontend/src/pages/integrations/azure-devops/authorize.tsx index 2f4d17628..989b1a17a 100644 --- a/frontend/src/pages/integrations/azure-devops/authorize.tsx +++ b/frontend/src/pages/integrations/azure-devops/authorize.tsx @@ -52,8 +52,8 @@ export default function AzureDevopsCreateIntegrationPage() { setApiKey(e.target.value)} /> From a6b852fab9c6f9164a6f0760f3f575a1d6a93755 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 20 Aug 2024 19:46:03 +0400 Subject: [PATCH 7/7] Fix: Type errors / cleanup --- .../integration-sync-secret.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index d1dabcd0e..91d9a68b5 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -26,7 +26,7 @@ import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; import { z } from "zod"; -import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas"; +import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; @@ -276,8 +276,8 @@ const syncSecretsAzureKeyVault = async ({ }; secrets: Record; accessToken: string; - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; }) => { interface GetAzureKeyVaultSecret { id: string; // secret URI @@ -967,8 +967,8 @@ const syncSecretsHeroku = async ({ secrets, accessToken }: { - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integration: TIntegrations & { projectId: string; environment: { @@ -2638,8 +2638,8 @@ const syncSecretsTerraformCloud = async ({ accessToken, integrationDAL }: { - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integration: TIntegrations & { projectId: string; environment: { @@ -3786,8 +3786,8 @@ export const syncIntegrationSecrets = async ({ appendices, projectId }: { - createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; - updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; integrationDAL: Pick; integration: TIntegrations & { projectId: string;