Skip to content

Commit 41f47c8

Browse files
Add Azure DevOps integration (#20202)
* Add node package * first nit changes * nit proto udpate * fixup * [server] add azure support * fixup * fixup * server fixup * [dashboard] changes * fixup * fixup * Fix server bugs * Fixup * Fix dashboard * Fix user integration * Fix permission update modal * tmp * Add unit tests and fix get file content issue * Add readme * fix tag and branch parser * Update README.md * Remove API tests * Disable azure devops support for PAYG * Revert "Remove API tests" This reverts commit a525cbd. * Fix tests * Rebase fixup * nit fixing * revert me * Fix integration udpate * Fix ENT-780 * Don't support azure devops on PAYG * dashboard: add comments and remove new Azure DevOps supports on user settings page * Fix push warning and make project a part of owner * Proper handle errors * Fix token can't refresh issue * Fix api * Add project context supports * Update components/server/src/azure-devops/azure-context-parser.spec.ts Co-authored-by: Filip Troníček <[email protected]> * Fix readablestream error * Fix clone url * Address feedback - dashboard provider type update - doc for checkWriteAccess - Requirement.DEFAULT * 1 * avatar * Revert "revert me" This reverts commit 189c431. --------- Co-authored-by: Filip Troníček <[email protected]>
1 parent 9e0f6e1 commit 41f47c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3090
-175
lines changed

components/dashboard/src/components/RepositoryFinder.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,20 @@ export default function RepositoryFinder({
318318
});
319319
}
320320

321+
if (searchString.length >= 3 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) {
322+
// ENT-780
323+
result.push({
324+
id: "azure-devops",
325+
element: (
326+
<div className="text-sm text-pk-content-tertiary flex items-center">
327+
<Exclamation2 className="w-4 h-4 mr-2" />
328+
<span>Azure DevOps doesn't support repository searching.</span>
329+
</div>
330+
),
331+
isSelectable: false,
332+
});
333+
}
334+
321335
if (searchString.length < 3) {
322336
// add an element that tells the user to type more
323337
result.push({
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
8+
import { isGitpodIo } from "../../utils";
9+
import { useMemo } from "react";
10+
11+
const optionsForPAYG = [
12+
{ type: AuthProviderType.GITHUB, label: "GitHub" },
13+
{ type: AuthProviderType.GITLAB, label: "GitLab" },
14+
{ type: AuthProviderType.BITBUCKET_SERVER, label: "Bitbucket Server" },
15+
{ type: AuthProviderType.BITBUCKET, label: "Bitbucket Cloud" },
16+
];
17+
18+
const optionsForEnterprise = [...optionsForPAYG, { type: AuthProviderType.AZURE_DEVOPS, label: "Azure DevOps" }];
19+
20+
export const isSupportAzureDevOpsIntegration = () => {
21+
return isGitpodIo();
22+
};
23+
24+
export const useAuthProviderOptionsQuery = (isOrgLevel: boolean) => {
25+
return useMemo(() => {
26+
const isPAYG = isGitpodIo();
27+
// Azure DevOps is not supported for PAYG users and is only available for org-level integrations
28+
// because auth flow is identified by auth provider's host, which will always be `dev.azure.com`
29+
//
30+
// Don't remove this until we can setup an generial application for Azure DevOps (investigate needed)
31+
if (isPAYG || !isOrgLevel) {
32+
return optionsForPAYG;
33+
}
34+
return optionsForEnterprise;
35+
}, [isOrgLevel]);
36+
};

components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type CreateAuthProviderArgs = {
1414
clientId: string;
1515
clientSecret: string;
1616
orgId: string;
17+
authorizationUrl?: string;
18+
tokenUrl?: string;
1719
};
1820
};
1921
export const useCreateOrgAuthProviderMutation = () => {
@@ -28,6 +30,8 @@ export const useCreateOrgAuthProviderMutation = () => {
2830
oauth2Config: {
2931
clientId: provider.clientId,
3032
clientSecret: provider.clientSecret,
33+
authorizationUrl: provider.authorizationUrl,
34+
tokenUrl: provider.tokenUrl,
3135
},
3236
type: provider.type,
3337
}),

components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type CreateAuthProviderArgs = {
1414
clientId: string;
1515
clientSecret: string;
1616
userId: string;
17+
authorizationUrl?: string;
18+
tokenUrl?: string;
1719
};
1820
};
1921
export const useCreateUserAuthProviderMutation = () => {
@@ -28,6 +30,8 @@ export const useCreateUserAuthProviderMutation = () => {
2830
oauth2Config: {
2931
clientId: provider.clientId,
3032
clientSecret: provider.clientSecret,
33+
authorizationUrl: provider.authorizationUrl,
34+
tokenUrl: provider.tokenUrl,
3135
},
3236
type: provider.type,
3337
}),

components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type UpdateAuthProviderArgs = {
1414
id: string;
1515
clientId: string;
1616
clientSecret: string;
17+
authorizationUrl?: string;
18+
tokenUrl?: string;
1719
};
1820
};
1921
export const useUpdateOrgAuthProviderMutation = () => {
@@ -26,6 +28,8 @@ export const useUpdateOrgAuthProviderMutation = () => {
2628
authProviderId: provider.id,
2729
clientId: provider.clientId,
2830
clientSecret: provider.clientSecret,
31+
authorizationUrl: provider.authorizationUrl,
32+
tokenUrl: provider.tokenUrl,
2933
}),
3034
);
3135
return response.authProvider!;

components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type UpdateAuthProviderArgs = {
1414
id: string;
1515
clientId: string;
1616
clientSecret: string;
17+
authorizationUrl?: string;
18+
tokenUrl?: string;
1719
};
1820
};
1921
export const useUpdateUserAuthProviderMutation = () => {
@@ -26,6 +28,8 @@ export const useUpdateUserAuthProviderMutation = () => {
2628
authProviderId: provider.id,
2729
clientId: provider.clientId,
2830
clientSecret: provider.clientSecret,
31+
authorizationUrl: provider.authorizationUrl,
32+
tokenUrl: provider.tokenUrl,
2933
}),
3034
);
3135
return response.authProvider!;
Lines changed: 1 addition & 0 deletions
Loading

components/dashboard/src/provider-utils.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_
88
import bitbucket from "./images/bitbucket.svg";
99
import github from "./images/github.svg";
1010
import gitlab from "./images/gitlab.svg";
11+
import azuredevops from "./images/azuredevops.svg";
1112
import { gitpodHostUrl } from "./service/service";
1213

1314
function iconForAuthProvider(type: string | AuthProviderType) {
@@ -24,6 +25,9 @@ function iconForAuthProvider(type: string | AuthProviderType) {
2425
case "BitbucketServer":
2526
case AuthProviderType.BITBUCKET_SERVER:
2627
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={bitbucket} alt="" />;
28+
case "AzureDevOps":
29+
case AuthProviderType.AZURE_DEVOPS:
30+
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={azuredevops} alt="" />;
2731
default:
2832
return <></>;
2933
}
@@ -39,6 +43,8 @@ export function toAuthProviderLabel(type: AuthProviderType) {
3943
return "Bitbucket Cloud";
4044
case AuthProviderType.BITBUCKET_SERVER:
4145
return "Bitbucket Server";
46+
case AuthProviderType.AZURE_DEVOPS:
47+
return "Azure DevOps";
4248
default:
4349
return "-";
4450
}
@@ -52,6 +58,8 @@ function simplifyProviderName(host: string) {
5258
return "GitLab";
5359
case "bitbucket.org":
5460
return "Bitbucket";
61+
case "dev.azure.com":
62+
return "Azure DevOps";
5563
default:
5664
return host;
5765
}

components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import { useCreateOrgAuthProviderMutation } from "../../data/auth-providers/crea
2424
import { useUpdateOrgAuthProviderMutation } from "../../data/auth-providers/update-org-auth-provider-mutation";
2525
import { authProviderClient, userClient } from "../../service/public-api";
2626
import { LoadingButton } from "@podkit/buttons/LoadingButton";
27+
import {
28+
isSupportAzureDevOpsIntegration,
29+
useAuthProviderOptionsQuery,
30+
} from "../../data/auth-providers/auth-provider-options-query";
2731

2832
type Props = {
2933
provider?: AuthProvider;
@@ -37,6 +41,10 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
3741
const [host, setHost] = useState<string>(props.provider?.host ?? "");
3842
const [clientId, setClientId] = useState<string>(props.provider?.oauth2Config?.clientId ?? "");
3943
const [clientSecret, setClientSecret] = useState<string>(props.provider?.oauth2Config?.clientSecret ?? "");
44+
const [authorizationUrl, setAuthorizationUrl] = useState(props.provider?.oauth2Config?.authorizationUrl ?? "");
45+
const [tokenUrl, setTokenUrl] = useState(props.provider?.oauth2Config?.tokenUrl ?? "");
46+
const availableProviderOptions = useAuthProviderOptionsQuery(true);
47+
const supportAzureDevOps = isSupportAzureDevOpsIntegration();
4048

4149
const [savedProvider, setSavedProvider] = useState(props.provider);
4250
const isNew = !savedProvider;
@@ -82,6 +90,21 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
8290
clientSecret.trim().length > 0,
8391
);
8492

93+
const {
94+
message: authorizationUrlError,
95+
onBlur: authorizationUrlOnBlur,
96+
isValid: authorizationUrlValid,
97+
} = useOnBlurError(
98+
`Authorization URL is missing.`,
99+
type !== AuthProviderType.AZURE_DEVOPS || authorizationUrl.trim().length > 0,
100+
);
101+
102+
const {
103+
message: tokenUrlError,
104+
onBlur: tokenUrlOnBlur,
105+
isValid: tokenUrlValid,
106+
} = useOnBlurError(`Token URL is missing.`, type !== AuthProviderType.AZURE_DEVOPS || tokenUrl.trim().length > 0);
107+
85108
// Call our error onBlur handler, and remove prefixed "https://"
86109
const hostOnBlur = useCallback(() => {
87110
hostOnBlurErrorTracking();
@@ -112,6 +135,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
112135

113136
const trimmedId = clientId.trim();
114137
const trimmedSecret = clientSecret.trim();
138+
const trimmedAuthorizationUrl = authorizationUrl.trim();
139+
const trimmedTokenUrl = tokenUrl.trim();
115140

116141
try {
117142
let newProvider: AuthProvider;
@@ -123,6 +148,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
123148
orgId: team.id,
124149
clientId: trimmedId,
125150
clientSecret: trimmedSecret,
151+
authorizationUrl: trimmedAuthorizationUrl,
152+
tokenUrl: trimmedTokenUrl,
126153
},
127154
});
128155
} else {
@@ -131,6 +158,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
131158
id: savedProvider.id,
132159
clientId: trimmedId,
133160
clientSecret: clientSecret === "redacted" ? "" : trimmedSecret,
161+
authorizationUrl: trimmedAuthorizationUrl,
162+
tokenUrl: trimmedTokenUrl,
134163
},
135164
});
136165
}
@@ -181,6 +210,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
181210
}, [
182211
clientId,
183212
clientSecret,
213+
authorizationUrl,
214+
tokenUrl,
184215
host,
185216
invalidateOrgAuthProviders,
186217
isNew,
@@ -196,8 +227,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
196227
]);
197228

198229
const isValid = useMemo(
199-
() => clientIdValid && clientSecretValid && hostValid,
200-
[clientIdValid, clientSecretValid, hostValid],
230+
() => clientIdValid && clientSecretValid && hostValid && authorizationUrlValid && tokenUrlValid,
231+
[clientIdValid, clientSecretValid, hostValid, authorizationUrlValid, tokenUrlValid],
201232
);
202233

203234
const getNumber = (paramValue: string | null) => {
@@ -223,7 +254,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
223254
<ModalBody>
224255
{isNew && (
225256
<Subheading>
226-
Configure a Git Integration with a self-managed instance of GitLab, GitHub, or Bitbucket Server.
257+
Configure a Git Integration with a self-managed instance of GitLab, GitHub{" "}
258+
{supportAzureDevOps ? ", Bitbucket Server or Azure DevOps" : "or Bitbucket"}.
227259
</Subheading>
228260
)}
229261

@@ -235,10 +267,11 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
235267
topMargin={false}
236268
onChange={(val) => setType(getNumber(val))}
237269
>
238-
<option value={AuthProviderType.GITHUB}>GitHub</option>
239-
<option value={AuthProviderType.GITLAB}>GitLab</option>
240-
<option value={AuthProviderType.BITBUCKET}>Bitbucket Cloud</option>
241-
<option value={AuthProviderType.BITBUCKET_SERVER}>Bitbucket Server</option>
270+
{availableProviderOptions.map((option) => (
271+
<option key={option.type} value={option.type}>
272+
{option.label}
273+
</option>
274+
))}
242275
</SelectInputField>
243276
<TextInputField
244277
label="Provider Host Name"
@@ -254,6 +287,25 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
254287
<InputWithCopy value={redirectURL} tip="Copy the redirect URI to clipboard" />
255288
</InputField>
256289

290+
{type === AuthProviderType.AZURE_DEVOPS && (
291+
<>
292+
<TextInputField
293+
label="Authorization URL"
294+
value={authorizationUrl}
295+
error={authorizationUrlError}
296+
onBlur={authorizationUrlOnBlur}
297+
onChange={setAuthorizationUrl}
298+
/>
299+
<TextInputField
300+
label="Token URL"
301+
value={tokenUrl}
302+
error={tokenUrlError}
303+
onBlur={tokenUrlOnBlur}
304+
onChange={setTokenUrl}
305+
/>
306+
</>
307+
)}
308+
257309
<TextInputField
258310
label={type === AuthProviderType.GITLAB ? "Application ID" : "Client ID"}
259311
value={clientId}
@@ -314,6 +366,8 @@ const getPlaceholderForIntegrationType = (type: AuthProviderType) => {
314366
return "bitbucket.org";
315367
case AuthProviderType.BITBUCKET_SERVER:
316368
return "bitbucket.example.com";
369+
case AuthProviderType.AZURE_DEVOPS:
370+
return "dev.azure.com";
317371
default:
318372
return "";
319373
}
@@ -337,6 +391,9 @@ const RedirectUrlDescription: FunctionComponent<RedirectUrlDescriptionProps> = (
337391
case AuthProviderType.BITBUCKET_SERVER:
338392
docsUrl = "https://www.gitpod.io/docs/configure/authentication/bitbucket-server";
339393
break;
394+
case AuthProviderType.AZURE_DEVOPS:
395+
docsUrl = "https://www.gitpod.io/docs/configure/authentication/azure-devops";
396+
break;
340397
default:
341398
return null;
342399
}

components/dashboard/src/user-settings/AuthEntryItem.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ContextMenuEntry } from "../components/ContextMenu";
99
import { Item, ItemFieldIcon, ItemField, ItemFieldContextMenu } from "../components/ItemsList";
1010
import { AuthProviderDescription } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
1111
import { toAuthProviderLabel } from "../provider-utils";
12+
import { getScopeNameForScope } from "@gitpod/public-api-common/lib/auth-providers";
1213

1314
interface AuthEntryItemParams {
1415
ap: AuthProviderDescription;
@@ -53,7 +54,7 @@ export const AuthEntryItem = (props: AuthEntryItemParams) => {
5354
</ItemField>
5455
<ItemField className="hidden xl:w-1/3 xl:flex xl:flex-col my-auto">
5556
<span className="my-auto truncate text-gray-500 overflow-ellipsis dark:text-gray-400">
56-
{props.getPermissions(props.ap.id)?.join(", ") || "–"}
57+
{props.getPermissions(props.ap.id)?.map(getScopeNameForScope)?.join(", ") || "–"}
5758
</span>
5859
<span className="text-sm my-auto text-gray-400 dark:text-gray-500">Permissions</span>
5960
</ItemField>

0 commit comments

Comments
 (0)