diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index bd7c19228..d6930c594 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -460,16 +460,21 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { */ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { const res = ( - await request.get<{ reponame: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, { - headers: { - "Circle-Token": accessToken, - "Accept-Encoding": "application/json" + await request.get<{ reponame: string; username: string; vcs_url: string }[]>( + `${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + } } - }) + ) ).data; - const apps = res?.map((a) => ({ - name: a?.reponame + const apps = res.map((a) => ({ + owner: a.username, // username maps to unique organization name in CircleCI + name: a.reponame, // reponame maps to project name within an organization in CircleCI + appId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI })); return apps; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 94d7a95d6..d0fa2279a 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1929,22 +1929,62 @@ const syncSecretsCircleCI = async ({ secrets: Record; accessToken: string; }) => { - const circleciOrganizationDetail = ( - await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, { + const getProjectSlug = async () => { + const requestConfig = { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json" } - }) - ).data[0]; + }; - const { slug } = circleciOrganizationDetail; + try { + const projectDetails = ( + await request.get<{ slug: string }>( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`, + requestConfig + ) + ).data; + + return projectDetails.slug; + } catch (err) { + if (err instanceof AxiosError) { + if (err.response?.data?.message !== "Not Found") { + throw new Error("Failed to get project slug from CircleCI during first attempt."); + } + } + } + + // For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization + try { + const circleCiOrganization = ( + await request.get<{ slug: string; name: string }[]>( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, + requestConfig + ) + ).data; + + // Case 1: This is a new integration where the organization name is stored under `integration.owner` + if (integration.owner) { + const org = circleCiOrganization.find((o) => o.name === integration.owner); + if (org) { + return `${org.slug}/${integration.app}`; + } + } + + // Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one + return `${circleCiOrganization[0].slug}/${integration.app}`; + } catch (err) { + throw new Error("Failed to get project slug from CircleCI during second attempt."); + } + }; + + const projectSlug = await getProjectSlug(); // sync secrets to CircleCI await Promise.all( Object.keys(secrets).map(async (key) => request.post( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`, { name: key, value: secrets[key].value @@ -1962,7 +2002,7 @@ const syncSecretsCircleCI = async ({ // get secrets from CircleCI const getSecretsRes = ( await request.get<{ items: { name: string }[] }>( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`, { headers: { "Circle-Token": accessToken, @@ -1976,15 +2016,12 @@ const syncSecretsCircleCI = async ({ await Promise.all( getSecretsRes.map(async (sec) => { if (!(sec.name in secrets)) { - return request.delete( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, - { - headers: { - "Circle-Token": accessToken, - "Content-Type": "application/json" - } + return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" } - ); + }); } }) ); diff --git a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx index b3eb5fa88..1b208c394 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx @@ -69,7 +69,7 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() { subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment." >
-
+
{ - if (integrationAuthApps) { - if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0]?.name); - } else { - setTargetApp("none"); - } - } - }, [integrationAuthApps]); - const handleButtonClick = async () => { try { if (!integrationAuth?.id) return; + if (!targetProjectId || targetOrganization === "none") { + createNotification({ + type: "error", + text: "Please select a project" + }); + setIsLoading(false); + return; + } + setIsLoading(true); + const selectedApp = integrationAuthApps?.find( + (integrationAuthApp) => integrationAuthApp.appId === targetProjectId + ); + + if (!selectedApp) { + createNotification({ + type: "error", + text: "Invalid project selected" + }); + setIsLoading(false); + return; + } + await mutateAsync({ integrationAuthId: integrationAuth?.id, isActive: true, - app: targetApp, - appId: integrationAuthApps?.find( - (integrationAuthApp) => integrationAuthApp.name === targetApp - )?.appId, + app: selectedApp.name, // project name + owner: selectedApp.owner, // organization name + appId: selectedApp.appId, // project id (used for syncing) sourceEnvironment: selectedSourceEnvironment, secretPath }); @@ -92,11 +105,28 @@ export default function CircleCICreateIntegrationPage() { } }; - return integrationAuth && - workspace && - selectedSourceEnvironment && - integrationAuthApps && - targetApp ? ( + const filteredProjects = useMemo(() => { + if (!integrationAuthApps) return []; + + return integrationAuthApps.filter((integrationAuthApp) => { + return integrationAuthApp.owner === targetOrganization; + }); + }, [integrationAuthApps, targetOrganization]); + + const filteredOrganizations = useMemo(() => { + const organizations = new Set(); + + if (integrationAuthApps) { + integrationAuthApps.forEach((integrationAuthApp) => { + if (!integrationAuthApp.owner) return; + organizations.add(integrationAuthApp.owner); + }); + } + + return Array.from(organizations); + }, [integrationAuthApps]); + + return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps ? (
Set Up CircleCI Integration @@ -108,7 +138,7 @@ export default function CircleCICreateIntegrationPage() { subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables." >
-
+
+ setTargetApp(val)} + value={targetOrganization} + onValueChange={(val) => { + setTargetOrganization(val); + setTargetProjectId("none"); + }} className="w-full border border-mineshaft-500" - isDisabled={integrationAuthApps.length === 0} + isDisabled={filteredOrganizations.length === 0} > - {integrationAuthApps.length > 0 ? ( - integrationAuthApps.map((integrationAuthApp) => ( - - {integrationAuthApp.name} + {filteredOrganizations.length > 0 ? ( + filteredOrganizations.map((org) => ( + + {org} )) ) : ( - No projects found + No organizations found )} + + {targetOrganization && ( + + + + )}
)} + {integration.integration === "circleci" && integration.owner && ( +
+ +
+ {integration.owner} +
+
+ )} {integration.integration === "terraform-cloud" && integration.targetService && (