From a14f25996d99d2c718d029c0603ff058f120dffe Mon Sep 17 00:00:00 2001 From: mustard Date: Wed, 11 Sep 2024 18:46:55 +0000 Subject: [PATCH 01/44] Add node package --- components/server/package.json | 1 + yarn.lock | 133 +++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/components/server/package.json b/components/server/package.json index 4fa087659096ee..8243cf4cb0b039 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -67,6 +67,7 @@ "@probot/get-private-key": "^1.1.1", "@types/jaeger-client": "^3.18.3", "async-batch": "^1.1.2", + "azure-devops-node-api": "^14.0.2", "base-64": "^1.0.0", "bitbucket": "^2.7.0", "body-parser": "^1.19.2", diff --git a/yarn.lock b/yarn.lock index 371165eed2369c..c6a52058d73d43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4963,6 +4963,14 @@ axobject-query@^3.1.1: dependencies: dequal "^2.0.3" +azure-devops-node-api@^14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-14.0.2.tgz#8370a7e4af741b59a8f264ae1ccf28aaf8fb2e34" + integrity sha512-TwjAEnWnOSZ2oypkDyqppgvJw43qArEfPiJtEWLL3NBgdvAuOuB0xgFz/Eiz4H6Dk0Yv52wCodZxtZvAMhJXwQ== + dependencies: + tunnel "0.0.6" + typed-rest-client "^2.0.1" + babel-jest@^27.4.2, babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -5465,6 +5473,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -6550,6 +6569,15 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -6618,6 +6646,14 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +des.js@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" + integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + destroy@~1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" @@ -7093,6 +7129,18 @@ es-abstract@^1.20.4, es-abstract@^1.21.2, es-abstract@^1.21.3: unbox-primitive "^1.0.2" which-typed-array "^1.1.10" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-get-iterator@^1.1.1, es-get-iterator@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz" @@ -8160,6 +8208,11 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.4, function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" @@ -8284,6 +8337,17 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: has-proto "^1.0.1" has-symbols "^1.0.3" +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -8641,6 +8705,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -8692,6 +8763,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" @@ -10157,6 +10235,11 @@ js-cookie@^3.0.1: resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz" integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== +js-md4@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" + integrity sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -11308,6 +11391,11 @@ object-inspect@^1.12.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-is@^1.1.4: version "1.1.5" resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" @@ -12830,6 +12918,13 @@ qs@6.9.7: resolved "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== +qs@^6.10.3: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@^6.11.0, qs@^6.11.2, qs@^6.9.4: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" @@ -13906,6 +14001,18 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -13967,6 +14074,16 @@ side-channel@^1.0.3, side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + sigmund@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" @@ -15130,6 +15247,17 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +typed-rest-client@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-2.0.2.tgz#82d451b9a219bf8fa688b698b2581327be4b920d" + integrity sha512-rmAQM2gZw/PQpK5+5aSs+I6ZBv4PFC2BT1o+0ADS1SgSejA+14EmbI2Lt8uXwkX7oeOMkwFmg0pHKwe8D9IT5A== + dependencies: + des.js "^1.1.0" + js-md4 "^0.3.2" + qs "^6.10.3" + tunnel "0.0.6" + underscore "^1.12.1" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" @@ -15253,6 +15381,11 @@ underscore.deferred@^0.4.0: resolved "https://registry.yarnpkg.com/underscore.deferred/-/underscore.deferred-0.4.0.tgz#2753de633b9ff7db601a2f3fa2af92b3dd290e6c" integrity sha512-OByG6SGS1FlbQrOijhS/B+QBiKbbtoOt6KvLrF/042W/96poPkVIVfaYxvoxW7ifDwG6RgKFV2X2x/EYEPXObA== +underscore@^1.12.1: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + underscore@~1.13.2: version "1.13.6" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" From ae5fd77481afac894103ec0d6565a88bf1a73757 Mon Sep 17 00:00:00 2001 From: mustard Date: Wed, 11 Sep 2024 19:02:20 +0000 Subject: [PATCH 02/44] first nit changes --- .../dashboard/src/images/azuredevops.svg | 1 + components/dashboard/src/provider-utils.tsx | 8 ++ .../src/user-settings/Integrations.tsx | 19 ++++ components/gitpod-protocol/src/protocol.ts | 2 +- components/public-api/buf.gen.yaml | 2 +- .../public-api/gitpod/v1/authprovider.proto | 1 + .../public-api/go/v1/authprovider.pb.go | 94 ++++++++++--------- .../io/gitpod/publicapi/v1/Authprovider.java | 52 +++++----- .../src/public-api-converter.ts | 4 + .../src/gitpod/v1/authprovider_pb.ts | 6 ++ 10 files changed, 122 insertions(+), 67 deletions(-) create mode 100644 components/dashboard/src/images/azuredevops.svg diff --git a/components/dashboard/src/images/azuredevops.svg b/components/dashboard/src/images/azuredevops.svg new file mode 100644 index 00000000000000..df4d5ef8978f40 --- /dev/null +++ b/components/dashboard/src/images/azuredevops.svg @@ -0,0 +1 @@ + diff --git a/components/dashboard/src/provider-utils.tsx b/components/dashboard/src/provider-utils.tsx index f1c56442747aea..c12e9e1c73c21c 100644 --- a/components/dashboard/src/provider-utils.tsx +++ b/components/dashboard/src/provider-utils.tsx @@ -8,6 +8,7 @@ import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_ import bitbucket from "./images/bitbucket.svg"; import github from "./images/github.svg"; import gitlab from "./images/gitlab.svg"; +import azuredevops from "./images/azuredevops.svg"; import { gitpodHostUrl } from "./service/service"; function iconForAuthProvider(type: string | AuthProviderType) { @@ -24,6 +25,9 @@ function iconForAuthProvider(type: string | AuthProviderType) { case "BitbucketServer": case AuthProviderType.BITBUCKET_SERVER: return ; + case "AzureDevOps": + case AuthProviderType.AZURE_DEVOPS: + return ; default: return <>; } @@ -39,6 +43,8 @@ export function toAuthProviderLabel(type: AuthProviderType) { return "Bitbucket Cloud"; case AuthProviderType.BITBUCKET_SERVER: return "Bitbucket Server"; + case AuthProviderType.AZURE_DEVOPS: + return "Azure DevOps"; default: return "-"; } @@ -52,6 +58,8 @@ function simplifyProviderName(host: string) { return "GitLab"; case "bitbucket.org": return "Bitbucket"; + case "dev.azure.com": + return "Azure DevOps"; default: return host; } diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index 415606cbab470c..ec1053f1ada37d 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -701,6 +701,22 @@ export function GitIntegrationModal( }; const getRedirectUrlDescription = (type: AuthProviderType, host: string) => { + if (type === AuthProviderType.AZURE_DEVOPS) { + return ( + + Use this redirect URI to update the OAuth application and set it up.  + + Learn more + + . + + ); + } let settingsUrl = ``; switch (type) { case AuthProviderType.GITHUB: @@ -759,6 +775,8 @@ export function GitIntegrationModal( return "bitbucket.org"; case AuthProviderType.BITBUCKET_SERVER: return "bitbucket.example.com"; + case AuthProviderType.AZURE_DEVOPS: + return "dev.azure.com/your-organization"; default: return ""; } @@ -812,6 +830,7 @@ export function GitIntegrationModal( + )} diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 5e138830d3adb2..d7c9591cc0ffcc 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -1404,7 +1404,7 @@ export interface OAuth2Config { } export namespace AuthProviderEntry { - export type Type = "GitHub" | "GitLab" | "Bitbucket" | "BitbucketServer" | string; + export type Type = "GitHub" | "GitLab" | "Bitbucket" | "BitbucketServer" | "AzureDevOps" | string; export type Status = "pending" | "verified"; export type NewEntry = Pick & { clientId?: string; diff --git a/components/public-api/buf.gen.yaml b/components/public-api/buf.gen.yaml index fb2bd6835a98ac..1277fa762b2c13 100644 --- a/components/public-api/buf.gen.yaml +++ b/components/public-api/buf.gen.yaml @@ -14,7 +14,7 @@ plugins: - module=github.com/gitpod-io/gitpod/components/public-api/go - name: protoc-proxy-gen out: go - path: /workspace/go/bin/protoc-proxy-gen + path: /root/go-packages/bin/protoc-proxy-gen opt: - module=github.com/gitpod-io/gitpod/components/public-api/go diff --git a/components/public-api/gitpod/v1/authprovider.proto b/components/public-api/gitpod/v1/authprovider.proto index f6dfcb51283946..750fbf45e62d35 100644 --- a/components/public-api/gitpod/v1/authprovider.proto +++ b/components/public-api/gitpod/v1/authprovider.proto @@ -139,4 +139,5 @@ enum AuthProviderType { AUTH_PROVIDER_TYPE_GITLAB = 2; AUTH_PROVIDER_TYPE_BITBUCKET = 3; AUTH_PROVIDER_TYPE_BITBUCKET_SERVER = 4; + AUTH_PROVIDER_TYPE_AZURE_DEVOPS = 5; } diff --git a/components/public-api/go/v1/authprovider.pb.go b/components/public-api/go/v1/authprovider.pb.go index d7aaac8e73574c..fd29fdc1e02743 100644 --- a/components/public-api/go/v1/authprovider.pb.go +++ b/components/public-api/go/v1/authprovider.pb.go @@ -33,6 +33,7 @@ const ( AuthProviderType_AUTH_PROVIDER_TYPE_GITLAB AuthProviderType = 2 AuthProviderType_AUTH_PROVIDER_TYPE_BITBUCKET AuthProviderType = 3 AuthProviderType_AUTH_PROVIDER_TYPE_BITBUCKET_SERVER AuthProviderType = 4 + AuthProviderType_AUTH_PROVIDER_TYPE_AZURE_DEVOPS AuthProviderType = 5 ) // Enum value maps for AuthProviderType. @@ -43,6 +44,7 @@ var ( 2: "AUTH_PROVIDER_TYPE_GITLAB", 3: "AUTH_PROVIDER_TYPE_BITBUCKET", 4: "AUTH_PROVIDER_TYPE_BITBUCKET_SERVER", + 5: "AUTH_PROVIDER_TYPE_AZURE_DEVOPS", } AuthProviderType_value = map[string]int32{ "AUTH_PROVIDER_TYPE_UNSPECIFIED": 0, @@ -50,6 +52,7 @@ var ( "AUTH_PROVIDER_TYPE_GITLAB": 2, "AUTH_PROVIDER_TYPE_BITBUCKET": 3, "AUTH_PROVIDER_TYPE_BITBUCKET_SERVER": 4, + "AUTH_PROVIDER_TYPE_AZURE_DEVOPS": 5, } ) @@ -1250,7 +1253,7 @@ var file_gitpod_v1_authprovider_proto_rawDesc = []byte{ 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2a, 0xbf, 0x01, 0x0a, 0x10, 0x41, 0x75, + 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2a, 0xe4, 0x01, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x1e, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, @@ -1262,53 +1265,56 @@ var file_gitpod_v1_authprovider_proto_rawDesc = []byte{ 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x10, 0x03, 0x12, 0x27, 0x0a, 0x23, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, 0x4b, - 0x45, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x04, 0x32, 0x86, 0x05, 0x0a, 0x13, - 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, - 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x21, 0x2e, 0x67, 0x69, - 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, - 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, - 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x60, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x70, - 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, + 0x45, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x41, + 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x44, 0x45, 0x56, 0x4f, 0x50, 0x53, 0x10, 0x05, + 0x32, 0x86, 0x05, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, + 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, + 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x12, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x60, 0x0a, 0x11, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x12, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x63, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, + 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x1c, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x2e, 0x67, + 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x67, + 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x63, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, - 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, + 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x0a, 0x16, 0x69, 0x6f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, - 0x64, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x5a, 0x37, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, - 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, - 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, - 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, + 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x0a, 0x16, 0x69, 0x6f, 0x2e, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java b/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java index c872927d70ce3a..3879e57392edcb 100644 --- a/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java +++ b/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java @@ -58,6 +58,10 @@ public enum AuthProviderType * AUTH_PROVIDER_TYPE_BITBUCKET_SERVER = 4; */ AUTH_PROVIDER_TYPE_BITBUCKET_SERVER(4), + /** + * AUTH_PROVIDER_TYPE_AZURE_DEVOPS = 5; + */ + AUTH_PROVIDER_TYPE_AZURE_DEVOPS(5), UNRECOGNIZED(-1), ; @@ -94,6 +98,10 @@ public enum AuthProviderType * AUTH_PROVIDER_TYPE_BITBUCKET_SERVER = 4; */ public static final int AUTH_PROVIDER_TYPE_BITBUCKET_SERVER_VALUE = 4; + /** + * AUTH_PROVIDER_TYPE_AZURE_DEVOPS = 5; + */ + public static final int AUTH_PROVIDER_TYPE_AZURE_DEVOPS_VALUE = 5; public final int getNumber() { @@ -125,6 +133,7 @@ public static AuthProviderType forNumber(int value) { case 2: return AUTH_PROVIDER_TYPE_GITLAB; case 3: return AUTH_PROVIDER_TYPE_BITBUCKET; case 4: return AUTH_PROVIDER_TYPE_BITBUCKET_SERVER; + case 5: return AUTH_PROVIDER_TYPE_AZURE_DEVOPS; default: return null; } } @@ -13449,30 +13458,31 @@ public io.gitpod.publicapi.v1.Authprovider.OAuth2Config getDefaultInstanceForTyp ".v1.OAuth2ConfigR\014oauth2ConfigB\007\n\005owner\"" + "P\n\014OAuth2Config\022\033\n\tclient_id\030\001 \001(\tR\010clie" + "ntId\022#\n\rclient_secret\030\002 \001(\tR\014clientSecre" + - "t*\277\001\n\020AuthProviderType\022\"\n\036AUTH_PROVIDER_" + + "t*\344\001\n\020AuthProviderType\022\"\n\036AUTH_PROVIDER_" + "TYPE_UNSPECIFIED\020\000\022\035\n\031AUTH_PROVIDER_TYPE" + "_GITHUB\020\001\022\035\n\031AUTH_PROVIDER_TYPE_GITLAB\020\002" + "\022 \n\034AUTH_PROVIDER_TYPE_BITBUCKET\020\003\022\'\n#AU" + - "TH_PROVIDER_TYPE_BITBUCKET_SERVER\020\0042\206\005\n\023" + - "AuthProviderService\022c\n\022CreateAuthProvide" + - "r\022$.gitpod.v1.CreateAuthProviderRequest\032" + - "%.gitpod.v1.CreateAuthProviderResponse\"\000" + - "\022Z\n\017GetAuthProvider\022!.gitpod.v1.GetAuthP" + - "roviderRequest\032\".gitpod.v1.GetAuthProvid" + - "erResponse\"\000\022`\n\021ListAuthProviders\022#.gitp" + - "od.v1.ListAuthProvidersRequest\032$.gitpod." + - "v1.ListAuthProvidersResponse\"\000\022\201\001\n\034ListA" + - "uthProviderDescriptions\022..gitpod.v1.List" + - "AuthProviderDescriptionsRequest\032/.gitpod" + - ".v1.ListAuthProviderDescriptionsResponse" + - "\"\000\022c\n\022UpdateAuthProvider\022$.gitpod.v1.Upd" + - "ateAuthProviderRequest\032%.gitpod.v1.Updat" + - "eAuthProviderResponse\"\000\022c\n\022DeleteAuthPro" + - "vider\022$.gitpod.v1.DeleteAuthProviderRequ" + - "est\032%.gitpod.v1.DeleteAuthProviderRespon" + - "se\"\000BQ\n\026io.gitpod.publicapi.v1Z7github.c" + - "om/gitpod-io/gitpod/components/public-ap" + - "i/go/v1b\006proto3" + "TH_PROVIDER_TYPE_BITBUCKET_SERVER\020\004\022#\n\037A" + + "UTH_PROVIDER_TYPE_AZURE_DEVOPS\020\0052\206\005\n\023Aut" + + "hProviderService\022c\n\022CreateAuthProvider\022$" + + ".gitpod.v1.CreateAuthProviderRequest\032%.g" + + "itpod.v1.CreateAuthProviderResponse\"\000\022Z\n" + + "\017GetAuthProvider\022!.gitpod.v1.GetAuthProv" + + "iderRequest\032\".gitpod.v1.GetAuthProviderR" + + "esponse\"\000\022`\n\021ListAuthProviders\022#.gitpod." + + "v1.ListAuthProvidersRequest\032$.gitpod.v1." + + "ListAuthProvidersResponse\"\000\022\201\001\n\034ListAuth" + + "ProviderDescriptions\022..gitpod.v1.ListAut" + + "hProviderDescriptionsRequest\032/.gitpod.v1" + + ".ListAuthProviderDescriptionsResponse\"\000\022" + + "c\n\022UpdateAuthProvider\022$.gitpod.v1.Update" + + "AuthProviderRequest\032%.gitpod.v1.UpdateAu" + + "thProviderResponse\"\000\022c\n\022DeleteAuthProvid" + + "er\022$.gitpod.v1.DeleteAuthProviderRequest" + + "\032%.gitpod.v1.DeleteAuthProviderResponse\"" + + "\000BQ\n\026io.gitpod.publicapi.v1Z7github.com/" + + "gitpod-io/gitpod/components/public-api/g" + + "o/v1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, diff --git a/components/public-api/typescript-common/src/public-api-converter.ts b/components/public-api/typescript-common/src/public-api-converter.ts index aac5ae1ac8c09b..4ee82737edb2d7 100644 --- a/components/public-api/typescript-common/src/public-api-converter.ts +++ b/components/public-api/typescript-common/src/public-api-converter.ts @@ -1203,6 +1203,8 @@ export class PublicAPIConverter { return AuthProviderType.BITBUCKET; case "BitbucketServer": return AuthProviderType.BITBUCKET_SERVER; + case "AzureDevOps": + return AuthProviderType.AZURE_DEVOPS; default: return AuthProviderType.UNSPECIFIED; // not allowed } @@ -1218,6 +1220,8 @@ export class PublicAPIConverter { return "Bitbucket"; case AuthProviderType.BITBUCKET_SERVER: return "BitbucketServer"; + case AuthProviderType.AZURE_DEVOPS: + return "AzureDevOps"; default: return ""; // not allowed } diff --git a/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts b/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts index f38300d4998a45..0e659d548fcc34 100644 --- a/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts +++ b/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts @@ -43,6 +43,11 @@ export enum AuthProviderType { * @generated from enum value: AUTH_PROVIDER_TYPE_BITBUCKET_SERVER = 4; */ BITBUCKET_SERVER = 4, + + /** + * @generated from enum value: AUTH_PROVIDER_TYPE_AZURE_DEVOPS = 5; + */ + AZURE_DEVOPS = 5, } // Retrieve enum metadata with: proto3.getEnumType(AuthProviderType) proto3.util.setEnumType(AuthProviderType, "gitpod.v1.AuthProviderType", [ @@ -51,6 +56,7 @@ proto3.util.setEnumType(AuthProviderType, "gitpod.v1.AuthProviderType", [ { no: 2, name: "AUTH_PROVIDER_TYPE_GITLAB" }, { no: 3, name: "AUTH_PROVIDER_TYPE_BITBUCKET" }, { no: 4, name: "AUTH_PROVIDER_TYPE_BITBUCKET_SERVER" }, + { no: 5, name: "AUTH_PROVIDER_TYPE_AZURE_DEVOPS" }, ]); /** From fe82871d36b91aa76d590578a7b5b1a9cdff2725 Mon Sep 17 00:00:00 2001 From: mustard Date: Thu, 12 Sep 2024 06:22:53 +0000 Subject: [PATCH 03/44] nit proto udpate --- components/gitpod-protocol/src/protocol.ts | 12 +- .../public-api/gitpod/v1/authprovider.proto | 4 + .../public-api/go/v1/authprovider.pb.go | 288 ++++--- .../io/gitpod/publicapi/v1/Authprovider.java | 715 +++++++++++++++++- .../fixtures/toAuthProvider_1.golden | 4 +- .../src/public-api-converter.ts | 2 + .../src/gitpod/v1/authprovider_pb.ts | 24 + 7 files changed, 889 insertions(+), 160 deletions(-) diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index d7c9591cc0ffcc..20f85742ed2c25 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -1406,14 +1406,18 @@ export interface OAuth2Config { export namespace AuthProviderEntry { export type Type = "GitHub" | "GitLab" | "Bitbucket" | "BitbucketServer" | "AzureDevOps" | string; export type Status = "pending" | "verified"; + export interface OAuth2CustomConfig { + authorizationUrl?: string; + tokenUrl?: string; + } export type NewEntry = Pick & { clientId?: string; clientSecret?: string; - }; + } & CustomOAuth2Config; export type UpdateEntry = Pick & { clientId?: string; clientSecret?: string; - }; + } & CustomOAuth2Config; export type NewOrgEntry = NewEntry & { organizationId: string; }; @@ -1421,8 +1425,8 @@ export namespace AuthProviderEntry { clientId?: string; clientSecret?: string; organizationId: string; - }; - export type UpdateOAuth2Config = Pick; + } & CustomOAuth2Config; + export type UpdateOAuth2Config = Pick & CustomOAuth2Config; export function redact(entry: AuthProviderEntry): AuthProviderEntry { return { ...entry, diff --git a/components/public-api/gitpod/v1/authprovider.proto b/components/public-api/gitpod/v1/authprovider.proto index 750fbf45e62d35..484493890e9ea0 100644 --- a/components/public-api/gitpod/v1/authprovider.proto +++ b/components/public-api/gitpod/v1/authprovider.proto @@ -82,6 +82,8 @@ message UpdateAuthProviderRequest { string auth_provider_id = 1; optional string client_id = 2; optional string client_secret = 3; + optional string authorization_url = 4; + optional string token_url = 5; } message UpdateAuthProviderResponse { @@ -130,6 +132,8 @@ message AuthProvider { message OAuth2Config { string client_id = 1; string client_secret = 2; + string authorization_url = 3; + string token_url = 4; } enum AuthProviderType { diff --git a/components/public-api/go/v1/authprovider.pb.go b/components/public-api/go/v1/authprovider.pb.go index fd29fdc1e02743..27d7effd0856fc 100644 --- a/components/public-api/go/v1/authprovider.pb.go +++ b/components/public-api/go/v1/authprovider.pb.go @@ -623,9 +623,11 @@ type UpdateAuthProviderRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - AuthProviderId string `protobuf:"bytes,1,opt,name=auth_provider_id,json=authProviderId,proto3" json:"auth_provider_id,omitempty"` - ClientId *string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3,oneof" json:"client_id,omitempty"` - ClientSecret *string `protobuf:"bytes,3,opt,name=client_secret,json=clientSecret,proto3,oneof" json:"client_secret,omitempty"` + AuthProviderId string `protobuf:"bytes,1,opt,name=auth_provider_id,json=authProviderId,proto3" json:"auth_provider_id,omitempty"` + ClientId *string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3,oneof" json:"client_id,omitempty"` + ClientSecret *string `protobuf:"bytes,3,opt,name=client_secret,json=clientSecret,proto3,oneof" json:"client_secret,omitempty"` + AuthorizationUrl *string `protobuf:"bytes,4,opt,name=authorization_url,json=authorizationUrl,proto3,oneof" json:"authorization_url,omitempty"` + TokenUrl *string `protobuf:"bytes,5,opt,name=token_url,json=tokenUrl,proto3,oneof" json:"token_url,omitempty"` } func (x *UpdateAuthProviderRequest) Reset() { @@ -681,6 +683,20 @@ func (x *UpdateAuthProviderRequest) GetClientSecret() string { return "" } +func (x *UpdateAuthProviderRequest) GetAuthorizationUrl() string { + if x != nil && x.AuthorizationUrl != nil { + return *x.AuthorizationUrl + } + return "" +} + +func (x *UpdateAuthProviderRequest) GetTokenUrl() string { + if x != nil && x.TokenUrl != nil { + return *x.TokenUrl + } + return "" +} + type UpdateAuthProviderResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1058,8 +1074,10 @@ type OAuth2Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - ClientSecret string `protobuf:"bytes,2,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientSecret string `protobuf:"bytes,2,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` + AuthorizationUrl string `protobuf:"bytes,3,opt,name=authorization_url,json=authorizationUrl,proto3" json:"authorization_url,omitempty"` + TokenUrl string `protobuf:"bytes,4,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` } func (x *OAuth2Config) Reset() { @@ -1108,6 +1126,20 @@ func (x *OAuth2Config) GetClientSecret() string { return "" } +func (x *OAuth2Config) GetAuthorizationUrl() string { + if x != nil { + return x.AuthorizationUrl + } + return "" +} + +func (x *OAuth2Config) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + var File_gitpod_v1_authprovider_proto protoreflect.FileDescriptor var file_gitpod_v1_authprovider_proto_rawDesc = []byte{ @@ -1188,7 +1220,7 @@ var file_gitpod_v1_authprovider_proto_rawDesc = []byte{ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb1, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa9, 0x02, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, @@ -1197,124 +1229,136 @@ var file_gitpod_v1_authprovider_proto_rawDesc = []byte{ 0x09, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x28, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x22, 0x5a, 0x0a, 0x1a, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x45, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x22, 0x1c, 0x0a, - 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x17, - 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, - 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0xa2, 0x03, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x12, 0x29, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x72, 0x67, - 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x70, - 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, - 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, - 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, - 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, - 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x12, 0x3c, 0x0a, 0x0d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x32, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x0c, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x07, - 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0x50, 0x0a, 0x0c, 0x4f, 0x41, 0x75, 0x74, 0x68, - 0x32, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2a, 0xe4, 0x01, 0x0a, 0x10, 0x41, 0x75, - 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, - 0x0a, 0x1e, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, - 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x10, - 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, - 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x4c, 0x41, 0x42, 0x10, 0x02, - 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, - 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, - 0x10, 0x03, 0x12, 0x27, 0x0a, 0x23, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, - 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, 0x4b, - 0x45, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x41, - 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x44, 0x45, 0x56, 0x4f, 0x50, 0x53, 0x10, 0x05, - 0x32, 0x86, 0x05, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, - 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, - 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x12, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x11, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x10, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x03, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x0c, + 0x0a, 0x0a, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x42, 0x10, 0x0a, 0x0e, + 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x14, + 0x0a, 0x12, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x75, 0x72, 0x6c, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x75, + 0x72, 0x6c, 0x22, 0x5a, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3c, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x45, + 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x49, 0x64, 0x22, 0x1c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x17, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa2, 0x03, 0x0a, 0x0c, 0x41, + 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x07, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, + 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, + 0x0a, 0x0c, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x55, 0x72, + 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0b, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0d, 0x6f, 0x61, 0x75, 0x74, + 0x68, 0x32, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x41, 0x75, 0x74, + 0x68, 0x32, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x07, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, + 0x9a, 0x01, 0x0a, 0x0c, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x32, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, + 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, + 0x1b, 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x72, 0x6c, 0x2a, 0xe4, 0x01, 0x0a, + 0x10, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x22, 0x0a, 0x1e, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, + 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, + 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, + 0x55, 0x42, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, + 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x4c, 0x41, + 0x42, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, + 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, + 0x4b, 0x45, 0x54, 0x10, 0x03, 0x12, 0x27, 0x0a, 0x23, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, + 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, + 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x04, 0x12, 0x23, + 0x0a, 0x1f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x44, 0x45, 0x56, 0x4f, 0x50, + 0x53, 0x10, 0x05, 0x32, 0x86, 0x05, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x5a, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x60, 0x0a, 0x11, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x23, - 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x1c, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x60, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x2e, 0x67, - 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x67, - 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x63, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, - 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, - 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x0a, 0x16, 0x69, 0x6f, 0x2e, - 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x70, 0x69, - 0x2e, 0x76, 0x31, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, - 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x12, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, + 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x2e, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x63, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, + 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, + 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x0a, 0x16, + 0x69, 0x6f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, + 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java b/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java index 3879e57392edcb..d85ebb3351cb7a 100644 --- a/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java +++ b/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Authprovider.java @@ -7121,6 +7121,40 @@ public interface UpdateAuthProviderRequestOrBuilder extends */ com.google.protobuf.ByteString getClientSecretBytes(); + + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return Whether the authorizationUrl field is set. + */ + boolean hasAuthorizationUrl(); + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return The authorizationUrl. + */ + java.lang.String getAuthorizationUrl(); + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return The bytes for authorizationUrl. + */ + com.google.protobuf.ByteString + getAuthorizationUrlBytes(); + + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return Whether the tokenUrl field is set. + */ + boolean hasTokenUrl(); + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return The tokenUrl. + */ + java.lang.String getTokenUrl(); + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return The bytes for tokenUrl. + */ + com.google.protobuf.ByteString + getTokenUrlBytes(); } /** * Protobuf type {@code gitpod.v1.UpdateAuthProviderRequest} @@ -7147,6 +7181,8 @@ private UpdateAuthProviderRequest() { authProviderId_ = ""; clientId_ = ""; clientSecret_ = ""; + authorizationUrl_ = ""; + tokenUrl_ = ""; } public static final com.google.protobuf.Descriptors.Descriptor @@ -7296,6 +7332,100 @@ public java.lang.String getClientSecret() { } } + public static final int AUTHORIZATION_URL_FIELD_NUMBER = 4; + @SuppressWarnings("serial") + private volatile java.lang.Object authorizationUrl_ = ""; + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return Whether the authorizationUrl field is set. + */ + @java.lang.Override + public boolean hasAuthorizationUrl() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return The authorizationUrl. + */ + @java.lang.Override + public java.lang.String getAuthorizationUrl() { + java.lang.Object ref = authorizationUrl_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + authorizationUrl_ = s; + return s; + } + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return The bytes for authorizationUrl. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getAuthorizationUrlBytes() { + java.lang.Object ref = authorizationUrl_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + authorizationUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int TOKEN_URL_FIELD_NUMBER = 5; + @SuppressWarnings("serial") + private volatile java.lang.Object tokenUrl_ = ""; + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return Whether the tokenUrl field is set. + */ + @java.lang.Override + public boolean hasTokenUrl() { + return ((bitField0_ & 0x00000008) != 0); + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return The tokenUrl. + */ + @java.lang.Override + public java.lang.String getTokenUrl() { + java.lang.Object ref = tokenUrl_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tokenUrl_ = s; + return s; + } + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return The bytes for tokenUrl. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getTokenUrlBytes() { + java.lang.Object ref = tokenUrl_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tokenUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -7319,6 +7449,12 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (((bitField0_ & 0x00000002) != 0)) { com.google.protobuf.GeneratedMessage.writeString(output, 3, clientSecret_); } + if (((bitField0_ & 0x00000004) != 0)) { + com.google.protobuf.GeneratedMessage.writeString(output, 4, authorizationUrl_); + } + if (((bitField0_ & 0x00000008) != 0)) { + com.google.protobuf.GeneratedMessage.writeString(output, 5, tokenUrl_); + } getUnknownFields().writeTo(output); } @@ -7337,6 +7473,12 @@ public int getSerializedSize() { if (((bitField0_ & 0x00000002) != 0)) { size += com.google.protobuf.GeneratedMessage.computeStringSize(3, clientSecret_); } + if (((bitField0_ & 0x00000004) != 0)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(4, authorizationUrl_); + } + if (((bitField0_ & 0x00000008) != 0)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(5, tokenUrl_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -7364,6 +7506,16 @@ public boolean equals(final java.lang.Object obj) { if (!getClientSecret() .equals(other.getClientSecret())) return false; } + if (hasAuthorizationUrl() != other.hasAuthorizationUrl()) return false; + if (hasAuthorizationUrl()) { + if (!getAuthorizationUrl() + .equals(other.getAuthorizationUrl())) return false; + } + if (hasTokenUrl() != other.hasTokenUrl()) return false; + if (hasTokenUrl()) { + if (!getTokenUrl() + .equals(other.getTokenUrl())) return false; + } if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -7385,6 +7537,14 @@ public int hashCode() { hash = (37 * hash) + CLIENT_SECRET_FIELD_NUMBER; hash = (53 * hash) + getClientSecret().hashCode(); } + if (hasAuthorizationUrl()) { + hash = (37 * hash) + AUTHORIZATION_URL_FIELD_NUMBER; + hash = (53 * hash) + getAuthorizationUrl().hashCode(); + } + if (hasTokenUrl()) { + hash = (37 * hash) + TOKEN_URL_FIELD_NUMBER; + hash = (53 * hash) + getTokenUrl().hashCode(); + } hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -7519,6 +7679,8 @@ public Builder clear() { authProviderId_ = ""; clientId_ = ""; clientSecret_ = ""; + authorizationUrl_ = ""; + tokenUrl_ = ""; return this; } @@ -7564,6 +7726,14 @@ private void buildPartial0(io.gitpod.publicapi.v1.Authprovider.UpdateAuthProvide result.clientSecret_ = clientSecret_; to_bitField0_ |= 0x00000002; } + if (((from_bitField0_ & 0x00000008) != 0)) { + result.authorizationUrl_ = authorizationUrl_; + to_bitField0_ |= 0x00000004; + } + if (((from_bitField0_ & 0x00000010) != 0)) { + result.tokenUrl_ = tokenUrl_; + to_bitField0_ |= 0x00000008; + } result.bitField0_ |= to_bitField0_; } @@ -7594,6 +7764,16 @@ public Builder mergeFrom(io.gitpod.publicapi.v1.Authprovider.UpdateAuthProviderR bitField0_ |= 0x00000004; onChanged(); } + if (other.hasAuthorizationUrl()) { + authorizationUrl_ = other.authorizationUrl_; + bitField0_ |= 0x00000008; + onChanged(); + } + if (other.hasTokenUrl()) { + tokenUrl_ = other.tokenUrl_; + bitField0_ |= 0x00000010; + onChanged(); + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -7635,6 +7815,16 @@ public Builder mergeFrom( bitField0_ |= 0x00000004; break; } // case 26 + case 34: { + authorizationUrl_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000008; + break; + } // case 34 + case 42: { + tokenUrl_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000010; + break; + } // case 42 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -7882,6 +8072,164 @@ public Builder setClientSecretBytes( return this; } + private java.lang.Object authorizationUrl_ = ""; + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return Whether the authorizationUrl field is set. + */ + public boolean hasAuthorizationUrl() { + return ((bitField0_ & 0x00000008) != 0); + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return The authorizationUrl. + */ + public java.lang.String getAuthorizationUrl() { + java.lang.Object ref = authorizationUrl_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + authorizationUrl_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return The bytes for authorizationUrl. + */ + public com.google.protobuf.ByteString + getAuthorizationUrlBytes() { + java.lang.Object ref = authorizationUrl_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + authorizationUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @param value The authorizationUrl to set. + * @return This builder for chaining. + */ + public Builder setAuthorizationUrl( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + authorizationUrl_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @return This builder for chaining. + */ + public Builder clearAuthorizationUrl() { + authorizationUrl_ = getDefaultInstance().getAuthorizationUrl(); + bitField0_ = (bitField0_ & ~0x00000008); + onChanged(); + return this; + } + /** + * optional string authorization_url = 4 [json_name = "authorizationUrl"]; + * @param value The bytes for authorizationUrl to set. + * @return This builder for chaining. + */ + public Builder setAuthorizationUrlBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + authorizationUrl_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + + private java.lang.Object tokenUrl_ = ""; + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return Whether the tokenUrl field is set. + */ + public boolean hasTokenUrl() { + return ((bitField0_ & 0x00000010) != 0); + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return The tokenUrl. + */ + public java.lang.String getTokenUrl() { + java.lang.Object ref = tokenUrl_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tokenUrl_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return The bytes for tokenUrl. + */ + public com.google.protobuf.ByteString + getTokenUrlBytes() { + java.lang.Object ref = tokenUrl_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tokenUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @param value The tokenUrl to set. + * @return This builder for chaining. + */ + public Builder setTokenUrl( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + tokenUrl_ = value; + bitField0_ |= 0x00000010; + onChanged(); + return this; + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @return This builder for chaining. + */ + public Builder clearTokenUrl() { + tokenUrl_ = getDefaultInstance().getTokenUrl(); + bitField0_ = (bitField0_ & ~0x00000010); + onChanged(); + return this; + } + /** + * optional string token_url = 5 [json_name = "tokenUrl"]; + * @param value The bytes for tokenUrl to set. + * @return This builder for chaining. + */ + public Builder setTokenUrlBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + tokenUrl_ = value; + bitField0_ |= 0x00000010; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:gitpod.v1.UpdateAuthProviderRequest) } @@ -12686,6 +13034,30 @@ public interface OAuth2ConfigOrBuilder extends */ com.google.protobuf.ByteString getClientSecretBytes(); + + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return The authorizationUrl. + */ + java.lang.String getAuthorizationUrl(); + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return The bytes for authorizationUrl. + */ + com.google.protobuf.ByteString + getAuthorizationUrlBytes(); + + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return The tokenUrl. + */ + java.lang.String getTokenUrl(); + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return The bytes for tokenUrl. + */ + com.google.protobuf.ByteString + getTokenUrlBytes(); } /** * Protobuf type {@code gitpod.v1.OAuth2Config} @@ -12711,6 +13083,8 @@ private OAuth2Config(com.google.protobuf.GeneratedMessage.Builder builder) { private OAuth2Config() { clientId_ = ""; clientSecret_ = ""; + authorizationUrl_ = ""; + tokenUrl_ = ""; } public static final com.google.protobuf.Descriptors.Descriptor @@ -12804,6 +13178,84 @@ public java.lang.String getClientSecret() { } } + public static final int AUTHORIZATION_URL_FIELD_NUMBER = 3; + @SuppressWarnings("serial") + private volatile java.lang.Object authorizationUrl_ = ""; + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return The authorizationUrl. + */ + @java.lang.Override + public java.lang.String getAuthorizationUrl() { + java.lang.Object ref = authorizationUrl_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + authorizationUrl_ = s; + return s; + } + } + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return The bytes for authorizationUrl. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getAuthorizationUrlBytes() { + java.lang.Object ref = authorizationUrl_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + authorizationUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int TOKEN_URL_FIELD_NUMBER = 4; + @SuppressWarnings("serial") + private volatile java.lang.Object tokenUrl_ = ""; + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return The tokenUrl. + */ + @java.lang.Override + public java.lang.String getTokenUrl() { + java.lang.Object ref = tokenUrl_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tokenUrl_ = s; + return s; + } + } + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return The bytes for tokenUrl. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getTokenUrlBytes() { + java.lang.Object ref = tokenUrl_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tokenUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -12824,6 +13276,12 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (!com.google.protobuf.GeneratedMessage.isStringEmpty(clientSecret_)) { com.google.protobuf.GeneratedMessage.writeString(output, 2, clientSecret_); } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(authorizationUrl_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 3, authorizationUrl_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(tokenUrl_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 4, tokenUrl_); + } getUnknownFields().writeTo(output); } @@ -12839,6 +13297,12 @@ public int getSerializedSize() { if (!com.google.protobuf.GeneratedMessage.isStringEmpty(clientSecret_)) { size += com.google.protobuf.GeneratedMessage.computeStringSize(2, clientSecret_); } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(authorizationUrl_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(3, authorizationUrl_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(tokenUrl_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(4, tokenUrl_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -12858,6 +13322,10 @@ public boolean equals(final java.lang.Object obj) { .equals(other.getClientId())) return false; if (!getClientSecret() .equals(other.getClientSecret())) return false; + if (!getAuthorizationUrl() + .equals(other.getAuthorizationUrl())) return false; + if (!getTokenUrl() + .equals(other.getTokenUrl())) return false; if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -12873,6 +13341,10 @@ public int hashCode() { hash = (53 * hash) + getClientId().hashCode(); hash = (37 * hash) + CLIENT_SECRET_FIELD_NUMBER; hash = (53 * hash) + getClientSecret().hashCode(); + hash = (37 * hash) + AUTHORIZATION_URL_FIELD_NUMBER; + hash = (53 * hash) + getAuthorizationUrl().hashCode(); + hash = (37 * hash) + TOKEN_URL_FIELD_NUMBER; + hash = (53 * hash) + getTokenUrl().hashCode(); hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -13006,6 +13478,8 @@ public Builder clear() { bitField0_ = 0; clientId_ = ""; clientSecret_ = ""; + authorizationUrl_ = ""; + tokenUrl_ = ""; return this; } @@ -13045,6 +13519,12 @@ private void buildPartial0(io.gitpod.publicapi.v1.Authprovider.OAuth2Config resu if (((from_bitField0_ & 0x00000002) != 0)) { result.clientSecret_ = clientSecret_; } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.authorizationUrl_ = authorizationUrl_; + } + if (((from_bitField0_ & 0x00000008) != 0)) { + result.tokenUrl_ = tokenUrl_; + } } @java.lang.Override @@ -13069,6 +13549,16 @@ public Builder mergeFrom(io.gitpod.publicapi.v1.Authprovider.OAuth2Config other) bitField0_ |= 0x00000002; onChanged(); } + if (!other.getAuthorizationUrl().isEmpty()) { + authorizationUrl_ = other.authorizationUrl_; + bitField0_ |= 0x00000004; + onChanged(); + } + if (!other.getTokenUrl().isEmpty()) { + tokenUrl_ = other.tokenUrl_; + bitField0_ |= 0x00000008; + onChanged(); + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -13105,6 +13595,16 @@ public Builder mergeFrom( bitField0_ |= 0x00000002; break; } // case 18 + case 26: { + authorizationUrl_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000004; + break; + } // case 26 + case 34: { + tokenUrl_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000008; + break; + } // case 34 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -13266,6 +13766,150 @@ public Builder setClientSecretBytes( return this; } + private java.lang.Object authorizationUrl_ = ""; + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return The authorizationUrl. + */ + public java.lang.String getAuthorizationUrl() { + java.lang.Object ref = authorizationUrl_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + authorizationUrl_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return The bytes for authorizationUrl. + */ + public com.google.protobuf.ByteString + getAuthorizationUrlBytes() { + java.lang.Object ref = authorizationUrl_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + authorizationUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @param value The authorizationUrl to set. + * @return This builder for chaining. + */ + public Builder setAuthorizationUrl( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + authorizationUrl_ = value; + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @return This builder for chaining. + */ + public Builder clearAuthorizationUrl() { + authorizationUrl_ = getDefaultInstance().getAuthorizationUrl(); + bitField0_ = (bitField0_ & ~0x00000004); + onChanged(); + return this; + } + /** + * string authorization_url = 3 [json_name = "authorizationUrl"]; + * @param value The bytes for authorizationUrl to set. + * @return This builder for chaining. + */ + public Builder setAuthorizationUrlBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + authorizationUrl_ = value; + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + + private java.lang.Object tokenUrl_ = ""; + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return The tokenUrl. + */ + public java.lang.String getTokenUrl() { + java.lang.Object ref = tokenUrl_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tokenUrl_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return The bytes for tokenUrl. + */ + public com.google.protobuf.ByteString + getTokenUrlBytes() { + java.lang.Object ref = tokenUrl_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tokenUrl_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @param value The tokenUrl to set. + * @return This builder for chaining. + */ + public Builder setTokenUrl( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + tokenUrl_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @return This builder for chaining. + */ + public Builder clearTokenUrl() { + tokenUrl_ = getDefaultInstance().getTokenUrl(); + bitField0_ = (bitField0_ & ~0x00000008); + onChanged(); + return this; + } + /** + * string token_url = 4 [json_name = "tokenUrl"]; + * @param value The bytes for tokenUrl to set. + * @return This builder for chaining. + */ + public Builder setTokenUrlBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + tokenUrl_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:gitpod.v1.OAuth2Config) } @@ -13431,12 +14075,15 @@ public io.gitpod.publicapi.v1.Authprovider.OAuth2Config getDefaultInstanceForTyp "derDescriptionsResponse\022F\n\014descriptions\030" + "\001 \003(\0132\".gitpod.v1.AuthProviderDescriptio" + "nR\014descriptions\022=\n\npagination\030\002 \001(\0132\035.gi" + - "tpod.v1.PaginationResponseR\npagination\"\261" + - "\001\n\031UpdateAuthProviderRequest\022(\n\020auth_pro" + + "tpod.v1.PaginationResponseR\npagination\"\251" + + "\002\n\031UpdateAuthProviderRequest\022(\n\020auth_pro" + "vider_id\030\001 \001(\tR\016authProviderId\022 \n\tclient" + "_id\030\002 \001(\tH\000R\010clientId\210\001\001\022(\n\rclient_secre" + - "t\030\003 \001(\tH\001R\014clientSecret\210\001\001B\014\n\n_client_id" + - "B\020\n\016_client_secret\"Z\n\032UpdateAuthProvider" + + "t\030\003 \001(\tH\001R\014clientSecret\210\001\001\0220\n\021authorizat" + + "ion_url\030\004 \001(\tH\002R\020authorizationUrl\210\001\001\022 \n\t" + + "token_url\030\005 \001(\tH\003R\010tokenUrl\210\001\001B\014\n\n_clien" + + "t_idB\020\n\016_client_secretB\024\n\022_authorization" + + "_urlB\014\n\n_token_url\"Z\n\032UpdateAuthProvider" + "Response\022<\n\rauth_provider\030\001 \001(\0132\027.gitpod" + ".v1.AuthProviderR\014authProvider\"E\n\031Delete" + "AuthProviderRequest\022(\n\020auth_provider_id\030" + @@ -13456,33 +14103,35 @@ public io.gitpod.publicapi.v1.Authprovider.OAuth2Config getDefaultInstanceForTyp "login\030\n \001(\010R\013enableLogin\022\026\n\006scopes\030\013 \003(\t" + "R\006scopes\022<\n\roauth2_config\030\014 \001(\0132\027.gitpod" + ".v1.OAuth2ConfigR\014oauth2ConfigB\007\n\005owner\"" + - "P\n\014OAuth2Config\022\033\n\tclient_id\030\001 \001(\tR\010clie" + - "ntId\022#\n\rclient_secret\030\002 \001(\tR\014clientSecre" + - "t*\344\001\n\020AuthProviderType\022\"\n\036AUTH_PROVIDER_" + - "TYPE_UNSPECIFIED\020\000\022\035\n\031AUTH_PROVIDER_TYPE" + - "_GITHUB\020\001\022\035\n\031AUTH_PROVIDER_TYPE_GITLAB\020\002" + - "\022 \n\034AUTH_PROVIDER_TYPE_BITBUCKET\020\003\022\'\n#AU" + - "TH_PROVIDER_TYPE_BITBUCKET_SERVER\020\004\022#\n\037A" + - "UTH_PROVIDER_TYPE_AZURE_DEVOPS\020\0052\206\005\n\023Aut" + - "hProviderService\022c\n\022CreateAuthProvider\022$" + - ".gitpod.v1.CreateAuthProviderRequest\032%.g" + - "itpod.v1.CreateAuthProviderResponse\"\000\022Z\n" + - "\017GetAuthProvider\022!.gitpod.v1.GetAuthProv" + - "iderRequest\032\".gitpod.v1.GetAuthProviderR" + - "esponse\"\000\022`\n\021ListAuthProviders\022#.gitpod." + - "v1.ListAuthProvidersRequest\032$.gitpod.v1." + - "ListAuthProvidersResponse\"\000\022\201\001\n\034ListAuth" + - "ProviderDescriptions\022..gitpod.v1.ListAut" + - "hProviderDescriptionsRequest\032/.gitpod.v1" + - ".ListAuthProviderDescriptionsResponse\"\000\022" + - "c\n\022UpdateAuthProvider\022$.gitpod.v1.Update" + - "AuthProviderRequest\032%.gitpod.v1.UpdateAu" + - "thProviderResponse\"\000\022c\n\022DeleteAuthProvid" + - "er\022$.gitpod.v1.DeleteAuthProviderRequest" + - "\032%.gitpod.v1.DeleteAuthProviderResponse\"" + - "\000BQ\n\026io.gitpod.publicapi.v1Z7github.com/" + - "gitpod-io/gitpod/components/public-api/g" + - "o/v1b\006proto3" + "\232\001\n\014OAuth2Config\022\033\n\tclient_id\030\001 \001(\tR\010cli" + + "entId\022#\n\rclient_secret\030\002 \001(\tR\014clientSecr" + + "et\022+\n\021authorization_url\030\003 \001(\tR\020authoriza" + + "tionUrl\022\033\n\ttoken_url\030\004 \001(\tR\010tokenUrl*\344\001\n" + + "\020AuthProviderType\022\"\n\036AUTH_PROVIDER_TYPE_" + + "UNSPECIFIED\020\000\022\035\n\031AUTH_PROVIDER_TYPE_GITH" + + "UB\020\001\022\035\n\031AUTH_PROVIDER_TYPE_GITLAB\020\002\022 \n\034A" + + "UTH_PROVIDER_TYPE_BITBUCKET\020\003\022\'\n#AUTH_PR" + + "OVIDER_TYPE_BITBUCKET_SERVER\020\004\022#\n\037AUTH_P" + + "ROVIDER_TYPE_AZURE_DEVOPS\020\0052\206\005\n\023AuthProv" + + "iderService\022c\n\022CreateAuthProvider\022$.gitp" + + "od.v1.CreateAuthProviderRequest\032%.gitpod" + + ".v1.CreateAuthProviderResponse\"\000\022Z\n\017GetA" + + "uthProvider\022!.gitpod.v1.GetAuthProviderR" + + "equest\032\".gitpod.v1.GetAuthProviderRespon" + + "se\"\000\022`\n\021ListAuthProviders\022#.gitpod.v1.Li" + + "stAuthProvidersRequest\032$.gitpod.v1.ListA" + + "uthProvidersResponse\"\000\022\201\001\n\034ListAuthProvi" + + "derDescriptions\022..gitpod.v1.ListAuthProv" + + "iderDescriptionsRequest\032/.gitpod.v1.List" + + "AuthProviderDescriptionsResponse\"\000\022c\n\022Up" + + "dateAuthProvider\022$.gitpod.v1.UpdateAuthP" + + "roviderRequest\032%.gitpod.v1.UpdateAuthPro" + + "viderResponse\"\000\022c\n\022DeleteAuthProvider\022$." + + "gitpod.v1.DeleteAuthProviderRequest\032%.gi" + + "tpod.v1.DeleteAuthProviderResponse\"\000BQ\n\026" + + "io.gitpod.publicapi.v1Z7github.com/gitpo" + + "d-io/gitpod/components/public-api/go/v1b" + + "\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -13542,7 +14191,7 @@ public io.gitpod.publicapi.v1.Authprovider.OAuth2Config getDefaultInstanceForTyp internal_static_gitpod_v1_UpdateAuthProviderRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_gitpod_v1_UpdateAuthProviderRequest_descriptor, - new java.lang.String[] { "AuthProviderId", "ClientId", "ClientSecret", }); + new java.lang.String[] { "AuthProviderId", "ClientId", "ClientSecret", "AuthorizationUrl", "TokenUrl", }); internal_static_gitpod_v1_UpdateAuthProviderResponse_descriptor = getDescriptor().getMessageTypes().get(9); internal_static_gitpod_v1_UpdateAuthProviderResponse_fieldAccessorTable = new @@ -13578,7 +14227,7 @@ public io.gitpod.publicapi.v1.Authprovider.OAuth2Config getDefaultInstanceForTyp internal_static_gitpod_v1_OAuth2Config_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_gitpod_v1_OAuth2Config_descriptor, - new java.lang.String[] { "ClientId", "ClientSecret", }); + new java.lang.String[] { "ClientId", "ClientSecret", "AuthorizationUrl", "TokenUrl", }); descriptor.resolveAllFeaturesImmutable(); io.gitpod.publicapi.v1.Pagination.getDescriptor(); } diff --git a/components/public-api/typescript-common/fixtures/toAuthProvider_1.golden b/components/public-api/typescript-common/fixtures/toAuthProvider_1.golden index dca9fd44e7f0f6..3779d2ecec9c81 100644 --- a/components/public-api/typescript-common/fixtures/toAuthProvider_1.golden +++ b/components/public-api/typescript-common/fixtures/toAuthProvider_1.golden @@ -12,7 +12,9 @@ "scopes": [], "oauth2Config": { "clientId": "clientId123", - "clientSecret": "should not appear in result" + "clientSecret": "should not appear in result", + "authorizationUrl": "auth.service/authorize", + "tokenUrl": "auth.service/token" } }, "err": "" diff --git a/components/public-api/typescript-common/src/public-api-converter.ts b/components/public-api/typescript-common/src/public-api-converter.ts index 4ee82737edb2d7..836b91eabf192a 100644 --- a/components/public-api/typescript-common/src/public-api-converter.ts +++ b/components/public-api/typescript-common/src/public-api-converter.ts @@ -1190,6 +1190,8 @@ export class PublicAPIConverter { return new OAuth2Config({ clientId: ap.oauth?.clientId, clientSecret: ap.oauth?.clientSecret, + authorizationUrl: ap.oauth?.authorizationUrl, + tokenUrl: ap.oauth?.tokenUrl, }); } diff --git a/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts b/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts index 0e659d548fcc34..83b877112befe1 100644 --- a/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts +++ b/components/public-api/typescript/src/gitpod/v1/authprovider_pb.ts @@ -455,6 +455,16 @@ export class UpdateAuthProviderRequest extends Message) { super(); proto3.util.initPartial(data, this); @@ -466,6 +476,8 @@ export class UpdateAuthProviderRequest extends Message): UpdateAuthProviderRequest { @@ -775,6 +787,16 @@ export class OAuth2Config extends Message { */ clientSecret = ""; + /** + * @generated from field: string authorization_url = 3; + */ + authorizationUrl = ""; + + /** + * @generated from field: string token_url = 4; + */ + tokenUrl = ""; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -785,6 +807,8 @@ export class OAuth2Config extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "client_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "client_secret", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "authorization_url", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "token_url", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OAuth2Config { From 11b738f01490a50c4b802b84f813865891b69b6c Mon Sep 17 00:00:00 2001 From: mustard Date: Thu, 12 Sep 2024 06:26:43 +0000 Subject: [PATCH 04/44] fixup --- components/gitpod-protocol/src/protocol.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 20f85742ed2c25..1a81957ff294bd 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -1413,11 +1413,11 @@ export namespace AuthProviderEntry { export type NewEntry = Pick & { clientId?: string; clientSecret?: string; - } & CustomOAuth2Config; + } & OAuth2CustomConfig; export type UpdateEntry = Pick & { clientId?: string; clientSecret?: string; - } & CustomOAuth2Config; + } & OAuth2CustomConfig; export type NewOrgEntry = NewEntry & { organizationId: string; }; @@ -1425,8 +1425,8 @@ export namespace AuthProviderEntry { clientId?: string; clientSecret?: string; organizationId: string; - } & CustomOAuth2Config; - export type UpdateOAuth2Config = Pick & CustomOAuth2Config; + } & OAuth2CustomConfig; + export type UpdateOAuth2Config = Pick & OAuth2CustomConfig; export function redact(entry: AuthProviderEntry): AuthProviderEntry { return { ...entry, From 11d18a8b0a29b2bac44166a16e32c8f2ecedbad2 Mon Sep 17 00:00:00 2001 From: mustard Date: Thu, 12 Sep 2024 16:21:32 +0000 Subject: [PATCH 05/44] [server] add azure support --- .../server/src/auth/auth-provider-service.ts | 8 + .../server/src/auth/host-container-mapping.ts | 3 + .../server/src/azure-devops/azure-api.ts | 193 +++++ .../src/azure-devops/azure-auth-provider.ts | 93 +++ .../azure-devops/azure-container-module.ts | 34 + .../azure-devops/azure-context-parser.spec.ts | 676 ++++++++++++++++++ .../src/azure-devops/azure-context-parser.ts | 261 +++++++ .../src/azure-devops/azure-converter.ts | 46 ++ .../azure-devops/azure-file-provider.spec.ts | 96 +++ .../src/azure-devops/azure-file-provider.ts | 80 +++ .../azure-repository-provider.spec.ts | 69 ++ .../azure-devops/azure-repository-provider.ts | 89 +++ .../src/azure-devops/azure-token-helper.ts | 56 ++ .../src/azure-devops/azure-token-validator.ts | 36 + .../server/src/azure-devops/azure-urls.ts | 15 + components/server/src/azure-devops/scopes.ts | 22 + 16 files changed, 1777 insertions(+) create mode 100644 components/server/src/azure-devops/azure-api.ts create mode 100644 components/server/src/azure-devops/azure-auth-provider.ts create mode 100644 components/server/src/azure-devops/azure-container-module.ts create mode 100644 components/server/src/azure-devops/azure-context-parser.spec.ts create mode 100644 components/server/src/azure-devops/azure-context-parser.ts create mode 100644 components/server/src/azure-devops/azure-converter.ts create mode 100644 components/server/src/azure-devops/azure-file-provider.spec.ts create mode 100644 components/server/src/azure-devops/azure-file-provider.ts create mode 100644 components/server/src/azure-devops/azure-repository-provider.spec.ts create mode 100644 components/server/src/azure-devops/azure-repository-provider.ts create mode 100644 components/server/src/azure-devops/azure-token-helper.ts create mode 100644 components/server/src/azure-devops/azure-token-validator.ts create mode 100644 components/server/src/azure-devops/azure-urls.ts create mode 100644 components/server/src/azure-devops/scopes.ts diff --git a/components/server/src/auth/auth-provider-service.ts b/components/server/src/auth/auth-provider-service.ts index 3d8f2c3a7a1902..cd985d98c467e3 100644 --- a/components/server/src/auth/auth-provider-service.ts +++ b/components/server/src/auth/auth-provider-service.ts @@ -14,6 +14,7 @@ import { oauthUrls as githubUrls } from "../github/github-urls"; import { oauthUrls as gitlabUrls } from "../gitlab/gitlab-urls"; import { oauthUrls as bbsUrls } from "../bitbucket-server/bitbucket-server-urls"; import { oauthUrls as bbUrls } from "../bitbucket/bitbucket-urls"; +import { oauthUrls as azureUrls } from "../azure-devops/azure-urls"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import fetch from "node-fetch"; import { Authorizer } from "../authorization/authorizer"; @@ -338,6 +339,13 @@ export class AuthProviderService { case "Bitbucket": urls = bbUrls(host); break; + case "Azure DevOps": + const { authorizationUrl, tokenUrl } = newEntry; + if (!authorizationUrl || !tokenUrl) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "authorizationUrl and tokenUrl are required."); + } + urls = azureUrls({ authorizationUrl, tokenUrl }); + break; } if (!urls) { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Unexpected service type."); diff --git a/components/server/src/auth/host-container-mapping.ts b/components/server/src/auth/host-container-mapping.ts index 37933287b1045d..6e21114adef33c 100644 --- a/components/server/src/auth/host-container-mapping.ts +++ b/components/server/src/auth/host-container-mapping.ts @@ -10,6 +10,7 @@ import { gitlabContainerModule } from "../gitlab/gitlab-container-module"; import { genericAuthContainerModule } from "./oauth-container-module"; import { bitbucketContainerModule } from "../bitbucket/bitbucket-container-module"; import { bitbucketServerContainerModule } from "../bitbucket-server/bitbucket-server-container-module"; +import { azureDevOpsContainerModule } from "../azure-devops/azure-container-module"; @injectable() export class HostContainerMapping { @@ -25,6 +26,8 @@ export class HostContainerMapping { return [bitbucketContainerModule]; case "BitbucketServer": return [bitbucketServerContainerModule]; + case "Azure DevOps": + return [azureDevOpsContainerModule]; default: return undefined; } diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts new file mode 100644 index 00000000000000..8e04b82cd30887 --- /dev/null +++ b/components/server/src/azure-devops/azure-api.ts @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { injectable, inject } from "inversify"; +import { Commit, User } from "@gitpod/gitpod-protocol"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; + +import { AuthProviderParams } from "../auth/auth-provider"; +import { AzureDevOpsTokenHelper } from "./azure-token-helper"; +import { WebApi, getBearerHandler } from "azure-devops-node-api"; +import { AzureDevOpsScopes } from "./scopes"; +import { MaybeContent } from "../repohost"; +import { GitVersionDescriptor, GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces"; + +@injectable() +export class AzureDevOpsApi { + @inject(AuthProviderParams) readonly config: AuthProviderParams; + @inject(AzureDevOpsTokenHelper) protected readonly tokenHelper: AzureDevOpsTokenHelper; + + private async create(userOrToken: User | string, serverUrl?: string) { + let bearerToken: string | undefined; + if (typeof userOrToken === "string") { + bearerToken = userOrToken; + } else { + const azureToken = await this.tokenHelper.getTokenWithScopes( + userOrToken, + AzureDevOpsScopes.Requirements.DEFAULT, + ); + bearerToken = azureToken.value; + } + return new WebApi(this.config.host, getBearerHandler(bearerToken)); + } + + private async createGitApi(userOrToken: User | string) { + const api = await this.create(userOrToken); + return api.getGitApi(); + } + + private getVersionDescriptor(commit: Pick): GitVersionDescriptor { + const result: GitVersionDescriptor = {}; + if (commit.refType === "revision") { + result.versionType = GitVersionType.Commit; + result.version = commit.revision; + } + if (commit.refType === "branch") { + result.versionType = GitVersionType.Branch; + result.version = commit.ref; + } + if (commit.refType === "tag") { + result.versionType = GitVersionType.Tag; + result.version = commit.ref; + } + return result; + } + + /** + * + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/commits/get-commits + */ + async getCommits( + userOrToken: User | string, + repository: string, + azProject: string, + opts?: Partial<{ + filterCommit: Pick; + $top: number; + itemPath: string; + }>, + ) { + const gitApi = await this.createGitApi(userOrToken); + return gitApi.getCommits( + repository, + { + itemVersion: opts?.filterCommit ? this.getVersionDescriptor(opts.filterCommit) : undefined, + $top: opts?.$top, + itemPath: opts?.itemPath, + }, + azProject, + ); + } + + /** + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/items/get + */ + async getFileContent( + userOrToken: User | string, + commit: Pick, + path: string, + ): Promise { + const gitApi = await this.createGitApi(userOrToken); + try { + const readableStream = await gitApi.getItemContent( + commit.repository.name, + path, + commit.repository.owner, + undefined, + undefined, + undefined, + undefined, + undefined, + this.getVersionDescriptor(commit), + ); + let content = ""; + for await (const chunk of readableStream) { + content += chunk.toString(); + } + return content; + } catch (err) { + log.error("Error while fetching file content", err); + throw new Error(`Failed to fetch file content: ${err}`); + } + } + + async getRepositories(userOrToken: User | string, azProject: string) { + const gitApi = await this.createGitApi(userOrToken); + return gitApi.getRepositories(azProject); + } + + async getRepository(userOrToken: User | string, azProject: string, repository: string) { + const gitApi = await this.createGitApi(userOrToken); + return gitApi.getRepository(repository, azProject); + } + + async getBranches( + userOrToken: User | string, + azProject: string, + repository: string, + opts?: { filterBranch?: string }, + ) { + const gitApi = await this.createGitApi(userOrToken); + // If not found, sdk will return null + return ( + (await gitApi.getBranches( + repository, + azProject, + opts?.filterBranch + ? this.getVersionDescriptor({ ref: opts.filterBranch, refType: "branch", revision: "" }) + : undefined, + )) ?? [] + ); + } + + async getBranch(userOrToken: User | string, azProject: string, repository: string, branch: string) { + const gitApi = await this.createGitApi(userOrToken); + return gitApi.getBranch(repository, branch, azProject); + } + + async getTagCommit(userOrToken: User | string, azProject: string, repository: string, tag: string) { + const commits = await this.getCommits(userOrToken, repository, azProject, { + filterCommit: { ref: tag, refType: "tag", revision: "" }, + $top: 1, + }); + if (commits.length === 0) { + throw new Error(`Tag ${tag} not found`); + } + return commits[0]; + } + + async getCommit(userOrToken: User | string, azProject: string, repository: string, commitId: string) { + const gitApi = await this.createGitApi(userOrToken); + return gitApi.getCommit(commitId, repository, azProject); + } + + async getPullRequest(userOrToken: User | string, azProject: string, repository: string, prId: number) { + const gitApi = await this.createGitApi(userOrToken); + return gitApi.getPullRequest(repository, prId, azProject); + } + + /** + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/profile/profiles/get + */ + async getAuthenticatedUser(userOrToken: User | string) { + const api = await this.create(userOrToken, "https://app.vssps.visualstudio.com"); + const profileApi = await api.getProfileApi(); + // official interface has no `displayName` field although it's exists in the response + const profile = await profileApi.getProfile("me", true); + const anyProfile = profile as Partial<{ displayName: string; emailAddress: string; publicAlias: string }>; + return { + id: profile.id, + displayName: anyProfile.displayName ?? profile.coreAttributes["DisplayName"]?.value, + publicAlias: anyProfile.publicAlias ?? profile.coreAttributes["PublicAlias"]?.value, + emailAddress: anyProfile.emailAddress ?? profile.coreAttributes["EmailAddress"]?.value, + avatar: profile.coreAttributes["Avatar"]?.value?.value, + /** + * The time at which this profile was last changed. + */ + timeStamp: profile.timeStamp, + }; + } +} diff --git a/components/server/src/azure-devops/azure-auth-provider.ts b/components/server/src/azure-devops/azure-auth-provider.ts new file mode 100644 index 00000000000000..584432dd09d1ac --- /dev/null +++ b/components/server/src/azure-devops/azure-auth-provider.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import express from "express"; +import { injectable, inject } from "inversify"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { AuthProviderInfo } from "@gitpod/gitpod-protocol"; +import { AzureDevOpsScopes } from "./scopes"; +import { AzureDevOpsApi } from "./azure-api"; +import { GenericAuthProvider } from "../auth/generic-auth-provider"; +import { AuthUserSetup } from "../auth/auth-provider"; +import { oauthUrls } from "./azure-urls"; + +@injectable() +export class AzureDevOpsAuthProvider extends GenericAuthProvider { + @inject(AzureDevOpsApi) protected readonly api: AzureDevOpsApi; + + get info(): AuthProviderInfo { + return { + ...this.defaultInfo(), + scopes: AzureDevOpsScopes.All, + requirements: { + default: AzureDevOpsScopes.Requirements.DEFAULT, + publicRepo: AzureDevOpsScopes.Requirements.REPO, + privateRepo: AzureDevOpsScopes.Requirements.REPO, + }, + }; + } + + /** + * Augmented OAuthConfig for GitLab + */ + protected get oauthConfig() { + const oauth = this.params.oauth!; + const defaultUrls = oauthUrls(oauth); + const scopeSeparator = " "; + return { + ...oauth, + authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, + tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, + settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, + scope: AzureDevOpsScopes.All.join(scopeSeparator), + scopeSeparator, + }; + } + + authorize( + req: express.Request, + res: express.Response, + next: express.NextFunction, + state: string, + scope?: string[], + ) { + super.authorize(req, res, next, state, scope ? scope : AzureDevOpsScopes.Requirements.DEFAULT); + } + + protected get baseURL() { + return `https://${this.params.host}`; + } + + protected async readAuthUserSetup(accessToken: string, tokenResponse: object) { + try { + const profile = await this.api.getAuthenticatedUser(accessToken); + return { + authUser: { + authId: profile.id, + authName: profile.displayName, + primaryEmail: profile.emailAddress, + name: profile.displayName, + avatarUrl: profile.avatar, + }, + currentScopes: this.readScopesFromVerifyParams(tokenResponse), + }; + } catch (error) { + log.error(`Reading current user info failed`, error, { error }); + throw error; + } + } + + protected readScopesFromVerifyParams(params: any) { + if (params && typeof params.scope === "string") { + return this.normalizeScopes((params.scope as string).split(" ")); + } + return []; + } + protected normalizeScopes(scopes: string[]) { + const set = new Set(scopes); + return Array.from(set).sort(); + } +} diff --git a/components/server/src/azure-devops/azure-container-module.ts b/components/server/src/azure-devops/azure-container-module.ts new file mode 100644 index 00000000000000..d9beb510a9a2d8 --- /dev/null +++ b/components/server/src/azure-devops/azure-container-module.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { ContainerModule } from "inversify"; +import { AuthProvider } from "../auth/auth-provider"; +import { FileProvider, RepositoryProvider, RepositoryHost } from "../repohost"; +import { IContextParser } from "../workspace/context-parser"; +import { IGitTokenValidator } from "../workspace/git-token-validator"; +import { AzureDevOpsApi } from "./azure-api"; +import { AzureDevOpsFileProvider } from "./azure-file-provider"; +import { AzureDevOpsAuthProvider } from "./azure-auth-provider"; +import { AzureDevOpsContextParser } from "./azure-context-parser"; +import { AzureDevOpsRepositoryProvider } from "./azure-repository-provider"; +import { AzureDevOpsTokenHelper } from "./azure-token-helper"; +import { AzureDevOpsTokenValidator } from "./azure-token-validator"; + +export const azureDevOpsContainerModule = new ContainerModule((bind, _unbind, _isBound, rebind) => { + bind(RepositoryHost).toSelf().inSingletonScope(); + bind(AzureDevOpsApi).toSelf().inSingletonScope(); + bind(AzureDevOpsContextParser).toSelf().inSingletonScope(); + bind(AzureDevOpsFileProvider).toSelf().inSingletonScope(); + bind(AuthProvider).toService(AzureDevOpsFileProvider); + bind(AzureDevOpsAuthProvider).toSelf().inSingletonScope(); + bind(FileProvider).toService(AzureDevOpsAuthProvider); + bind(AzureDevOpsRepositoryProvider).toSelf().inSingletonScope(); + bind(RepositoryProvider).toService(AzureDevOpsRepositoryProvider); + bind(IContextParser).toService(AzureDevOpsContextParser); + bind(AzureDevOpsTokenHelper).toSelf().inSingletonScope(); + bind(AzureDevOpsTokenValidator).toSelf().inSingletonScope(); + bind(IGitTokenValidator).toService(AzureDevOpsTokenValidator); +}); diff --git a/components/server/src/azure-devops/azure-context-parser.spec.ts b/components/server/src/azure-devops/azure-context-parser.spec.ts new file mode 100644 index 00000000000000..d08be1be4e14ac --- /dev/null +++ b/components/server/src/azure-devops/azure-context-parser.spec.ts @@ -0,0 +1,676 @@ +/** + * Copyright (c) 2020 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +// import { suite, test, timeout, retries, skip } from "@testdeck/mocha"; +// import * as chai from "chai"; +// const expect = chai.expect; + +// import { GitlabContextParser } from "./azure-context-parser"; +// import { User } from "@gitpod/gitpod-protocol"; +// import { ContainerModule, Container } from "inversify"; +// import { DevData } from "../dev/dev-data"; +// import { GitLabApi, GitLab } from "./azure-api"; +// import { AuthProviderParams } from "../auth/auth-provider"; +// import { NotFoundError } from "../errors"; +// import { GitLabTokenHelper } from "./azure-token-helper"; +// import { TokenProvider } from "../user/token-provider"; +// import { HostContextProvider } from "../auth/host-context-provider"; +// import { ifEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; + +// @suite(timeout(10000), retries(2), skip(ifEnvVarNotSet("GITPOD_TEST_TOKEN_GITLAB"))) +// class TestGitlabContextParser { +// protected parser: GitlabContextParser; +// protected user: User; + +// public before() { +// const container = new Container(); +// container.load( +// new ContainerModule((bind, unbind, isBound, rebind) => { +// bind(GitlabContextParser).toSelf().inSingletonScope(); +// bind(GitLabApi).toSelf().inSingletonScope(); +// bind(AuthProviderParams).toConstantValue(TestGitlabContextParser.AUTH_HOST_CONFIG); +// bind(GitLabTokenHelper).toSelf().inSingletonScope(); +// bind(TokenProvider).toConstantValue({ +// getTokenForHost: async () => DevData.createGitlabTestToken(), +// }); +// bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); +// }), +// ); +// this.parser = container.get(GitlabContextParser); +// this.user = DevData.createTestUser(); +// } +// static readonly AUTH_HOST_CONFIG: Partial = { +// id: "Public-GitLab", +// type: "GitLab", +// verified: true, +// description: "", +// icon: "", +// host: "gitlab.com", +// }; +// static readonly BLO_BLA_ERROR_DATA = { +// host: "gitlab.com", +// lastUpdate: undefined, +// owner: "blo", +// repoName: "bla", +// userIsOwner: false, +// userScopes: ["read_user", "api"], +// }; + +// @test public async testErrorContext_01() { +// try { +// await this.parser.handle({}, this.user, "https://gitlab.com/blo/bla"); +// } catch (e) { +// expect(NotFoundError.is(e)); +// expect(e.data).to.deep.equal(TestGitlabContextParser.BLO_BLA_ERROR_DATA); +// } +// } + +// @test public async testTreeContext_01() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/AlexTugarev/gp-test"); +// expect(result).to.deep.include({ +// ref: "master", +// refType: "branch", +// path: "", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - master", +// }); +// } + +// @test public async testTreeContext_01_regression() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/gitlab-org/gitlab"); +// console.log("result"); +// expect(result).to.deep.include({ +// ref: "master", +// refType: "branch", +// path: "", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gitlab-org", +// name: "gitlab", +// cloneUrl: "https://gitlab.com/gitlab-org/gitlab.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gitlab-org/gitlab - master", +// }); +// } + +// @test public async testTreeContext_02() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/AlexTugarev/gp-test/tree/wip"); +// expect(result).to.deep.include({ +// ref: "wip", +// refType: "branch", +// path: "", +// revision: "622f8e28d71f40d8f9475a9e44de7c3b03547c9c", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - wip", +// }); +// } + +// @test public async testTreeContext_03() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/AlexTugarev/gp-test/tree/wip/README.md", +// ); +// expect(result).to.deep.include({ +// ref: "wip", +// refType: "branch", +// path: "README.md", +// revision: "622f8e28d71f40d8f9475a9e44de7c3b03547c9c", +// isFile: true, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - wip", +// }); +// } + +// @test public async testTreeContext_04() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/AlexTugarev/gp-test/tree/master"); +// expect(result).to.deep.include({ +// ref: "master", +// refType: "branch", +// path: "", +// revision: "3cbb7be8212f00bcbea6a2ff9ae889219b391e63", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - master", +// }); +// } + +// @test public async testTreeContext_tag_01() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/AlexTugarev/gp-test/tree/test-tag"); +// expect(result).to.deep.include({ +// ref: "test-tag", +// refType: "tag", +// revision: "af65de8b249855785bbc7a8073ebcf21f55bc8fb", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - test-tag", +// }); +// } + +// @test public async testCommitsContext_01() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/AlexTugarev/gp-test/-/commits/wip"); +// expect(result).to.deep.include({ +// ref: "wip", +// refType: "branch", +// path: "", +// revision: "622f8e28d71f40d8f9475a9e44de7c3b03547c9c", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - wip", +// }); +// } + +// @test public async testCommitContext_01() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/AlexTugarev/gp-test/-/commit/80948e8cc8f0e851e89a10bc7c2ee234d1a5fbe7", +// ); +// expect(result).to.deep.include({ +// ref: "", +// refType: "revision", +// path: "", +// revision: "80948e8cc8f0e851e89a10bc7c2ee234d1a5fbe7", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "AlexTugarev/gp-test - Update /folder/empty.file.jpeg", +// }); +// } + +// @test public async testCommitContext_02_notExistingCommit() { +// try { +// await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/AlexTugarev/gp-test/-/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", +// ); +// // ensure that an error has been thrown +// chai.assert.fail(); +// } catch (e) { +// if (GitLab.ApiError.is(e)) { +// expect(e.code).equals(404); +// } else { +// chai.assert.fail("Unknown Error: " + JSON.stringify(e)); +// } +// } +// } + +// @test public async testPullRequestContext_01() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/AlexTugarev/gp-test/merge_requests/2", +// ); +// expect(result).to.deep.include({ +// title: "WIP Awesome Feature", +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev2", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev2/gp-test.git", +// defaultBranch: "master", +// private: false, +// fork: { +// parent: { +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// host: "gitlab.com", +// name: "gp-test", +// owner: "AlexTugarev", +// }, +// }, +// }, +// ref: "wip2", +// refType: "branch", +// revision: "75efd33f61487832325a4864b7c1e14f4e41c9f7", +// nr: 2, +// base: { +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// ref: "master", +// refType: "branch", +// }, +// }); +// } + +// @test public async testIssueContext_01() { +// const result = await this.parser.handle({}, this.user, "https://gitlab.com/AlexTugarev/gp-test/issues/1"); +// expect(result).to.deep.include({ +// title: "Write a Readme", +// repository: { +// host: "gitlab.com", +// owner: "AlexTugarev", +// name: "gp-test", +// cloneUrl: "https://gitlab.com/AlexTugarev/gp-test.git", +// defaultBranch: "master", +// private: false, +// }, +// owner: "AlexTugarev", +// nr: 1, +// ref: "master", +// refType: "branch", +// revision: "3cbb7be8212f00bcbea6a2ff9ae889219b391e63", +// localBranch: "somefox/write-a-readme-1", +// }); +// } + +// @test public async testTreeContextSubgroup_01() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup", +// ); +// expect(result).to.deep.include({ +// ref: "master", +// refType: "branch", +// path: "", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - master", +// }); +// } + +// @test public async testTreeContextSubgroup_02() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/tree/branchtwo", +// ); +// expect(result).to.deep.include({ +// ref: "branchtwo", +// refType: "branch", +// path: "", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - branchtwo", +// }); +// } + +// @test public async testTreeContextSubgroup_03() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/tree/branchtwo/README.md", +// ); +// expect(result).to.deep.include({ +// ref: "branchtwo", +// refType: "branch", +// path: "README.md", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// isFile: true, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - branchtwo", +// }); +// } + +// @test public async testTreeContextSubgroup_04() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/tree/master", +// ); +// expect(result).to.deep.include({ +// ref: "master", +// refType: "branch", +// path: "", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - master", +// }); +// } + +// @test public async testTreeContextSubgroup_05() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/blob/master/test%20dir%20with%20spaces/file%20with%20spaces.txt", +// ); +// expect(result).to.deep.include({ +// ref: "master", +// refType: "branch", +// path: "test dir with spaces/file with spaces.txt", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// isFile: true, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - master", +// }); +// } + +// @test public async testTreeContextSubgroup_tag_01() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/tree/test-tag", +// ); +// expect(result).to.deep.include({ +// ref: "test-tag", +// refType: "tag", +// revision: "ebc1978ce17df9b2215699960123c4fd301bf5fb", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - test-tag", +// }); +// } + +// @test public async testIssueContextSubgroup_01() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/issues/1", +// ); +// expect(result).to.deep.include({ +// title: "Important issue", +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// owner: "gp-test-group/gp-test-subgroup", +// nr: 1, +// ref: "master", +// refType: "branch", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// localBranch: "somefox/important-issue-1", +// }); +// } + +// @test public async testTreeContextBranchWithSlash() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/tree/corneliusludmann/important-issue-1", +// ); +// expect(result).to.deep.include({ +// ref: "corneliusludmann/important-issue-1", +// refType: "branch", +// path: "", +// revision: "d71db0ad147954553233ba66df6f54a2ae7c74bd", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - corneliusludmann/important-issue-1", +// }); +// } + +// @test public async testTreeContextBranchWithSlash_NewURLwithDash() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/-/tree/corneliusludmann/important-issue-1", +// ); +// expect(result).to.deep.include({ +// ref: "corneliusludmann/important-issue-1", +// refType: "branch", +// path: "", +// revision: "d71db0ad147954553233ba66df6f54a2ae7c74bd", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - corneliusludmann/important-issue-1", +// }); +// } + +// @test public async testTreeContextTagWithSlash() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/tree/tag/withslash", +// ); +// expect(result).to.deep.include({ +// ref: "tag/withslash", +// refType: "tag", +// revision: "f2e1ef56733e7507f766d8ada01f6570c0f3e921", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - tag/withslash", +// }); +// } + +// @test public async testTreeContextBranchWithSlashAndPathWithSpaces() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup/blob/corneliusludmann/important-issue-1/test%20dir%20with%20spaces/file%20with%20spaces.txt", +// ); +// expect(result).to.deep.include({ +// ref: "corneliusludmann/important-issue-1", +// refType: "branch", +// revision: "d71db0ad147954553233ba66df6f54a2ae7c74bd", +// path: "test dir with spaces/file with spaces.txt", +// isFile: true, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group/gp-test-subgroup", +// name: "gp-test-project-in-subgroup", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-subgroup/gp-test-project-in-subgroup - corneliusludmann/important-issue-1", +// }); +// } + +// @test public async testTreeContextBranchWithHashSign01() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-project/-/tree/feature/%23123-issue", +// ); +// expect(result).to.deep.include({ +// ref: "feature/#123-issue", +// refType: "branch", +// path: "", +// revision: "8b389233c0a3a55a5cd75f89d2c96761420bf2c8", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group", +// name: "gp-test-project", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-project.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-project - feature/#123-issue", +// }); +// } + +// @test public async testTreeContextBranchWithHashSign02() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-project/-/tree/issue-%23123", +// ); +// expect(result).to.deep.include({ +// ref: "issue-#123", +// refType: "branch", +// path: "", +// revision: "8b389233c0a3a55a5cd75f89d2c96761420bf2c8", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group", +// name: "gp-test-project", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-project.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-project - issue-#123", +// }); +// } + +// @test public async testTreeContextBranchWithAndSign() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-project/-/tree/another&branch", +// ); +// expect(result).to.deep.include({ +// ref: "another&branch", +// refType: "branch", +// path: "", +// revision: "8b389233c0a3a55a5cd75f89d2c96761420bf2c8", +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group", +// name: "gp-test-project", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-project.git", +// defaultBranch: "master", +// private: false, +// }, +// title: "gp-test-group/gp-test-project - another&branch", +// }); +// } + +// @test public async testEmptyProject() { +// const result = await this.parser.handle( +// {}, +// this.user, +// "https://gitlab.com/gp-test-group/gp-test-empty-project", +// ); +// expect(result).to.deep.include({ +// isFile: false, +// repository: { +// host: "gitlab.com", +// owner: "gp-test-group", +// name: "gp-test-empty-project", +// cloneUrl: "https://gitlab.com/gp-test-group/gp-test-empty-project.git", +// private: false, +// defaultBranch: "main", +// }, +// title: "gp-test-group/gp-test-empty-project - main", +// }); +// } +// } + +// module.exports = new TestGitlabContextParser(); diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts new file mode 100644 index 00000000000000..bf880b8f918090 --- /dev/null +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { injectable, inject } from "inversify"; + +import { AzureDevOpsApi } from "./azure-api"; +import { IContextParser, AbstractContextParser, URLParts } from "../workspace/context-parser"; +import { AzureDevOpsTokenHelper } from "./azure-token-helper"; +import { NavigatorContext, PullRequestContext, User, WorkspaceContext } from "@gitpod/gitpod-protocol"; +import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { NotFoundError, UnauthorizedError } from "../errors"; +import { toBranch, toRepository } from "./azure-converter"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; + +@injectable() +export class AzureDevOpsContextParser extends AbstractContextParser implements IContextParser { + @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; + @inject(AzureDevOpsTokenHelper) protected readonly tokenHelper: AzureDevOpsTokenHelper; + + public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { + const span = TraceContext.startSpan("AzureDevOpsContextParser", ctx); + span.setTag("contextUrl", contextUrl); + + try { + const { host, owner, repoName, moreSegments, searchParams } = await this.parseURL(user, contextUrl); + if (moreSegments.length > 0) { + switch (moreSegments[0]) { + case "pullrequest": { + return await this.handlePullRequestContext( + user, + host, + owner, + repoName, + parseInt(moreSegments[1]), + ); + } + case "commit": { + return await this.handleCommitContext(user, host, owner, repoName, moreSegments[1]); + } + default: { + const version = searchParams.get("version"); + if (!version) { + break; + } + if (version.startsWith("GB")) { + return await this.handleBranchContext(user, host, owner, repoName, version.slice(2)); + } + if (version.startsWith("GT")) { + return await this.handleTagContext(user, host, owner, repoName, version.slice(2)); + } + } + } + } + + return await this.handleDefaultContext(user, host, owner, repoName); + } catch (error) { + // TODO(hw): [AZ] proper handle errors + // if (error && error.code === 401) { + // const token = await this.tokenHelper.getCurrentToken(user); + // throw UnauthorizedError.create({ + // host: this.config.host, + // providerType: "Gitlab", + // requiredScopes: GitLabScope.Requirements.DEFAULT, + // repoName: RepoURL.parseRepoUrl(contextUrl)?.repo, + // providerIsConnected: !!token, + // isMissingScopes: containsScopes(token?.scopes, GitLabScope.Requirements.DEFAULT), + // }); + // } + throw error; + } finally { + span.finish(); + } + } + + public async parseURL(user: User, contextUrl: string): Promise { + const url = new URL(contextUrl); + const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); + const segments = pathname.split("/").filter((e) => e !== ""); + const host = this.host; + if (segments.length < 4 || segments[2] !== "_git") { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid Azure DevOps URL"); + } + // const azOrganization = segments[0]; + const azProject = segments[1]; + const repo = segments[3]; + return { + host, + owner: azProject, + repoName: repo, + moreSegments: segments.slice(4), + searchParams: url.searchParams, + }; + } + + // https://dev.azure.com/services-azure/test-project/_git/repo2 + protected async handleDefaultContext( + user: User, + host: string, + azProject: string, + repo: string, + ): Promise { + try { + const repository = await this.azureDevOpsApi.getRepository(user, azProject, repo); + const result: NavigatorContext = { + path: "", + isFile: false, + title: `${azProject}/${repo}`, + repository: toRepository(repository), + revision: "", + }; + if (!repository.defaultBranch) { + return result; + } + try { + const branch = toBranch( + await this.azureDevOpsApi.getBranch(user, azProject, repo, repository.defaultBranch), + ); + if (!branch) { + return result; + } + result.revision = branch.commit.sha; + result.title = `${result.title} - ${branch.name}`; + result.ref = branch.name; + // TODO(hw): [AZ] support other refType + result.refType = "revision"; + return result; + } catch (error) { + // TODO(hw): [AZ] specific error handling + log.error("Failed to fetch default branch", error); + throw error; + } + } catch (error) { + if (UnauthorizedError.is(error)) { + throw error; + } + throw await NotFoundError.create( + await this.tokenHelper.getCurrentToken(user), + user, + host, + azProject, + repo, + error.message, + ); + } + } + + // PR + // https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1 + protected async handlePullRequestContext( + user: User, + host: string, + azProject: string, + repo: string, + pr: number, + ): Promise { + const pullRequest = await this.azureDevOpsApi.getPullRequest(user, azProject, repo, pr); + const sourceRepo = toRepository(pullRequest.forkSource?.repository ?? pullRequest.repository!); + const targetRepo = toRepository(pullRequest.repository!); + const result: PullRequestContext = { + nr: pr, + base: { + repository: targetRepo, + ref: pullRequest.targetRefName!.replace("refs/headers/", ""), + refType: "branch", + } as any as PullRequestContext["base"], + title: "", + repository: sourceRepo, + ref: pullRequest.sourceRefName!.replace("refs/headers/", ""), + refType: "branch", + revision: pullRequest.lastMergeSourceCommit!.commitId!, + }; + return result; + } + + // branch: develop-2 + // https://dev.azure.com/services-azure/test-project/_git/repo2?path=%2F&version=GBdevelop-2&_a=contents + // https://dev.azure.com/services-azure/test-project/_git/repo2?path=/.gitpod.yml&version=GBdevelop-2&_a=contents + // https://dev.azure.com/services-azure/test-project/_git/repo2-fork?path=/src/index.js&version=GBdevelop-2 + protected async handleBranchContext( + user: User, + host: string, + azProject: string, + repo: string, + branch: string, + ): Promise { + const [repository, branchInfo] = await Promise.all([ + this.azureDevOpsApi.getRepository(user, azProject, repo), + this.azureDevOpsApi.getBranch(user, azProject, repo, branch), + ]); + const result: NavigatorContext = { + path: "", + ref: branch, + refType: "branch", + // TODO(hw): [AZ] verify if empty repo will be broken or not + revision: branchInfo.commit?.commitId ?? "", + isFile: false, + title: `${azProject}/${repo} - ${branch}`, + repository: toRepository(repository), + }; + return result; + } + + // tag: v0.0.1 + // https://dev.azure.com/services-azure/test-project/_git/repo2-fork?version=GTv0.0.1 + // https://dev.azure.com/services-azure/test-project/_git/repo2-fork?version=GTv0.0.1&path=/.gitpod.yml + protected async handleTagContext( + user: User, + host: string, + azProject: string, + repo: string, + tag: string, + ): Promise { + const [repository, tagCommit] = await Promise.all([ + this.azureDevOpsApi.getRepository(user, azProject, repo), + this.azureDevOpsApi.getTagCommit(user, azProject, repo, tag), + ]); + const result: NavigatorContext = { + path: "", + ref: tag, + refType: "tag", + revision: tagCommit.commitId!, + isFile: false, + title: `${azProject}/${repo} - ${tag}`, + repository: toRepository(repository), + }; + return result; + } + + // commit + // https://dev.azure.com/services-azure/test-project/_git/repo2-fork/commit/4c47246a2eacd9700aab401902775c248e85aee7 + // https://dev.azure.com/services-azure/test-project/_git/repo2-fork/commit/4c47246a2eacd9700aab401902775c248e85aee7?refName=refs%2Fheads%2Fdevelop + // https://dev.azure.com/services-azure/test-project/_git/repo2-fork/commit/4c47246a2eacd9700aab401902775c248e85aee7?refName=refs/heads/develop&path=/.gitpod.yml + protected async handleCommitContext( + user: User, + host: string, + azProject: string, + repo: string, + commit: string, + ): Promise { + const [repoInfo, commitInfo] = await Promise.all([ + this.azureDevOpsApi.getRepository(user, azProject, repo), + this.azureDevOpsApi.getCommit(user, azProject, repo, commit), + ]); + const result: NavigatorContext = { + path: "", + ref: "", + refType: "revision", + revision: commitInfo.commitId!, + isFile: false, + title: `${azProject}/${repo} - ${commitInfo.comment}`, + // @ts-ignore + owner: azProject, + repository: toRepository(repoInfo), + }; + return result; + } +} diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts new file mode 100644 index 00000000000000..cb95baf91cbcc5 --- /dev/null +++ b/components/server/src/azure-devops/azure-converter.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { Branch, CommitInfo, Repository } from "@gitpod/gitpod-protocol"; +import { GitBranchStats, GitCommitRef } from "azure-devops-node-api/interfaces/GitInterfaces"; +import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces"; + +export function toRepository(d: GitRepository): Repository { + return { + host: d.remoteUrl!, + owner: d.project?.name ?? "unknown", + name: d.name ?? "unknown", + cloneUrl: d.remoteUrl!, + description: d.defaultBranch, + webUrl: d.webUrl, + defaultBranch: d.defaultBranch, + }; +} + +export function toBranch(d: GitBranchStats): Branch | undefined { + if (!d.commit) { + return; + } + const commit = toCommit(d.commit); + if (!commit) { + return; + } + return { + htmlUrl: d.commit!.url!, + name: d.name!, + commit, + }; +} + +export function toCommit(d: GitCommitRef): CommitInfo | undefined { + return { + sha: d.commitId!, + author: d.author?.name || "unknown", + authorAvatarUrl: "", // TODO: fetch avatar URL + authorDate: d.author?.date?.toISOString(), + commitMessage: d.comment || "missing commit message", + }; +} diff --git a/components/server/src/azure-devops/azure-file-provider.spec.ts b/components/server/src/azure-devops/azure-file-provider.spec.ts new file mode 100644 index 00000000000000..6aee66f59f9a89 --- /dev/null +++ b/components/server/src/azure-devops/azure-file-provider.spec.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +// import { User } from "@gitpod/gitpod-protocol"; +// import { ifEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; +// import { expect } from "chai"; +// import { Container, ContainerModule } from "inversify"; +// import { suite, retries, test, timeout, skip } from "@testdeck/mocha"; +// import { AuthProviderParams } from "../auth/auth-provider"; +// import { DevData } from "../dev/dev-data"; +// import { TokenProvider } from "../user/token-provider"; +// import { GitLab, GitLabApi } from "./azure-api"; +// import { GitlabFileProvider } from "./azure-file-provider"; +// import { GitLabTokenHelper } from "./azure-token-helper"; + +// @suite(timeout(10000), retries(2), skip(ifEnvVarNotSet("GITPOD_TEST_TOKEN_GITLAB"))) +// class TestFileProvider { +// static readonly AUTH_HOST_CONFIG: Partial = { +// id: "Public-GitLab", +// type: "GitLab", +// verified: true, +// description: "", +// icon: "", +// host: "gitlab.com", +// }; + +// protected fileProvider: GitlabFileProvider; +// protected user: User; +// protected container: Container; + +// public before() { +// this.container = new Container(); +// this.container.load( +// new ContainerModule((bind, unbind, isBound, rebind) => { +// bind(GitLabApi).toSelf().inSingletonScope(); +// bind(AuthProviderParams).toConstantValue(TestFileProvider.AUTH_HOST_CONFIG); +// bind(GitLabTokenHelper).toSelf().inSingletonScope(); +// bind(TokenProvider).toConstantValue({ +// getTokenForHost: async () => DevData.createGitlabTestToken(), +// }); +// bind(GitlabFileProvider).toSelf().inSingletonScope(); +// }), +// ); +// this.fileProvider = this.container.get(GitlabFileProvider); +// this.user = DevData.createTestUser(); +// } + +// @test public async testFileContent() { +// const result = await this.fileProvider.getFileContent( +// { +// repository: { +// owner: "AlexTugarev", +// name: "gp-test", +// host: "gitlab.com", +// cloneUrl: "unused in test", +// }, +// revision: "af65de8b249855785bbc7a8073ebcf21f55bc8fb", +// }, +// this.user, +// "README.md", +// ); +// expect(result).to.equal(`# gp-test + +// 123`); +// } + +// // manual test helper to create many repos +// @test.skip public async createManyRepos() { +// const api = this.container.get(GitLabApi); +// for (let i = 151; i < 200; i++) { +// try { +// const project = await api.run(this.user, (g) => +// g.Projects.create({ +// name: `test_project_${i}`, +// namespaceId: 57982169, +// initializeWithReadme: true, +// description: "generated project to test pagination", +// }), +// ); +// if (GitLab.ApiError.is(project)) { +// console.error(`attempt ${i} error: ${JSON.stringify(project.message)}`); +// } else { +// console.log(project.name_with_namespace + " created ✅"); +// } +// await new Promise((resolve) => setTimeout(resolve, 500)); +// } catch (error) { +// console.error(error); +// } +// } +// } +// } + +// module.exports = new TestFileProvider(); diff --git a/components/server/src/azure-devops/azure-file-provider.ts b/components/server/src/azure-devops/azure-file-provider.ts new file mode 100644 index 00000000000000..3b30e933ca13e8 --- /dev/null +++ b/components/server/src/azure-devops/azure-file-provider.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { injectable, inject } from "inversify"; + +import { FileProvider, MaybeContent } from "../repohost/file-provider"; +import { Commit, User, Repository } from "@gitpod/gitpod-protocol"; +import { AzureDevOpsApi } from "./azure-api"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; + +@injectable() +export class AzureDevOpsFileProvider implements FileProvider { + @inject(AzureDevOpsApi) protected readonly api: AzureDevOpsApi; + + public async getGitpodFileContent(commit: Commit, user: User): Promise { + const yamlVersion1 = await Promise.all([ + this.api.getFileContent(user, commit, ".gitpod.yml"), + this.api.getFileContent(user, commit, ".gitpod"), + ]); + return yamlVersion1.filter((f) => !!f)[0]; + } + + public async getLastChangeRevision( + repository: Repository, + revisionOrBranch: string, + user: User, + path: string, + ): Promise { + const results = await Promise.allSettled([ + this.api.getCommits(user, repository.name, "test-project", { + filterCommit: { + revision: revisionOrBranch, + refType: "revision", + }, + $top: 1, + itemPath: path, + }), + this.api.getCommits(user, repository.name, "test-project", { + filterCommit: { + revision: "", + ref: revisionOrBranch, + refType: "tag", + }, + $top: 1, + itemPath: path, + }), + this.api.getCommits(user, repository.name, "test-project", { + filterCommit: { + revision: "", + ref: revisionOrBranch, + refType: "branch", + }, + $top: 1, + itemPath: path, + }), + ]); + for (const result of results) { + if (result.status === "rejected") { + continue; + } + if (result.value && result.value.length > 0 && result.value[0].commitId) { + return result.value[0].commitId; + } + } + // TODO(hw): [AZ] proper handle error + throw new Error(`File ${path} does not exist in repository ${repository.owner}/${repository.name}`); + } + + public async getFileContent(commit: Commit, user: User, path: string): Promise { + try { + const result = await this.api.getFileContent(user, commit, path); + return result; + } catch (error) { + log.debug(error); + } + } +} diff --git a/components/server/src/azure-devops/azure-repository-provider.spec.ts b/components/server/src/azure-devops/azure-repository-provider.spec.ts new file mode 100644 index 00000000000000..a67b3e2b3ba060 --- /dev/null +++ b/components/server/src/azure-devops/azure-repository-provider.spec.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +// import { User } from "@gitpod/gitpod-protocol"; +// import { ifEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; +// import { expect } from "chai"; +// import { Container, ContainerModule } from "inversify"; +// import { suite, retries, test, timeout, skip } from "@testdeck/mocha"; +// import { AuthProviderParams } from "../auth/auth-provider"; +// import { HostContextProvider } from "../auth/host-context-provider"; +// import { DevData } from "../dev/dev-data"; +// import { TokenProvider } from "../user/token-provider"; +// import { GitLabApi } from "./azure-api"; +// import { GitlabContextParser } from "./azure-context-parser"; +// import { GitlabRepositoryProvider } from "./azure-repository-provider"; +// import { GitLabTokenHelper } from "./azure-token-helper"; + +// @suite(timeout(10000), retries(2), skip(ifEnvVarNotSet("GITPOD_TEST_TOKEN_GITLAB"))) +// class TestGitlabRepositoryProvider { +// static readonly AUTH_HOST_CONFIG: Partial = { +// id: "Public-GitLab", +// type: "GitLab", +// verified: true, +// description: "", +// icon: "", +// host: "gitlab.com", +// }; + +// protected repositoryProvider: GitlabRepositoryProvider; +// protected user: User; + +// public before() { +// const container = new Container(); +// container.load( +// new ContainerModule((bind, unbind, isBound, rebind) => { +// bind(GitlabContextParser).toSelf().inSingletonScope(); +// bind(GitLabApi).toSelf().inSingletonScope(); +// bind(AuthProviderParams).toConstantValue(TestGitlabRepositoryProvider.AUTH_HOST_CONFIG); +// bind(GitLabTokenHelper).toSelf().inSingletonScope(); +// bind(TokenProvider).toConstantValue({ +// getTokenForHost: async () => DevData.createGitlabTestToken(), +// }); +// bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); +// bind(GitlabRepositoryProvider).toSelf().inSingletonScope(); +// }), +// ); +// this.repositoryProvider = container.get(GitlabRepositoryProvider); +// this.user = DevData.createTestUser(); +// } + +// @test public async testFetchCommitHistory() { +// const result = await this.repositoryProvider.getCommitHistory( +// this.user, +// "AlexTugarev", +// "gp-test", +// "80948e8cc8f0e851e89a10bc7c2ee234d1a5fbe7", +// 100, +// ); +// expect(result).to.deep.equal([ +// "4447fbc4d46e6fd1ee41fb1b992702521ae078eb", +// "f2d9790f2752a794517b949c65a773eb864844cd", +// ]); +// } +// } + +// module.exports = new TestGitlabRepositoryProvider(); diff --git a/components/server/src/azure-devops/azure-repository-provider.ts b/components/server/src/azure-devops/azure-repository-provider.ts new file mode 100644 index 00000000000000..0402d7ce32c6b4 --- /dev/null +++ b/components/server/src/azure-devops/azure-repository-provider.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { injectable, inject } from "inversify"; + +import { User, Repository, Branch, CommitInfo, RepositoryInfo } from "@gitpod/gitpod-protocol"; +import { AzureDevOpsApi } from "./azure-api"; +import { RepositoryProvider } from "../repohost/repository-provider"; + +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { toBranch, toCommit, toRepository } from "./azure-converter"; + +@injectable() +export class AzureDevOpsRepositoryProvider implements RepositoryProvider { + @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; + + async getRepo(user: User, owner: string, name: string): Promise { + const resp = await this.azureDevOpsApi.getRepository(user, owner, name); + return toRepository(resp); + } + + async getBranch(user: User, owner: string, repo: string, branch: string): Promise { + const response = await this.azureDevOpsApi.getBranch(user, owner, repo, branch); + const item = toBranch(response); + if (!item) { + // TODO(hw): [AZ] + throw new Error("Failed to fetch commit."); + } + return item; + } + + async getBranches(user: User, owner: string, repo: string): Promise { + const branches: Branch[] = []; + const response = await this.azureDevOpsApi.getBranches(user, owner, repo); + for (const b of response) { + const item = toBranch(b); + if (!item) { + continue; + } + branches.push(item); + } + return branches; + } + + async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { + const response = await this.azureDevOpsApi.getCommit(user, owner, repo, ref); + return toCommit(response); + } + + async getUserRepos(user: User): Promise { + // FIXME(janx): Not implemented yet + return []; + } + + async hasReadAccess(user: User, owner: string, repo: string): Promise { + try { + const response = await this.azureDevOpsApi.getRepository(user, owner, repo); + return !!response.id; + } catch (err) { + log.warn({ userId: user.id }, "hasReadAccess error", err, { owner, repo }); + return false; + } + } + + public async getCommitHistory( + user: User, + owner: string, + repo: string, + revision: string, + maxDepth: number = 100, + ): Promise { + const result = await this.azureDevOpsApi.getCommits(user, repo, owner, { + filterCommit: revision ? { revision, refType: "revision" } : undefined, + $top: maxDepth, + }); + return result + .slice(1) + .map((c) => c.commitId) + .filter((c) => !!c) as string[]; + } + + public async searchRepos(user: User, searchString: string, limit: number): Promise { + // Not supported yet, see ENT-780 + return []; + } +} diff --git a/components/server/src/azure-devops/azure-token-helper.ts b/components/server/src/azure-devops/azure-token-helper.ts new file mode 100644 index 00000000000000..8fc9cf72f99ee7 --- /dev/null +++ b/components/server/src/azure-devops/azure-token-helper.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { User, Token } from "@gitpod/gitpod-protocol"; +import { UnauthorizedError } from "../errors"; +import { AuthProviderParams } from "../auth/auth-provider"; +import { injectable, inject } from "inversify"; +import { AzureDevOpsScopes } from "./scopes"; +import { TokenProvider } from "../user/token-provider"; + +@injectable() +export class AzureDevOpsTokenHelper { + @inject(AuthProviderParams) readonly config: AuthProviderParams; + @inject(TokenProvider) protected readonly tokenProvider: TokenProvider; + + async getCurrentToken(user: User) { + try { + return await this.getTokenWithScopes(user, [ + /* any scopes */ + ]); + } catch { + // no token + } + } + + async getTokenWithScopes(user: User, requiredScopes: string[]) { + const { host } = this.config; + try { + const token = await this.tokenProvider.getTokenForHost(user, host); + if (token && this.containsScopes(token, requiredScopes)) { + return token; + } + } catch (e) { + console.error(e); + } + if (requiredScopes.length === 0) { + requiredScopes = AzureDevOpsScopes.Requirements.DEFAULT; + } + throw UnauthorizedError.create({ + host, + providerType: "AzureDevOps", + requiredScopes: AzureDevOpsScopes.Requirements.DEFAULT, + providerIsConnected: false, + isMissingScopes: true, + }); + } + + protected containsScopes(token: Token, wantedScopes: string[] | undefined): boolean { + const set = new Set(wantedScopes); + token.scopes.forEach((s) => set.delete(s)); + return set.size === 0; + } +} diff --git a/components/server/src/azure-devops/azure-token-validator.ts b/components/server/src/azure-devops/azure-token-validator.ts new file mode 100644 index 00000000000000..ee71d3f027df1a --- /dev/null +++ b/components/server/src/azure-devops/azure-token-validator.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { injectable, inject } from "inversify"; +import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; +import { AzureDevOpsApi } from "./azure-api"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; + +@injectable() +export class AzureDevOpsTokenValidator implements IGitTokenValidator { + @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; + + async checkWriteAccess(params: IGitTokenValidatorParams): Promise { + let found = false; + let isPrivateRepo: boolean | undefined; + let writeAccessToRepo: boolean | undefined = false; + try { + await this.azureDevOpsApi.getRepository(params.token, params.owner, params.repo); + found = true; + isPrivateRepo = true; + writeAccessToRepo = true; + } catch (error) { + log.error(error); + } + return { + found, + isPrivateRepo, + writeAccessToRepo, + mayWritePrivate: true, + mayWritePublic: true, + }; + } +} diff --git a/components/server/src/azure-devops/azure-urls.ts b/components/server/src/azure-devops/azure-urls.ts new file mode 100644 index 00000000000000..0675d4427b00cc --- /dev/null +++ b/components/server/src/azure-devops/azure-urls.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { AuthProviderEntry } from "@gitpod/gitpod-protocol"; + +export function oauthUrls(customConfig: Required) { + return { + authorizationUrl: customConfig.authorizationUrl, + tokenUrl: customConfig.tokenUrl, + settingsUrl: "", + }; +} diff --git a/components/server/src/azure-devops/scopes.ts b/components/server/src/azure-devops/scopes.ts new file mode 100644 index 00000000000000..b7c89f91d4f8ec --- /dev/null +++ b/components/server/src/azure-devops/scopes.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +/** + * @see https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops + */ +export namespace AzureDevOpsScopes { + export const READ_USER = "https://app.vssps.visualstudio.com/vso.profile"; + export const READ_REPO = "https://app.vssps.visualstudio.com/vso.code_write"; + // extend token lifetime + export const OFFLINE_ACCESS = "offline_access"; + + export const All = [READ_USER, READ_REPO]; + export const Requirements = { + DEFAULT: [READ_USER, OFFLINE_ACCESS], + + REPO: [READ_REPO], + }; +} From 31939b8f38ea19124bcd7acceb8e6243b6781bd1 Mon Sep 17 00:00:00 2001 From: mustard Date: Thu, 12 Sep 2024 16:26:18 +0000 Subject: [PATCH 06/44] fixup --- components/server/src/azure-devops/azure-api.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index 8e04b82cd30887..db674960a6e24a 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -31,7 +31,7 @@ export class AzureDevOpsApi { ); bearerToken = azureToken.value; } - return new WebApi(this.config.host, getBearerHandler(bearerToken)); + return new WebApi(serverUrl ?? this.config.host, getBearerHandler(bearerToken)); } private async createGitApi(userOrToken: User | string) { @@ -114,11 +114,17 @@ export class AzureDevOpsApi { } } + /** + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/repositories/list + */ async getRepositories(userOrToken: User | string, azProject: string) { const gitApi = await this.createGitApi(userOrToken); return gitApi.getRepositories(azProject); } + /** + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/repositories/get-repository + */ async getRepository(userOrToken: User | string, azProject: string, repository: string) { const gitApi = await this.createGitApi(userOrToken); return gitApi.getRepository(repository, azProject); @@ -159,11 +165,17 @@ export class AzureDevOpsApi { return commits[0]; } + /** + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/commits/get + */ async getCommit(userOrToken: User | string, azProject: string, repository: string, commitId: string) { const gitApi = await this.createGitApi(userOrToken); return gitApi.getCommit(commitId, repository, azProject); } + /** + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-request?view=azure-devops-rest-7.1 + */ async getPullRequest(userOrToken: User | string, azProject: string, repository: string, prId: number) { const gitApi = await this.createGitApi(userOrToken); return gitApi.getPullRequest(repository, prId, azProject); From f22d0a32e82b06aa7e4e8bc0df9ca36241626112 Mon Sep 17 00:00:00 2001 From: mustard Date: Thu, 12 Sep 2024 17:05:26 +0000 Subject: [PATCH 07/44] fixup --- components/server/src/auth/auth-provider-service.ts | 2 +- components/server/src/auth/host-container-mapping.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/server/src/auth/auth-provider-service.ts b/components/server/src/auth/auth-provider-service.ts index cd985d98c467e3..8ed8f078e22e9a 100644 --- a/components/server/src/auth/auth-provider-service.ts +++ b/components/server/src/auth/auth-provider-service.ts @@ -339,7 +339,7 @@ export class AuthProviderService { case "Bitbucket": urls = bbUrls(host); break; - case "Azure DevOps": + case "AzureDevOps": const { authorizationUrl, tokenUrl } = newEntry; if (!authorizationUrl || !tokenUrl) { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "authorizationUrl and tokenUrl are required."); diff --git a/components/server/src/auth/host-container-mapping.ts b/components/server/src/auth/host-container-mapping.ts index 6e21114adef33c..097b82e3cbd11c 100644 --- a/components/server/src/auth/host-container-mapping.ts +++ b/components/server/src/auth/host-container-mapping.ts @@ -26,7 +26,7 @@ export class HostContainerMapping { return [bitbucketContainerModule]; case "BitbucketServer": return [bitbucketServerContainerModule]; - case "Azure DevOps": + case "AzureDevOps": return [azureDevOpsContainerModule]; default: return undefined; From 6ca56e4b974e721d3864a25448fab9bdc8b1c704 Mon Sep 17 00:00:00 2001 From: mustard Date: Thu, 12 Sep 2024 17:22:10 +0000 Subject: [PATCH 08/44] server fixup --- components/server/src/api/auth-provider-service-api.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/server/src/api/auth-provider-service-api.ts b/components/server/src/api/auth-provider-service-api.ts index a15ceeb0b27926..432c3ed001ea3d 100644 --- a/components/server/src/api/auth-provider-service-api.ts +++ b/components/server/src/api/auth-provider-service-api.ts @@ -57,6 +57,8 @@ export class AuthProviderServiceAPI implements ServiceImpl Date: Thu, 12 Sep 2024 18:01:43 +0000 Subject: [PATCH 09/44] [dashboard] changes --- .../create-org-auth-provider-mutation.ts | 9 ++- .../create-user-auth-provider-mutation.ts | 9 ++- .../update-org-auth-provider-mutation.ts | 11 +++- .../update-user-auth-provider-mutation.ts | 11 +++- .../git-integrations/GitIntegrationModal.tsx | 55 ++++++++++++++++++- 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts index 5e26c643a0e610..9fba3da48e707e 100644 --- a/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts @@ -6,7 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { getOrgAuthProvidersQueryKey } from "./org-auth-providers-query"; -import { CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { AuthProviderType, CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; type CreateAuthProviderArgs = { @@ -14,6 +14,8 @@ type CreateAuthProviderArgs = { clientId: string; clientSecret: string; orgId: string; + authorizationUrl?: string; + tokenUrl?: string; }; }; export const useCreateOrgAuthProviderMutation = () => { @@ -21,6 +23,9 @@ export const useCreateOrgAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: CreateAuthProviderArgs) => { + const authorizationUrl = + provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; + const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.createAuthProvider( new CreateAuthProviderRequest({ owner: { case: "organizationId", value: provider.orgId }, @@ -28,6 +33,8 @@ export const useCreateOrgAuthProviderMutation = () => { oauth2Config: { clientId: provider.clientId, clientSecret: provider.clientSecret, + authorizationUrl, + tokenUrl, }, type: provider.type, }), diff --git a/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts index 998e08da506e9b..a02049226fae1d 100644 --- a/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts @@ -5,7 +5,7 @@ */ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { AuthProviderType, CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; import { getUserAuthProvidersQueryKey } from "./user-auth-providers-query"; @@ -14,6 +14,8 @@ type CreateAuthProviderArgs = { clientId: string; clientSecret: string; userId: string; + authorizationUrl?: string; + tokenUrl?: string; }; }; export const useCreateUserAuthProviderMutation = () => { @@ -21,6 +23,9 @@ export const useCreateUserAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: CreateAuthProviderArgs) => { + const authorizationUrl = + provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; + const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.createAuthProvider( new CreateAuthProviderRequest({ owner: { case: "ownerId", value: provider.userId }, @@ -28,6 +33,8 @@ export const useCreateUserAuthProviderMutation = () => { oauth2Config: { clientId: provider.clientId, clientSecret: provider.clientSecret, + authorizationUrl, + tokenUrl, }, type: provider.type, }), diff --git a/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts index 5c24d0d0afd65a..b8c92edcf82f8b 100644 --- a/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts @@ -6,7 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { getOrgAuthProvidersQueryKey } from "./org-auth-providers-query"; -import { UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { AuthProviderType, UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; type UpdateAuthProviderArgs = { @@ -14,6 +14,10 @@ type UpdateAuthProviderArgs = { id: string; clientId: string; clientSecret: string; + /** verify locally only, will not be update */ + type: AuthProviderType; + authorizationUrl?: string; + tokenUrl?: string; }; }; export const useUpdateOrgAuthProviderMutation = () => { @@ -21,11 +25,16 @@ export const useUpdateOrgAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: UpdateAuthProviderArgs) => { + const authorizationUrl = + provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; + const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.updateAuthProvider( new UpdateAuthProviderRequest({ authProviderId: provider.id, clientId: provider.clientId, clientSecret: provider.clientSecret, + authorizationUrl, + tokenUrl, }), ); return response.authProvider!; diff --git a/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts index 046fa61ad376eb..093f8f5a42be39 100644 --- a/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts @@ -5,7 +5,7 @@ */ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { AuthProviderType, UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; import { getUserAuthProvidersQueryKey } from "./user-auth-providers-query"; @@ -14,6 +14,10 @@ type UpdateAuthProviderArgs = { id: string; clientId: string; clientSecret: string; + /** verify locally only, will not be update */ + type: AuthProviderType; + authorizationUrl?: string; + tokenUrl?: string; }; }; export const useUpdateUserAuthProviderMutation = () => { @@ -21,11 +25,16 @@ export const useUpdateUserAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: UpdateAuthProviderArgs) => { + const authorizationUrl = + provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; + const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.updateAuthProvider( new UpdateAuthProviderRequest({ authProviderId: provider.id, clientId: provider.clientId, clientSecret: provider.clientSecret, + authorizationUrl, + tokenUrl, }), ); return response.authProvider!; diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx index c168355699031b..993163ae20db53 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx @@ -37,6 +37,8 @@ export const GitIntegrationModal: FunctionComponent = (props) => { const [host, setHost] = useState(props.provider?.host ?? ""); const [clientId, setClientId] = useState(props.provider?.oauth2Config?.clientId ?? ""); const [clientSecret, setClientSecret] = useState(props.provider?.oauth2Config?.clientSecret ?? ""); + const [authorizationUrl, setAuthorizationUrl] = useState(props.provider?.oauth2Config?.authorizationUrl ?? ""); + const [tokenUrl, setTokenUrl] = useState(props.provider?.oauth2Config?.tokenUrl ?? ""); const [savedProvider, setSavedProvider] = useState(props.provider); const isNew = !savedProvider; @@ -82,6 +84,21 @@ export const GitIntegrationModal: FunctionComponent = (props) => { clientSecret.trim().length > 0, ); + const { + message: authorizationUrlError, + onBlur: authorizationUrlOnBlur, + isValid: authorizationUrlValid, + } = useOnBlurError( + `Authorization URL is missing.`, + type !== AuthProviderType.AZURE_DEVOPS || authorizationUrl.trim().length > 0, + ); + + const { + message: tokenUrlError, + onBlur: tokenUrlOnBlur, + isValid: tokenUrlValid, + } = useOnBlurError(`Token URL is missing.`, type !== AuthProviderType.AZURE_DEVOPS || tokenUrl.trim().length > 0); + // Call our error onBlur handler, and remove prefixed "https://" const hostOnBlur = useCallback(() => { hostOnBlurErrorTracking(); @@ -112,6 +129,8 @@ export const GitIntegrationModal: FunctionComponent = (props) => { const trimmedId = clientId.trim(); const trimmedSecret = clientSecret.trim(); + const trimmedAuthorizationUrl = authorizationUrl.trim(); + const trimmedTokenUrl = tokenUrl.trim(); try { let newProvider: AuthProvider; @@ -123,6 +142,8 @@ export const GitIntegrationModal: FunctionComponent = (props) => { orgId: team.id, clientId: trimmedId, clientSecret: trimmedSecret, + authorizationUrl: trimmedAuthorizationUrl, + tokenUrl: trimmedTokenUrl, }, }); } else { @@ -131,6 +152,9 @@ export const GitIntegrationModal: FunctionComponent = (props) => { id: savedProvider.id, clientId: trimmedId, clientSecret: clientSecret === "redacted" ? "" : trimmedSecret, + type, + authorizationUrl: trimmedAuthorizationUrl, + tokenUrl: trimmedTokenUrl, }, }); } @@ -181,6 +205,8 @@ export const GitIntegrationModal: FunctionComponent = (props) => { }, [ clientId, clientSecret, + authorizationUrl, + tokenUrl, host, invalidateOrgAuthProviders, isNew, @@ -196,8 +222,8 @@ export const GitIntegrationModal: FunctionComponent = (props) => { ]); const isValid = useMemo( - () => clientIdValid && clientSecretValid && hostValid, - [clientIdValid, clientSecretValid, hostValid], + () => clientIdValid && clientSecretValid && hostValid && authorizationUrlValid && tokenUrlValid, + [clientIdValid, clientSecretValid, hostValid, authorizationUrlValid, tokenUrlValid], ); const getNumber = (paramValue: string | null) => { @@ -239,6 +265,7 @@ export const GitIntegrationModal: FunctionComponent = (props) => { + = (props) => { + {type === AuthProviderType.AZURE_DEVOPS && ( + <> + + + + )} + { return "bitbucket.org"; case AuthProviderType.BITBUCKET_SERVER: return "bitbucket.example.com"; + case AuthProviderType.AZURE_DEVOPS: + return "dev.azure.com/your-organization"; default: return ""; } @@ -337,6 +385,9 @@ const RedirectUrlDescription: FunctionComponent = ( case AuthProviderType.BITBUCKET_SERVER: docsUrl = "https://www.gitpod.io/docs/configure/authentication/bitbucket-server"; break; + case AuthProviderType.AZURE_DEVOPS: + docsUrl = "https://www.gitpod.io/docs/configure/authentication/azure-devops"; + break; default: return null; } From 4bcaa902022d2154e13d67c0cc4ce1d712409e77 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 12 Sep 2024 18:14:51 +0000 Subject: [PATCH 10/44] fixup --- components/dashboard/src/user-settings/Integrations.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index ec1053f1ada37d..3185047de4f84c 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -604,6 +604,7 @@ export function GitIntegrationModal( newProvider = await updateProvider.mutateAsync({ provider: { id: providerEntry?.id || "", + type: providerEntry?.type ?? AuthProviderType.UNSPECIFIED, clientId, clientSecret: clientSecret === "redacted" ? "" : clientSecret, }, From de778bef10b4229a133bc6315aa1b2262abcbd94 Mon Sep 17 00:00:00 2001 From: mustard Date: Fri, 13 Sep 2024 07:19:16 +0000 Subject: [PATCH 11/44] fixup --- .../server/src/azure-devops/azure-container-module.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/server/src/azure-devops/azure-container-module.ts b/components/server/src/azure-devops/azure-container-module.ts index d9beb510a9a2d8..7a11381e319ae2 100644 --- a/components/server/src/azure-devops/azure-container-module.ts +++ b/components/server/src/azure-devops/azure-container-module.ts @@ -20,14 +20,14 @@ import { AzureDevOpsTokenValidator } from "./azure-token-validator"; export const azureDevOpsContainerModule = new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(RepositoryHost).toSelf().inSingletonScope(); bind(AzureDevOpsApi).toSelf().inSingletonScope(); - bind(AzureDevOpsContextParser).toSelf().inSingletonScope(); bind(AzureDevOpsFileProvider).toSelf().inSingletonScope(); - bind(AuthProvider).toService(AzureDevOpsFileProvider); - bind(AzureDevOpsAuthProvider).toSelf().inSingletonScope(); - bind(FileProvider).toService(AzureDevOpsAuthProvider); + bind(FileProvider).toService(AzureDevOpsFileProvider); + bind(AzureDevOpsContextParser).toSelf().inSingletonScope(); + bind(IContextParser).toService(AzureDevOpsContextParser); bind(AzureDevOpsRepositoryProvider).toSelf().inSingletonScope(); bind(RepositoryProvider).toService(AzureDevOpsRepositoryProvider); - bind(IContextParser).toService(AzureDevOpsContextParser); + bind(AuthProvider).toService(AzureDevOpsAuthProvider); + bind(AzureDevOpsAuthProvider).toSelf().inSingletonScope(); bind(AzureDevOpsTokenHelper).toSelf().inSingletonScope(); bind(AzureDevOpsTokenValidator).toSelf().inSingletonScope(); bind(IGitTokenValidator).toService(AzureDevOpsTokenValidator); From f4b6b74fa71ca16ea7500d7879f15a34062d87c3 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 13 Sep 2024 08:41:44 +0000 Subject: [PATCH 12/44] Fix server bugs --- .../server/src/azure-devops/azure-api.ts | 2 +- .../src/azure-devops/azure-auth-provider.ts | 5 +- .../src/azure-devops/azure-context-parser.ts | 48 ++++++++++++------- .../src/azure-devops/azure-converter.ts | 13 +++-- .../azure-devops/azure-repository-provider.ts | 4 +- components/server/src/azure-devops/scopes.ts | 5 +- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index db674960a6e24a..b30d434c5e88d1 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -31,7 +31,7 @@ export class AzureDevOpsApi { ); bearerToken = azureToken.value; } - return new WebApi(serverUrl ?? this.config.host, getBearerHandler(bearerToken)); + return new WebApi(serverUrl ?? `https://${this.config.host}`, getBearerHandler(bearerToken)); } private async createGitApi(userOrToken: User | string) { diff --git a/components/server/src/azure-devops/azure-auth-provider.ts b/components/server/src/azure-devops/azure-auth-provider.ts index 584432dd09d1ac..b53ee0004c9526 100644 --- a/components/server/src/azure-devops/azure-auth-provider.ts +++ b/components/server/src/azure-devops/azure-auth-provider.ts @@ -42,7 +42,10 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, - scope: AzureDevOpsScopes.All.join(scopeSeparator), + // offline_access is required but will not respond as scopes + scope: [...AzureDevOpsScopes.All, ...AzureDevOpsScopes.Requirements.APPEND_WHEN_FETCHING].join( + scopeSeparator, + ), scopeSeparator, }; } diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts index bf880b8f918090..cb4586de2d439e 100644 --- a/components/server/src/azure-devops/azure-context-parser.ts +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -13,13 +13,15 @@ import { NavigatorContext, PullRequestContext, User, WorkspaceContext } from "@g import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { NotFoundError, UnauthorizedError } from "../errors"; -import { toBranch, toRepository } from "./azure-converter"; +import { normalizeBranchName, toBranch, toRepository } from "./azure-converter"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { AuthProviderParams } from "../auth/auth-provider"; @injectable() export class AzureDevOpsContextParser extends AbstractContextParser implements IContextParser { @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; @inject(AzureDevOpsTokenHelper) protected readonly tokenHelper: AzureDevOpsTokenHelper; + @inject(AuthProviderParams) readonly config: AuthProviderParams; public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { const span = TraceContext.startSpan("AzureDevOpsContextParser", ctx); @@ -81,6 +83,19 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); const segments = pathname.split("/").filter((e) => e !== ""); const host = this.host; + // case when there is only one repository in the project + // https://dev.azure.com/services-azure/_git/project2 + if (segments.length === 3 && segments[1] === "_git") { + const azProject = segments[2]; + const repo = azProject; + return { + host, + owner: azProject, + repoName: repo, + moreSegments: segments.slice(3), + searchParams: url.searchParams, + }; + } if (segments.length < 4 || segments[2] !== "_git") { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid Azure DevOps URL"); } @@ -109,24 +124,22 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I path: "", isFile: false, title: `${azProject}/${repo}`, - repository: toRepository(repository), + repository: toRepository(this.config.host, repository), revision: "", }; if (!repository.defaultBranch) { return result; } try { - const branch = toBranch( - await this.azureDevOpsApi.getBranch(user, azProject, repo, repository.defaultBranch), - ); + const branchName = normalizeBranchName(repository.defaultBranch); + const branch = toBranch(await this.azureDevOpsApi.getBranch(user, azProject, repo, branchName)); if (!branch) { return result; } result.revision = branch.commit.sha; - result.title = `${result.title} - ${branch.name}`; - result.ref = branch.name; - // TODO(hw): [AZ] support other refType - result.refType = "revision"; + result.title = `${result.title} - ${branchName}`; + result.ref = branchName; + result.refType = "branch"; return result; } catch (error) { // TODO(hw): [AZ] specific error handling @@ -158,18 +171,21 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I pr: number, ): Promise { const pullRequest = await this.azureDevOpsApi.getPullRequest(user, azProject, repo, pr); - const sourceRepo = toRepository(pullRequest.forkSource?.repository ?? pullRequest.repository!); - const targetRepo = toRepository(pullRequest.repository!); + const sourceRepo = toRepository( + this.config.host, + pullRequest.forkSource?.repository ?? pullRequest.repository!, + ); + const targetRepo = toRepository(this.config.host, pullRequest.repository!); const result: PullRequestContext = { nr: pr, base: { repository: targetRepo, - ref: pullRequest.targetRefName!.replace("refs/headers/", ""), + ref: normalizeBranchName(pullRequest.targetRefName!), refType: "branch", } as any as PullRequestContext["base"], title: "", repository: sourceRepo, - ref: pullRequest.sourceRefName!.replace("refs/headers/", ""), + ref: normalizeBranchName(pullRequest.sourceRefName!), refType: "branch", revision: pullRequest.lastMergeSourceCommit!.commitId!, }; @@ -199,7 +215,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I revision: branchInfo.commit?.commitId ?? "", isFile: false, title: `${azProject}/${repo} - ${branch}`, - repository: toRepository(repository), + repository: toRepository(this.config.host, repository), }; return result; } @@ -225,7 +241,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I revision: tagCommit.commitId!, isFile: false, title: `${azProject}/${repo} - ${tag}`, - repository: toRepository(repository), + repository: toRepository(this.config.host, repository), }; return result; } @@ -254,7 +270,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I title: `${azProject}/${repo} - ${commitInfo.comment}`, // @ts-ignore owner: azProject, - repository: toRepository(repoInfo), + repository: toRepository(this.config.host, repoInfo), }; return result; } diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index cb95baf91cbcc5..309478ed877ceb 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -8,18 +8,23 @@ import { Branch, CommitInfo, Repository } from "@gitpod/gitpod-protocol"; import { GitBranchStats, GitCommitRef } from "azure-devops-node-api/interfaces/GitInterfaces"; import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces"; -export function toRepository(d: GitRepository): Repository { +export function toRepository(host: string, d: GitRepository): Repository { + const branchName = normalizeBranchName(d.defaultBranch); return { - host: d.remoteUrl!, + host, owner: d.project?.name ?? "unknown", name: d.name ?? "unknown", cloneUrl: d.remoteUrl!, - description: d.defaultBranch, + description: branchName, webUrl: d.webUrl, - defaultBranch: d.defaultBranch, + defaultBranch: branchName, }; } +export function normalizeBranchName(ref: string | undefined): string { + return ref?.replace("refs/heads/", "") ?? "main"; +} + export function toBranch(d: GitBranchStats): Branch | undefined { if (!d.commit) { return; diff --git a/components/server/src/azure-devops/azure-repository-provider.ts b/components/server/src/azure-devops/azure-repository-provider.ts index 0402d7ce32c6b4..b3a4ccee7afd7c 100644 --- a/components/server/src/azure-devops/azure-repository-provider.ts +++ b/components/server/src/azure-devops/azure-repository-provider.ts @@ -12,14 +12,16 @@ import { RepositoryProvider } from "../repohost/repository-provider"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { toBranch, toCommit, toRepository } from "./azure-converter"; +import { AuthProviderParams } from "../auth/auth-provider"; @injectable() export class AzureDevOpsRepositoryProvider implements RepositoryProvider { + @inject(AuthProviderParams) readonly config: AuthProviderParams; @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; async getRepo(user: User, owner: string, name: string): Promise { const resp = await this.azureDevOpsApi.getRepository(user, owner, name); - return toRepository(resp); + return toRepository(this.config.host, resp); } async getBranch(user: User, owner: string, repo: string, branch: string): Promise { diff --git a/components/server/src/azure-devops/scopes.ts b/components/server/src/azure-devops/scopes.ts index b7c89f91d4f8ec..a7aaf0991426bb 100644 --- a/components/server/src/azure-devops/scopes.ts +++ b/components/server/src/azure-devops/scopes.ts @@ -15,7 +15,10 @@ export namespace AzureDevOpsScopes { export const All = [READ_USER, READ_REPO]; export const Requirements = { - DEFAULT: [READ_USER, OFFLINE_ACCESS], + DEFAULT: [READ_USER], + + // offline_access is required but will not respond as scopes + APPEND_WHEN_FETCHING: [OFFLINE_ACCESS], REPO: [READ_REPO], }; From 393ff4e0b07427b66b3bd434cea55e0a2d679afc Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 13 Sep 2024 09:25:56 +0000 Subject: [PATCH 13/44] Fixup --- .../server/src/azure-devops/azure-api.ts | 72 ++++++++++++----- .../src/azure-devops/azure-auth-provider.ts | 4 +- .../src/azure-devops/azure-context-parser.ts | 79 ++++++++++++++----- .../src/azure-devops/azure-converter.ts | 5 ++ .../src/azure-devops/azure-file-provider.ts | 19 +++-- .../azure-devops/azure-repository-provider.ts | 20 +++-- .../src/azure-devops/azure-token-validator.ts | 4 +- 7 files changed, 144 insertions(+), 59 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index b30d434c5e88d1..5bff703e8371fb 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -20,7 +20,10 @@ export class AzureDevOpsApi { @inject(AuthProviderParams) readonly config: AuthProviderParams; @inject(AzureDevOpsTokenHelper) protected readonly tokenHelper: AzureDevOpsTokenHelper; - private async create(userOrToken: User | string, serverUrl?: string) { + private async create(userOrToken: User | string, opts: { serverUrl?: string; orgId?: string } = {}) { + if (!opts.serverUrl && !opts.orgId) { + throw new Error("Either serverUrl or orgId must be provided"); + } let bearerToken: string | undefined; if (typeof userOrToken === "string") { bearerToken = userOrToken; @@ -31,11 +34,11 @@ export class AzureDevOpsApi { ); bearerToken = azureToken.value; } - return new WebApi(serverUrl ?? `https://${this.config.host}`, getBearerHandler(bearerToken)); + return new WebApi(opts.serverUrl ?? `https://${this.config.host}/${opts.orgId}`, getBearerHandler(bearerToken)); } - private async createGitApi(userOrToken: User | string) { - const api = await this.create(userOrToken); + private async createGitApi(userOrToken: User | string, orgId: string) { + const api = await this.create(userOrToken, { orgId }); return api.getGitApi(); } @@ -62,15 +65,16 @@ export class AzureDevOpsApi { */ async getCommits( userOrToken: User | string, - repository: string, + azOrgId: string, azProject: string, + repository: string, opts?: Partial<{ filterCommit: Pick; $top: number; itemPath: string; }>, ) { - const gitApi = await this.createGitApi(userOrToken); + const gitApi = await this.createGitApi(userOrToken, azOrgId); return gitApi.getCommits( repository, { @@ -87,10 +91,11 @@ export class AzureDevOpsApi { */ async getFileContent( userOrToken: User | string, + azOrgId: string, commit: Pick, path: string, ): Promise { - const gitApi = await this.createGitApi(userOrToken); + const gitApi = await this.createGitApi(userOrToken, azOrgId); try { const readableStream = await gitApi.getItemContent( commit.repository.name, @@ -117,26 +122,27 @@ export class AzureDevOpsApi { /** * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/repositories/list */ - async getRepositories(userOrToken: User | string, azProject: string) { - const gitApi = await this.createGitApi(userOrToken); + async getRepositories(userOrToken: User | string, azOrgId: string, azProject: string) { + const gitApi = await this.createGitApi(userOrToken, azOrgId); return gitApi.getRepositories(azProject); } /** * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/repositories/get-repository */ - async getRepository(userOrToken: User | string, azProject: string, repository: string) { - const gitApi = await this.createGitApi(userOrToken); + async getRepository(userOrToken: User | string, azOrgId: string, azProject: string, repository: string) { + const gitApi = await this.createGitApi(userOrToken, azOrgId); return gitApi.getRepository(repository, azProject); } async getBranches( userOrToken: User | string, + azOrgId: string, azProject: string, repository: string, opts?: { filterBranch?: string }, ) { - const gitApi = await this.createGitApi(userOrToken); + const gitApi = await this.createGitApi(userOrToken, azOrgId); // If not found, sdk will return null return ( (await gitApi.getBranches( @@ -149,13 +155,25 @@ export class AzureDevOpsApi { ); } - async getBranch(userOrToken: User | string, azProject: string, repository: string, branch: string) { - const gitApi = await this.createGitApi(userOrToken); + async getBranch( + userOrToken: User | string, + azOrgId: string, + azProject: string, + repository: string, + branch: string, + ) { + const gitApi = await this.createGitApi(userOrToken, azOrgId); return gitApi.getBranch(repository, branch, azProject); } - async getTagCommit(userOrToken: User | string, azProject: string, repository: string, tag: string) { - const commits = await this.getCommits(userOrToken, repository, azProject, { + async getTagCommit( + userOrToken: User | string, + azOrgId: string, + azProject: string, + repository: string, + tag: string, + ) { + const commits = await this.getCommits(userOrToken, azOrgId, azProject, repository, { filterCommit: { ref: tag, refType: "tag", revision: "" }, $top: 1, }); @@ -168,16 +186,28 @@ export class AzureDevOpsApi { /** * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/commits/get */ - async getCommit(userOrToken: User | string, azProject: string, repository: string, commitId: string) { - const gitApi = await this.createGitApi(userOrToken); + async getCommit( + userOrToken: User | string, + azOrgId: string, + azProject: string, + repository: string, + commitId: string, + ) { + const gitApi = await this.createGitApi(userOrToken, azOrgId); return gitApi.getCommit(commitId, repository, azProject); } /** * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-request?view=azure-devops-rest-7.1 */ - async getPullRequest(userOrToken: User | string, azProject: string, repository: string, prId: number) { - const gitApi = await this.createGitApi(userOrToken); + async getPullRequest( + userOrToken: User | string, + azOrgId: string, + azProject: string, + repository: string, + prId: number, + ) { + const gitApi = await this.createGitApi(userOrToken, azOrgId); return gitApi.getPullRequest(repository, prId, azProject); } @@ -185,7 +215,7 @@ export class AzureDevOpsApi { * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/profile/profiles/get */ async getAuthenticatedUser(userOrToken: User | string) { - const api = await this.create(userOrToken, "https://app.vssps.visualstudio.com"); + const api = await this.create(userOrToken, { serverUrl: "https://app.vssps.visualstudio.com" }); const profileApi = await api.getProfileApi(); // official interface has no `displayName` field although it's exists in the response const profile = await profileApi.getProfile("me", true); diff --git a/components/server/src/azure-devops/azure-auth-provider.ts b/components/server/src/azure-devops/azure-auth-provider.ts index b53ee0004c9526..c0316dc9be0f18 100644 --- a/components/server/src/azure-devops/azure-auth-provider.ts +++ b/components/server/src/azure-devops/azure-auth-provider.ts @@ -16,7 +16,7 @@ import { oauthUrls } from "./azure-urls"; @injectable() export class AzureDevOpsAuthProvider extends GenericAuthProvider { - @inject(AzureDevOpsApi) protected readonly api: AzureDevOpsApi; + @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; get info(): AuthProviderInfo { return { @@ -66,7 +66,7 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { protected async readAuthUserSetup(accessToken: string, tokenResponse: object) { try { - const profile = await this.api.getAuthenticatedUser(accessToken); + const profile = await this.azureDevOpsApi.getAuthenticatedUser(accessToken); return { authUser: { authId: profile.id, diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts index cb4586de2d439e..39ff47696d9a7a 100644 --- a/components/server/src/azure-devops/azure-context-parser.ts +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -13,7 +13,7 @@ import { NavigatorContext, PullRequestContext, User, WorkspaceContext } from "@g import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { NotFoundError, UnauthorizedError } from "../errors"; -import { normalizeBranchName, toBranch, toRepository } from "./azure-converter"; +import { getProjectAndRepoName, normalizeBranchName, toBranch, toRepository } from "./azure-converter"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { AuthProviderParams } from "../auth/auth-provider"; @@ -28,20 +28,35 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I span.setTag("contextUrl", contextUrl); try { - const { host, owner, repoName, moreSegments, searchParams } = await this.parseURL(user, contextUrl); + const { + host, + owner: azOrganization, + repoName: projectAndRepo, + moreSegments, + searchParams, + } = await this.parseURL(user, contextUrl); + const [azProject, repoName] = getProjectAndRepoName(projectAndRepo); if (moreSegments.length > 0) { switch (moreSegments[0]) { case "pullrequest": { return await this.handlePullRequestContext( user, host, - owner, + azOrganization, + azProject, repoName, parseInt(moreSegments[1]), ); } case "commit": { - return await this.handleCommitContext(user, host, owner, repoName, moreSegments[1]); + return await this.handleCommitContext( + user, + host, + azOrganization, + azProject, + repoName, + moreSegments[1], + ); } default: { const version = searchParams.get("version"); @@ -49,16 +64,30 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I break; } if (version.startsWith("GB")) { - return await this.handleBranchContext(user, host, owner, repoName, version.slice(2)); + return await this.handleBranchContext( + user, + host, + azOrganization, + azProject, + repoName, + version.slice(2), + ); } if (version.startsWith("GT")) { - return await this.handleTagContext(user, host, owner, repoName, version.slice(2)); + return await this.handleTagContext( + user, + host, + azOrganization, + azProject, + repoName, + version.slice(2), + ); } } } } - return await this.handleDefaultContext(user, host, owner, repoName); + return await this.handleDefaultContext(user, host, azOrganization, azProject, repoName); } catch (error) { // TODO(hw): [AZ] proper handle errors // if (error && error.code === 401) { @@ -86,12 +115,13 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I // case when there is only one repository in the project // https://dev.azure.com/services-azure/_git/project2 if (segments.length === 3 && segments[1] === "_git") { + const azOrganization = segments[0]; const azProject = segments[2]; const repo = azProject; return { host, - owner: azProject, - repoName: repo, + owner: azOrganization, + repoName: `${azProject}/${repo}`, moreSegments: segments.slice(3), searchParams: url.searchParams, }; @@ -99,13 +129,13 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I if (segments.length < 4 || segments[2] !== "_git") { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid Azure DevOps URL"); } - // const azOrganization = segments[0]; + const azOrganization = segments[0]; const azProject = segments[1]; const repo = segments[3]; return { host, - owner: azProject, - repoName: repo, + owner: azOrganization, + repoName: `${azProject}/${repo}`, moreSegments: segments.slice(4), searchParams: url.searchParams, }; @@ -115,11 +145,12 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I protected async handleDefaultContext( user: User, host: string, + azOrganization: string, azProject: string, repo: string, ): Promise { try { - const repository = await this.azureDevOpsApi.getRepository(user, azProject, repo); + const repository = await this.azureDevOpsApi.getRepository(user, azOrganization, azProject, repo); const result: NavigatorContext = { path: "", isFile: false, @@ -132,7 +163,9 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I } try { const branchName = normalizeBranchName(repository.defaultBranch); - const branch = toBranch(await this.azureDevOpsApi.getBranch(user, azProject, repo, branchName)); + const branch = toBranch( + await this.azureDevOpsApi.getBranch(user, azOrganization, azProject, repo, branchName), + ); if (!branch) { return result; } @@ -166,11 +199,12 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I protected async handlePullRequestContext( user: User, host: string, + azOrganization: string, azProject: string, repo: string, pr: number, ): Promise { - const pullRequest = await this.azureDevOpsApi.getPullRequest(user, azProject, repo, pr); + const pullRequest = await this.azureDevOpsApi.getPullRequest(user, azOrganization, azProject, repo, pr); const sourceRepo = toRepository( this.config.host, pullRequest.forkSource?.repository ?? pullRequest.repository!, @@ -199,13 +233,14 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I protected async handleBranchContext( user: User, host: string, + azOrganization: string, azProject: string, repo: string, branch: string, ): Promise { const [repository, branchInfo] = await Promise.all([ - this.azureDevOpsApi.getRepository(user, azProject, repo), - this.azureDevOpsApi.getBranch(user, azProject, repo, branch), + this.azureDevOpsApi.getRepository(user, azOrganization, azProject, repo), + this.azureDevOpsApi.getBranch(user, azOrganization, azProject, repo, branch), ]); const result: NavigatorContext = { path: "", @@ -226,13 +261,14 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I protected async handleTagContext( user: User, host: string, + azOrganization: string, azProject: string, repo: string, tag: string, ): Promise { const [repository, tagCommit] = await Promise.all([ - this.azureDevOpsApi.getRepository(user, azProject, repo), - this.azureDevOpsApi.getTagCommit(user, azProject, repo, tag), + this.azureDevOpsApi.getRepository(user, azOrganization, azProject, repo), + this.azureDevOpsApi.getTagCommit(user, azOrganization, azProject, repo, tag), ]); const result: NavigatorContext = { path: "", @@ -253,13 +289,14 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I protected async handleCommitContext( user: User, host: string, + azOrganization: string, azProject: string, repo: string, commit: string, ): Promise { const [repoInfo, commitInfo] = await Promise.all([ - this.azureDevOpsApi.getRepository(user, azProject, repo), - this.azureDevOpsApi.getCommit(user, azProject, repo, commit), + this.azureDevOpsApi.getRepository(user, azOrganization, azProject, repo), + this.azureDevOpsApi.getCommit(user, azOrganization, azProject, repo, commit), ]); const result: NavigatorContext = { path: "", diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index 309478ed877ceb..88288ff38b075a 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -25,6 +25,11 @@ export function normalizeBranchName(ref: string | undefined): string { return ref?.replace("refs/heads/", "") ?? "main"; } +export function getProjectAndRepoName(projectAndRepo: Repository["name"]) { + const [azProject, repoName] = projectAndRepo.split("/"); + return [azProject, repoName] as const; +} + export function toBranch(d: GitBranchStats): Branch | undefined { if (!d.commit) { return; diff --git a/components/server/src/azure-devops/azure-file-provider.ts b/components/server/src/azure-devops/azure-file-provider.ts index 3b30e933ca13e8..29471316f3ba9c 100644 --- a/components/server/src/azure-devops/azure-file-provider.ts +++ b/components/server/src/azure-devops/azure-file-provider.ts @@ -10,15 +10,17 @@ import { FileProvider, MaybeContent } from "../repohost/file-provider"; import { Commit, User, Repository } from "@gitpod/gitpod-protocol"; import { AzureDevOpsApi } from "./azure-api"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { getProjectAndRepoName } from "./azure-converter"; @injectable() export class AzureDevOpsFileProvider implements FileProvider { - @inject(AzureDevOpsApi) protected readonly api: AzureDevOpsApi; + @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; public async getGitpodFileContent(commit: Commit, user: User): Promise { + const azOrgId = commit.repository.owner; const yamlVersion1 = await Promise.all([ - this.api.getFileContent(user, commit, ".gitpod.yml"), - this.api.getFileContent(user, commit, ".gitpod"), + this.azureDevOpsApi.getFileContent(user, azOrgId, commit, ".gitpod.yml"), + this.azureDevOpsApi.getFileContent(user, azOrgId, commit, ".gitpod"), ]); return yamlVersion1.filter((f) => !!f)[0]; } @@ -29,8 +31,10 @@ export class AzureDevOpsFileProvider implements FileProvider { user: User, path: string, ): Promise { + const azOrgId = repository.owner; + const [azProject, repoName] = getProjectAndRepoName(repository.name); const results = await Promise.allSettled([ - this.api.getCommits(user, repository.name, "test-project", { + this.azureDevOpsApi.getCommits(user, azOrgId, azProject, repoName, { filterCommit: { revision: revisionOrBranch, refType: "revision", @@ -38,7 +42,7 @@ export class AzureDevOpsFileProvider implements FileProvider { $top: 1, itemPath: path, }), - this.api.getCommits(user, repository.name, "test-project", { + this.azureDevOpsApi.getCommits(user, azOrgId, azProject, repoName, { filterCommit: { revision: "", ref: revisionOrBranch, @@ -47,7 +51,7 @@ export class AzureDevOpsFileProvider implements FileProvider { $top: 1, itemPath: path, }), - this.api.getCommits(user, repository.name, "test-project", { + this.azureDevOpsApi.getCommits(user, azOrgId, azProject, repoName, { filterCommit: { revision: "", ref: revisionOrBranch, @@ -71,7 +75,8 @@ export class AzureDevOpsFileProvider implements FileProvider { public async getFileContent(commit: Commit, user: User, path: string): Promise { try { - const result = await this.api.getFileContent(user, commit, path); + const azOrgId = commit.repository.owner; + const result = await this.azureDevOpsApi.getFileContent(user, azOrgId, commit, path); return result; } catch (error) { log.debug(error); diff --git a/components/server/src/azure-devops/azure-repository-provider.ts b/components/server/src/azure-devops/azure-repository-provider.ts index b3a4ccee7afd7c..eaa0685ad1e866 100644 --- a/components/server/src/azure-devops/azure-repository-provider.ts +++ b/components/server/src/azure-devops/azure-repository-provider.ts @@ -11,7 +11,7 @@ import { AzureDevOpsApi } from "./azure-api"; import { RepositoryProvider } from "../repohost/repository-provider"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { toBranch, toCommit, toRepository } from "./azure-converter"; +import { getProjectAndRepoName, toBranch, toCommit, toRepository } from "./azure-converter"; import { AuthProviderParams } from "../auth/auth-provider"; @injectable() @@ -20,12 +20,14 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; async getRepo(user: User, owner: string, name: string): Promise { - const resp = await this.azureDevOpsApi.getRepository(user, owner, name); + const [azProject, repoName] = getProjectAndRepoName(name); + const resp = await this.azureDevOpsApi.getRepository(user, owner, azProject, repoName); return toRepository(this.config.host, resp); } async getBranch(user: User, owner: string, repo: string, branch: string): Promise { - const response = await this.azureDevOpsApi.getBranch(user, owner, repo, branch); + const [azProject, repoName] = getProjectAndRepoName(repo); + const response = await this.azureDevOpsApi.getBranch(user, owner, azProject, repoName, branch); const item = toBranch(response); if (!item) { // TODO(hw): [AZ] @@ -36,7 +38,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { async getBranches(user: User, owner: string, repo: string): Promise { const branches: Branch[] = []; - const response = await this.azureDevOpsApi.getBranches(user, owner, repo); + const [azProject, repoName] = getProjectAndRepoName(repo); + const response = await this.azureDevOpsApi.getBranches(user, owner, azProject, repoName); for (const b of response) { const item = toBranch(b); if (!item) { @@ -48,7 +51,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { } async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { - const response = await this.azureDevOpsApi.getCommit(user, owner, repo, ref); + const [azProject, repoName] = getProjectAndRepoName(repo); + const response = await this.azureDevOpsApi.getCommit(user, owner, azProject, repoName, ref); return toCommit(response); } @@ -59,7 +63,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { async hasReadAccess(user: User, owner: string, repo: string): Promise { try { - const response = await this.azureDevOpsApi.getRepository(user, owner, repo); + const [azProject, repoName] = getProjectAndRepoName(repo); + const response = await this.azureDevOpsApi.getRepository(user, owner, azProject, repoName); return !!response.id; } catch (err) { log.warn({ userId: user.id }, "hasReadAccess error", err, { owner, repo }); @@ -74,7 +79,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { revision: string, maxDepth: number = 100, ): Promise { - const result = await this.azureDevOpsApi.getCommits(user, repo, owner, { + const [azProject, repoName] = getProjectAndRepoName(repo); + const result = await this.azureDevOpsApi.getCommits(user, owner, azProject, repoName, { filterCommit: revision ? { revision, refType: "revision" } : undefined, $top: maxDepth, }); diff --git a/components/server/src/azure-devops/azure-token-validator.ts b/components/server/src/azure-devops/azure-token-validator.ts index ee71d3f027df1a..e88aee6e3b18b7 100644 --- a/components/server/src/azure-devops/azure-token-validator.ts +++ b/components/server/src/azure-devops/azure-token-validator.ts @@ -8,6 +8,7 @@ import { injectable, inject } from "inversify"; import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; import { AzureDevOpsApi } from "./azure-api"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { getProjectAndRepoName } from "./azure-converter"; @injectable() export class AzureDevOpsTokenValidator implements IGitTokenValidator { @@ -18,7 +19,8 @@ export class AzureDevOpsTokenValidator implements IGitTokenValidator { let isPrivateRepo: boolean | undefined; let writeAccessToRepo: boolean | undefined = false; try { - await this.azureDevOpsApi.getRepository(params.token, params.owner, params.repo); + const [azProject, repoName] = getProjectAndRepoName(params.repo); + await this.azureDevOpsApi.getRepository(params.token, params.owner, azProject, repoName); found = true; isPrivateRepo = true; writeAccessToRepo = true; From b33e0ca69b185bb3b5e4f6231cf18a054b26a953 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 13 Sep 2024 09:26:30 +0000 Subject: [PATCH 14/44] Fix dashboard --- .../src/teams/git-integrations/GitIntegrationModal.tsx | 2 +- components/dashboard/src/user-settings/Integrations.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx index 993163ae20db53..448582508f69f8 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx @@ -361,7 +361,7 @@ const getPlaceholderForIntegrationType = (type: AuthProviderType) => { case AuthProviderType.BITBUCKET_SERVER: return "bitbucket.example.com"; case AuthProviderType.AZURE_DEVOPS: - return "dev.azure.com/your-organization"; + return "dev.azure.com"; default: return ""; } diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index 3185047de4f84c..51bb1a94520428 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -777,7 +777,7 @@ export function GitIntegrationModal( case AuthProviderType.BITBUCKET_SERVER: return "bitbucket.example.com"; case AuthProviderType.AZURE_DEVOPS: - return "dev.azure.com/your-organization"; + return "dev.azure.com"; default: return ""; } From 262eb92c94eb6e1e344b3558a3c4dad0c382c043 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 13 Sep 2024 09:53:44 +0000 Subject: [PATCH 15/44] Fix user integration --- .../src/user-settings/Integrations.tsx | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index 51bb1a94520428..06e37825300841 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -556,6 +556,9 @@ export function GitIntegrationModal( const [host, setHost] = useState(""); const [clientId, setClientId] = useState(""); const [clientSecret, setClientSecret] = useState(""); + const [authorizationUrl, setAuthorizationUrl] = useState(""); + const [tokenUrl, setTokenUrl] = useState(""); + const [busy, setBusy] = useState(false); const [errorMessage, setErrorMessage] = useState(); const [validationError, setValidationError] = useState(); @@ -571,6 +574,8 @@ export function GitIntegrationModal( setHost(props.provider.host); setClientId(props.provider.oauth2Config?.clientId || ""); setClientSecret(props.provider.oauth2Config?.clientSecret || ""); + setAuthorizationUrl(props.provider.oauth2Config?.authorizationUrl || ""); + setTokenUrl(props.provider.oauth2Config?.tokenUrl || ""); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -579,7 +584,7 @@ export function GitIntegrationModal( setErrorMessage(undefined); validate(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [clientId, clientSecret, type]); + }, [clientId, clientSecret, authorizationUrl, tokenUrl, type]); const onClose = () => props.onClose && props.onClose(); const onUpdate = () => props.onUpdate && props.onUpdate(); @@ -595,6 +600,8 @@ export function GitIntegrationModal( provider: { clientId, clientSecret, + authorizationUrl, + tokenUrl, type, host, userId: props.userId, @@ -607,6 +614,8 @@ export function GitIntegrationModal( type: providerEntry?.type ?? AuthProviderType.UNSPECIFIED, clientId, clientSecret: clientSecret === "redacted" ? "" : clientSecret, + authorizationUrl, + tokenUrl, }, }); } @@ -683,6 +692,12 @@ export function GitIntegrationModal( const updateClientSecret = (value: string) => { setClientSecret(value.trim()); }; + const updateAuthorizationUrl = (value: string) => { + setAuthorizationUrl(value.trim()); + }; + const updateTokenUrl = (value: string) => { + setTokenUrl(value.trim()); + }; const validate = () => { const errors: string[] = []; @@ -692,6 +707,14 @@ export function GitIntegrationModal( if (clientSecret.trim().length === 0) { errors.push(`${type === AuthProviderType.GITLAB ? "Secret" : "Client Secret"} is missing.`); } + if (type === AuthProviderType.AZURE_DEVOPS) { + if (authorizationUrl.trim().length === 0) { + errors.push("Authorization URL is missing."); + } + if (tokenUrl.trim().length === 0) { + errors.push("Token URL is missing."); + } + } if (errors.length === 0) { setValidationError(undefined); return true; @@ -869,6 +892,30 @@ export function GitIntegrationModal( {getRedirectUrlDescription(type, host)} + {type === AuthProviderType.AZURE_DEVOPS && ( + <> +
+ + updateAuthorizationUrl(e.target.value)} + /> +
+
+ + updateTokenUrl(e.target.value)} + /> +
+ + )}
)} From 2696492c6268a623809e6edfa1bb8db48f81a9f5 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 09:46:16 +0000 Subject: [PATCH 24/44] Revert "Remove API tests" This reverts commit a525cbd161e210d6e75bed0d62a339505f7c48df. --- .../server/src/azure-devops/azure-api.spec.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 components/server/src/azure-devops/azure-api.spec.ts diff --git a/components/server/src/azure-devops/azure-api.spec.ts b/components/server/src/azure-devops/azure-api.spec.ts new file mode 100644 index 00000000000000..596a07c93da1cf --- /dev/null +++ b/components/server/src/azure-devops/azure-api.spec.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { User } from "@gitpod/gitpod-protocol"; +import { ifEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; +import { expect } from "chai"; +import { Container } from "inversify"; +import { suite, test, timeout, skip } from "@testdeck/mocha"; +import { DevData, DevTestHelper } from "../dev/dev-data"; +import { AzureDevOpsApi } from "./azure-api"; + +DevTestHelper.echoAzureTestTips(); + +@suite(timeout(10000), skip(ifEnvVarNotSet(DevTestHelper.AzureTestEnv))) +class TestAzureDevOpsFileProvider { + protected azureDevOpsApi: AzureDevOpsApi; + protected user: User; + protected container: Container; + + public before() { + this.container = DevTestHelper.createAzureSCMContainer(); + this.azureDevOpsApi = this.container.get(AzureDevOpsApi); + this.user = DevData.createTestUser(); + } + + @test public async happyPath() { + const result = await this.azureDevOpsApi.getRepository(this.user, "services-azure", "test-project", "repo2"); + expect(result.name).to.equal("repo2"); + } +} + +module.exports = new TestAzureDevOpsFileProvider(); From 6563f4f9ab73d18e21cd008ef31d400461bfce94 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 10:16:59 +0000 Subject: [PATCH 25/44] Fix tests --- components/server/package.json | 2 +- components/server/src/dev/dev-data.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/server/package.json b/components/server/package.json index 26eb748d5abf60..8243cf4cb0b039 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -19,7 +19,7 @@ "purge": "yarn clean && yarn clean:node && yarn run rimraf yarn.lock", "test:leeway": "yarn build && yarn test", "test": "yarn test:unit && yarn test:db", - "test:unit": "mocha './**/*.js' --exclude './node_modules/**' --exit", + "test:unit": "mocha './**/*.spec.js' --exclude './node_modules/**' --exit", "test:db": "cleanup() { echo 'Cleanup started'; yarn stop-services; }; trap cleanup EXIT; . $(leeway run components/gitpod-db:db-test-env) && yarn start-services && mocha './**/*.spec.db.js' --exclude './node_modules/**' --exit", "start-services": "yarn start-testdb && yarn start-redis && yarn start-spicedb", "stop-services": "yarn stop-redis && yarn stop-spicedb", diff --git a/components/server/src/dev/dev-data.ts b/components/server/src/dev/dev-data.ts index 6246204afcf6a5..6bf01ba7eda1ec 100644 --- a/components/server/src/dev/dev-data.ts +++ b/components/server/src/dev/dev-data.ts @@ -19,6 +19,7 @@ import { HostContextProvider } from "../auth/host-context-provider"; import { Container, ContainerModule } from "inversify"; import { AzureDevOpsFileProvider } from "../azure-devops/azure-file-provider"; import { AzureDevOpsRepositoryProvider } from "../azure-devops/azure-repository-provider"; +import { Config } from "../config"; export namespace DevData { export function createTestUser(): User { @@ -163,6 +164,7 @@ export namespace DevTestHelper { }; container.load( new ContainerModule((bind, unbind, isBound, rebind) => { + bind(Config).toConstantValue({}); bind(AzureDevOpsContextParser).toSelf().inSingletonScope(); bind(AzureDevOpsApi).toSelf().inSingletonScope(); bind(AuthProviderParams).toConstantValue(AUTH_HOST_CONFIG); From 8ed2ce395157658f7081cfcf303b25acadda93f7 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 10:43:03 +0000 Subject: [PATCH 26/44] Rebase fixup --- components/gitpod-protocol/src/protocol.ts | 10 ++++++++ .../typescript-common/src/auth-providers.ts | 9 +++++-- .../server/src/azure-devops/azure-api.ts | 7 ++---- .../src/azure-devops/azure-auth-provider.ts | 16 ++++++------ .../src/azure-devops/azure-token-helper.ts | 7 +++--- components/server/src/azure-devops/scopes.ts | 25 ------------------- 6 files changed, 30 insertions(+), 44 deletions(-) delete mode 100644 components/server/src/azure-devops/scopes.ts diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 1a81957ff294bd..64f7cad9eacbe6 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -1406,8 +1406,18 @@ export interface OAuth2Config { export namespace AuthProviderEntry { export type Type = "GitHub" | "GitLab" | "Bitbucket" | "BitbucketServer" | "AzureDevOps" | string; export type Status = "pending" | "verified"; + + /** + * Some auth providers require additional configuration like Azure DevOps. + */ export interface OAuth2CustomConfig { + /** + * The URL to the authorize endpoint of the provider. + */ authorizationUrl?: string; + /** + * The URL to the oauth token endpoint of the provider. + */ tokenUrl?: string; } export type NewEntry = Pick & { diff --git a/components/public-api/typescript-common/src/auth-providers.ts b/components/public-api/typescript-common/src/auth-providers.ts index aa4e7a684ff916..b367a90268d3bc 100644 --- a/components/public-api/typescript-common/src/auth-providers.ts +++ b/components/public-api/typescript-common/src/auth-providers.ts @@ -94,10 +94,15 @@ export namespace BitbucketServerOAuthScopes { } export namespace AzureDevOpsOAuthScopes { - export const READ_USER = "https://app.vssps.visualstudio.com/vso.profile"; - export const READ_REPO = "https://app.vssps.visualstudio.com/vso.code_write"; + const READ_USER = "https://app.vssps.visualstudio.com/vso.profile"; + const READ_REPO = "https://app.vssps.visualstudio.com/vso.code_write"; + + // extend token lifetime + const OFFLINE_ACCESS = "offline_access"; + export const APPEND_WHEN_FETCHING = [OFFLINE_ACCESS]; export const ALL = [READ_USER, READ_REPO]; + export const REPO = [READ_REPO]; export const DEFAULT = ALL; } diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index c457bf800c5699..9edb9161f400b7 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -11,9 +11,9 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { AuthProviderParams } from "../auth/auth-provider"; import { AzureDevOpsTokenHelper } from "./azure-token-helper"; import { WebApi, getBearerHandler } from "azure-devops-node-api"; -import { AzureDevOpsScopes } from "./scopes"; import { MaybeContent } from "../repohost"; import { GitVersionDescriptor, GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces"; +import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; @injectable() export class AzureDevOpsApi { @@ -28,10 +28,7 @@ export class AzureDevOpsApi { if (typeof userOrToken === "string") { bearerToken = userOrToken; } else { - const azureToken = await this.tokenHelper.getTokenWithScopes( - userOrToken, - AzureDevOpsScopes.Requirements.DEFAULT, - ); + const azureToken = await this.tokenHelper.getTokenWithScopes(userOrToken, AzureDevOpsOAuthScopes.DEFAULT); bearerToken = azureToken.value; } return new WebApi(opts.serverUrl ?? `https://${this.config.host}/${opts.orgId}`, getBearerHandler(bearerToken)); diff --git a/components/server/src/azure-devops/azure-auth-provider.ts b/components/server/src/azure-devops/azure-auth-provider.ts index c0316dc9be0f18..35c507dd2f4e84 100644 --- a/components/server/src/azure-devops/azure-auth-provider.ts +++ b/components/server/src/azure-devops/azure-auth-provider.ts @@ -8,11 +8,11 @@ import express from "express"; import { injectable, inject } from "inversify"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { AuthProviderInfo } from "@gitpod/gitpod-protocol"; -import { AzureDevOpsScopes } from "./scopes"; import { AzureDevOpsApi } from "./azure-api"; import { GenericAuthProvider } from "../auth/generic-auth-provider"; import { AuthUserSetup } from "../auth/auth-provider"; import { oauthUrls } from "./azure-urls"; +import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; @injectable() export class AzureDevOpsAuthProvider extends GenericAuthProvider { @@ -21,11 +21,11 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { get info(): AuthProviderInfo { return { ...this.defaultInfo(), - scopes: AzureDevOpsScopes.All, + scopes: AzureDevOpsOAuthScopes.ALL, requirements: { - default: AzureDevOpsScopes.Requirements.DEFAULT, - publicRepo: AzureDevOpsScopes.Requirements.REPO, - privateRepo: AzureDevOpsScopes.Requirements.REPO, + default: AzureDevOpsOAuthScopes.DEFAULT, + publicRepo: AzureDevOpsOAuthScopes.REPO, + privateRepo: AzureDevOpsOAuthScopes.REPO, }, }; } @@ -43,9 +43,7 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, // offline_access is required but will not respond as scopes - scope: [...AzureDevOpsScopes.All, ...AzureDevOpsScopes.Requirements.APPEND_WHEN_FETCHING].join( - scopeSeparator, - ), + scope: [...AzureDevOpsOAuthScopes.ALL, ...AzureDevOpsOAuthScopes.APPEND_WHEN_FETCHING].join(scopeSeparator), scopeSeparator, }; } @@ -57,7 +55,7 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { state: string, scope?: string[], ) { - super.authorize(req, res, next, state, scope ? scope : AzureDevOpsScopes.Requirements.DEFAULT); + super.authorize(req, res, next, state, scope ? scope : AzureDevOpsOAuthScopes.DEFAULT); } protected get baseURL() { diff --git a/components/server/src/azure-devops/azure-token-helper.ts b/components/server/src/azure-devops/azure-token-helper.ts index 8fc9cf72f99ee7..4a97fc8caa67ab 100644 --- a/components/server/src/azure-devops/azure-token-helper.ts +++ b/components/server/src/azure-devops/azure-token-helper.ts @@ -8,8 +8,8 @@ import { User, Token } from "@gitpod/gitpod-protocol"; import { UnauthorizedError } from "../errors"; import { AuthProviderParams } from "../auth/auth-provider"; import { injectable, inject } from "inversify"; -import { AzureDevOpsScopes } from "./scopes"; import { TokenProvider } from "../user/token-provider"; +import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; @injectable() export class AzureDevOpsTokenHelper { @@ -36,13 +36,14 @@ export class AzureDevOpsTokenHelper { } catch (e) { console.error(e); } + if (requiredScopes.length === 0) { - requiredScopes = AzureDevOpsScopes.Requirements.DEFAULT; + requiredScopes = AzureDevOpsOAuthScopes.DEFAULT; } throw UnauthorizedError.create({ host, providerType: "AzureDevOps", - requiredScopes: AzureDevOpsScopes.Requirements.DEFAULT, + requiredScopes: AzureDevOpsOAuthScopes.DEFAULT, providerIsConnected: false, isMissingScopes: true, }); diff --git a/components/server/src/azure-devops/scopes.ts b/components/server/src/azure-devops/scopes.ts deleted file mode 100644 index a7aaf0991426bb..00000000000000 --- a/components/server/src/azure-devops/scopes.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2024 Gitpod GmbH. All rights reserved. - * Licensed under the GNU Affero General Public License (AGPL). - * See License.AGPL.txt in the project root for license information. - */ - -/** - * @see https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops - */ -export namespace AzureDevOpsScopes { - export const READ_USER = "https://app.vssps.visualstudio.com/vso.profile"; - export const READ_REPO = "https://app.vssps.visualstudio.com/vso.code_write"; - // extend token lifetime - export const OFFLINE_ACCESS = "offline_access"; - - export const All = [READ_USER, READ_REPO]; - export const Requirements = { - DEFAULT: [READ_USER], - - // offline_access is required but will not respond as scopes - APPEND_WHEN_FETCHING: [OFFLINE_ACCESS], - - REPO: [READ_REPO], - }; -} From c4e75c0167a2983c2465b03fd9f01836e8c1d49b Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 11:08:18 +0000 Subject: [PATCH 27/44] nit fixing --- .../auth-providers/create-org-auth-provider-mutation.ts | 9 +++------ .../auth-providers/create-user-auth-provider-mutation.ts | 9 +++------ .../auth-providers/update-org-auth-provider-mutation.ts | 7 ++----- .../auth-providers/update-user-auth-provider-mutation.ts | 7 ++----- components/dashboard/src/images/azuredevops.svg | 2 +- components/public-api/buf.gen.yaml | 2 +- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts index 9fba3da48e707e..eadffc8622d638 100644 --- a/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/create-org-auth-provider-mutation.ts @@ -6,7 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { getOrgAuthProvidersQueryKey } from "./org-auth-providers-query"; -import { AuthProviderType, CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; type CreateAuthProviderArgs = { @@ -23,9 +23,6 @@ export const useCreateOrgAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: CreateAuthProviderArgs) => { - const authorizationUrl = - provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; - const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.createAuthProvider( new CreateAuthProviderRequest({ owner: { case: "organizationId", value: provider.orgId }, @@ -33,8 +30,8 @@ export const useCreateOrgAuthProviderMutation = () => { oauth2Config: { clientId: provider.clientId, clientSecret: provider.clientSecret, - authorizationUrl, - tokenUrl, + authorizationUrl: provider.authorizationUrl, + tokenUrl: provider.tokenUrl, }, type: provider.type, }), diff --git a/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts index a02049226fae1d..0f12d6c5f66877 100644 --- a/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/create-user-auth-provider-mutation.ts @@ -5,7 +5,7 @@ */ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { AuthProviderType, CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { CreateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; import { getUserAuthProvidersQueryKey } from "./user-auth-providers-query"; @@ -23,9 +23,6 @@ export const useCreateUserAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: CreateAuthProviderArgs) => { - const authorizationUrl = - provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; - const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.createAuthProvider( new CreateAuthProviderRequest({ owner: { case: "ownerId", value: provider.userId }, @@ -33,8 +30,8 @@ export const useCreateUserAuthProviderMutation = () => { oauth2Config: { clientId: provider.clientId, clientSecret: provider.clientSecret, - authorizationUrl, - tokenUrl, + authorizationUrl: provider.authorizationUrl, + tokenUrl: provider.tokenUrl, }, type: provider.type, }), diff --git a/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts index b8c92edcf82f8b..259492a4960b22 100644 --- a/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts @@ -25,16 +25,13 @@ export const useUpdateOrgAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: UpdateAuthProviderArgs) => { - const authorizationUrl = - provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; - const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.updateAuthProvider( new UpdateAuthProviderRequest({ authProviderId: provider.id, clientId: provider.clientId, clientSecret: provider.clientSecret, - authorizationUrl, - tokenUrl, + authorizationUrl: provider.authorizationUrl, + tokenUrl: provider.tokenUrl, }), ); return response.authProvider!; diff --git a/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts index 093f8f5a42be39..a0c878ccc317aa 100644 --- a/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts @@ -25,16 +25,13 @@ export const useUpdateUserAuthProviderMutation = () => { return useMutation({ mutationFn: async ({ provider }: UpdateAuthProviderArgs) => { - const authorizationUrl = - provider.type === AuthProviderType.AZURE_DEVOPS ? provider.authorizationUrl : undefined; - const tokenUrl = provider.type === AuthProviderType.AZURE_DEVOPS ? provider.tokenUrl : undefined; const response = await authProviderClient.updateAuthProvider( new UpdateAuthProviderRequest({ authProviderId: provider.id, clientId: provider.clientId, clientSecret: provider.clientSecret, - authorizationUrl, - tokenUrl, + authorizationUrl: provider.authorizationUrl, + tokenUrl: provider.tokenUrl, }), ); return response.authProvider!; diff --git a/components/dashboard/src/images/azuredevops.svg b/components/dashboard/src/images/azuredevops.svg index df4d5ef8978f40..3a1be63f162e06 100644 --- a/components/dashboard/src/images/azuredevops.svg +++ b/components/dashboard/src/images/azuredevops.svg @@ -1 +1 @@ - +Icon-devops-261 diff --git a/components/public-api/buf.gen.yaml b/components/public-api/buf.gen.yaml index 1277fa762b2c13..fb2bd6835a98ac 100644 --- a/components/public-api/buf.gen.yaml +++ b/components/public-api/buf.gen.yaml @@ -14,7 +14,7 @@ plugins: - module=github.com/gitpod-io/gitpod/components/public-api/go - name: protoc-proxy-gen out: go - path: /root/go-packages/bin/protoc-proxy-gen + path: /workspace/go/bin/protoc-proxy-gen opt: - module=github.com/gitpod-io/gitpod/components/public-api/go From 189c431eca33b06812ae33beaf3f5c95a28604ee Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 15:28:45 +0000 Subject: [PATCH 28/44] revert me --- components/dashboard/src/utils.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index 4787e6fbd446d1..6b2d1fffd39c23 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -53,12 +53,14 @@ export const poll = async ( }; export function isGitpodIo() { - return ( - window.location.hostname === "gitpod.io" || - window.location.hostname === "gitpod-staging.com" || - window.location.hostname.endsWith("gitpod-dev.com") || - window.location.hostname.endsWith("gitpod-io-dev.com") - ); + return window.location.hostname === "gitpod.io"; + // TODO(hw): Revert me before merging + // return ( + // window.location.hostname === "gitpod.io" || + // window.location.hostname === "gitpod-staging.com" || + // window.location.hostname.endsWith("gitpod-dev.com") || + // window.location.hostname.endsWith("gitpod-io-dev.com") + // ); } function trimResource(resource: string): string { From 375d891ac2694d3167605032abf3cedc8e0e346f Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 15:29:05 +0000 Subject: [PATCH 29/44] Fix integration udpate --- .../server/src/auth/auth-provider-service.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/components/server/src/auth/auth-provider-service.ts b/components/server/src/auth/auth-provider-service.ts index 8ed8f078e22e9a..3b02e5c9ce8779 100644 --- a/components/server/src/auth/auth-provider-service.ts +++ b/components/server/src/auth/auth-provider-service.ts @@ -20,6 +20,8 @@ import fetch from "node-fetch"; import { Authorizer } from "../authorization/authorizer"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { getRequiredScopes, getScopesForAuthProviderType } from "@gitpod/public-api-common/lib/auth-providers"; +import { PublicAPIConverter } from "@gitpod/public-api-common/lib/public-api-converter"; +import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; @injectable() export class AuthProviderService { @@ -28,6 +30,7 @@ export class AuthProviderService { @inject(TeamDB) private readonly teamDB: TeamDB, @inject(Config) protected readonly config: Config, @inject(Authorizer) private readonly auth: Authorizer, + @inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter, ) {} /** @@ -237,10 +240,19 @@ export class AuthProviderService { throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found."); } + const isAzure = this.apiConverter.toAuthProviderType(existing.type) === AuthProviderType.AZURE_DEVOPS; + if (!isAzure) { + entry.authorizationUrl = undefined; + entry.tokenUrl = undefined; + } + // Explicitly check if any update needs to be performed const changedId = entry.clientId && entry.clientId !== existing.oauth.clientId; const changedSecret = entry.clientSecret && entry.clientSecret !== existing.oauth.clientSecret; - const changed = changedId || changedSecret; + const azureChanged = + isAzure && + (existing.oauth.authorizationUrl !== entry.authorizationUrl || existing.oauth.tokenUrl !== entry.tokenUrl); + const changed = changedId || changedSecret || azureChanged; if (!changed) { return existing; @@ -251,6 +263,8 @@ export class AuthProviderService { ...existing.oauth, clientId: entry.clientId || existing.oauth?.clientId, clientSecret: entry.clientSecret || existing.oauth?.clientSecret, // FE may send empty ("") if not changed + authorizationUrl: entry.authorizationUrl || existing.oauth?.authorizationUrl, + tokenUrl: entry.tokenUrl || existing.oauth?.tokenUrl, }; const authProvider: AuthProviderEntry = { ...existing, @@ -299,9 +313,20 @@ export class AuthProviderService { if (!existing) { throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found."); } + + const isAzure = this.apiConverter.toAuthProviderType(existing.type) === AuthProviderType.AZURE_DEVOPS; + if (!isAzure) { + entry.authorizationUrl = undefined; + entry.tokenUrl = undefined; + } + + const azureChanged = + isAzure && + (existing.oauth.authorizationUrl !== entry.authorizationUrl || existing.oauth.tokenUrl !== entry.tokenUrl); const changed = entry.clientId !== existing.oauth.clientId || - (entry.clientSecret && entry.clientSecret !== existing.oauth.clientSecret); + (entry.clientSecret && entry.clientSecret !== existing.oauth.clientSecret) || + azureChanged; if (!changed) { return existing; @@ -312,6 +337,8 @@ export class AuthProviderService { ...existing.oauth, clientId: entry.clientId || existing.oauth?.clientId, clientSecret: entry.clientSecret || existing.oauth?.clientSecret, // FE may send empty ("") if not changed + authorizationUrl: entry.authorizationUrl || existing.oauth.authorizationUrl, + tokenUrl: entry.tokenUrl || existing.oauth.tokenUrl, }; const authProvider: AuthProviderEntry = { ...existing, From cea694607df5c5622bdbab431b62c75f6478f7af Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 19:32:12 +0000 Subject: [PATCH 30/44] Fix ENT-780 --- .../dashboard/src/components/RepositoryFinder.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 108a763e487793..98c4e0d1bb4c0a 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -318,6 +318,20 @@ export default function RepositoryFinder({ }); } + if (searchString.length >= 3 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) { + // ENT-780 + result.push({ + id: "azure-devops", + element: ( +
+ + Azure DevOps doesn't support repository searching. +
+ ), + isSelectable: false, + }); + } + if (searchString.length < 3) { // add an element that tells the user to type more result.push({ From f5467abe4effc5f55aa7f76394cf8541091b7443 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 19:45:05 +0000 Subject: [PATCH 31/44] Don't support azure devops on PAYG --- .../auth-provider-options-query.ts | 8 ++++++-- .../git-integrations/GitIntegrationModal.tsx | 9 +++++++-- .../src/user-settings/AuthEntryItem.tsx | 3 ++- .../dashboard/src/user-settings/Integrations.tsx | 14 ++++++++++++-- .../typescript-common/src/auth-providers.ts | 16 ++++++++++++---- .../server/src/auth/auth-provider-service.ts | 4 ++++ 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts b/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts index 279aa9a2c8f1df..654a3a11238fd1 100644 --- a/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts +++ b/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts @@ -17,9 +17,13 @@ const optionsForPAYG = [ const optionsForEnterprise = [...optionsForPAYG, { type: AuthProviderType.AZURE_DEVOPS, label: "Azure DevOps" }]; +export const isSupportAzureDevOpsIntegration = () => { + return isGitpodIo(); +}; + export const useAuthProviderOptionsQuery = () => { - const isPAYG = isGitpodIo(); return useMemo(() => { + const isPAYG = isGitpodIo(); return isPAYG ? optionsForPAYG : optionsForEnterprise; - }, [isPAYG]); + }, []); }; diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx index c760bc4d0b85c6..7749cd09397570 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx @@ -24,7 +24,10 @@ import { useCreateOrgAuthProviderMutation } from "../../data/auth-providers/crea import { useUpdateOrgAuthProviderMutation } from "../../data/auth-providers/update-org-auth-provider-mutation"; import { authProviderClient, userClient } from "../../service/public-api"; import { LoadingButton } from "@podkit/buttons/LoadingButton"; -import { useAuthProviderOptionsQuery } from "../../data/auth-providers/auth-provider-options-query"; +import { + isSupportAzureDevOpsIntegration, + useAuthProviderOptionsQuery, +} from "../../data/auth-providers/auth-provider-options-query"; type Props = { provider?: AuthProvider; @@ -41,6 +44,7 @@ export const GitIntegrationModal: FunctionComponent = (props) => { const [authorizationUrl, setAuthorizationUrl] = useState(props.provider?.oauth2Config?.authorizationUrl ?? ""); const [tokenUrl, setTokenUrl] = useState(props.provider?.oauth2Config?.tokenUrl ?? ""); const availableProviderOptions = useAuthProviderOptionsQuery(); + const supportAzureDevOps = isSupportAzureDevOpsIntegration(); const [savedProvider, setSavedProvider] = useState(props.provider); const isNew = !savedProvider; @@ -251,7 +255,8 @@ export const GitIntegrationModal: FunctionComponent = (props) => { {isNew && ( - Configure a Git Integration with a self-managed instance of GitLab, GitHub, or Bitbucket Server. + Configure a Git Integration with a self-managed instance of GitLab, GitHub{" "} + {supportAzureDevOps ? ", Bitbucket Server or Azure DevOps" : "or Bitbucket"}. )} diff --git a/components/dashboard/src/user-settings/AuthEntryItem.tsx b/components/dashboard/src/user-settings/AuthEntryItem.tsx index e076726ceb5aac..7c457ede4aee2e 100644 --- a/components/dashboard/src/user-settings/AuthEntryItem.tsx +++ b/components/dashboard/src/user-settings/AuthEntryItem.tsx @@ -9,6 +9,7 @@ import { ContextMenuEntry } from "../components/ContextMenu"; import { Item, ItemFieldIcon, ItemField, ItemFieldContextMenu } from "../components/ItemsList"; import { AuthProviderDescription } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { toAuthProviderLabel } from "../provider-utils"; +import { getScopeNameForScope } from "@gitpod/public-api-common/lib/auth-providers"; interface AuthEntryItemParams { ap: AuthProviderDescription; @@ -53,7 +54,7 @@ export const AuthEntryItem = (props: AuthEntryItemParams) => { - {props.getPermissions(props.ap.id)?.join(", ") || "–"} + {props.getPermissions(props.ap.id)?.map(getScopeNameForScope)?.join(", ") || "–"} Permissions diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index 51da4acaf1fb4a..ba4a21650d56dd 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -4,7 +4,12 @@ * See License.AGPL.txt in the project root for license information. */ -import { getRequiredScopes, getScopesForAuthProviderType } from "@gitpod/public-api-common/lib/auth-providers"; +import { + AzureDevOpsOAuthScopes, + getRequiredScopes, + getScopeNameForScope, + getScopesForAuthProviderType, +} from "@gitpod/public-api-common/lib/auth-providers"; import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; import { useQuery } from "@tanstack/react-query"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; @@ -90,6 +95,11 @@ const getDescriptionForScope = (scope: string) => { return "Allow creating, merging and declining pull requests (note: Bitbucket doesn't support revoking scopes)"; case "webhook": return "Allow installing webhooks (used when enabling prebuilds for a repository, note: Bitbucket doesn't support revoking scopes)"; + // Azure DevOps + case AzureDevOpsOAuthScopes.WRITE_REPO: + return "Code read and write permissions"; + case AzureDevOpsOAuthScopes.READ_USER: + return "Read user profile"; default: return ""; } @@ -334,7 +344,7 @@ function GitProviders() { Date: Thu, 26 Sep 2024 19:54:13 +0000 Subject: [PATCH 32/44] dashboard: add comments and remove new Azure DevOps supports on user settings page --- .../auth-providers/auth-provider-options-query.ts | 13 ++++++++++--- .../teams/git-integrations/GitIntegrationModal.tsx | 2 +- .../dashboard/src/user-settings/Integrations.tsx | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts b/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts index 654a3a11238fd1..958d74581376af 100644 --- a/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts +++ b/components/dashboard/src/data/auth-providers/auth-provider-options-query.ts @@ -21,9 +21,16 @@ export const isSupportAzureDevOpsIntegration = () => { return isGitpodIo(); }; -export const useAuthProviderOptionsQuery = () => { +export const useAuthProviderOptionsQuery = (isOrgLevel: boolean) => { return useMemo(() => { const isPAYG = isGitpodIo(); - return isPAYG ? optionsForPAYG : optionsForEnterprise; - }, []); + // Azure DevOps is not supported for PAYG users and is only available for org-level integrations + // because auth flow is identified by auth provider's host, which will always be `dev.azure.com` + // + // Don't remove this until we can setup an generial application for Azure DevOps (investigate needed) + if (isPAYG || !isOrgLevel) { + return optionsForPAYG; + } + return optionsForEnterprise; + }, [isOrgLevel]); }; diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx index 7749cd09397570..1ea79257516d54 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx @@ -43,7 +43,7 @@ export const GitIntegrationModal: FunctionComponent = (props) => { const [clientSecret, setClientSecret] = useState(props.provider?.oauth2Config?.clientSecret ?? ""); const [authorizationUrl, setAuthorizationUrl] = useState(props.provider?.oauth2Config?.authorizationUrl ?? ""); const [tokenUrl, setTokenUrl] = useState(props.provider?.oauth2Config?.tokenUrl ?? ""); - const availableProviderOptions = useAuthProviderOptionsQuery(); + const availableProviderOptions = useAuthProviderOptionsQuery(true); const supportAzureDevOps = isSupportAzureDevOpsIntegration(); const [savedProvider, setSavedProvider] = useState(props.provider); diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index ba4a21650d56dd..166e2b1f744031 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -577,7 +577,7 @@ export function GitIntegrationModal( const createProvider = useCreateUserAuthProviderMutation(); const updateProvider = useUpdateUserAuthProviderMutation(); - const availableProviderOptions = useAuthProviderOptionsQuery(); + const availableProviderOptions = useAuthProviderOptionsQuery(false); useEffect(() => { setMode(props.mode); From f33d5a388f49afb48383a9a9ac27c7c814de540e Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 21:16:23 +0000 Subject: [PATCH 33/44] Fix push warning and make project a part of owner --- .../server/src/azure-devops/azure-api.ts | 7 ++- .../azure-devops/azure-context-parser.spec.ts | 56 +++++++++---------- .../src/azure-devops/azure-context-parser.ts | 21 +++---- .../src/azure-devops/azure-converter.ts | 22 ++++++-- .../azure-devops/azure-file-provider.spec.ts | 8 +-- .../src/azure-devops/azure-file-provider.ts | 14 ++--- .../azure-repository-provider.spec.ts | 4 +- .../azure-devops/azure-repository-provider.ts | 26 ++++----- .../src/azure-devops/azure-token-validator.ts | 6 +- .../server/src/repohost/repo-url.spec.ts | 11 ++++ components/server/src/repohost/repo-url.ts | 4 ++ 11 files changed, 105 insertions(+), 74 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index 9edb9161f400b7..d5084f2d23e4de 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -10,7 +10,7 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { AuthProviderParams } from "../auth/auth-provider"; import { AzureDevOpsTokenHelper } from "./azure-token-helper"; -import { WebApi, getBearerHandler } from "azure-devops-node-api"; +import { WebApi, getPersonalAccessTokenHandler } from "azure-devops-node-api"; import { MaybeContent } from "../repohost"; import { GitVersionDescriptor, GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces"; import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; @@ -31,7 +31,10 @@ export class AzureDevOpsApi { const azureToken = await this.tokenHelper.getTokenWithScopes(userOrToken, AzureDevOpsOAuthScopes.DEFAULT); bearerToken = azureToken.value; } - return new WebApi(opts.serverUrl ?? `https://${this.config.host}/${opts.orgId}`, getBearerHandler(bearerToken)); + return new WebApi( + opts.serverUrl ?? `https://${this.config.host}/${opts.orgId}`, + getPersonalAccessTokenHandler(bearerToken), + ); } private async createGitApi(userOrToken: User | string, orgId: string) { diff --git a/components/server/src/azure-devops/azure-context-parser.spec.ts b/components/server/src/azure-devops/azure-context-parser.spec.ts index 7dbd3165fcec79..d37e8ceccad4ae 100644 --- a/components/server/src/azure-devops/azure-context-parser.spec.ts +++ b/components/server/src/azure-devops/azure-context-parser.spec.ts @@ -38,8 +38,8 @@ class TestAzureDevOpsContextParser { title: "empty-project/empty-project", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "empty-project/empty-project", + owner: "services-azure/empty-project", + name: "empty-project", cloneUrl: "https://services-azure@dev.azure.com/services-azure/empty-project/_git/empty-project", description: "main", webUrl: "https://dev.azure.com/services-azure/empty-project/_git/empty-project", @@ -61,8 +61,8 @@ class TestAzureDevOpsContextParser { title: "test-project/repo2 - main", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", @@ -85,8 +85,8 @@ class TestAzureDevOpsContextParser { base: { repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", @@ -95,11 +95,11 @@ class TestAzureDevOpsContextParser { ref: "main", refType: "branch", }, - title: "", + title: "Test Pull Request", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", @@ -123,8 +123,8 @@ class TestAzureDevOpsContextParser { title: "test-project/repo2 - develop-2", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", @@ -146,8 +146,8 @@ class TestAzureDevOpsContextParser { title: "test-project/repo2 - develop-2", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", @@ -169,8 +169,8 @@ class TestAzureDevOpsContextParser { title: "test-project/repo2-fork - develop-2", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2-fork", + owner: "services-azure/test-project", + name: "repo2-fork", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", @@ -194,8 +194,8 @@ class TestAzureDevOpsContextParser { title: "test-project/repo2-fork - v0.0.1", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2-fork", + owner: "services-azure/test-project", + name: "repo2-fork", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", @@ -217,8 +217,8 @@ class TestAzureDevOpsContextParser { title: "test-project/repo2-fork - v0.0.1", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2-fork", + owner: "services-azure/test-project", + name: "repo2-fork", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", @@ -243,11 +243,11 @@ class TestAzureDevOpsContextParser { revision: "4c47246a2eacd9700aab401902775c248e85aee7", isFile: false, title: "test-project/repo2-fork - Updated .gitpod.yml", - owner: "services-azure", + owner: "services-azure/test-project", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2-fork", + owner: "services-azure/test-project", + name: "repo2-fork", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", @@ -267,11 +267,11 @@ class TestAzureDevOpsContextParser { revision: "4c47246a2eacd9700aab401902775c248e85aee7", isFile: false, title: "test-project/repo2-fork - Updated .gitpod.yml", - owner: "services-azure", + owner: "services-azure/test-project", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2-fork", + owner: "services-azure/test-project", + name: "repo2-fork", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", @@ -291,11 +291,11 @@ class TestAzureDevOpsContextParser { revision: "4c47246a2eacd9700aab401902775c248e85aee7", isFile: false, title: "test-project/repo2-fork - Updated .gitpod.yml", - owner: "services-azure", + owner: "services-azure/test-project", repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2-fork", + owner: "services-azure/test-project", + name: "repo2-fork", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts index c645cce6ad757c..6012c06ca4e76e 100644 --- a/components/server/src/azure-devops/azure-context-parser.ts +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -13,7 +13,7 @@ import { NavigatorContext, PullRequestContext, User, WorkspaceContext } from "@g import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { NotFoundError, UnauthorizedError } from "../errors"; -import { getProjectAndRepoName, normalizeBranchName, toBranch, toRepository } from "./azure-converter"; +import { getOrgAndProject, normalizeBranchName, toBranch, toRepository } from "./azure-converter"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { AuthProviderParams } from "../auth/auth-provider"; @@ -30,12 +30,12 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I try { const { host, - owner: azOrganization, - repoName: projectAndRepo, + owner: orgAndProject, + repoName, moreSegments, searchParams, } = await this.parseURL(user, contextUrl); - const [azProject, repoName] = getProjectAndRepoName(projectAndRepo); + const [azOrganization, azProject] = getOrgAndProject(orgAndProject); if (moreSegments.length > 0) { switch (moreSegments[0]) { case "pullrequest": { @@ -118,8 +118,8 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I const repo = azProject; return { host, - owner: azOrganization, - repoName: `${azProject}/${repo}`, + owner: `${azOrganization}/${azProject}`, + repoName: repo, moreSegments: segments.slice(3), searchParams: url.searchParams, }; @@ -132,8 +132,8 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I const repo = segments[3]; return { host, - owner: azOrganization, - repoName: `${azProject}/${repo}`, + owner: `${azOrganization}/${azProject}`, + repoName: repo, moreSegments: segments.slice(4), searchParams: url.searchParams, }; @@ -208,6 +208,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I pullRequest.forkSource?.repository ?? pullRequest.repository!, azOrganization, ); + const targetRepo = toRepository(this.config.host, pullRequest.repository!, azOrganization); const result: PullRequestContext = { nr: pr, @@ -216,7 +217,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I ref: normalizeBranchName(pullRequest.targetRefName!), refType: "branch", } as any as PullRequestContext["base"], - title: "", + title: pullRequest.title ?? `${targetRepo.name} #${pr}`, repository: sourceRepo, ref: normalizeBranchName(pullRequest.sourceRefName!), refType: "branch", @@ -305,7 +306,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I isFile: false, title: `${azProject}/${repo} - ${commitInfo.comment}`, // @ts-ignore - owner: azOrganization, + owner: `${azOrganization}/${azProject}`, repository: toRepository(this.config.host, repoInfo, azOrganization), }; return result; diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index e7224c158e5a73..16eda49dc4108b 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -9,9 +9,17 @@ import { GitBranchStats, GitCommitRef } from "azure-devops-node-api/interfaces/G import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces"; export function toRepository(host: string, d: GitRepository, azOrgId?: string): Repository { - const owner = azOrgId ?? d.webUrl?.replace("https://dev.azure.com/", "").split("/").pop() ?? "unknown"; // should not be unknown + const azOrg = azOrgId ?? d.webUrl?.replace("https://dev.azure.com/", "").split("/").pop(); + const azProject = d.project?.name; + if (!azOrg || !azProject) { + throw new Error("Invalid repository owner"); + } + if (!d.name) { + throw new Error("Invalid repository name"); + } + const owner = `${azOrg}/${azProject}`; const branchName = normalizeBranchName(d.defaultBranch); - const name = [d.project?.name, d.name ?? "unknown"].join("/"); + const name = d.name ?? "unknown"; return { host, owner, @@ -27,9 +35,13 @@ export function normalizeBranchName(ref: string | undefined): string { return ref?.replace("refs/heads/", "") ?? "main"; } -export function getProjectAndRepoName(projectAndRepo: Repository["name"]) { - const [azProject, repoName] = projectAndRepo.split("/"); - return [azProject, repoName] as const; +export function getOrgAndProject(orgAndProject: Repository["owner"]) { + const parts = orgAndProject.split("/"); + if (parts.length < 2) { + throw new Error(`Invalid owner format: ${orgAndProject}`); + } + const [azOrg, azProject] = parts; + return [azOrg, azProject] as const; } export function toBranch(d: GitBranchStats): Branch | undefined { diff --git a/components/server/src/azure-devops/azure-file-provider.spec.ts b/components/server/src/azure-devops/azure-file-provider.spec.ts index 81d86a619e476f..be3593133e6476 100644 --- a/components/server/src/azure-devops/azure-file-provider.spec.ts +++ b/components/server/src/azure-devops/azure-file-provider.spec.ts @@ -31,8 +31,8 @@ class TestAzureDevOpsFileProvider { { repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", @@ -58,8 +58,8 @@ class TestAzureDevOpsFileProvider { { repository: { host: "dev.azure.com", - owner: "services-azure", - name: "test-project/repo2", + owner: "services-azure/test-project", + name: "repo2", cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", diff --git a/components/server/src/azure-devops/azure-file-provider.ts b/components/server/src/azure-devops/azure-file-provider.ts index 49d33e43ec5421..18da3679287518 100644 --- a/components/server/src/azure-devops/azure-file-provider.ts +++ b/components/server/src/azure-devops/azure-file-provider.ts @@ -10,15 +10,15 @@ import { FileProvider, MaybeContent } from "../repohost/file-provider"; import { Commit, User, Repository } from "@gitpod/gitpod-protocol"; import { AzureDevOpsApi } from "./azure-api"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { getProjectAndRepoName } from "./azure-converter"; +import { getOrgAndProject } from "./azure-converter"; @injectable() export class AzureDevOpsFileProvider implements FileProvider { @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; public async getGitpodFileContent(commit: Commit, user: User): Promise { - const azOrgId = commit.repository.owner; - const [azProject, repoName] = getProjectAndRepoName(commit.repository.name); + const [azOrgId, azProject] = getOrgAndProject(commit.repository.owner); + const repoName = commit.repository.name; const yamlVersion1 = await Promise.all([ this.azureDevOpsApi.getFileContent(user, azOrgId, azProject, repoName, commit, ".gitpod.yml"), this.azureDevOpsApi.getFileContent(user, azOrgId, azProject, repoName, commit, ".gitpod"), @@ -32,8 +32,8 @@ export class AzureDevOpsFileProvider implements FileProvider { user: User, path: string, ): Promise { - const azOrgId = repository.owner; - const [azProject, repoName] = getProjectAndRepoName(repository.name); + const [azOrgId, azProject] = getOrgAndProject(repository.owner); + const repoName = repository.name; const results = await Promise.allSettled([ this.azureDevOpsApi.getCommits(user, azOrgId, azProject, repoName, { filterCommit: { @@ -76,8 +76,8 @@ export class AzureDevOpsFileProvider implements FileProvider { public async getFileContent(commit: Commit, user: User, path: string): Promise { try { - const azOrgId = commit.repository.owner; - const [azProject, repoName] = getProjectAndRepoName(commit.repository.name); + const [azOrgId, azProject] = getOrgAndProject(commit.repository.owner); + const repoName = commit.repository.name; return await this.azureDevOpsApi.getFileContent(user, azOrgId, azProject, repoName, commit, path); } catch (error) { log.debug(error); diff --git a/components/server/src/azure-devops/azure-repository-provider.spec.ts b/components/server/src/azure-devops/azure-repository-provider.spec.ts index d61c6a30706aec..6fedb8ce086ef7 100644 --- a/components/server/src/azure-devops/azure-repository-provider.spec.ts +++ b/components/server/src/azure-devops/azure-repository-provider.spec.ts @@ -27,8 +27,8 @@ class TestAzureDevOpsRepositoryProvider { @test public async testFetchCommitHistory() { const result = await this.repositoryProvider.getCommitHistory( this.user, - "services-azure", - "test-project/repo2-fork", + "services-azure/test-project", + "repo2-fork", "dafbf184f68e7ee4dc6d1174d962cab84b605eb2", 100, ); diff --git a/components/server/src/azure-devops/azure-repository-provider.ts b/components/server/src/azure-devops/azure-repository-provider.ts index 51ee0c78387e25..dfadcff561a771 100644 --- a/components/server/src/azure-devops/azure-repository-provider.ts +++ b/components/server/src/azure-devops/azure-repository-provider.ts @@ -11,7 +11,7 @@ import { AzureDevOpsApi } from "./azure-api"; import { RepositoryProvider } from "../repohost/repository-provider"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { getProjectAndRepoName, toBranch, toCommit, toRepository } from "./azure-converter"; +import { getOrgAndProject, toBranch, toCommit, toRepository } from "./azure-converter"; import { AuthProviderParams } from "../auth/auth-provider"; @injectable() @@ -20,14 +20,14 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { @inject(AzureDevOpsApi) protected readonly azureDevOpsApi: AzureDevOpsApi; async getRepo(user: User, owner: string, name: string): Promise { - const [azProject, repoName] = getProjectAndRepoName(name); - const resp = await this.azureDevOpsApi.getRepository(user, owner, azProject, repoName); + const [azOrg, azProject] = getOrgAndProject(owner); + const resp = await this.azureDevOpsApi.getRepository(user, azOrg, azProject, name); return toRepository(this.config.host, resp, owner); } async getBranch(user: User, owner: string, repo: string, branch: string): Promise { - const [azProject, repoName] = getProjectAndRepoName(repo); - const response = await this.azureDevOpsApi.getBranch(user, owner, azProject, repoName, branch); + const [azOrg, azProject] = getOrgAndProject(owner); + const response = await this.azureDevOpsApi.getBranch(user, azOrg, azProject, repo, branch); const item = toBranch(response); if (!item) { // TODO(hw): [AZ] @@ -38,8 +38,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { async getBranches(user: User, owner: string, repo: string): Promise { const branches: Branch[] = []; - const [azProject, repoName] = getProjectAndRepoName(repo); - const response = await this.azureDevOpsApi.getBranches(user, owner, azProject, repoName); + const [azOrg, azProject] = getOrgAndProject(owner); + const response = await this.azureDevOpsApi.getBranches(user, azOrg, azProject, repo); for (const b of response) { const item = toBranch(b); if (!item) { @@ -51,8 +51,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { } async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { - const [azProject, repoName] = getProjectAndRepoName(repo); - const response = await this.azureDevOpsApi.getCommit(user, owner, azProject, repoName, ref); + const [azOrg, azProject] = getOrgAndProject(owner); + const response = await this.azureDevOpsApi.getCommit(user, azOrg, azProject, repo, ref); return toCommit(response); } @@ -63,8 +63,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { async hasReadAccess(user: User, owner: string, repo: string): Promise { try { - const [azProject, repoName] = getProjectAndRepoName(repo); - const response = await this.azureDevOpsApi.getRepository(user, owner, azProject, repoName); + const [azOrg, azProject] = getOrgAndProject(owner); + const response = await this.azureDevOpsApi.getRepository(user, azOrg, azProject, repo); return !!response.id; } catch (err) { log.warn({ userId: user.id }, "hasReadAccess error", err, { owner, repo }); @@ -79,8 +79,8 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { revision: string, maxDepth: number = 100, ): Promise { - const [azProject, repoName] = getProjectAndRepoName(repo); - const result = await this.azureDevOpsApi.getCommits(user, owner, azProject, repoName, { + const [azOrg, azProject] = getOrgAndProject(owner); + const result = await this.azureDevOpsApi.getCommits(user, azOrg, azProject, repo, { filterCommit: revision ? { revision, refType: "revision" } : undefined, $top: maxDepth, }); diff --git a/components/server/src/azure-devops/azure-token-validator.ts b/components/server/src/azure-devops/azure-token-validator.ts index e88aee6e3b18b7..173b22e539f901 100644 --- a/components/server/src/azure-devops/azure-token-validator.ts +++ b/components/server/src/azure-devops/azure-token-validator.ts @@ -8,7 +8,7 @@ import { injectable, inject } from "inversify"; import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; import { AzureDevOpsApi } from "./azure-api"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { getProjectAndRepoName } from "./azure-converter"; +import { getOrgAndProject } from "./azure-converter"; @injectable() export class AzureDevOpsTokenValidator implements IGitTokenValidator { @@ -19,8 +19,8 @@ export class AzureDevOpsTokenValidator implements IGitTokenValidator { let isPrivateRepo: boolean | undefined; let writeAccessToRepo: boolean | undefined = false; try { - const [azProject, repoName] = getProjectAndRepoName(params.repo); - await this.azureDevOpsApi.getRepository(params.token, params.owner, azProject, repoName); + const [azOrg, azProject] = getOrgAndProject(params.owner); + await this.azureDevOpsApi.getRepository(params.token, azOrg, azProject, params.repo); found = true; isPrivateRepo = true; writeAccessToRepo = true; diff --git a/components/server/src/repohost/repo-url.spec.ts b/components/server/src/repohost/repo-url.spec.ts index 9b28b06b63f2ba..484257283bdcf4 100644 --- a/components/server/src/repohost/repo-url.spec.ts +++ b/components/server/src/repohost/repo-url.spec.ts @@ -82,6 +82,17 @@ export class RepoUrlTest { repo: "repoName", }); } + + @test public parseAzureScmUrl() { + const testUrl = RepoURL.parseRepoUrl( + "https://services-azure@dev.azure.com/services-azure/open-to-edit-project/_git/repo2.kai.klasen.git", + ); + expect(testUrl).to.deep.include({ + host: "dev.azure.com", + owner: "services-azure/open-to-edit-project", + repo: "repo2.kai.klasen", + }); + } } module.exports = new RepoUrlTest(); diff --git a/components/server/src/repohost/repo-url.ts b/components/server/src/repohost/repo-url.ts index 500287dee974ba..5475b5e959d0bf 100644 --- a/components/server/src/repohost/repo-url.ts +++ b/components/server/src/repohost/repo-url.ts @@ -32,6 +32,10 @@ export namespace RepoURL { repoKind = "users"; owner = owner.substring(1); } + // Azure DevOps + if (owner.endsWith("/_git")) { + owner = owner.slice(0, -5); + } const repo = endSegment.endsWith(".git") ? endSegment.slice(0, -4) : endSegment; return { host, owner, repo, repoKind }; } From bae8f0afdc155aa39eb2f44e732bf8089ddd849f Mon Sep 17 00:00:00 2001 From: Huiwen Date: Thu, 26 Sep 2024 22:07:04 +0000 Subject: [PATCH 34/44] Proper handle errors --- .../server/src/azure-devops/azure-api.spec.ts | 19 ++++++++++++ .../server/src/azure-devops/azure-api.ts | 15 +++++++++- .../src/azure-devops/azure-context-parser.ts | 29 ++++++++++--------- .../src/azure-devops/azure-converter.ts | 9 +++--- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.spec.ts b/components/server/src/azure-devops/azure-api.spec.ts index 596a07c93da1cf..2dfeed039b8960 100644 --- a/components/server/src/azure-devops/azure-api.spec.ts +++ b/components/server/src/azure-devops/azure-api.spec.ts @@ -11,6 +11,7 @@ import { Container } from "inversify"; import { suite, test, timeout, skip } from "@testdeck/mocha"; import { DevData, DevTestHelper } from "../dev/dev-data"; import { AzureDevOpsApi } from "./azure-api"; +import { RepositoryNotFoundError } from "@gitpod/public-api-common/lib/public-api-errors"; DevTestHelper.echoAzureTestTips(); @@ -30,6 +31,24 @@ class TestAzureDevOpsFileProvider { const result = await this.azureDevOpsApi.getRepository(this.user, "services-azure", "test-project", "repo2"); expect(result.name).to.equal("repo2"); } + + @test public async test401() { + try { + await this.azureDevOpsApi.getPullRequest("not-a-user", "services-azure", "test-project", "repo2", 5); + expect.fail("should have failed"); + } catch (error) { + expect(error.statusCode).to.equal(401); + } + } + + @test public async test404() { + try { + await this.azureDevOpsApi.getRepository(this.user, "services-azure", "test-project", "must_be_repo_404"); + expect.fail("should have failed"); + } catch (error) { + expect(error instanceof RepositoryNotFoundError).to.equal(true); + } + } } module.exports = new TestAzureDevOpsFileProvider(); diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index d5084f2d23e4de..0cbd337e3df6e9 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -14,6 +14,7 @@ import { WebApi, getPersonalAccessTokenHandler } from "azure-devops-node-api"; import { MaybeContent } from "../repohost"; import { GitVersionDescriptor, GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces"; import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; +import { RepositoryNotFoundError } from "@gitpod/public-api-common/lib/public-api-errors"; @injectable() export class AzureDevOpsApi { @@ -145,7 +146,19 @@ export class AzureDevOpsApi { */ async getRepository(userOrToken: User | string, azOrgId: string, azProject: string, repository: string) { const gitApi = await this.createGitApi(userOrToken, azOrgId); - return gitApi.getRepository(repository, azProject); + const repo = await gitApi.getRepository(repository, azProject); + if (!repo) { + throw new RepositoryNotFoundError({ + host: this.config.host, + owner: `${azOrgId}/${azProject}`, + repoName: repository, + userIsOwner: false, + userScopes: [], + lastUpdate: "", + errorMessage: "Repository not found.", + }); + } + return repo; } async getBranches( diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts index 6012c06ca4e76e..c5b4590cc6430b 100644 --- a/components/server/src/azure-devops/azure-context-parser.ts +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -16,6 +16,9 @@ import { NotFoundError, UnauthorizedError } from "../errors"; import { getOrgAndProject, normalizeBranchName, toBranch, toRepository } from "./azure-converter"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { AuthProviderParams } from "../auth/auth-provider"; +import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; +import { RepoURL } from "../repohost"; +import { containsScopes } from "../prebuilds/token-scopes-inclusion"; @injectable() export class AzureDevOpsContextParser extends AbstractContextParser implements IContextParser { @@ -87,18 +90,18 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I return await this.handleDefaultContext(user, host, azOrganization, azProject, repoName); } catch (error) { - // TODO(hw): [AZ] proper handle errors - // if (error && error.code === 401) { - // const token = await this.tokenHelper.getCurrentToken(user); - // throw UnauthorizedError.create({ - // host: this.config.host, - // providerType: "Gitlab", - // requiredScopes: GitLabScope.Requirements.DEFAULT, - // repoName: RepoURL.parseRepoUrl(contextUrl)?.repo, - // providerIsConnected: !!token, - // isMissingScopes: containsScopes(token?.scopes, GitLabScope.Requirements.DEFAULT), - // }); - // } + if (error && error.statusCode === 401) { + const token = await this.tokenHelper.getCurrentToken(user); + throw UnauthorizedError.create({ + host: this.config.host, + providerType: "AzureDevOps", + requiredScopes: AzureDevOpsOAuthScopes.DEFAULT, + repoName: RepoURL.parseRepoUrl(contextUrl)?.repo, + providerIsConnected: !!token, + isMissingScopes: containsScopes(token?.scopes, AzureDevOpsOAuthScopes.DEFAULT), + }); + } + log.debug("AzureDevOps context parser: Failed to parse.", error); throw error; } finally { span.finish(); @@ -125,7 +128,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I }; } if (segments.length < 4 || segments[2] !== "_git") { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid Azure DevOps URL"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid Azure DevOps repository URL"); } const azOrganization = segments[0]; const azProject = segments[1]; diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index 16eda49dc4108b..30b2187afb6b5b 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -5,6 +5,7 @@ */ import { Branch, CommitInfo, Repository } from "@gitpod/gitpod-protocol"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { GitBranchStats, GitCommitRef } from "azure-devops-node-api/interfaces/GitInterfaces"; import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces"; @@ -12,14 +13,14 @@ export function toRepository(host: string, d: GitRepository, azOrgId?: string): const azOrg = azOrgId ?? d.webUrl?.replace("https://dev.azure.com/", "").split("/").pop(); const azProject = d.project?.name; if (!azOrg || !azProject) { - throw new Error("Invalid repository owner"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid repository owner"); } if (!d.name) { - throw new Error("Invalid repository name"); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid repository name"); } const owner = `${azOrg}/${azProject}`; const branchName = normalizeBranchName(d.defaultBranch); - const name = d.name ?? "unknown"; + const name = d.name; return { host, owner, @@ -38,7 +39,7 @@ export function normalizeBranchName(ref: string | undefined): string { export function getOrgAndProject(orgAndProject: Repository["owner"]) { const parts = orgAndProject.split("/"); if (parts.length < 2) { - throw new Error(`Invalid owner format: ${orgAndProject}`); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid owner format`); } const [azOrg, azProject] = parts; return [azOrg, azProject] as const; From 87afe7f23e4edd19f786b17eb28c6039407fe198 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 07:37:53 +0000 Subject: [PATCH 35/44] Fix token can't refresh issue --- .../src/azure-devops/azure-auth-provider.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/components/server/src/azure-devops/azure-auth-provider.ts b/components/server/src/azure-devops/azure-auth-provider.ts index 35c507dd2f4e84..8088ea2d7b8599 100644 --- a/components/server/src/azure-devops/azure-auth-provider.ts +++ b/components/server/src/azure-devops/azure-auth-provider.ts @@ -30,6 +30,16 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { }; } + private mergeAzureScope(scope: string[]) { + // offline_access is required but will not respond as scopes + for (const s of AzureDevOpsOAuthScopes.APPEND_WHEN_FETCHING) { + if (!scope.includes(s)) { + scope.push(s); + } + } + return scope; + } + /** * Augmented OAuthConfig for GitLab */ @@ -42,8 +52,7 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, - // offline_access is required but will not respond as scopes - scope: [...AzureDevOpsOAuthScopes.ALL, ...AzureDevOpsOAuthScopes.APPEND_WHEN_FETCHING].join(scopeSeparator), + scope: AzureDevOpsOAuthScopes.ALL.join(scopeSeparator), scopeSeparator, }; } @@ -55,7 +64,7 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { state: string, scope?: string[], ) { - super.authorize(req, res, next, state, scope ? scope : AzureDevOpsOAuthScopes.DEFAULT); + super.authorize(req, res, next, state, this.mergeAzureScope(scope ? scope : AzureDevOpsOAuthScopes.DEFAULT)); } protected get baseURL() { From 8a0e87f65b2c479c52aa271ebf56f286ed7369a4 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 09:22:05 +0000 Subject: [PATCH 36/44] Fix api --- components/server/src/azure-devops/azure-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index 0cbd337e3df6e9..001fdc15ea0695 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -10,7 +10,7 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { AuthProviderParams } from "../auth/auth-provider"; import { AzureDevOpsTokenHelper } from "./azure-token-helper"; -import { WebApi, getPersonalAccessTokenHandler } from "azure-devops-node-api"; +import { WebApi, getHandlerFromToken } from "azure-devops-node-api"; import { MaybeContent } from "../repohost"; import { GitVersionDescriptor, GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces"; import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; @@ -34,7 +34,7 @@ export class AzureDevOpsApi { } return new WebApi( opts.serverUrl ?? `https://${this.config.host}/${opts.orgId}`, - getPersonalAccessTokenHandler(bearerToken), + getHandlerFromToken(bearerToken), ); } From e621899ed1238a4ad8ea41ccdf3321c064e6c71e Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 09:31:27 +0000 Subject: [PATCH 37/44] Add project context supports --- .../azure-devops/azure-context-parser.spec.ts | 19 +++++++++ .../src/azure-devops/azure-context-parser.ts | 42 ++++++++++--------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/components/server/src/azure-devops/azure-context-parser.spec.ts b/components/server/src/azure-devops/azure-context-parser.spec.ts index d37e8ceccad4ae..65910a2bbf2a4d 100644 --- a/components/server/src/azure-devops/azure-context-parser.spec.ts +++ b/components/server/src/azure-devops/azure-context-parser.spec.ts @@ -49,6 +49,25 @@ class TestAzureDevOpsContextParser { }); } + @test public async testEmptyProjectWithoutGitSegment() { + const result = await this.parser.handle({}, this.user, "https://dev.azure.com/services-azure/empty-project"); + expect(result).to.deep.include({ + path: "", + isFile: false, + title: "empty-project/empty-project", + repository: { + host: "dev.azure.com", + owner: "services-azure/empty-project", + name: "empty-project", + cloneUrl: "https://services-azure@dev.azure.com/services-azure/empty-project/_git/empty-project", + description: "main", + webUrl: "https://dev.azure.com/services-azure/empty-project/_git/empty-project", + defaultBranch: "main", + }, + revision: "", + }); + } + @test public async testDefault() { const result = await this.parser.handle( {}, diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts index c5b4590cc6430b..df33d85a7ab38f 100644 --- a/components/server/src/azure-devops/azure-context-parser.ts +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -113,31 +113,35 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); const segments = pathname.split("/").filter((e) => e !== ""); const host = this.host; - // case when there is only one repository in the project - // https://dev.azure.com/services-azure/_git/project2 - if (segments.length === 3 && segments[1] === "_git") { - const azOrganization = segments[0]; - const azProject = segments[2]; - const repo = azProject; - return { - host, - owner: `${azOrganization}/${azProject}`, - repoName: repo, - moreSegments: segments.slice(3), - searchParams: url.searchParams, - }; - } - if (segments.length < 4 || segments[2] !== "_git") { + let azOrganization = ""; + let azProject = ""; + let repo = ""; + let moreSegments: string[] = []; + if (segments.length === 2) { + // https://dev.azure.com/services-azure/empty-project + azOrganization = segments[0]; + azProject = segments[1]; + repo = azProject; + } else if (segments.length === 3 && segments[1] === "_git") { + // https://dev.azure.com/services-azure/_git/project2 + azOrganization = segments[0]; + azProject = segments[2]; + repo = azProject; + moreSegments = segments.slice(3); + } else if (segments.length < 4 || segments[2] !== "_git") { + // https://dev.azure.com/services-azure/project2/_git/project2 throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid Azure DevOps repository URL"); + } else { + moreSegments = segments.slice(4); + azOrganization = segments[0]; + azProject = segments[1]; + repo = segments[3]; } - const azOrganization = segments[0]; - const azProject = segments[1]; - const repo = segments[3]; return { host, owner: `${azOrganization}/${azProject}`, repoName: repo, - moreSegments: segments.slice(4), + moreSegments, searchParams: url.searchParams, }; } From ce12d3d5576538257c5ddab6c404d312d64998c8 Mon Sep 17 00:00:00 2001 From: mustard Date: Fri, 27 Sep 2024 20:06:53 +0800 Subject: [PATCH 38/44] Update components/server/src/azure-devops/azure-context-parser.spec.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Filip Troníček --- components/server/src/azure-devops/azure-context-parser.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/server/src/azure-devops/azure-context-parser.spec.ts b/components/server/src/azure-devops/azure-context-parser.spec.ts index 65910a2bbf2a4d..a48716b12cbdcb 100644 --- a/components/server/src/azure-devops/azure-context-parser.spec.ts +++ b/components/server/src/azure-devops/azure-context-parser.spec.ts @@ -26,7 +26,7 @@ class TestAzureDevOpsContextParser { this.user = DevData.createTestUser(); } - @test public async testEmptyProoject() { + @test public async testEmptyProject() { const result = await this.parser.handle( {}, this.user, From ac749a47c4d9c6eac795561fbfc36d4f26589f03 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 15:25:26 +0000 Subject: [PATCH 39/44] Fix readablestream error --- .../server/src/azure-devops/azure-api.ts | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index 001fdc15ea0695..ae6b7c6b96c440 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -15,6 +15,7 @@ import { MaybeContent } from "../repohost"; import { GitVersionDescriptor, GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces"; import { AzureDevOpsOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers"; import { RepositoryNotFoundError } from "@gitpod/public-api-common/lib/public-api-errors"; +import { IncomingMessage } from "node:http"; @injectable() export class AzureDevOpsApi { @@ -112,14 +113,14 @@ export class AzureDevOpsApi { undefined, this.getVersionDescriptor(commit), ); + const err = AzureReadableStreamError.tryCreate(readableStream); + if (err) { + throw err; + } let content = ""; for await (const chunk of readableStream) { content += chunk.toString(); } - const err = AzureReadableStreamError.tryCreate(content); - if (err) { - throw err; - } return content; } catch (err) { if (err instanceof AzureReadableStreamError) { @@ -265,21 +266,16 @@ export class AzureReadableStreamError extends Error { super(message); } - /** - * SDK will respond exception (json string) into readableStream - */ - static tryCreate(content: string): AzureReadableStreamError | undefined { - try { - // `{"$id":"1","innerException":null,"message":"TF400813: The user 'a9f4cac8-b940-6b73-be49-ec49d5d15535' is not authorized to access this resource.","typeName":"Microsoft.TeamFoundation.Framework.Server.UnauthorizedRequestException, Microsoft.TeamFoundation.Framework.Server","typeKey":"UnauthorizedRequestException","errorCode":0,"eventId":3000}` - const obj = JSON.parse(content); - if (!!obj && typeof obj === "object" && "message" in obj && typeof obj.message === "string") { - const msg = obj.message as string; - const type = msg.includes("could not be found") ? "not_found" : "unknown"; - return new AzureReadableStreamError(msg, type); - } - return; - } catch (error) { - return; + private static isIncomingMessage(stream: NodeJS.ReadableStream): stream is IncomingMessage { + return "statusCode" in stream && typeof (stream as any).statusCode === "number"; + } + + static tryCreate(stream: NodeJS.ReadableStream): AzureReadableStreamError | undefined { + if (AzureReadableStreamError.isIncomingMessage(stream)) { + return new AzureReadableStreamError( + `HTTP ${stream.statusCode} ${stream.statusMessage}`, + stream.statusCode === 404 ? "not_found" : "unknown", + ); } } } From be43556d661f82c23f72def705bbae66232f891e Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 15:56:40 +0000 Subject: [PATCH 40/44] Fix clone url --- .../server/src/azure-devops/azure-api.ts | 12 ++++----- .../azure-devops/azure-context-parser.spec.ts | 26 +++++++++---------- .../src/azure-devops/azure-converter.ts | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/components/server/src/azure-devops/azure-api.ts b/components/server/src/azure-devops/azure-api.ts index ae6b7c6b96c440..469d1f3d6bfefb 100644 --- a/components/server/src/azure-devops/azure-api.ts +++ b/components/server/src/azure-devops/azure-api.ts @@ -124,7 +124,7 @@ export class AzureDevOpsApi { return content; } catch (err) { if (err instanceof AzureReadableStreamError) { - if (err.type === "not_found") { + if (err.statusCode === 404) { return undefined; } throw err; @@ -262,7 +262,7 @@ export class AzureDevOpsApi { } export class AzureReadableStreamError extends Error { - constructor(message: string, public type: "not_found" | "unknown" = "unknown") { + constructor(message: string, public statusCode?: number) { super(message); } @@ -272,10 +272,10 @@ export class AzureReadableStreamError extends Error { static tryCreate(stream: NodeJS.ReadableStream): AzureReadableStreamError | undefined { if (AzureReadableStreamError.isIncomingMessage(stream)) { - return new AzureReadableStreamError( - `HTTP ${stream.statusCode} ${stream.statusMessage}`, - stream.statusCode === 404 ? "not_found" : "unknown", - ); + if (stream.statusCode === 200) { + return; + } + return new AzureReadableStreamError(`HTTP ${stream.statusCode} ${stream.statusMessage}`, stream.statusCode); } } } diff --git a/components/server/src/azure-devops/azure-context-parser.spec.ts b/components/server/src/azure-devops/azure-context-parser.spec.ts index a48716b12cbdcb..4913ece1b5a892 100644 --- a/components/server/src/azure-devops/azure-context-parser.spec.ts +++ b/components/server/src/azure-devops/azure-context-parser.spec.ts @@ -40,7 +40,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/empty-project", name: "empty-project", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/empty-project/_git/empty-project", + cloneUrl: "https://dev.azure.com/services-azure/empty-project/_git/empty-project", description: "main", webUrl: "https://dev.azure.com/services-azure/empty-project/_git/empty-project", defaultBranch: "main", @@ -59,7 +59,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/empty-project", name: "empty-project", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/empty-project/_git/empty-project", + cloneUrl: "https://dev.azure.com/services-azure/empty-project/_git/empty-project", description: "main", webUrl: "https://dev.azure.com/services-azure/empty-project/_git/empty-project", defaultBranch: "main", @@ -82,7 +82,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", defaultBranch: "main", @@ -106,7 +106,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", defaultBranch: "main", @@ -119,7 +119,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", defaultBranch: "main", @@ -144,7 +144,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", defaultBranch: "main", @@ -167,7 +167,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2", defaultBranch: "main", @@ -190,7 +190,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2-fork", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", defaultBranch: "main", @@ -215,7 +215,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2-fork", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", defaultBranch: "main", @@ -238,7 +238,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2-fork", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", defaultBranch: "main", @@ -267,7 +267,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2-fork", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", defaultBranch: "main", @@ -291,7 +291,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2-fork", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", defaultBranch: "main", @@ -315,7 +315,7 @@ class TestAzureDevOpsContextParser { host: "dev.azure.com", owner: "services-azure/test-project", name: "repo2-fork", - cloneUrl: "https://services-azure@dev.azure.com/services-azure/test-project/_git/repo2-fork", + cloneUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", description: "main", webUrl: "https://dev.azure.com/services-azure/test-project/_git/repo2-fork", defaultBranch: "main", diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index 30b2187afb6b5b..5fca027ffc43bd 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -25,7 +25,7 @@ export function toRepository(host: string, d: GitRepository, azOrgId?: string): host, owner, name, - cloneUrl: d.remoteUrl!, + cloneUrl: d.webUrl!, description: branchName, webUrl: d.webUrl, defaultBranch: branchName, From d5ff0f3640177c437ed1132854edc626ec46fd71 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 16:36:58 +0000 Subject: [PATCH 41/44] Address feedback - dashboard provider type update - doc for checkWriteAccess - Requirement.DEFAULT --- .../auth-providers/update-org-auth-provider-mutation.ts | 4 +--- .../auth-providers/update-user-auth-provider-mutation.ts | 4 +--- .../src/teams/git-integrations/GitIntegrationModal.tsx | 1 - components/dashboard/src/user-settings/Integrations.tsx | 1 - .../public-api/typescript-common/src/auth-providers.ts | 9 ++++++--- .../server/src/azure-devops/azure-auth-provider.ts | 2 +- components/server/src/azure-devops/azure-converter.ts | 2 +- .../server/src/azure-devops/azure-repository-provider.ts | 2 +- .../server/src/azure-devops/azure-token-validator.ts | 4 ++++ components/server/src/dev/dev-data.ts | 2 +- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts index 259492a4960b22..66570b84be28c2 100644 --- a/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/update-org-auth-provider-mutation.ts @@ -6,7 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { getOrgAuthProvidersQueryKey } from "./org-auth-providers-query"; -import { AuthProviderType, UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; type UpdateAuthProviderArgs = { @@ -14,8 +14,6 @@ type UpdateAuthProviderArgs = { id: string; clientId: string; clientSecret: string; - /** verify locally only, will not be update */ - type: AuthProviderType; authorizationUrl?: string; tokenUrl?: string; }; diff --git a/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts b/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts index a0c878ccc317aa..cf99c98001467f 100644 --- a/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts +++ b/components/dashboard/src/data/auth-providers/update-user-auth-provider-mutation.ts @@ -5,7 +5,7 @@ */ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { AuthProviderType, UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { UpdateAuthProviderRequest } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { authProviderClient } from "../../service/public-api"; import { getUserAuthProvidersQueryKey } from "./user-auth-providers-query"; @@ -14,8 +14,6 @@ type UpdateAuthProviderArgs = { id: string; clientId: string; clientSecret: string; - /** verify locally only, will not be update */ - type: AuthProviderType; authorizationUrl?: string; tokenUrl?: string; }; diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx index 1ea79257516d54..8223092119770a 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationModal.tsx @@ -158,7 +158,6 @@ export const GitIntegrationModal: FunctionComponent = (props) => { id: savedProvider.id, clientId: trimmedId, clientSecret: clientSecret === "redacted" ? "" : trimmedSecret, - type, authorizationUrl: trimmedAuthorizationUrl, tokenUrl: trimmedTokenUrl, }, diff --git a/components/dashboard/src/user-settings/Integrations.tsx b/components/dashboard/src/user-settings/Integrations.tsx index 166e2b1f744031..23486e4d211212 100644 --- a/components/dashboard/src/user-settings/Integrations.tsx +++ b/components/dashboard/src/user-settings/Integrations.tsx @@ -624,7 +624,6 @@ export function GitIntegrationModal( newProvider = await updateProvider.mutateAsync({ provider: { id: providerEntry?.id || "", - type: providerEntry?.type ?? AuthProviderType.UNSPECIFIED, clientId, clientSecret: clientSecret === "redacted" ? "" : clientSecret, authorizationUrl, diff --git a/components/public-api/typescript-common/src/auth-providers.ts b/components/public-api/typescript-common/src/auth-providers.ts index 6a3c6ae25195a6..3981a42a83e011 100644 --- a/components/public-api/typescript-common/src/auth-providers.ts +++ b/components/public-api/typescript-common/src/auth-providers.ts @@ -104,6 +104,9 @@ export namespace AzureDevOpsOAuthScopes { export const ALL = [READ_USER, WRITE_REPO]; export const REPO = [WRITE_REPO]; export const DEFAULT = ALL; + export const Requirements = { + DEFAULT: [READ_USER, WRITE_REPO], + }; } export function getScopesForAuthProviderType(type: AuthProviderType | string) { @@ -159,9 +162,9 @@ export function getRequiredScopes(type: AuthProviderType | string) { case AuthProviderType.AZURE_DEVOPS: case "AzureDevOps": return { - default: AzureDevOpsOAuthScopes.DEFAULT, - publicRepo: AzureDevOpsOAuthScopes.DEFAULT, - privateRepo: AzureDevOpsOAuthScopes.DEFAULT, + default: AzureDevOpsOAuthScopes.Requirements.DEFAULT, + publicRepo: AzureDevOpsOAuthScopes.Requirements.DEFAULT, + privateRepo: AzureDevOpsOAuthScopes.Requirements.DEFAULT, }; } } diff --git a/components/server/src/azure-devops/azure-auth-provider.ts b/components/server/src/azure-devops/azure-auth-provider.ts index 8088ea2d7b8599..665be7a1be6901 100644 --- a/components/server/src/azure-devops/azure-auth-provider.ts +++ b/components/server/src/azure-devops/azure-auth-provider.ts @@ -23,7 +23,7 @@ export class AzureDevOpsAuthProvider extends GenericAuthProvider { ...this.defaultInfo(), scopes: AzureDevOpsOAuthScopes.ALL, requirements: { - default: AzureDevOpsOAuthScopes.DEFAULT, + default: AzureDevOpsOAuthScopes.Requirements.DEFAULT, publicRepo: AzureDevOpsOAuthScopes.REPO, privateRepo: AzureDevOpsOAuthScopes.REPO, }, diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index 5fca027ffc43bd..b19bdc5a2cf31c 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -54,7 +54,7 @@ export function toBranch(d: GitBranchStats): Branch | undefined { return; } return { - htmlUrl: d.commit!.url!, + htmlUrl: d.commit.url!, name: d.name!, commit, }; diff --git a/components/server/src/azure-devops/azure-repository-provider.ts b/components/server/src/azure-devops/azure-repository-provider.ts index dfadcff561a771..18acad3194809f 100644 --- a/components/server/src/azure-devops/azure-repository-provider.ts +++ b/components/server/src/azure-devops/azure-repository-provider.ts @@ -57,7 +57,7 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { } async getUserRepos(user: User): Promise { - // FIXME(janx): Not implemented yet + // FIXME(hw): Not implemented yet return []; } diff --git a/components/server/src/azure-devops/azure-token-validator.ts b/components/server/src/azure-devops/azure-token-validator.ts index 173b22e539f901..fadf2ae6703775 100644 --- a/components/server/src/azure-devops/azure-token-validator.ts +++ b/components/server/src/azure-devops/azure-token-validator.ts @@ -21,8 +21,12 @@ export class AzureDevOpsTokenValidator implements IGitTokenValidator { try { const [azOrg, azProject] = getOrgAndProject(params.owner); await this.azureDevOpsApi.getRepository(params.token, azOrg, azProject, params.repo); + // once repository is found, we know the token is valid found = true; + // we don't know if the repository is private or public from API isPrivateRepo = true; + // there's no API to check if the token has write access to the repository + // we required vso.code_write scope, so default should be true writeAccessToRepo = true; } catch (error) { log.error(error); diff --git a/components/server/src/dev/dev-data.ts b/components/server/src/dev/dev-data.ts index 6bf01ba7eda1ec..b8463e43544958 100644 --- a/components/server/src/dev/dev-data.ts +++ b/components/server/src/dev/dev-data.ts @@ -155,7 +155,7 @@ export namespace DevTestHelper { export function createAzureSCMContainer() { const container = new Container(); const AUTH_HOST_CONFIG: Partial = { - id: "Public-GitHub", + id: "0000-Azure-DevOps", type: "AzureDevOps", verified: true, description: "", From 0314a7d70b12c737b27e6dc8e821125895bd3fa5 Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 17:00:47 +0000 Subject: [PATCH 42/44] 1 --- .../src/azure-devops/azure-context-parser.ts | 1 + .../server/src/azure-devops/azure-converter.ts | 6 ++++-- .../src/azure-devops/azure-repository-provider.ts | 14 ++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/components/server/src/azure-devops/azure-context-parser.ts b/components/server/src/azure-devops/azure-context-parser.ts index df33d85a7ab38f..29460eafee3bcb 100644 --- a/components/server/src/azure-devops/azure-context-parser.ts +++ b/components/server/src/azure-devops/azure-context-parser.ts @@ -169,6 +169,7 @@ export class AzureDevOpsContextParser extends AbstractContextParser implements I try { const branchName = normalizeBranchName(repository.defaultBranch); const branch = toBranch( + result.repository, await this.azureDevOpsApi.getBranch(user, azOrganization, azProject, repo, branchName), ); if (!branch) { diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index b19bdc5a2cf31c..0329da5b9deed7 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -45,7 +45,7 @@ export function getOrgAndProject(orgAndProject: Repository["owner"]) { return [azOrg, azProject] as const; } -export function toBranch(d: GitBranchStats): Branch | undefined { +export function toBranch(repo: Repository, d: GitBranchStats): Branch | undefined { if (!d.commit) { return; } @@ -53,8 +53,10 @@ export function toBranch(d: GitBranchStats): Branch | undefined { if (!commit) { return; } + const url = new URL(repo.cloneUrl); + url.searchParams.set("version", `GB${d.name}`); return { - htmlUrl: d.commit.url!, + htmlUrl: url.toString(), name: d.name!, commit, }; diff --git a/components/server/src/azure-devops/azure-repository-provider.ts b/components/server/src/azure-devops/azure-repository-provider.ts index 18acad3194809f..22a154b17a44bb 100644 --- a/components/server/src/azure-devops/azure-repository-provider.ts +++ b/components/server/src/azure-devops/azure-repository-provider.ts @@ -27,8 +27,11 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { async getBranch(user: User, owner: string, repo: string, branch: string): Promise { const [azOrg, azProject] = getOrgAndProject(owner); - const response = await this.azureDevOpsApi.getBranch(user, azOrg, azProject, repo, branch); - const item = toBranch(response); + const [repository, branchResp] = await Promise.all([ + await this.getRepo(user, owner, repo), + await this.azureDevOpsApi.getBranch(user, azOrg, azProject, repo, branch), + ]); + const item = toBranch(repository, branchResp); if (!item) { // TODO(hw): [AZ] throw new Error("Failed to fetch commit."); @@ -39,9 +42,12 @@ export class AzureDevOpsRepositoryProvider implements RepositoryProvider { async getBranches(user: User, owner: string, repo: string): Promise { const branches: Branch[] = []; const [azOrg, azProject] = getOrgAndProject(owner); - const response = await this.azureDevOpsApi.getBranches(user, azOrg, azProject, repo); + const [repository, response] = await Promise.all([ + await this.getRepo(user, owner, repo), + await this.azureDevOpsApi.getBranches(user, azOrg, azProject, repo), + ]); for (const b of response) { - const item = toBranch(b); + const item = toBranch(repository, b); if (!item) { continue; } From 2ac4ecd5a302e696448cc4e8680201ceecb1593f Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 17:09:15 +0000 Subject: [PATCH 43/44] avatar --- components/server/src/azure-devops/azure-converter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/server/src/azure-devops/azure-converter.ts b/components/server/src/azure-devops/azure-converter.ts index 0329da5b9deed7..7a5b3875512d58 100644 --- a/components/server/src/azure-devops/azure-converter.ts +++ b/components/server/src/azure-devops/azure-converter.ts @@ -66,7 +66,7 @@ export function toCommit(d: GitCommitRef): CommitInfo | undefined { return { sha: d.commitId!, author: d.author?.name || "unknown", - authorAvatarUrl: "", // TODO: fetch avatar URL + authorAvatarUrl: d.author?.imageUrl ?? "", authorDate: d.author?.date?.toISOString(), commitMessage: d.comment || "missing commit message", }; From d7f086deff682b656dc76d11f47218c3df7776ad Mon Sep 17 00:00:00 2001 From: Huiwen Date: Fri, 27 Sep 2024 17:12:47 +0000 Subject: [PATCH 44/44] Revert "revert me" This reverts commit 189c431eca33b06812ae33beaf3f5c95a28604ee. --- components/dashboard/src/utils.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index 6b2d1fffd39c23..4787e6fbd446d1 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -53,14 +53,12 @@ export const poll = async ( }; export function isGitpodIo() { - return window.location.hostname === "gitpod.io"; - // TODO(hw): Revert me before merging - // return ( - // window.location.hostname === "gitpod.io" || - // window.location.hostname === "gitpod-staging.com" || - // window.location.hostname.endsWith("gitpod-dev.com") || - // window.location.hostname.endsWith("gitpod-io-dev.com") - // ); + return ( + window.location.hostname === "gitpod.io" || + window.location.hostname === "gitpod-staging.com" || + window.location.hostname.endsWith("gitpod-dev.com") || + window.location.hostname.endsWith("gitpod-io-dev.com") + ); } function trimResource(resource: string): string {