diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts index 3c9358e68..c87eb06d2 100644 --- a/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts @@ -3,6 +3,7 @@ import { AxiosError } from "axios"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TeamCityConnectionMethod } from "./teamcity-connection-enums"; @@ -12,6 +13,14 @@ import { TTeamCityListProjectsResponse } from "./teamcity-connection-types"; +export const getTeamCityInstanceUrl = async (config: TTeamCityConnectionConfig) => { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return instanceUrl; +}; + export const getTeamCityConnectionListItem = () => { return { name: "TeamCity" as const, @@ -21,7 +30,8 @@ export const getTeamCityConnectionListItem = () => { }; export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => { - const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + const instanceUrl = await getTeamCityInstanceUrl(config); + const { accessToken } = config.credentials; try { @@ -46,7 +56,7 @@ export const validateTeamCityConnectionCredentials = async (config: TTeamCityCon }; export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => { - const instanceUrl = removeTrailingSlash(appConnection.credentials.instanceUrl); + const instanceUrl = await getTeamCityInstanceUrl(appConnection); const { accessToken } = appConnection.credentials; const resp = await request.get( diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 8e9554d00..143fa4622 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -145,7 +145,9 @@ export const SecretSyncFns = { case SecretSync.TeamCity: return TeamCitySyncFns.syncSecrets(secretSync, secretMap); default: - throw new Error(`Unhandled sync destination for sync secrets fns: ${secretSync.destination}`); + throw new Error( + `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); } }, getSecrets: async ( @@ -205,7 +207,9 @@ export const SecretSyncFns = { secretMap = await TeamCitySyncFns.getSecrets(secretSync); break; default: - throw new Error(`Unhandled sync destination for get secrets fns: ${secretSync.destination}`); + throw new Error( + `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); } return secretMap; @@ -258,7 +262,9 @@ export const SecretSyncFns = { case SecretSync.TeamCity: return TeamCitySyncFns.removeSecrets(secretSync, secretMap); default: - throw new Error(`Unhandled sync destination for remove secrets fns: ${secretSync.destination}`); + throw new Error( + `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); } } }; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts index 523bce9a5..cc3903780 100644 --- a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts @@ -1,5 +1,5 @@ import { request } from "@app/lib/config/request"; -import { removeTrailingSlash } from "@app/lib/fn"; +import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { @@ -89,7 +89,7 @@ export const TeamCitySyncFns = { destinationConfig: { project, buildConfig } } = secretSync; - const instanceUrl = removeTrailingSlash(connection.credentials.instanceUrl); + const instanceUrl = await getTeamCityInstanceUrl(connection); const { accessToken } = connection.credentials; for await (const entry of Object.entries(secretMap)) { @@ -145,7 +145,7 @@ export const TeamCitySyncFns = { destinationConfig: { project, buildConfig } } = secretSync; - const instanceUrl = removeTrailingSlash(connection.credentials.instanceUrl); + const instanceUrl = await getTeamCityInstanceUrl(connection); const { accessToken } = connection.credentials; const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); @@ -175,7 +175,7 @@ export const TeamCitySyncFns = { destinationConfig: { project, buildConfig } } = secretSync; - const instanceUrl = removeTrailingSlash(connection.credentials.instanceUrl); + const instanceUrl = await getTeamCityInstanceUrl(connection); const { accessToken } = connection.credentials; return listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); diff --git a/docs/integrations/app-connections/teamcity.mdx b/docs/integrations/app-connections/teamcity.mdx index 2b7d7f587..1ffafe637 100644 --- a/docs/integrations/app-connections/teamcity.mdx +++ b/docs/integrations/app-connections/teamcity.mdx @@ -18,17 +18,28 @@ Infisical supports connecting to TeamCity using an Access Token to securely sync Click the **Create access token** button and provide a name for your token (e.g., "Infisical Integration"). You may set an expiration date or leave it blank for no expiry. - The permission scope can either be **Same as current user** or **Limit per project**. If you're choosing **Limit per project**, make sure you select the relevant project and enable the **View build configuration settings** and **Edit project** permissions. + The permission scope can either be **Same as current user** or **Limit per project**. + + If you're choosing **Limit per project**, make sure you select the relevant project and enable the permissions relevant to your use case: + + + + - View build configuration settings + - Edit project + + + + ![TeamCity Token Popup](/images/app-connections/teamcity/teamcity-token-popup.png) + Setting your permission scope to **Same as current user** will allow your integration to access multiple projects as long as the current user has read and write access to them. - Setting your permission scope to **Limit per project** requires the **View build configuration settings** and **Edit project** permissions. + If you configure an expiry date for your access token, you must manually rotate to a new token before the expiration date to prevent service interruption. - ![TeamCity Token Popup](/images/app-connections/teamcity/teamcity-token-popup.png) - After creation, a modal with the API token will be displayed. Copy this token immediately and store it securely, as you won't be able to view it again after closing this dialog. + After creation, a modal with the Access Token will be displayed. Copy this token immediately and store it securely, as you won't be able to view it again after closing this dialog. ![TeamCity Token Copy Popup](/images/app-connections/teamcity/teamcity-token-copy.png) diff --git a/docs/mint.json b/docs/mint.json index 59c58ab43..e2158a181 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -434,10 +434,10 @@ "integrations/app-connections/humanitec", "integrations/app-connections/mssql", "integrations/app-connections/postgres", + "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", "integrations/app-connections/vercel", - "integrations/app-connections/windmill", - "integrations/app-connections/teamcity" + "integrations/app-connections/windmill" ] } ] @@ -458,10 +458,10 @@ "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", - "integrations/secret-syncs/windmill", - "integrations/secret-syncs/teamcity" + "integrations/secret-syncs/windmill" ] } ] @@ -1045,6 +1045,18 @@ "api-reference/endpoints/app-connections/postgres/delete" ] }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/app-connections/teamcity/list", + "api-reference/endpoints/app-connections/teamcity/available", + "api-reference/endpoints/app-connections/teamcity/get-by-id", + "api-reference/endpoints/app-connections/teamcity/get-by-name", + "api-reference/endpoints/app-connections/teamcity/create", + "api-reference/endpoints/app-connections/teamcity/update", + "api-reference/endpoints/app-connections/teamcity/delete" + ] + }, { "group": "Terraform Cloud", "pages": [ @@ -1080,18 +1092,6 @@ "api-reference/endpoints/app-connections/windmill/update", "api-reference/endpoints/app-connections/windmill/delete" ] - }, - { - "group": "TeamCity", - "pages": [ - "api-reference/endpoints/app-connections/teamcity/list", - "api-reference/endpoints/app-connections/teamcity/available", - "api-reference/endpoints/app-connections/teamcity/get-by-id", - "api-reference/endpoints/app-connections/teamcity/get-by-name", - "api-reference/endpoints/app-connections/teamcity/create", - "api-reference/endpoints/app-connections/teamcity/update", - "api-reference/endpoints/app-connections/teamcity/delete" - ] } ] }, @@ -1222,6 +1222,20 @@ "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" ] }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/secret-syncs/teamcity/list", + "api-reference/endpoints/secret-syncs/teamcity/get-by-id", + "api-reference/endpoints/secret-syncs/teamcity/get-by-name", + "api-reference/endpoints/secret-syncs/teamcity/create", + "api-reference/endpoints/secret-syncs/teamcity/update", + "api-reference/endpoints/secret-syncs/teamcity/delete", + "api-reference/endpoints/secret-syncs/teamcity/sync-secrets", + "api-reference/endpoints/secret-syncs/teamcity/import-secrets", + "api-reference/endpoints/secret-syncs/teamcity/remove-secrets" + ] + }, { "group": "Terraform Cloud", "pages": [ @@ -1262,20 +1276,6 @@ "api-reference/endpoints/secret-syncs/windmill/import-secrets", "api-reference/endpoints/secret-syncs/windmill/remove-secrets" ] - }, - { - "group": "TeamCity", - "pages": [ - "api-reference/endpoints/secret-syncs/teamcity/list", - "api-reference/endpoints/secret-syncs/teamcity/get-by-id", - "api-reference/endpoints/secret-syncs/teamcity/get-by-name", - "api-reference/endpoints/secret-syncs/teamcity/create", - "api-reference/endpoints/secret-syncs/teamcity/update", - "api-reference/endpoints/secret-syncs/teamcity/delete", - "api-reference/endpoints/secret-syncs/teamcity/sync-secrets", - "api-reference/endpoints/secret-syncs/teamcity/import-secrets", - "api-reference/endpoints/secret-syncs/teamcity/remove-secrets" - ] } ] }, diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx index f4fdf526b..f9948fcaa 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx @@ -90,9 +90,15 @@ export const TeamCitySyncFields = () => { isOptional label="Build Configuration" helperText={ - - Not selecting a Build Configuration will sync your secrets with the entire project. - + +
+ Don't see the configuration you're looking for?{" "} + +
+
} > { )} /> + + + Not selecting a Build Configuration will sync your secrets to the entire project. + ); }; diff --git a/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts b/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts index 69a32b670..972df6c96 100644 --- a/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts @@ -9,6 +9,6 @@ export type TTeamCityConnection = TRootAppConnection & { app: AppConnection.Team method: TeamCityConnectionMethod.AccessToken; credentials: { accessToken: string; - instanceUrl?: string; + instanceUrl: string; }; }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx index ef0421a86..c7479e43f 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx @@ -34,14 +34,7 @@ const formSchema = z.discriminatedUnion("method", [ method: z.literal(TeamCityConnectionMethod.AccessToken), credentials: z.object({ accessToken: z.string().trim().min(1, "Access Token required"), - instanceUrl: z - .string() - .trim() - .transform((value) => value || undefined) - .refine((value) => (!value ? true : z.string().url().safeParse(value).success), { - message: "Invalid instance URL" - }) - .optional() + instanceUrl: z.string().trim().url("Invalid Instance URL") }) }) ]);