mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
Add Bitbucket Cloud as an auth provider with Perms syncing (#46309)
This commit is contained in:
parent
3851a2a1dd
commit
b4458950c1
@ -31,6 +31,8 @@ All notable changes to Sourcegraph are documented in this file.
|
||||
- When setting and resetting passwords, if the user's primary email address is not yet verified, using the password reset link sent via email will now also verify the email address. [#46307](https://github.com/sourcegraph/sourcegraph/pull/46307)
|
||||
- Added new code host details and updated edit code host pages in site admin area. [#46327](https://github.com/sourcegraph/sourcegraph/pull/46327)
|
||||
- If the experimental setting `insightsDataRetention` is enabled, the number of Code Insights data points that can be viewed will be limited by the site configuration setting `insights.maximumSampleSize`, set to 30 by default. Older points beyond that number will be periodically archived. [#46206](https://github.com/sourcegraph/sourcegraph/pull/46206), [#46440](https://github.com/sourcegraph/sourcegraph/pull/46440)
|
||||
- Bitbucket Cloud can now be added as an authentication provider on Sourcegraph. [#46309](https://github.com/sourcegraph/sourcegraph/pull/46309)
|
||||
- Bitbucket Cloud code host connections now support permissions syncing. [#46312](https://github.com/sourcegraph/sourcegraph/pull/46312)
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { mdiGithub, mdiGitlab } from '@mdi/js'
|
||||
import { mdiBitbucket, mdiGithub, mdiGitlab } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import { partition } from 'lodash'
|
||||
import { Navigate, useLocation } from 'react-router-dom-v5-compat'
|
||||
@ -95,6 +95,11 @@ export const SignInPage: React.FunctionComponent<React.PropsWithChildren<SignInP
|
||||
<Icon aria-hidden={true} svgPath={mdiGitlab} />{' '}
|
||||
</>
|
||||
)}
|
||||
{provider.serviceType === 'bitbucketCloud' && (
|
||||
<>
|
||||
<Icon aria-hidden={true} svgPath={mdiBitbucket} />{' '}
|
||||
</>
|
||||
)}
|
||||
Continue with {provider.displayName}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { mdiGithub, mdiGitlab } from '@mdi/js'
|
||||
import { mdiBitbucket, mdiGithub, mdiGitlab } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import cookies from 'js-cookie'
|
||||
import { Observable, of } from 'rxjs'
|
||||
@ -256,6 +256,8 @@ export const SignUpForm: React.FunctionComponent<React.PropsWithChildren<SignUpF
|
||||
<Icon aria-hidden={true} svgPath={mdiGithub} />
|
||||
) : provider.serviceType === 'gitlab' ? (
|
||||
<Icon aria-hidden={true} svgPath={mdiGitlab} />
|
||||
) : provider.serviceType === 'bitbucketCloud' ? (
|
||||
<Icon aria-hidden={true} svPath={mdiBitbucket} />
|
||||
) : null}{' '}
|
||||
Continue with {provider.displayName}
|
||||
</Button>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import AccountCircleIcon from 'mdi-react/AccountCircleIcon'
|
||||
import BitbucketIcon from 'mdi-react/BitbucketIcon'
|
||||
import GithubIcon from 'mdi-react/GithubIcon'
|
||||
import GitLabIcon from 'mdi-react/GitlabIcon'
|
||||
|
||||
@ -40,4 +41,8 @@ export const defaultExternalAccounts: Record<ExternalAccountKind, ExternalAccoun
|
||||
title: 'SAML',
|
||||
icon: AccountCircleIcon,
|
||||
},
|
||||
bitbucketCloud: {
|
||||
title: 'Bitbucket Cloud',
|
||||
icon: BitbucketIcon,
|
||||
},
|
||||
}
|
||||
|
||||
@ -7,7 +7,15 @@ export type DeployType = 'kubernetes' | 'docker-container' | 'docker-compose' |
|
||||
*/
|
||||
|
||||
export interface AuthProvider {
|
||||
serviceType: 'github' | 'gitlab' | 'http-header' | 'openidconnect' | 'sourcegraph-operator' | 'saml' | 'builtin'
|
||||
serviceType:
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'bitbucketCloud'
|
||||
| 'http-header'
|
||||
| 'openidconnect'
|
||||
| 'sourcegraph-operator'
|
||||
| 'saml'
|
||||
| 'builtin'
|
||||
displayName: string
|
||||
isBuiltin: boolean
|
||||
authenticationURL: string
|
||||
|
||||
@ -24,6 +24,16 @@ interface GitLabExternalData {
|
||||
web_url: string
|
||||
}
|
||||
|
||||
interface BitbucketCloudExternalData {
|
||||
display_name: string
|
||||
username: string
|
||||
links: {
|
||||
self: {
|
||||
href: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SamlExternalData {
|
||||
Values: {
|
||||
emailaddress?: Attribute
|
||||
@ -152,6 +162,21 @@ const getNormalizedAccount = (
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'bitbucketCloud':
|
||||
{
|
||||
const bbCloudExternalData = accountExternalData as BitbucketCloudExternalData
|
||||
normalizedAccount = {
|
||||
...normalizedAccount,
|
||||
external: {
|
||||
id: account.id,
|
||||
// map Bitbucket Cloud fields
|
||||
userName: bbCloudExternalData.display_name,
|
||||
userLogin: bbCloudExternalData.username,
|
||||
userUrl: bbCloudExternalData.links.self.href,
|
||||
},
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'saml':
|
||||
{
|
||||
const samlExternalData = accountExternalData as SamlExternalData
|
||||
|
||||
@ -6,6 +6,7 @@ Sourcegraph supports the following ways for users to sign in:
|
||||
- [Builtin password authentication](#builtin-password-authentication)
|
||||
- [GitHub](#github)
|
||||
- [GitLab](#gitlab)
|
||||
- [Bitbucket Cloud](#bitbucket-cloud)
|
||||
- [SAML](saml/index.md)
|
||||
- [OpenID Connect](#openid-connect)
|
||||
- [Google Workspace (Google accounts)](#google-workspace-google-accounts)
|
||||
@ -309,6 +310,31 @@ You can use the following filters to control how users can create accounts and s
|
||||
}
|
||||
```
|
||||
|
||||
## Bitbucket Cloud
|
||||
[Create a Bitbucket Cloud OAuth consumer](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/). Set the following values, replacing `sourcegraph.example.com` with the IP or hostname of your
|
||||
Sourcegraph instance:
|
||||
|
||||
- Callback URL: `https://sourcegraph.example.com/.auth/bitbucketcloud/callback`
|
||||
- Permissions:
|
||||
- `Account`: `Read`
|
||||
- `Repositories`: `Read` (if [permissions syncing](../repo/permissions.md) is desired)
|
||||
|
||||
After the consumer is created, you will need the `Key` and the `Secret`, which can be found by expanding OAuth consumer in the list.
|
||||
Then add the following lines to your [site configuration](config/site_config.md):
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"auth.providers": [
|
||||
{
|
||||
"type": "bitbucketcloud",
|
||||
"displayName": "Bitbucket Cloud",
|
||||
"clientKey": "replace-with-the-oauth-consumer-key",
|
||||
"clientSecret": "replace-with-the-oauth-consumer-secret",
|
||||
"allowSignup": false // If not set, it defaults to true allowing any Bitbucket Cloud user with access to your instance to sign up.
|
||||
}
|
||||
]
|
||||
```
|
||||
Replace the `clientKey` and `clientSecret` values with the values from your Bitbucket Cloud OAuth consumer.
|
||||
|
||||
## OpenID Connect
|
||||
|
||||
|
||||
@ -33,6 +33,11 @@ If enabled, the default rate is set at 7200 per hour (2 per second), which can b
|
||||
|
||||
**NOTE** Internal rate limiting is only currently applied when synchronizing changesets in [batch changes](../../batch_changes/index.md), repository permissions, and repository metadata from code hosts.
|
||||
|
||||
## User authentication
|
||||
|
||||
To configure Bitbucket Cloud as an authentication provider (which will enable sign-in via Bitbucket Cloud), see the
|
||||
[authentication documentation](../auth/index.md#bitbucket-cloud).
|
||||
|
||||
## Configuration
|
||||
|
||||
Bitbucket Cloud connections support the following configuration options, which are specified in the JSON editor in the site admin "Manage code hosts" area.
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dghubble/gologin"
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/licensing"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func Init(logger log.Logger, db database.DB) {
|
||||
const pkgName = "bitbucketcloudoauth"
|
||||
logger = logger.Scoped(pkgName, "Bitbucket Cloud OAuth config watch")
|
||||
conf.ContributeValidator(func(cfg conftypes.SiteConfigQuerier) conf.Problems {
|
||||
_, problems := parseConfig(logger, cfg, db)
|
||||
return problems
|
||||
})
|
||||
|
||||
go conf.Watch(func() {
|
||||
newProviders, _ := parseConfig(logger, conf.Get(), db)
|
||||
if len(newProviders) == 0 {
|
||||
providers.Update(pkgName, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := licensing.Check(licensing.FeatureSSO); err != nil {
|
||||
logger.Error("Check license for SSO (Bitbucket Cloud OAuth)", log.Error(err))
|
||||
providers.Update(pkgName, nil)
|
||||
return
|
||||
}
|
||||
|
||||
newProvidersList := make([]providers.Provider, 0, len(newProviders))
|
||||
for _, p := range newProviders {
|
||||
newProvidersList = append(newProvidersList, p.Provider)
|
||||
}
|
||||
providers.Update(pkgName, newProvidersList)
|
||||
})
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
*schema.BitbucketCloudAuthProvider
|
||||
providers.Provider
|
||||
}
|
||||
|
||||
func parseConfig(logger log.Logger, cfg conftypes.SiteConfigQuerier, db database.DB) (ps []Provider, problems conf.Problems) {
|
||||
configured := map[string]struct{}{}
|
||||
for _, pr := range cfg.SiteConfig().AuthProviders {
|
||||
if pr.Bitbucketcloud == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
provider, providerProblems := parseProvider(logger, pr.Bitbucketcloud, db, pr)
|
||||
problems = append(problems, conf.NewSiteProblems(providerProblems...)...)
|
||||
if provider == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := configured[provider.ServiceID]; ok {
|
||||
problems = append(problems, conf.NewSiteProblems(fmt.Sprintf(`Cannot have more than one auth provider with url %q, only the first one will be used`, provider.ServiceID))...)
|
||||
continue
|
||||
}
|
||||
|
||||
ps = append(ps, Provider{
|
||||
BitbucketCloudAuthProvider: pr.Bitbucketcloud,
|
||||
Provider: provider,
|
||||
})
|
||||
configured[provider.ServiceID] = struct{}{}
|
||||
}
|
||||
return ps, problems
|
||||
}
|
||||
|
||||
func getStateConfig() gologin.CookieConfig {
|
||||
cfg := gologin.CookieConfig{
|
||||
Name: "bitbucketcloud-state-cookie",
|
||||
Path: "/",
|
||||
MaxAge: 900, // 15 minutes
|
||||
HTTPOnly: true,
|
||||
Secure: conf.IsExternalURLSecure(),
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/oauth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
db := database.NewDB(logger, dbtest.NewDB(logger, t))
|
||||
|
||||
type args struct {
|
||||
cfg *conf.Unified
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantProviders []Provider
|
||||
wantProblems []string
|
||||
}{
|
||||
{
|
||||
name: "No configs",
|
||||
args: args{cfg: &conf.Unified{}},
|
||||
wantProviders: []Provider(nil),
|
||||
},
|
||||
{
|
||||
name: "1 Bitbucket Cloud config",
|
||||
args: args{cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{
|
||||
AuthProviders: []schema.AuthProviders{{
|
||||
Bitbucketcloud: &schema.BitbucketCloudAuthProvider{
|
||||
ClientKey: "myclientid",
|
||||
ClientSecret: "myclientsecret",
|
||||
DisplayName: "Bitbucket Cloud",
|
||||
Type: extsvc.TypeBitbucketCloud,
|
||||
Url: "https://bitbucket.org",
|
||||
ApiScope: "account,email",
|
||||
},
|
||||
}},
|
||||
}}},
|
||||
wantProviders: []Provider{
|
||||
{
|
||||
BitbucketCloudAuthProvider: &schema.BitbucketCloudAuthProvider{
|
||||
ClientKey: "myclientid",
|
||||
ClientSecret: "myclientsecret",
|
||||
DisplayName: "Bitbucket Cloud",
|
||||
Type: extsvc.TypeBitbucketCloud,
|
||||
Url: "https://bitbucket.org",
|
||||
ApiScope: "account,email",
|
||||
},
|
||||
Provider: provider("https://bitbucket.org/", oauth2.Config{
|
||||
ClientID: "myclientid",
|
||||
ClientSecret: "myclientsecret",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
||||
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
||||
},
|
||||
Scopes: []string{"account", "email"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2 Bitbucket Cloud configs with the same Url",
|
||||
args: args{cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{
|
||||
AuthProviders: []schema.AuthProviders{{
|
||||
Bitbucketcloud: &schema.BitbucketCloudAuthProvider{
|
||||
ClientKey: "myclientid",
|
||||
ClientSecret: "myclientsecret",
|
||||
DisplayName: "Bitbucket Cloud",
|
||||
Type: extsvc.TypeBitbucketCloud,
|
||||
Url: "https://bitbucket.org",
|
||||
ApiScope: "account,email",
|
||||
},
|
||||
}, {
|
||||
Bitbucketcloud: &schema.BitbucketCloudAuthProvider{
|
||||
ClientKey: "myclientid2",
|
||||
ClientSecret: "myclientsecret2",
|
||||
DisplayName: "Bitbucket Cloud Duplicate",
|
||||
Type: extsvc.TypeBitbucketCloud,
|
||||
Url: "https://bitbucket.org",
|
||||
ApiScope: "account,email",
|
||||
},
|
||||
}},
|
||||
}}},
|
||||
wantProviders: []Provider{
|
||||
{
|
||||
BitbucketCloudAuthProvider: &schema.BitbucketCloudAuthProvider{
|
||||
ClientKey: "myclientid",
|
||||
ClientSecret: "myclientsecret",
|
||||
DisplayName: "Bitbucket Cloud",
|
||||
Type: extsvc.TypeBitbucketCloud,
|
||||
Url: "https://bitbucket.org",
|
||||
ApiScope: "account,email",
|
||||
},
|
||||
Provider: provider("https://bitbucket.org/", oauth2.Config{
|
||||
ClientID: "myclientid",
|
||||
ClientSecret: "myclientsecret",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
||||
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
||||
},
|
||||
Scopes: []string{"account", "email"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
wantProblems: []string{
|
||||
`Cannot have more than one auth provider with url "https://bitbucket.org/", only the first one will be used`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotProviders, gotProblems := parseConfig(logtest.Scoped(t), tt.args.cfg, db)
|
||||
gotConfigs := make([]oauth2.Config, len(gotProviders))
|
||||
for k, p := range gotProviders {
|
||||
if p, ok := p.Provider.(*oauth.Provider); ok {
|
||||
p.Login, p.Callback = nil, nil
|
||||
gotConfigs[k] = p.OAuth2Config()
|
||||
p.OAuth2Config = nil
|
||||
p.ProviderOp.Login, p.ProviderOp.Callback = nil, nil
|
||||
}
|
||||
}
|
||||
wantConfigs := make([]oauth2.Config, len(tt.wantProviders))
|
||||
for k, p := range tt.wantProviders {
|
||||
k := k
|
||||
if q, ok := p.Provider.(*oauth.Provider); ok {
|
||||
q.SourceConfig = schema.AuthProviders{Bitbucketcloud: p.BitbucketCloudAuthProvider}
|
||||
wantConfigs[k] = q.OAuth2Config()
|
||||
q.OAuth2Config = nil
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(tt.wantProviders, gotProviders); diff != "" {
|
||||
t.Errorf("providers: %s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tt.wantProblems, gotProblems.Messages()); diff != "" {
|
||||
t.Errorf("problems: %s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantConfigs, gotConfigs); diff != "" {
|
||||
t.Errorf("problems: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func provider(serviceID string, oauth2Config oauth2.Config) *oauth.Provider {
|
||||
op := oauth.ProviderOp{
|
||||
AuthPrefix: authPrefix,
|
||||
OAuth2Config: func() oauth2.Config { return oauth2Config },
|
||||
StateConfig: getStateConfig(),
|
||||
ServiceID: serviceID,
|
||||
ServiceType: extsvc.TypeBitbucketCloud,
|
||||
}
|
||||
return &oauth.Provider{ProviderOp: op}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/oauth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
const authPrefix = auth.AuthURLPrefix + "/bitbucketcloud"
|
||||
|
||||
func init() {
|
||||
oauth.AddIsOAuth(func(p schema.AuthProviders) bool {
|
||||
return p.Bitbucketcloud != nil
|
||||
})
|
||||
}
|
||||
|
||||
func Middleware(db database.DB) *auth.Middleware {
|
||||
return &auth.Middleware{
|
||||
API: func(next http.Handler) http.Handler {
|
||||
return oauth.NewMiddleware(db, extsvc.TypeBitbucketCloud, authPrefix, true, next)
|
||||
},
|
||||
App: func(next http.Handler) http.Handler {
|
||||
return oauth.NewMiddleware(db, extsvc.TypeBitbucketCloud, authPrefix, false, next)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/external/session"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/oauth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
// TestMiddleware exercises the Middleware with requests that simulate the OAuth 2 login flow on
|
||||
// Bitbucket Cloud. This tests the logic between the client-issued HTTP requests and the responses from the
|
||||
// various endpoints, but does NOT cover the logic that is contained within `golang.org/x/oauth2`
|
||||
// and `github.com/dghubble/gologin` which ensures the correctness of the `/callback` handler.
|
||||
func TestMiddleware(t *testing.T) {
|
||||
cleanup := session.ResetMockSessionStore(t)
|
||||
defer cleanup()
|
||||
|
||||
db := database.NewMockDB()
|
||||
|
||||
const mockUserID = 123
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("got through"))
|
||||
})
|
||||
authedHandler := http.NewServeMux()
|
||||
authedHandler.Handle("/.api/", Middleware(nil).API(h))
|
||||
authedHandler.Handle("/", Middleware(nil).App(h))
|
||||
|
||||
mockBitbucketCloud := newMockProvider(t, db, "bitbucketcloudclient", "bitbucketcloudsecret", "https://bitbucket.org/")
|
||||
providers.MockProviders = []providers.Provider{mockBitbucketCloud.Provider}
|
||||
defer func() { providers.MockProviders = nil }()
|
||||
|
||||
doRequest := func(method, urlStr, body string, cookies []*http.Cookie, authed bool) *http.Response {
|
||||
req := httptest.NewRequest(method, urlStr, bytes.NewBufferString(body))
|
||||
for _, cookie := range cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla")
|
||||
if authed {
|
||||
req = req.WithContext(actor.WithActor(context.Background(), &actor.Actor{UID: mockUserID}))
|
||||
}
|
||||
respRecorder := httptest.NewRecorder()
|
||||
authedHandler.ServeHTTP(respRecorder, req)
|
||||
return respRecorder.Result()
|
||||
}
|
||||
|
||||
t.Run("unauthenticated homepage visit, sign-out cookie present -> sg sign-in", func(t *testing.T) {
|
||||
cookie := &http.Cookie{Name: auth.SignoutCookie, Value: "true"}
|
||||
|
||||
resp := doRequest("GET", "http://example.com/", "", []*http.Cookie{cookie}, false)
|
||||
if want := http.StatusOK; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
})
|
||||
t.Run("unauthenticated homepage visit, no sign-out cookie -> bitbucket cloud oauth flow", func(t *testing.T) {
|
||||
resp := doRequest("GET", "http://example.com/", "", nil, false)
|
||||
if want := http.StatusFound; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
if got, want := resp.Header.Get("Location"), "/.auth/bitbucketcloud/login?"; !strings.Contains(got, want) {
|
||||
t.Errorf("got redirect URL %v, want contains %v", got, want)
|
||||
}
|
||||
redirectURL, err := url.Parse(resp.Header.Get("Location"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := redirectURL.Query().Get("redirect"), "/"; got != want {
|
||||
t.Errorf("got return-to URL %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("unauthenticated subpage visit -> bitbucket cloud oauth flow", func(t *testing.T) {
|
||||
resp := doRequest("GET", "http://example.com/page", "", nil, false)
|
||||
if want := http.StatusFound; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
|
||||
if got, want := resp.Header.Get("Location"), "/.auth/bitbucketcloud/login?"; !strings.Contains(got, want) {
|
||||
t.Errorf("got redirect URL %v, want contains %v", got, want)
|
||||
}
|
||||
redirectURL, err := url.Parse(resp.Header.Get("Location"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := redirectURL.Query().Get("redirect"), "/page"; got != want {
|
||||
t.Errorf("got return-to URL %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unauthenticated API request -> pass through", func(t *testing.T) {
|
||||
resp := doRequest("GET", "http://example.com/.api/foo", "", nil, false)
|
||||
if want := http.StatusOK; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := string(body), "got through"; got != want {
|
||||
t.Errorf("got response body %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("login -> bitbucket cloud auth flow with redirect param", func(t *testing.T) {
|
||||
resp := doRequest("GET", "http://example.com/.auth/bitbucketcloud/login?pc="+mockBitbucketCloud.Provider.ConfigID().ID+"&redirect=%2Fpage", "", nil, false)
|
||||
if want := http.StatusFound; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
redirect := resp.Header.Get("Location")
|
||||
if got, want := redirect, "https://bitbucket.org/site/oauth2/authorize?"; !strings.HasPrefix(got, want) {
|
||||
t.Errorf("got redirect URL %v, want contains %v", got, want)
|
||||
}
|
||||
uredirect, err := url.Parse(redirect)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := uredirect.Query().Get("client_id"), mockBitbucketCloud.Provider.CachedInfo().ClientID; got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
if got, want := uredirect.Query().Get("scope"), "account email"; got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
if got, want := uredirect.Query().Get("response_type"), "code"; got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
state, err := oauth.DecodeState(uredirect.Query().Get("state"))
|
||||
if err != nil {
|
||||
t.Fatalf("could not decode state: %v", err)
|
||||
}
|
||||
if got, want := state.ProviderID, mockBitbucketCloud.Provider.ConfigID().ID; got != want {
|
||||
t.Fatalf("got state provider ID %v, want %v", got, want)
|
||||
}
|
||||
if got, want := state.Redirect, "/page"; got != want {
|
||||
t.Fatalf("got state redirect %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("Bitbucket Cloud OAuth callback with valid state param", func(t *testing.T) {
|
||||
encodedState, err := oauth.LoginState{
|
||||
Redirect: "/return-to-url",
|
||||
ProviderID: mockBitbucketCloud.Provider.ConfigID().ID,
|
||||
CSRF: "csrf-code",
|
||||
}.Encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
callbackCookies := []*http.Cookie{oauth.NewCookie(getStateConfig(), encodedState)}
|
||||
resp := doRequest("GET", "http://example.com/.auth/bitbucketcloud/callback?code=the-oauth-code&state="+encodedState, "", callbackCookies, false)
|
||||
if want := http.StatusFound; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
if got, want := mockBitbucketCloud.lastCallbackRequestURL, "http://example.com/callback?code=the-oauth-code&state="+encodedState; got == nil || got.String() != want {
|
||||
t.Errorf("got last bitbucket cloud callback request url %v, want %v", got, want)
|
||||
}
|
||||
mockBitbucketCloud.lastCallbackRequestURL = nil
|
||||
})
|
||||
t.Run("Bitbucket Cloud OAuth callback with state with unknown provider", func(t *testing.T) {
|
||||
encodedState, err := oauth.LoginState{
|
||||
Redirect: "/return-to-url",
|
||||
ProviderID: "unknown",
|
||||
CSRF: "csrf-code",
|
||||
}.Encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
callbackCookies := []*http.Cookie{oauth.NewCookie(getStateConfig(), encodedState)}
|
||||
resp := doRequest("GET", "http://example.com/.auth/bitbucketcloud/callback?code=the-oauth-code&state="+encodedState, "", callbackCookies, false)
|
||||
if want := http.StatusBadRequest; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
if mockBitbucketCloud.lastCallbackRequestURL != nil {
|
||||
t.Errorf("got last bitbucket.org callback request url was non-nil: %v", mockBitbucketCloud.lastCallbackRequestURL)
|
||||
}
|
||||
mockBitbucketCloud.lastCallbackRequestURL = nil
|
||||
})
|
||||
t.Run("authenticated app request", func(t *testing.T) {
|
||||
resp := doRequest("GET", "http://example.com/", "", nil, true)
|
||||
if want := http.StatusOK; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := string(body), "got through"; got != want {
|
||||
t.Errorf("got response body %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("authenticated API request", func(t *testing.T) {
|
||||
resp := doRequest("GET", "http://example.com/.api/foo", "", nil, true)
|
||||
if want := http.StatusOK; resp.StatusCode != want {
|
||||
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := string(body), "got through"; got != want {
|
||||
t.Errorf("got response body %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type MockProvider struct {
|
||||
*oauth.Provider
|
||||
lastCallbackRequestURL *url.URL
|
||||
}
|
||||
|
||||
func newMockProvider(t *testing.T, db database.DB, clientID, clientSecret, baseURL string) *MockProvider {
|
||||
var (
|
||||
mp MockProvider
|
||||
problems []string
|
||||
)
|
||||
cfg := schema.AuthProviders{Bitbucketcloud: &schema.BitbucketCloudAuthProvider{
|
||||
Url: baseURL,
|
||||
ClientSecret: clientSecret,
|
||||
ClientKey: clientID,
|
||||
ApiScope: "account,email",
|
||||
}}
|
||||
mp.Provider, problems = parseProvider(logtest.Scoped(t), cfg.Bitbucketcloud, db, cfg)
|
||||
if len(problems) > 0 {
|
||||
t.Fatalf("Expected 0 problems, but got %d: %+v", len(problems), problems)
|
||||
}
|
||||
if mp.Provider == nil {
|
||||
t.Fatalf("Expected provider")
|
||||
}
|
||||
mp.Provider.Callback = func(oauth2.Config) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.Method, "GET"; got != want {
|
||||
t.Errorf("In OAuth callback handler got %q request, wanted %q", got, want)
|
||||
}
|
||||
w.WriteHeader(http.StatusFound)
|
||||
mp.lastCallbackRequestURL = r.URL
|
||||
})
|
||||
}
|
||||
return &mp
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/dghubble/gologin"
|
||||
"github.com/dghubble/gologin/bitbucket"
|
||||
goauth2 "github.com/dghubble/gologin/oauth2"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/oauth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
const sessionKey = "bitbucketcloudoauth@0"
|
||||
const defaultBBCloudURL = "https://bitbucket.org"
|
||||
|
||||
func parseProvider(logger log.Logger, p *schema.BitbucketCloudAuthProvider, db database.DB, sourceCfg schema.AuthProviders) (provider *oauth.Provider, messages []string) {
|
||||
rawURL := p.Url
|
||||
if rawURL == "" {
|
||||
rawURL = defaultBBCloudURL
|
||||
}
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
parsedURL = extsvc.NormalizeBaseURL(parsedURL)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf("Could not parse Bitbucket Cloud URL %q. You will not be able to login via Bitbucket Cloud.", rawURL))
|
||||
return nil, messages
|
||||
}
|
||||
|
||||
if !validateClientKeyOrSecret(p.ClientKey) {
|
||||
messages = append(messages, "Bitbucket Cloud key contains unexpected characters, possibly hidden")
|
||||
}
|
||||
if !validateClientKeyOrSecret(p.ClientSecret) {
|
||||
messages = append(messages, "Bitbucket Cloud secret contains unexpected characters, possibly hidden")
|
||||
}
|
||||
|
||||
return oauth.NewProvider(oauth.ProviderOp{
|
||||
AuthPrefix: authPrefix,
|
||||
OAuth2Config: func() oauth2.Config {
|
||||
return oauth2.Config{
|
||||
ClientID: p.ClientKey,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Scopes: requestedScopes(p.ApiScope),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: parsedURL.ResolveReference(&url.URL{Path: "/site/oauth2/authorize"}).String(),
|
||||
TokenURL: parsedURL.ResolveReference(&url.URL{Path: "/site/oauth2/access_token"}).String(),
|
||||
},
|
||||
}
|
||||
},
|
||||
SourceConfig: sourceCfg,
|
||||
StateConfig: getStateConfig(),
|
||||
ServiceID: parsedURL.String(),
|
||||
ServiceType: extsvc.TypeBitbucketCloud,
|
||||
Login: func(oauth2Cfg oauth2.Config) http.Handler {
|
||||
return bitbucket.LoginHandler(&oauth2Cfg, nil)
|
||||
},
|
||||
Callback: func(oauth2Cfg oauth2.Config) http.Handler {
|
||||
return bitbucket.CallbackHandler(
|
||||
&oauth2Cfg,
|
||||
oauth.SessionIssuer(logger, db, &sessionIssuerHelper{
|
||||
baseURL: parsedURL,
|
||||
db: db,
|
||||
clientKey: p.ClientKey,
|
||||
allowSignup: p.AllowSignup,
|
||||
}, sessionKey),
|
||||
http.HandlerFunc(failureHandler),
|
||||
)
|
||||
},
|
||||
}), messages
|
||||
}
|
||||
|
||||
func failureHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// As a special case we want to handle `access_denied` errors by redirecting
|
||||
// back. This case arises when the user decides not to proceed by clicking `cancel`.
|
||||
if err := r.URL.Query().Get("error"); err != "access_denied" {
|
||||
// Fall back to default failure handler
|
||||
gologin.DefaultFailureHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
encodedState, err := goauth2.StateFromContext(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, "Authentication failed. Try signing in again (and clearing cookies for the current site). The error was: could not get OAuth state from context.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
state, err := oauth.DecodeState(encodedState)
|
||||
if err != nil {
|
||||
http.Error(w, "Authentication failed. Try signing in again (and clearing cookies for the current site). The error was: could not get decode OAuth state.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, auth.SafeRedirectURL(state.Redirect), http.StatusFound)
|
||||
}
|
||||
|
||||
var clientKeySecretValidator = lazyregexp.New("^[a-zA-Z0-9.]*$")
|
||||
|
||||
func validateClientKeyOrSecret(clientKeyOrSecret string) (valid bool) {
|
||||
return clientKeySecretValidator.MatchString(clientKeyOrSecret)
|
||||
}
|
||||
|
||||
func requestedScopes(apiScopes string) []string {
|
||||
return strings.Split(apiScopes, ",")
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func TestRequestedScopes(t *testing.T) {
|
||||
defer envvar.MockSourcegraphDotComMode(false)
|
||||
|
||||
tests := []struct {
|
||||
schema *schema.BitbucketCloudAuthProvider
|
||||
expScopes []string
|
||||
}{
|
||||
{
|
||||
schema: &schema.BitbucketCloudAuthProvider{
|
||||
ApiScope: "account,email,repository",
|
||||
},
|
||||
expScopes: []string{"account", "email", "repository"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
scopes := requestedScopes(test.schema.ApiScope)
|
||||
sort.Strings(scopes)
|
||||
if diff := cmp.Diff(test.expScopes, scopes); diff != "" {
|
||||
t.Fatalf("scopes: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/hubspot"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/hubspot/hubspotutil"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/oauth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
esauth "github.com/sourcegraph/sourcegraph/internal/extsvc/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
type sessionIssuerHelper struct {
|
||||
baseURL *url.URL
|
||||
clientKey string
|
||||
db database.DB
|
||||
allowSignup bool
|
||||
client bitbucketcloud.Client
|
||||
}
|
||||
|
||||
func (s *sessionIssuerHelper) AuthSucceededEventName() database.SecurityEventName {
|
||||
return database.SecurityEventBitbucketCloudAuthSucceeded
|
||||
}
|
||||
|
||||
func (s *sessionIssuerHelper) AuthFailedEventName() database.SecurityEventName {
|
||||
return database.SecurityEventBitbucketCloudAuthFailed
|
||||
}
|
||||
|
||||
func (s *sessionIssuerHelper) GetOrCreateUser(ctx context.Context, token *oauth2.Token, anonymousUserID, firstSourceURL, lastSourceURL string) (actr *actor.Actor, safeErrMsg string, err error) {
|
||||
var client bitbucketcloud.Client
|
||||
if s.client != nil {
|
||||
client = s.client
|
||||
} else {
|
||||
conf := &schema.BitbucketCloudConnection{
|
||||
Url: s.baseURL.String(),
|
||||
}
|
||||
client, err = bitbucketcloud.NewClient(s.baseURL.String(), conf, nil)
|
||||
if err != nil {
|
||||
return nil, "Could not initialize Bitbucket Cloud client", err
|
||||
}
|
||||
}
|
||||
|
||||
// The token used here is fresh from Bitbucket OAuth. It should be valid
|
||||
// for 1 hour, so we don't bother with setting up token refreshing yet.
|
||||
// If account creation/linking succeeds, the token will be stored in the
|
||||
// database with the refresh token, and refreshing can happen from that point.
|
||||
auther := &esauth.OAuthBearerToken{Token: token.AccessToken}
|
||||
client = client.WithAuthenticator(auther)
|
||||
bbUser, err := client.CurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, "Could not read Bitbucket user from callback request.", errors.Wrap(err, "could not read user from bitbucket")
|
||||
}
|
||||
|
||||
var data extsvc.AccountData
|
||||
if err := bitbucketcloud.SetExternalAccountData(&data, &bbUser.Account, token); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
emails, err := client.AllCurrentUserEmails(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
attempts, err := buildUserFetchAttempts(emails, s.allowSignup)
|
||||
if err != nil {
|
||||
return nil, "Could not find verified email address for Bitbucket user.", err
|
||||
}
|
||||
|
||||
var (
|
||||
firstSafeErrMsg string
|
||||
firstErr error
|
||||
)
|
||||
|
||||
for i, attempt := range attempts {
|
||||
userID, safeErrMsg, err := auth.GetAndSaveUser(ctx, s.db, auth.GetAndSaveUserOp{
|
||||
UserProps: database.NewUser{
|
||||
Username: bbUser.Username,
|
||||
Email: attempt.email,
|
||||
EmailIsVerified: true,
|
||||
DisplayName: bbUser.Nickname,
|
||||
AvatarURL: bbUser.Links["avatar"].Href,
|
||||
},
|
||||
ExternalAccount: extsvc.AccountSpec{
|
||||
ServiceType: extsvc.TypeBitbucketCloud,
|
||||
ServiceID: s.baseURL.String(),
|
||||
ClientID: s.clientKey,
|
||||
AccountID: bbUser.UUID,
|
||||
},
|
||||
ExternalAccountData: data,
|
||||
CreateIfNotExist: attempt.createIfNotExist,
|
||||
})
|
||||
if err == nil {
|
||||
go hubspotutil.SyncUser(attempt.email, hubspotutil.SignupEventID, &hubspot.ContactProperties{
|
||||
AnonymousUserID: anonymousUserID,
|
||||
FirstSourceURL: firstSourceURL,
|
||||
LastSourceURL: lastSourceURL,
|
||||
})
|
||||
return actor.FromUser(userID), "", nil
|
||||
}
|
||||
if i == 0 {
|
||||
firstSafeErrMsg, firstErr = safeErrMsg, err
|
||||
}
|
||||
}
|
||||
|
||||
// On failure, return the first error
|
||||
verifiedEmails := make([]string, 0, len(attempts))
|
||||
for i, attempt := range attempts {
|
||||
verifiedEmails[i] = attempt.email
|
||||
}
|
||||
return nil, fmt.Sprintf("No Sourcegraph user exists matching any of the verified emails: %s.\n\nFirst error was: %s", strings.Join(verifiedEmails, ", "), firstSafeErrMsg), firstErr
|
||||
}
|
||||
|
||||
type attempt struct {
|
||||
email string
|
||||
createIfNotExist bool
|
||||
}
|
||||
|
||||
func buildUserFetchAttempts(emails []*bitbucketcloud.UserEmail, allowSignup bool) ([]attempt, error) {
|
||||
attempts := []attempt{}
|
||||
for _, email := range emails {
|
||||
if email.IsConfirmed {
|
||||
attempts = append(attempts, attempt{
|
||||
email: email.Email,
|
||||
createIfNotExist: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(attempts) == 0 {
|
||||
return nil, errors.New("no verified email")
|
||||
}
|
||||
// If allowSignup is true, we will create an account using the first verified
|
||||
// email address from Bitbucket which we expect to be their primary address. Note
|
||||
// that the order of attempts is important. If we manage to connect with an
|
||||
// existing account we return early and don't attempt to create a new account.
|
||||
if allowSignup {
|
||||
attempts = append(attempts, attempt{
|
||||
email: attempts[0].email,
|
||||
createIfNotExist: true,
|
||||
})
|
||||
}
|
||||
|
||||
return attempts, nil
|
||||
}
|
||||
|
||||
func (s *sessionIssuerHelper) DeleteStateCookie(w http.ResponseWriter) {
|
||||
stateConfig := getStateConfig()
|
||||
stateConfig.MaxAge = -1
|
||||
http.SetCookie(w, oauth.NewCookie(stateConfig, ""))
|
||||
}
|
||||
|
||||
func (s *sessionIssuerHelper) SessionData(token *oauth2.Token) oauth.SessionData {
|
||||
return oauth.SessionData{
|
||||
ID: providers.ConfigID{
|
||||
ID: s.baseURL.String(),
|
||||
Type: extsvc.TypeBitbucketCloud,
|
||||
},
|
||||
AccessToken: token.AccessToken,
|
||||
TokenType: token.Type(),
|
||||
// TODO(pjlast): investigate exactly where and how we use this SessionData
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,302 @@
|
||||
package bitbucketcloudoauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
bitbucketlogin "github.com/dghubble/gologin/bitbucket"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
type emailResponse struct {
|
||||
Values []bitbucketcloud.UserEmail `json:"values"`
|
||||
}
|
||||
|
||||
var returnUsername string
|
||||
var returnAccountID string
|
||||
var returnEmails emailResponse
|
||||
|
||||
func createTestServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/user") {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"`
|
||||
}{
|
||||
Username: returnUsername,
|
||||
UUID: returnAccountID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/user/emails") {
|
||||
json.NewEncoder(w).Encode(returnEmails)
|
||||
return
|
||||
}
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
func TestSessionIssuerHelper_GetOrCreateUser(t *testing.T) {
|
||||
server := createTestServer()
|
||||
defer server.Close()
|
||||
bbURL, _ := url.Parse(server.URL)
|
||||
clientID := "client-id"
|
||||
|
||||
// Top-level mock data
|
||||
//
|
||||
// authSaveableUsers that will be accepted by auth.GetAndSaveUser
|
||||
authSaveableUsers := map[string]int32{
|
||||
"alice": 1,
|
||||
}
|
||||
|
||||
type input struct {
|
||||
description string
|
||||
bbUser *bitbucketlogin.User
|
||||
bbUserEmails []bitbucketcloud.UserEmail
|
||||
bbUserEmailsErr error
|
||||
allowSignup bool
|
||||
}
|
||||
cases := []struct {
|
||||
inputs []input
|
||||
expActor *actor.Actor
|
||||
expErr bool
|
||||
expAuthUserOp *auth.GetAndSaveUserOp
|
||||
}{
|
||||
{
|
||||
inputs: []input{{
|
||||
description: "bbUser, verified email -> session created",
|
||||
bbUser: &bitbucketlogin.User{Username: "alice"},
|
||||
bbUserEmails: []bitbucketcloud.UserEmail{
|
||||
{
|
||||
Email: "alice@example.com",
|
||||
IsConfirmed: true,
|
||||
IsPrimary: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
expActor: &actor.Actor{UID: 1},
|
||||
expAuthUserOp: &auth.GetAndSaveUserOp{
|
||||
UserProps: u("alice", "alice@example.com", true),
|
||||
ExternalAccount: acct(extsvc.TypeBitbucketCloud, server.URL+"/", clientID, "1234"),
|
||||
},
|
||||
},
|
||||
{
|
||||
inputs: []input{{
|
||||
description: "bbUser, primary email not verified but another is -> no session created",
|
||||
bbUser: &bitbucketlogin.User{Username: "alice"},
|
||||
bbUserEmails: []bitbucketcloud.UserEmail{
|
||||
{
|
||||
Email: "alice@example1.com",
|
||||
IsPrimary: true,
|
||||
IsConfirmed: false,
|
||||
},
|
||||
{
|
||||
Email: "alice@example2.com",
|
||||
IsPrimary: false,
|
||||
IsConfirmed: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
expActor: &actor.Actor{UID: 1},
|
||||
expAuthUserOp: &auth.GetAndSaveUserOp{
|
||||
UserProps: u("alice", "alice@example2.com", true),
|
||||
ExternalAccount: acct(extsvc.TypeBitbucketCloud, server.URL+"/", clientID, "1234"),
|
||||
},
|
||||
},
|
||||
{
|
||||
inputs: []input{{
|
||||
description: "bbUser, no emails -> no session created",
|
||||
bbUser: &bitbucketlogin.User{Username: "alice"},
|
||||
bbUserEmails: []bitbucketcloud.UserEmail{},
|
||||
}, {
|
||||
description: "bbUser, email fetching err -> no session created",
|
||||
bbUser: &bitbucketlogin.User{Username: "alice"},
|
||||
bbUserEmailsErr: errors.New("x"),
|
||||
}, {
|
||||
description: "bbUser, plenty of emails but none verified -> no session created",
|
||||
bbUser: &bitbucketlogin.User{Username: "alice"},
|
||||
bbUserEmails: []bitbucketcloud.UserEmail{
|
||||
{
|
||||
Email: "alice@example1.com",
|
||||
IsPrimary: true,
|
||||
IsConfirmed: false,
|
||||
},
|
||||
{
|
||||
Email: "alice@example2.com",
|
||||
IsPrimary: true,
|
||||
IsConfirmed: false,
|
||||
},
|
||||
{
|
||||
Email: "alice@example3.com",
|
||||
IsPrimary: true,
|
||||
IsConfirmed: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
description: "no bbUser -> no session created",
|
||||
}, {
|
||||
description: "bbUser, verified email, unsaveable -> no session created",
|
||||
bbUser: &bitbucketlogin.User{Username: "bob"},
|
||||
}},
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
for _, ci := range c.inputs {
|
||||
c, ci := c, ci
|
||||
t.Run(ci.description, func(t *testing.T) {
|
||||
if ci.bbUser != nil {
|
||||
returnUsername = ci.bbUser.Username
|
||||
returnAccountID = "1234"
|
||||
}
|
||||
returnEmails.Values = ci.bbUserEmails
|
||||
|
||||
var gotAuthUserOp *auth.GetAndSaveUserOp
|
||||
auth.MockGetAndSaveUser = func(ctx context.Context, op auth.GetAndSaveUserOp) (userID int32, safeErrMsg string, err error) {
|
||||
if gotAuthUserOp != nil {
|
||||
t.Fatal("GetAndSaveUser called more than once")
|
||||
}
|
||||
op.ExternalAccountData = extsvc.AccountData{} // ignore AccountData value
|
||||
gotAuthUserOp = &op
|
||||
|
||||
if uid, ok := authSaveableUsers[op.UserProps.Username]; ok {
|
||||
return uid, "", nil
|
||||
}
|
||||
return 0, "safeErr", errors.New("auth.GetAndSaveUser error")
|
||||
}
|
||||
defer func() {
|
||||
auth.MockGetAndSaveUser = nil
|
||||
}()
|
||||
|
||||
ctx := bitbucketlogin.WithUser(context.Background(), ci.bbUser)
|
||||
conf := &schema.BitbucketCloudConnection{
|
||||
Url: server.URL,
|
||||
ApiURL: server.URL,
|
||||
}
|
||||
bbClient, err := bitbucketcloud.NewClient(server.URL, conf, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := &sessionIssuerHelper{
|
||||
baseURL: extsvc.NormalizeBaseURL(bbURL),
|
||||
clientKey: clientID,
|
||||
allowSignup: ci.allowSignup,
|
||||
client: bbClient,
|
||||
}
|
||||
|
||||
tok := &oauth2.Token{AccessToken: "dummy-value-that-isnt-relevant-to-unit-correctness"}
|
||||
actr, _, err := s.GetOrCreateUser(ctx, tok, "", "", "")
|
||||
if c.expErr && err == nil {
|
||||
t.Errorf("expected err %v, but was nil", c.expErr)
|
||||
} else if !c.expErr && err != nil {
|
||||
t.Errorf("expected no error, but was %v", err)
|
||||
}
|
||||
|
||||
if got, exp := actr, c.expActor; !reflect.DeepEqual(got, exp) {
|
||||
t.Errorf("expected actor %v, got %v", exp, got)
|
||||
}
|
||||
|
||||
if got, exp := gotAuthUserOp, c.expAuthUserOp; !reflect.DeepEqual(got, exp) {
|
||||
t.Error(cmp.Diff(got, exp))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionIssuerHelper_SignupMatchesSecondaryAccount(t *testing.T) {
|
||||
server := createTestServer()
|
||||
defer server.Close()
|
||||
|
||||
returnEmails = emailResponse{Values: []bitbucketcloud.UserEmail{
|
||||
{
|
||||
Email: "primary@example.com",
|
||||
IsPrimary: true,
|
||||
IsConfirmed: true,
|
||||
},
|
||||
{
|
||||
Email: "secondary@example.com",
|
||||
IsPrimary: false,
|
||||
IsConfirmed: true,
|
||||
},
|
||||
}}
|
||||
|
||||
// We just want to make sure that we end up getting to the secondary email
|
||||
auth.MockGetAndSaveUser = func(ctx context.Context, op auth.GetAndSaveUserOp) (userID int32, safeErrMsg string, err error) {
|
||||
if op.CreateIfNotExist {
|
||||
// We should not get here as we should hit the second email address
|
||||
// before trying again with creation enabled.
|
||||
t.Fatal("Should not get here")
|
||||
}
|
||||
// Mock the second email address matching
|
||||
if op.UserProps.Email == "secondary@example.com" {
|
||||
return 1, "", nil
|
||||
}
|
||||
return 0, "no match", errors.New("no match")
|
||||
}
|
||||
defer func() {
|
||||
auth.MockGetAndSaveUser = nil
|
||||
}()
|
||||
|
||||
bbURL, _ := url.Parse(server.URL)
|
||||
clientID := "client-id"
|
||||
returnUsername = "alice"
|
||||
returnAccountID = "1234"
|
||||
|
||||
bbUser := &bitbucketlogin.User{
|
||||
Username: returnUsername,
|
||||
}
|
||||
|
||||
ctx := bitbucketlogin.WithUser(context.Background(), bbUser)
|
||||
conf := &schema.BitbucketCloudConnection{
|
||||
Url: server.URL,
|
||||
ApiURL: server.URL,
|
||||
}
|
||||
bbClient, err := bitbucketcloud.NewClient(server.URL, conf, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := &sessionIssuerHelper{
|
||||
baseURL: extsvc.NormalizeBaseURL(bbURL),
|
||||
clientKey: clientID,
|
||||
allowSignup: true,
|
||||
client: bbClient,
|
||||
}
|
||||
tok := &oauth2.Token{AccessToken: "dummy-value-that-isnt-relevant-to-unit-correctness"}
|
||||
_, _, err = s.GetOrCreateUser(ctx, tok, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func u(username, email string, emailIsVerified bool) database.NewUser {
|
||||
return database.NewUser{
|
||||
Username: username,
|
||||
Email: email,
|
||||
EmailIsVerified: emailIsVerified,
|
||||
}
|
||||
}
|
||||
|
||||
func acct(serviceType, serviceID, clientID, accountID string) extsvc.AccountSpec {
|
||||
return extsvc.AccountSpec{
|
||||
ServiceType: serviceType,
|
||||
ServiceID: serviceID,
|
||||
ClientID: clientID,
|
||||
AccountID: accountID,
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/external/app"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/bitbucketcloudoauth"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/githuboauth"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/gitlaboauth"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/httpheader"
|
||||
@ -33,6 +34,7 @@ func Init(logger log.Logger, db database.DB) {
|
||||
httpheader.Init()
|
||||
githuboauth.Init(logger, db)
|
||||
gitlaboauth.Init(logger, db)
|
||||
bitbucketcloudoauth.Init(logger, db)
|
||||
|
||||
// Register enterprise auth middleware
|
||||
auth.RegisterMiddlewares(
|
||||
@ -42,6 +44,7 @@ func Init(logger log.Logger, db database.DB) {
|
||||
httpheader.Middleware(db),
|
||||
githuboauth.Middleware(db),
|
||||
gitlaboauth.Middleware(db),
|
||||
bitbucketcloudoauth.Middleware(db),
|
||||
)
|
||||
// Register app-level sign-out handler
|
||||
app.RegisterSSOSignOutHandler(ssoSignOutHandler)
|
||||
@ -71,6 +74,8 @@ func Init(logger log.Logger, db database.DB) {
|
||||
name = "GitHub OAuth"
|
||||
case p.Gitlab != nil:
|
||||
name = "GitLab OAuth"
|
||||
case p.Bitbucketcloud != nil:
|
||||
name = "Bitbucket Cloud OAuth"
|
||||
case p.HttpHeader != nil:
|
||||
name = "HTTP header"
|
||||
case p.Openidconnect != nil:
|
||||
|
||||
@ -57,6 +57,8 @@ func (p *Provider) CachedInfo() *providers.Info {
|
||||
displayName = p.SourceConfig.Github.DisplayName
|
||||
case p.SourceConfig.Gitlab != nil && p.SourceConfig.Gitlab.DisplayName != "":
|
||||
displayName = p.SourceConfig.Gitlab.DisplayName
|
||||
case p.SourceConfig.Bitbucketcloud != nil && p.SourceConfig.Bitbucketcloud.DisplayName != "":
|
||||
displayName = p.SourceConfig.Bitbucketcloud.DisplayName
|
||||
}
|
||||
return &providers.Info{
|
||||
ServiceID: p.ServiceID,
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/authz/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/authz/bitbucketserver"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/authz/github"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/authz/gitlab"
|
||||
@ -60,6 +61,7 @@ func ProvidersFromConfig(
|
||||
extsvc.KindGitHub,
|
||||
extsvc.KindGitLab,
|
||||
extsvc.KindBitbucketServer,
|
||||
extsvc.KindBitbucketCloud,
|
||||
extsvc.KindPerforce,
|
||||
},
|
||||
LimitOffset: &database.LimitOffset{
|
||||
@ -72,6 +74,7 @@ func ProvidersFromConfig(
|
||||
gitLabConns []*types.GitLabConnection
|
||||
bitbucketServerConns []*types.BitbucketServerConnection
|
||||
perforceConns []*types.PerforceConnection
|
||||
bitbucketCloudConns []*types.BitbucketCloudConnection
|
||||
)
|
||||
for {
|
||||
svcs, err := store.List(ctx, opt)
|
||||
@ -116,6 +119,11 @@ func ProvidersFromConfig(
|
||||
URN: svc.URN(),
|
||||
BitbucketServerConnection: c,
|
||||
})
|
||||
case *schema.BitbucketCloudConnection:
|
||||
bitbucketCloudConns = append(bitbucketCloudConns, &types.BitbucketCloudConnection{
|
||||
URN: svc.URN(),
|
||||
BitbucketCloudConnection: c,
|
||||
})
|
||||
case *schema.PerforceConnection:
|
||||
perforceConns = append(perforceConns, &types.PerforceConnection{
|
||||
URN: svc.URN(),
|
||||
@ -170,6 +178,14 @@ func ProvidersFromConfig(
|
||||
invalidConnections = append(invalidConnections, pfInvalidConnections...)
|
||||
}
|
||||
|
||||
if len(bitbucketCloudConns) > 0 {
|
||||
bbcloudProviders, bbcloudProblems, bbcloudWarnings, bbcloudInvalidConnections := bitbucketcloud.NewAuthzProviders(db, bitbucketCloudConns, cfg.SiteConfig().AuthProviders)
|
||||
providers = append(providers, bbcloudProviders...)
|
||||
seriousProblems = append(seriousProblems, bbcloudProblems...)
|
||||
warnings = append(warnings, bbcloudWarnings...)
|
||||
invalidConnections = append(invalidConnections, bbcloudInvalidConnections...)
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: Warn the admin when both code host authz provider and the permissions user mapping are configured.
|
||||
if cfg.SiteConfig().PermissionsUserMapping != nil &&
|
||||
cfg.SiteConfig().PermissionsUserMapping.Enabled {
|
||||
|
||||
@ -489,7 +489,7 @@ func TestAuthzProvidersFromConfig(t *testing.T) {
|
||||
Config: extsvc.NewUnencryptedConfig(mustMarshalJSONString(bbs)),
|
||||
})
|
||||
}
|
||||
case extsvc.KindGitHub, extsvc.KindPerforce:
|
||||
case extsvc.KindGitHub, extsvc.KindPerforce, extsvc.KindBitbucketCloud:
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected kind: %s", kind)
|
||||
}
|
||||
@ -521,6 +521,7 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) {
|
||||
bitbucketServerConnections []*schema.BitbucketServerConnection
|
||||
githubConnections []*schema.GitHubConnection
|
||||
perforceConnections []*schema.PerforceConnection
|
||||
bitbucketCloudConnections []*schema.BitbucketCloudConnection
|
||||
|
||||
expInvalidConnections []string
|
||||
expSeriousProblems []string
|
||||
@ -577,7 +578,7 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) {
|
||||
expInvalidConnections: []string{"gitlab"},
|
||||
},
|
||||
{
|
||||
description: "Bitbucket connection with authz enabled but missing license for ACLs",
|
||||
description: "Bitbucket Server connection with authz enabled but missing license for ACLs",
|
||||
cfg: conf.Unified{},
|
||||
bitbucketServerConnections: []*schema.BitbucketServerConnection{
|
||||
{
|
||||
@ -600,6 +601,20 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) {
|
||||
expSeriousProblems: []string{"failed"},
|
||||
expInvalidConnections: []string{"bitbucketServer"},
|
||||
},
|
||||
{
|
||||
description: "Bitbucket Cloud connection with authz enabled but missing license for ACLs",
|
||||
cfg: conf.Unified{},
|
||||
bitbucketCloudConnections: []*schema.BitbucketCloudConnection{
|
||||
{
|
||||
Authorization: &schema.BitbucketCloudAuthorization{},
|
||||
Url: "https://bitbucket.org",
|
||||
Username: "admin",
|
||||
AppPassword: "secret-password",
|
||||
},
|
||||
},
|
||||
expSeriousProblems: []string{"failed"},
|
||||
expInvalidConnections: []string{"bitbucketCloud"},
|
||||
},
|
||||
{
|
||||
description: "Perforce connection with authz enabled but missing license for ACLs",
|
||||
cfg: conf.Unified{},
|
||||
@ -654,6 +669,13 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) {
|
||||
Config: extsvc.NewUnencryptedConfig(mustMarshalJSONString(gh)),
|
||||
})
|
||||
}
|
||||
case extsvc.KindBitbucketCloud:
|
||||
for _, bbcloud := range test.bitbucketCloudConnections {
|
||||
svcs = append(svcs, &types.ExternalService{
|
||||
Kind: kind,
|
||||
Config: extsvc.NewUnencryptedConfig(mustMarshalJSONString(bbcloud)),
|
||||
})
|
||||
}
|
||||
case extsvc.KindPerforce:
|
||||
for _, pf := range test.perforceConnections {
|
||||
svcs = append(svcs, &types.ExternalService{
|
||||
|
||||
97
enterprise/internal/authz/bitbucketcloud/authz.go
Normal file
97
enterprise/internal/authz/bitbucketcloud/authz.go
Normal file
@ -0,0 +1,97 @@
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/licensing"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
// NewAuthzProviders returns the set of Bitbucket Cloud authz providers derived from the connections.
|
||||
//
|
||||
// It also returns any simple validation problems with the config, separating these into "serious problems"
|
||||
// and "warnings". "Serious problems" are those that should make Sourcegraph set authz.allowAccessByDefault
|
||||
// to false. "Warnings" are all other validation problems.
|
||||
//
|
||||
// This constructor does not and should not directly check connectivity to external services - if
|
||||
// desired, callers should use `(*Provider).ValidateConnection` directly to get warnings related
|
||||
// to connection issues.
|
||||
func NewAuthzProviders(db database.DB, conns []*types.BitbucketCloudConnection, authProviders []schema.AuthProviders) (ps []authz.Provider, problems []string, warnings []string, invalidConnections []string) {
|
||||
bbcloudAuthProviders := make(map[string]*schema.BitbucketCloudAuthProvider)
|
||||
for _, p := range authProviders {
|
||||
if p.Bitbucketcloud != nil {
|
||||
var id string
|
||||
bbURL, err := url.Parse(p.Bitbucketcloud.GetURL())
|
||||
if err != nil {
|
||||
// error reporting for this should happen elsewhere, for now just use what is given
|
||||
id = p.Bitbucketcloud.GetURL()
|
||||
} else {
|
||||
// use codehost normalized URL as ID
|
||||
ch := extsvc.NewCodeHost(bbURL, p.Bitbucketcloud.Type)
|
||||
id = ch.ServiceID
|
||||
}
|
||||
bbcloudAuthProviders[id] = p.Bitbucketcloud
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range conns {
|
||||
p, err := newAuthzProvider(db, c)
|
||||
if err != nil {
|
||||
invalidConnections = append(invalidConnections, extsvc.TypeBitbucketCloud)
|
||||
problems = append(problems, err.Error())
|
||||
}
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := bbcloudAuthProviders[p.ServiceID()]; !exists {
|
||||
warnings = append(warnings,
|
||||
fmt.Sprintf("Bitbucket Cloud config for %[1]s has `authorization` enabled, "+
|
||||
"but no authentication provider matching %[1]q was found. "+
|
||||
"Check the [**site configuration**](/site-admin/configuration) to "+
|
||||
"verify an entry in [`auth.providers`](https://docs.sourcegraph.com/admin/auth) exists for %[1]s.",
|
||||
p.ServiceID()))
|
||||
}
|
||||
|
||||
ps = append(ps, p)
|
||||
}
|
||||
|
||||
return ps, problems, warnings, invalidConnections
|
||||
}
|
||||
|
||||
func newAuthzProvider(
|
||||
db database.DB,
|
||||
c *types.BitbucketCloudConnection,
|
||||
) (authz.Provider, error) {
|
||||
// If authorization is not set for this connection, we do not need an
|
||||
// authz provider.
|
||||
if c.Authorization == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := licensing.Check(licensing.FeatureACLs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bbClient, err := bitbucketcloud.NewClient(c.URN, c.BitbucketCloudConnection, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewProvider(db, c, ProviderOptions{
|
||||
BitbucketCloudClient: bbClient,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ValidateAuthz validates the authorization fields of the given Perforce
|
||||
// external service config.
|
||||
func ValidateAuthz(_ *schema.BitbucketCloudConnection) error {
|
||||
// newAuthzProvider always succeeds, so directly return nil here.
|
||||
return nil
|
||||
}
|
||||
109
enterprise/internal/authz/bitbucketcloud/authz_test.go
Normal file
109
enterprise/internal/authz/bitbucketcloud/authz_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/internal/licensing"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func TestNewAuthzProviders(t *testing.T) {
|
||||
db := database.NewMockDB()
|
||||
db.ExternalServicesFunc.SetDefaultReturn(database.NewMockExternalServiceStore())
|
||||
t.Run("no authorization", func(t *testing.T) {
|
||||
providers, problems, warnings, invalidConnections := NewAuthzProviders(
|
||||
db,
|
||||
[]*types.BitbucketCloudConnection{{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
Url: schema.DefaultBitbucketCloudURL,
|
||||
},
|
||||
}},
|
||||
[]schema.AuthProviders{},
|
||||
)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.Len(providers, 0, "unexpected a providers: %+v", providers)
|
||||
assert.Len(problems, 0, "unexpected problems: %+v", problems)
|
||||
assert.Len(warnings, 0, "unexpected warnings: %+v", warnings)
|
||||
assert.Len(invalidConnections, 0, "unexpected invalidConnections: %+v", invalidConnections)
|
||||
})
|
||||
|
||||
t.Run("no matching auth provider", func(t *testing.T) {
|
||||
licensing.MockCheckFeatureError("")
|
||||
providers, problems, warnings, invalidConnections := NewAuthzProviders(
|
||||
db,
|
||||
[]*types.BitbucketCloudConnection{
|
||||
{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
Url: "https://bitbucket.org/my-org", // incorrect
|
||||
Authorization: &schema.BitbucketCloudAuthorization{},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]schema.AuthProviders{{Bitbucketcloud: &schema.BitbucketCloudAuthProvider{}}},
|
||||
)
|
||||
|
||||
require.Len(t, providers, 1, "expect exactly one provider")
|
||||
assert.NotNil(t, providers[0])
|
||||
|
||||
assert.Empty(t, problems)
|
||||
assert.Empty(t, invalidConnections)
|
||||
|
||||
require.Len(t, warnings, 1, "expect exactly one warning")
|
||||
assert.Contains(t, warnings[0], "no authentication provider")
|
||||
})
|
||||
|
||||
t.Run("matching auth provider found", func(t *testing.T) {
|
||||
t.Run("default case", func(t *testing.T) {
|
||||
licensing.MockCheckFeatureError("")
|
||||
providers, problems, warnings, invalidConnections := NewAuthzProviders(
|
||||
db,
|
||||
[]*types.BitbucketCloudConnection{
|
||||
{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
Url: schema.DefaultBitbucketCloudURL,
|
||||
Authorization: &schema.BitbucketCloudAuthorization{},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]schema.AuthProviders{{Bitbucketcloud: &schema.BitbucketCloudAuthProvider{}}},
|
||||
)
|
||||
|
||||
require.Len(t, providers, 1, "expect exactly one provider")
|
||||
assert.NotNil(t, providers[0])
|
||||
|
||||
assert.Empty(t, problems)
|
||||
assert.Empty(t, warnings)
|
||||
assert.Empty(t, invalidConnections)
|
||||
})
|
||||
|
||||
t.Run("license does not have ACLs feature", func(t *testing.T) {
|
||||
licensing.MockCheckFeatureError("failed")
|
||||
providers, problems, warnings, invalidConnections := NewAuthzProviders(
|
||||
db,
|
||||
[]*types.BitbucketCloudConnection{
|
||||
{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
Url: schema.DefaultBitbucketCloudURL,
|
||||
Authorization: &schema.BitbucketCloudAuthorization{},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]schema.AuthProviders{{Bitbucketcloud: &schema.BitbucketCloudAuthProvider{}}},
|
||||
)
|
||||
|
||||
expectedError := []string{"failed"}
|
||||
expInvalidConnectionErr := []string{"bitbucketCloud"}
|
||||
assert.Equal(t, expectedError, problems)
|
||||
assert.Equal(t, expInvalidConnectionErr, invalidConnections)
|
||||
assert.Empty(t, providers)
|
||||
assert.Empty(t, warnings)
|
||||
})
|
||||
})
|
||||
}
|
||||
171
enterprise/internal/authz/bitbucketcloud/provider.go
Normal file
171
enterprise/internal/authz/bitbucketcloud/provider.go
Normal file
@ -0,0 +1,171 @@
|
||||
// Package bitbucketcloud contains an authorization provider for Bitbucket Cloud.
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/internal/httpcli"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// Provider is an implementation of AuthzProvider that provides repository and
|
||||
// user permissions as determined from Bitbucket Cloud.
|
||||
type Provider struct {
|
||||
urn string
|
||||
codeHost *extsvc.CodeHost
|
||||
client bitbucketcloud.Client
|
||||
pageSize int // Page size to use in paginated requests.
|
||||
db database.DB
|
||||
}
|
||||
|
||||
type ProviderOptions struct {
|
||||
BitbucketCloudClient bitbucketcloud.Client
|
||||
}
|
||||
|
||||
var _ authz.Provider = (*Provider)(nil)
|
||||
|
||||
// NewProvider returns a new Bitbucket Cloud authorization provider that uses
|
||||
// the given bitbucket.Client to talk to the Bitbucket Cloud API that is
|
||||
// the source of truth for permissions. Sourcegraph users will need a valid
|
||||
// Bitbucket Cloud external account for permissions to sync correctly.
|
||||
func NewProvider(db database.DB, conn *types.BitbucketCloudConnection, opts ProviderOptions) *Provider {
|
||||
baseURL, err := url.Parse(conn.Url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.BitbucketCloudClient == nil {
|
||||
opts.BitbucketCloudClient, err = bitbucketcloud.NewClient(conn.URN, conn.BitbucketCloudConnection, httpcli.ExternalClient)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
urn: conn.URN,
|
||||
codeHost: extsvc.NewCodeHost(baseURL, extsvc.TypeBitbucketCloud),
|
||||
client: opts.BitbucketCloudClient,
|
||||
pageSize: 1000,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateConnection validates that the Provider has access to the Bitbucket Cloud API
|
||||
// with the credentials it was configured with.
|
||||
func (p *Provider) ValidateConnection(ctx context.Context) []string {
|
||||
_, err := p.client.CurrentUser(ctx)
|
||||
if err != nil {
|
||||
return []string{err.Error()}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (p *Provider) URN() string {
|
||||
return p.urn
|
||||
}
|
||||
|
||||
// ServiceID returns the absolute URL that identifies the Bitbucket Server instance
|
||||
// this provider is configured with.
|
||||
func (p *Provider) ServiceID() string { return p.codeHost.ServiceID }
|
||||
|
||||
// ServiceType returns the type of this Provider, namely, "bitbucketCloud".
|
||||
func (p *Provider) ServiceType() string { return p.codeHost.ServiceType }
|
||||
|
||||
// FetchAccount satisfies the authz.Provider interface.
|
||||
func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*extsvc.Account, _ []string) (acct *extsvc.Account, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FetchUserPerms returns a list of repository IDs (on code host) that the given account
|
||||
// has read access on the code host. The repository ID has the same value as it would be
|
||||
// used as api.ExternalRepoSpec.ID. The returned list only includes private repository IDs.
|
||||
//
|
||||
// This method may return partial but valid results in case of error, and it is up to
|
||||
// callers to decide whether to discard.
|
||||
//
|
||||
// API docs: https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html#idm8296923984
|
||||
func (p *Provider) FetchUserPerms(ctx context.Context, account *extsvc.Account, opts authz.FetchPermsOptions) (*authz.ExternalUserPermissions, error) {
|
||||
switch {
|
||||
case account == nil:
|
||||
return nil, errors.New("no account provided")
|
||||
case !extsvc.IsHostOfAccount(p.codeHost, account):
|
||||
return nil, errors.Errorf("not a code host of the account: want %q but have %q",
|
||||
p.codeHost.ServiceID, account.AccountSpec.ServiceID)
|
||||
case account.Data == nil:
|
||||
return nil, errors.New("no account data provided")
|
||||
}
|
||||
|
||||
_, tok, err := bitbucketcloud.GetExternalAccountData(ctx, &account.AccountData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oauthToken := &auth.OAuthBearerToken{
|
||||
Token: tok.AccessToken,
|
||||
RefreshToken: tok.RefreshToken,
|
||||
Expiry: tok.Expiry,
|
||||
NeedsRefreshBuffer: 5,
|
||||
}
|
||||
oauthToken.RefreshFunc = database.GetAccountRefreshAndStoreOAuthTokenFunc(p.db, account.ID, bitbucketcloud.GetOAuthContext(p.codeHost.BaseURL.String()))
|
||||
|
||||
client := p.client.WithAuthenticator(oauthToken)
|
||||
|
||||
repos, _, err := client.Repos(ctx, &bitbucketcloud.PageToken{Pagelen: 100}, "", &bitbucketcloud.ReposOptions{RequestOptions: bitbucketcloud.RequestOptions{FetchAll: true}, Role: "member"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extIDs := make([]extsvc.RepoID, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
extIDs = append(extIDs, extsvc.RepoID(repo.UUID))
|
||||
}
|
||||
|
||||
return &authz.ExternalUserPermissions{
|
||||
Exacts: extIDs,
|
||||
}, err
|
||||
}
|
||||
|
||||
// FetchRepoPerms returns a list of user IDs (on code host) who have read access to
|
||||
// the given repo on the code host. The user ID has the same value as it would
|
||||
// be used as extsvc.Account.AccountID. The returned list includes both direct access
|
||||
// and inherited from the group membership.
|
||||
//
|
||||
// This method may return partial but valid results in case of error, and it is up to
|
||||
// callers to decide whether to discard.
|
||||
//
|
||||
// API docs: https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html#idm8283203728
|
||||
func (p *Provider) FetchRepoPerms(ctx context.Context, repo *extsvc.Repository, opts authz.FetchPermsOptions) ([]extsvc.AccountID, error) {
|
||||
repoNameParts := strings.Split(repo.URI, "/")
|
||||
repoOwner := repoNameParts[1]
|
||||
repoName := repoNameParts[2]
|
||||
|
||||
users, _, err := p.client.ListExplicitUserPermsForRepo(ctx, &bitbucketcloud.PageToken{Pagelen: 100}, repoOwner, repoName, &bitbucketcloud.RequestOptions{FetchAll: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Bitbucket Cloud API does not return the owner of the repository as part
|
||||
// of the explicit permissions list, so we need to fetch and add them.
|
||||
bbCloudRepo, err := p.client.Repo(ctx, repoOwner, repoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bbCloudRepo.Owner != nil {
|
||||
users = append(users, bbCloudRepo.Owner)
|
||||
}
|
||||
|
||||
userIDs := make([]extsvc.AccountID, len(users))
|
||||
for i, user := range users {
|
||||
userIDs[i] = extsvc.AccountID(user.UUID)
|
||||
}
|
||||
|
||||
return userIDs, nil
|
||||
}
|
||||
216
enterprise/internal/authz/bitbucketcloud/provider_test.go
Normal file
216
enterprise/internal/authz/bitbucketcloud/provider_test.go
Normal file
@ -0,0 +1,216 @@
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type mockDoer struct {
|
||||
do func(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (c *mockDoer) Do(r *http.Request) (*http.Response, error) {
|
||||
return c.do(r)
|
||||
}
|
||||
|
||||
func mustURL(t *testing.T, u string) *url.URL {
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func createTestServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/repositories") {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Values []bitbucketcloud.Repo `json:"values"`
|
||||
}{
|
||||
Values: []bitbucketcloud.Repo{
|
||||
{UUID: "1"},
|
||||
{UUID: "2"},
|
||||
{UUID: "3"},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/permissions-config/users") {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Values []bitbucketcloud.ExplicitUserPermsResponse `json:"values"`
|
||||
}{
|
||||
Values: []bitbucketcloud.ExplicitUserPermsResponse{
|
||||
{User: &bitbucketcloud.Account{UUID: "1"}},
|
||||
{User: &bitbucketcloud.Account{UUID: "2"}},
|
||||
{User: &bitbucketcloud.Account{UUID: "3"}},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/repositories/user/repo") {
|
||||
json.NewEncoder(w).Encode(bitbucketcloud.Repo{
|
||||
Owner: &bitbucketcloud.Account{UUID: "4"},
|
||||
})
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestProvider_FetchUserPerms(t *testing.T) {
|
||||
db := database.NewMockDB()
|
||||
t.Run("nil account", func(t *testing.T) {
|
||||
p := NewProvider(db,
|
||||
&types.BitbucketCloudConnection{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
ApiURL: "https://bitbucket.org",
|
||||
Url: "https://bitbucket.org",
|
||||
},
|
||||
}, ProviderOptions{})
|
||||
_, err := p.FetchUserPerms(context.Background(), nil, authz.FetchPermsOptions{})
|
||||
want := "no account provided"
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if got != want {
|
||||
t.Fatalf("err: want %q but got %q", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not the code host of the account", func(t *testing.T) {
|
||||
p := NewProvider(db,
|
||||
&types.BitbucketCloudConnection{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
ApiURL: "https://bitbucket.org",
|
||||
Url: "https://bitbucket.org",
|
||||
},
|
||||
}, ProviderOptions{})
|
||||
_, err := p.FetchUserPerms(context.Background(),
|
||||
&extsvc.Account{
|
||||
AccountSpec: extsvc.AccountSpec{
|
||||
ServiceType: extsvc.TypeGitHub,
|
||||
ServiceID: "https://github.com/",
|
||||
},
|
||||
},
|
||||
authz.FetchPermsOptions{},
|
||||
)
|
||||
want := `not a code host of the account: want "https://bitbucket.org/" but have "https://github.com/"`
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if got != want {
|
||||
t.Fatalf("err: want %q but got %q", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no account data provided", func(t *testing.T) {
|
||||
p := NewProvider(db,
|
||||
&types.BitbucketCloudConnection{
|
||||
BitbucketCloudConnection: &schema.BitbucketCloudConnection{
|
||||
ApiURL: "https://bitbucket.org",
|
||||
Url: "https://bitbucket.org",
|
||||
},
|
||||
}, ProviderOptions{})
|
||||
_, err := p.FetchUserPerms(context.Background(),
|
||||
&extsvc.Account{
|
||||
AccountSpec: extsvc.AccountSpec{
|
||||
ServiceType: extsvc.TypeBitbucketCloud,
|
||||
ServiceID: "https://bitbucket.org/",
|
||||
},
|
||||
},
|
||||
authz.FetchPermsOptions{},
|
||||
)
|
||||
want := `no account data provided`
|
||||
got := fmt.Sprintf("%v", err)
|
||||
if got != want {
|
||||
t.Fatalf("err: want %q but got %q", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
server := createTestServer()
|
||||
defer server.Close()
|
||||
|
||||
t.Run("fetch user permissions", func(t *testing.T) {
|
||||
conn := &schema.BitbucketCloudConnection{
|
||||
ApiURL: server.URL,
|
||||
Url: server.URL,
|
||||
}
|
||||
client, err := bitbucketcloud.NewClient(server.URL, conn, http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p := NewProvider(db,
|
||||
&types.BitbucketCloudConnection{
|
||||
BitbucketCloudConnection: conn,
|
||||
}, ProviderOptions{BitbucketCloudClient: client})
|
||||
|
||||
var acctData extsvc.AccountData
|
||||
err = bitbucketcloud.SetExternalAccountData(&acctData, &bitbucketcloud.Account{}, &oauth2.Token{AccessToken: "my-access-token"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
account := &extsvc.Account{
|
||||
AccountSpec: extsvc.AccountSpec{
|
||||
ServiceType: extsvc.TypeBitbucketCloud,
|
||||
ServiceID: extsvc.NormalizeBaseURL(mustURL(t, server.URL)).String(),
|
||||
},
|
||||
AccountData: acctData,
|
||||
}
|
||||
userPerms, err := p.FetchUserPerms(context.Background(), account, authz.FetchPermsOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expRepoIDs := []extsvc.RepoID{"1", "2", "3"}
|
||||
if diff := cmp.Diff(expRepoIDs, userPerms.Exacts); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvider_FetchRepoPerms(t *testing.T) {
|
||||
server := createTestServer()
|
||||
defer server.Close()
|
||||
db := database.NewMockDB()
|
||||
|
||||
conn := &schema.BitbucketCloudConnection{
|
||||
ApiURL: server.URL,
|
||||
Url: server.URL,
|
||||
}
|
||||
client, err := bitbucketcloud.NewClient(server.URL, conn, http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p := NewProvider(db,
|
||||
&types.BitbucketCloudConnection{
|
||||
BitbucketCloudConnection: conn,
|
||||
}, ProviderOptions{BitbucketCloudClient: client})
|
||||
|
||||
perms, err := p.FetchRepoPerms(context.Background(), &extsvc.Repository{
|
||||
URI: "bitbucket.org/user/repo",
|
||||
}, authz.FetchPermsOptions{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expUserIDs := []extsvc.AccountID{"1", "2", "3", "4"}
|
||||
if diff := cmp.Diff(expUserIDs, perms); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
426
enterprise/internal/batches/sources/mocks_test.go
generated
426
enterprise/internal/batches/sources/mocks_test.go
generated
@ -3589,6 +3589,9 @@ func (c SourcerStoreUserCredentialsFuncCall) Results() []interface{} {
|
||||
// github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud) used
|
||||
// for unit testing.
|
||||
type MockBitbucketCloudClient struct {
|
||||
// AllCurrentUserEmailsFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method AllCurrentUserEmails.
|
||||
AllCurrentUserEmailsFunc *BitbucketCloudClientAllCurrentUserEmailsFunc
|
||||
// AuthenticatorFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method Authenticator.
|
||||
AuthenticatorFunc *BitbucketCloudClientAuthenticatorFunc
|
||||
@ -3601,6 +3604,9 @@ type MockBitbucketCloudClient struct {
|
||||
// CurrentUserFunc is an instance of a mock function object controlling
|
||||
// the behavior of the method CurrentUser.
|
||||
CurrentUserFunc *BitbucketCloudClientCurrentUserFunc
|
||||
// CurrentUserEmailsFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method CurrentUserEmails.
|
||||
CurrentUserEmailsFunc *BitbucketCloudClientCurrentUserEmailsFunc
|
||||
// DeclinePullRequestFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method DeclinePullRequest.
|
||||
DeclinePullRequestFunc *BitbucketCloudClientDeclinePullRequestFunc
|
||||
@ -3613,6 +3619,10 @@ type MockBitbucketCloudClient struct {
|
||||
// GetPullRequestStatusesFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method GetPullRequestStatuses.
|
||||
GetPullRequestStatusesFunc *BitbucketCloudClientGetPullRequestStatusesFunc
|
||||
// ListExplicitUserPermsForRepoFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// ListExplicitUserPermsForRepo.
|
||||
ListExplicitUserPermsForRepoFunc *BitbucketCloudClientListExplicitUserPermsForRepoFunc
|
||||
// MergePullRequestFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method MergePullRequest.
|
||||
MergePullRequestFunc *BitbucketCloudClientMergePullRequestFunc
|
||||
@ -3637,6 +3647,11 @@ type MockBitbucketCloudClient struct {
|
||||
// All methods return zero values for all results, unless overwritten.
|
||||
func NewMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
return &MockBitbucketCloudClient{
|
||||
AllCurrentUserEmailsFunc: &BitbucketCloudClientAllCurrentUserEmailsFunc{
|
||||
defaultHook: func(context.Context) (r0 []*bitbucketcloud.UserEmail, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
AuthenticatorFunc: &BitbucketCloudClientAuthenticatorFunc{
|
||||
defaultHook: func() (r0 auth.Authenticator) {
|
||||
return
|
||||
@ -3657,6 +3672,11 @@ func NewMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
return
|
||||
},
|
||||
},
|
||||
CurrentUserEmailsFunc: &BitbucketCloudClientCurrentUserEmailsFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken) (r0 []*bitbucketcloud.UserEmail, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
DeclinePullRequestFunc: &BitbucketCloudClientDeclinePullRequestFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.Repo, int64) (r0 *bitbucketcloud.PullRequest, r1 error) {
|
||||
return
|
||||
@ -3677,6 +3697,11 @@ func NewMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListExplicitUserPermsForRepoFunc: &BitbucketCloudClientListExplicitUserPermsForRepoFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) (r0 []*bitbucketcloud.Account, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
MergePullRequestFunc: &BitbucketCloudClientMergePullRequestFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.Repo, int64, bitbucketcloud.MergePullRequestOpts) (r0 *bitbucketcloud.PullRequest, r1 error) {
|
||||
return
|
||||
@ -3693,7 +3718,7 @@ func NewMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
},
|
||||
},
|
||||
ReposFunc: &BitbucketCloudClientReposFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken, string) (r0 []*bitbucketcloud.Repo, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) (r0 []*bitbucketcloud.Repo, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
@ -3714,6 +3739,11 @@ func NewMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
// interface. All methods panic on invocation, unless overwritten.
|
||||
func NewStrictMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
return &MockBitbucketCloudClient{
|
||||
AllCurrentUserEmailsFunc: &BitbucketCloudClientAllCurrentUserEmailsFunc{
|
||||
defaultHook: func(context.Context) ([]*bitbucketcloud.UserEmail, error) {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.AllCurrentUserEmails")
|
||||
},
|
||||
},
|
||||
AuthenticatorFunc: &BitbucketCloudClientAuthenticatorFunc{
|
||||
defaultHook: func() auth.Authenticator {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.Authenticator")
|
||||
@ -3734,6 +3764,11 @@ func NewStrictMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.CurrentUser")
|
||||
},
|
||||
},
|
||||
CurrentUserEmailsFunc: &BitbucketCloudClientCurrentUserEmailsFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error) {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.CurrentUserEmails")
|
||||
},
|
||||
},
|
||||
DeclinePullRequestFunc: &BitbucketCloudClientDeclinePullRequestFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.Repo, int64) (*bitbucketcloud.PullRequest, error) {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.DeclinePullRequest")
|
||||
@ -3754,6 +3789,11 @@ func NewStrictMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.GetPullRequestStatuses")
|
||||
},
|
||||
},
|
||||
ListExplicitUserPermsForRepoFunc: &BitbucketCloudClientListExplicitUserPermsForRepoFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error) {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.ListExplicitUserPermsForRepo")
|
||||
},
|
||||
},
|
||||
MergePullRequestFunc: &BitbucketCloudClientMergePullRequestFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.Repo, int64, bitbucketcloud.MergePullRequestOpts) (*bitbucketcloud.PullRequest, error) {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.MergePullRequest")
|
||||
@ -3770,7 +3810,7 @@ func NewStrictMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
},
|
||||
},
|
||||
ReposFunc: &BitbucketCloudClientReposFunc{
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
defaultHook: func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
panic("unexpected invocation of MockBitbucketCloudClient.Repos")
|
||||
},
|
||||
},
|
||||
@ -3792,6 +3832,9 @@ func NewStrictMockBitbucketCloudClient() *MockBitbucketCloudClient {
|
||||
// implementation, unless overwritten.
|
||||
func NewMockBitbucketCloudClientFrom(i bitbucketcloud.Client) *MockBitbucketCloudClient {
|
||||
return &MockBitbucketCloudClient{
|
||||
AllCurrentUserEmailsFunc: &BitbucketCloudClientAllCurrentUserEmailsFunc{
|
||||
defaultHook: i.AllCurrentUserEmails,
|
||||
},
|
||||
AuthenticatorFunc: &BitbucketCloudClientAuthenticatorFunc{
|
||||
defaultHook: i.Authenticator,
|
||||
},
|
||||
@ -3804,6 +3847,9 @@ func NewMockBitbucketCloudClientFrom(i bitbucketcloud.Client) *MockBitbucketClou
|
||||
CurrentUserFunc: &BitbucketCloudClientCurrentUserFunc{
|
||||
defaultHook: i.CurrentUser,
|
||||
},
|
||||
CurrentUserEmailsFunc: &BitbucketCloudClientCurrentUserEmailsFunc{
|
||||
defaultHook: i.CurrentUserEmails,
|
||||
},
|
||||
DeclinePullRequestFunc: &BitbucketCloudClientDeclinePullRequestFunc{
|
||||
defaultHook: i.DeclinePullRequest,
|
||||
},
|
||||
@ -3816,6 +3862,9 @@ func NewMockBitbucketCloudClientFrom(i bitbucketcloud.Client) *MockBitbucketClou
|
||||
GetPullRequestStatusesFunc: &BitbucketCloudClientGetPullRequestStatusesFunc{
|
||||
defaultHook: i.GetPullRequestStatuses,
|
||||
},
|
||||
ListExplicitUserPermsForRepoFunc: &BitbucketCloudClientListExplicitUserPermsForRepoFunc{
|
||||
defaultHook: i.ListExplicitUserPermsForRepo,
|
||||
},
|
||||
MergePullRequestFunc: &BitbucketCloudClientMergePullRequestFunc{
|
||||
defaultHook: i.MergePullRequest,
|
||||
},
|
||||
@ -3837,6 +3886,115 @@ func NewMockBitbucketCloudClientFrom(i bitbucketcloud.Client) *MockBitbucketClou
|
||||
}
|
||||
}
|
||||
|
||||
// BitbucketCloudClientAllCurrentUserEmailsFunc describes the behavior when
|
||||
// the AllCurrentUserEmails method of the parent MockBitbucketCloudClient
|
||||
// instance is invoked.
|
||||
type BitbucketCloudClientAllCurrentUserEmailsFunc struct {
|
||||
defaultHook func(context.Context) ([]*bitbucketcloud.UserEmail, error)
|
||||
hooks []func(context.Context) ([]*bitbucketcloud.UserEmail, error)
|
||||
history []BitbucketCloudClientAllCurrentUserEmailsFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// AllCurrentUserEmails delegates to the next hook function in the queue and
|
||||
// stores the parameter and result values of this invocation.
|
||||
func (m *MockBitbucketCloudClient) AllCurrentUserEmails(v0 context.Context) ([]*bitbucketcloud.UserEmail, error) {
|
||||
r0, r1 := m.AllCurrentUserEmailsFunc.nextHook()(v0)
|
||||
m.AllCurrentUserEmailsFunc.appendCall(BitbucketCloudClientAllCurrentUserEmailsFuncCall{v0, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the AllCurrentUserEmails
|
||||
// method of the parent MockBitbucketCloudClient instance is invoked and the
|
||||
// hook queue is empty.
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) SetDefaultHook(hook func(context.Context) ([]*bitbucketcloud.UserEmail, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// AllCurrentUserEmails method of the parent MockBitbucketCloudClient
|
||||
// instance invokes the hook at the front of the queue and discards it.
|
||||
// After the queue is empty, the default hook function is invoked for any
|
||||
// future action.
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) PushHook(hook func(context.Context) ([]*bitbucketcloud.UserEmail, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) SetDefaultReturn(r0 []*bitbucketcloud.UserEmail, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context) ([]*bitbucketcloud.UserEmail, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) PushReturn(r0 []*bitbucketcloud.UserEmail, r1 error) {
|
||||
f.PushHook(func(context.Context) ([]*bitbucketcloud.UserEmail, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) nextHook() func(context.Context) ([]*bitbucketcloud.UserEmail, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) appendCall(r0 BitbucketCloudClientAllCurrentUserEmailsFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// BitbucketCloudClientAllCurrentUserEmailsFuncCall objects describing the
|
||||
// invocations of this function.
|
||||
func (f *BitbucketCloudClientAllCurrentUserEmailsFunc) History() []BitbucketCloudClientAllCurrentUserEmailsFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]BitbucketCloudClientAllCurrentUserEmailsFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// BitbucketCloudClientAllCurrentUserEmailsFuncCall is an object that
|
||||
// describes an invocation of method AllCurrentUserEmails on an instance of
|
||||
// MockBitbucketCloudClient.
|
||||
type BitbucketCloudClientAllCurrentUserEmailsFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []*bitbucketcloud.UserEmail
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientAllCurrentUserEmailsFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientAllCurrentUserEmailsFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// BitbucketCloudClientAuthenticatorFunc describes the behavior when the
|
||||
// Authenticator method of the parent MockBitbucketCloudClient instance is
|
||||
// invoked.
|
||||
@ -4280,6 +4438,121 @@ func (c BitbucketCloudClientCurrentUserFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// BitbucketCloudClientCurrentUserEmailsFunc describes the behavior when the
|
||||
// CurrentUserEmails method of the parent MockBitbucketCloudClient instance
|
||||
// is invoked.
|
||||
type BitbucketCloudClientCurrentUserEmailsFunc struct {
|
||||
defaultHook func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error)
|
||||
hooks []func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error)
|
||||
history []BitbucketCloudClientCurrentUserEmailsFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// CurrentUserEmails delegates to the next hook function in the queue and
|
||||
// stores the parameter and result values of this invocation.
|
||||
func (m *MockBitbucketCloudClient) CurrentUserEmails(v0 context.Context, v1 *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error) {
|
||||
r0, r1, r2 := m.CurrentUserEmailsFunc.nextHook()(v0, v1)
|
||||
m.CurrentUserEmailsFunc.appendCall(BitbucketCloudClientCurrentUserEmailsFuncCall{v0, v1, r0, r1, r2})
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the CurrentUserEmails
|
||||
// method of the parent MockBitbucketCloudClient instance is invoked and the
|
||||
// hook queue is empty.
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) SetDefaultHook(hook func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// CurrentUserEmails method of the parent MockBitbucketCloudClient instance
|
||||
// invokes the hook at the front of the queue and discards it. After the
|
||||
// queue is empty, the default hook function is invoked for any future
|
||||
// action.
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) PushHook(hook func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) SetDefaultReturn(r0 []*bitbucketcloud.UserEmail, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
f.SetDefaultHook(func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error) {
|
||||
return r0, r1, r2
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) PushReturn(r0 []*bitbucketcloud.UserEmail, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
f.PushHook(func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error) {
|
||||
return r0, r1, r2
|
||||
})
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) nextHook() func(context.Context, *bitbucketcloud.PageToken) ([]*bitbucketcloud.UserEmail, *bitbucketcloud.PageToken, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) appendCall(r0 BitbucketCloudClientCurrentUserEmailsFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// BitbucketCloudClientCurrentUserEmailsFuncCall objects describing the
|
||||
// invocations of this function.
|
||||
func (f *BitbucketCloudClientCurrentUserEmailsFunc) History() []BitbucketCloudClientCurrentUserEmailsFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]BitbucketCloudClientCurrentUserEmailsFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// BitbucketCloudClientCurrentUserEmailsFuncCall is an object that describes
|
||||
// an invocation of method CurrentUserEmails on an instance of
|
||||
// MockBitbucketCloudClient.
|
||||
type BitbucketCloudClientCurrentUserEmailsFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 *bitbucketcloud.PageToken
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []*bitbucketcloud.UserEmail
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 *bitbucketcloud.PageToken
|
||||
// Result2 is the value of the 3rd result returned from this method
|
||||
// invocation.
|
||||
Result2 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientCurrentUserEmailsFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientCurrentUserEmailsFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1, c.Result2}
|
||||
}
|
||||
|
||||
// BitbucketCloudClientDeclinePullRequestFunc describes the behavior when
|
||||
// the DeclinePullRequest method of the parent MockBitbucketCloudClient
|
||||
// instance is invoked.
|
||||
@ -4735,6 +5008,130 @@ func (c BitbucketCloudClientGetPullRequestStatusesFuncCall) Results() []interfac
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// BitbucketCloudClientListExplicitUserPermsForRepoFunc describes the
|
||||
// behavior when the ListExplicitUserPermsForRepo method of the parent
|
||||
// MockBitbucketCloudClient instance is invoked.
|
||||
type BitbucketCloudClientListExplicitUserPermsForRepoFunc struct {
|
||||
defaultHook func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error)
|
||||
hooks []func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error)
|
||||
history []BitbucketCloudClientListExplicitUserPermsForRepoFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// ListExplicitUserPermsForRepo delegates to the next hook function in the
|
||||
// queue and stores the parameter and result values of this invocation.
|
||||
func (m *MockBitbucketCloudClient) ListExplicitUserPermsForRepo(v0 context.Context, v1 *bitbucketcloud.PageToken, v2 string, v3 string, v4 *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error) {
|
||||
r0, r1, r2 := m.ListExplicitUserPermsForRepoFunc.nextHook()(v0, v1, v2, v3, v4)
|
||||
m.ListExplicitUserPermsForRepoFunc.appendCall(BitbucketCloudClientListExplicitUserPermsForRepoFuncCall{v0, v1, v2, v3, v4, r0, r1, r2})
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// ListExplicitUserPermsForRepo method of the parent
|
||||
// MockBitbucketCloudClient instance is invoked and the hook queue is empty.
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) SetDefaultHook(hook func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// ListExplicitUserPermsForRepo method of the parent
|
||||
// MockBitbucketCloudClient instance invokes the hook at the front of the
|
||||
// queue and discards it. After the queue is empty, the default hook
|
||||
// function is invoked for any future action.
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) PushHook(hook func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) SetDefaultReturn(r0 []*bitbucketcloud.Account, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
f.SetDefaultHook(func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error) {
|
||||
return r0, r1, r2
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) PushReturn(r0 []*bitbucketcloud.Account, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
f.PushHook(func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error) {
|
||||
return r0, r1, r2
|
||||
})
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) nextHook() func(context.Context, *bitbucketcloud.PageToken, string, string, *bitbucketcloud.RequestOptions) ([]*bitbucketcloud.Account, *bitbucketcloud.PageToken, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) appendCall(r0 BitbucketCloudClientListExplicitUserPermsForRepoFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// BitbucketCloudClientListExplicitUserPermsForRepoFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *BitbucketCloudClientListExplicitUserPermsForRepoFunc) History() []BitbucketCloudClientListExplicitUserPermsForRepoFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]BitbucketCloudClientListExplicitUserPermsForRepoFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// BitbucketCloudClientListExplicitUserPermsForRepoFuncCall is an object
|
||||
// that describes an invocation of method ListExplicitUserPermsForRepo on an
|
||||
// instance of MockBitbucketCloudClient.
|
||||
type BitbucketCloudClientListExplicitUserPermsForRepoFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 *bitbucketcloud.PageToken
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 string
|
||||
// Arg3 is the value of the 4th argument passed to this method
|
||||
// invocation.
|
||||
Arg3 string
|
||||
// Arg4 is the value of the 5th argument passed to this method
|
||||
// invocation.
|
||||
Arg4 *bitbucketcloud.RequestOptions
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []*bitbucketcloud.Account
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 *bitbucketcloud.PageToken
|
||||
// Result2 is the value of the 3rd result returned from this method
|
||||
// invocation.
|
||||
Result2 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientListExplicitUserPermsForRepoFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3, c.Arg4}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientListExplicitUserPermsForRepoFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1, c.Result2}
|
||||
}
|
||||
|
||||
// BitbucketCloudClientMergePullRequestFunc describes the behavior when the
|
||||
// MergePullRequest method of the parent MockBitbucketCloudClient instance
|
||||
// is invoked.
|
||||
@ -5069,24 +5466,24 @@ func (c BitbucketCloudClientRepoFuncCall) Results() []interface{} {
|
||||
// BitbucketCloudClientReposFunc describes the behavior when the Repos
|
||||
// method of the parent MockBitbucketCloudClient instance is invoked.
|
||||
type BitbucketCloudClientReposFunc struct {
|
||||
defaultHook func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)
|
||||
hooks []func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)
|
||||
defaultHook func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)
|
||||
hooks []func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)
|
||||
history []BitbucketCloudClientReposFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// Repos delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockBitbucketCloudClient) Repos(v0 context.Context, v1 *bitbucketcloud.PageToken, v2 string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
r0, r1, r2 := m.ReposFunc.nextHook()(v0, v1, v2)
|
||||
m.ReposFunc.appendCall(BitbucketCloudClientReposFuncCall{v0, v1, v2, r0, r1, r2})
|
||||
func (m *MockBitbucketCloudClient) Repos(v0 context.Context, v1 *bitbucketcloud.PageToken, v2 string, v3 *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
r0, r1, r2 := m.ReposFunc.nextHook()(v0, v1, v2, v3)
|
||||
m.ReposFunc.appendCall(BitbucketCloudClientReposFuncCall{v0, v1, v2, v3, r0, r1, r2})
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the Repos method of the
|
||||
// parent MockBitbucketCloudClient instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *BitbucketCloudClientReposFunc) SetDefaultHook(hook func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)) {
|
||||
func (f *BitbucketCloudClientReposFunc) SetDefaultHook(hook func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
@ -5094,7 +5491,7 @@ func (f *BitbucketCloudClientReposFunc) SetDefaultHook(hook func(context.Context
|
||||
// Repos method of the parent MockBitbucketCloudClient instance invokes the
|
||||
// hook at the front of the queue and discards it. After the queue is empty,
|
||||
// the default hook function is invoked for any future action.
|
||||
func (f *BitbucketCloudClientReposFunc) PushHook(hook func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)) {
|
||||
func (f *BitbucketCloudClientReposFunc) PushHook(hook func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
@ -5103,19 +5500,19 @@ func (f *BitbucketCloudClientReposFunc) PushHook(hook func(context.Context, *bit
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *BitbucketCloudClientReposFunc) SetDefaultReturn(r0 []*bitbucketcloud.Repo, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
f.SetDefaultHook(func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
f.SetDefaultHook(func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
return r0, r1, r2
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *BitbucketCloudClientReposFunc) PushReturn(r0 []*bitbucketcloud.Repo, r1 *bitbucketcloud.PageToken, r2 error) {
|
||||
f.PushHook(func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
f.PushHook(func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
return r0, r1, r2
|
||||
})
|
||||
}
|
||||
|
||||
func (f *BitbucketCloudClientReposFunc) nextHook() func(context.Context, *bitbucketcloud.PageToken, string) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
func (f *BitbucketCloudClientReposFunc) nextHook() func(context.Context, *bitbucketcloud.PageToken, string, *bitbucketcloud.ReposOptions) ([]*bitbucketcloud.Repo, *bitbucketcloud.PageToken, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
@ -5157,6 +5554,9 @@ type BitbucketCloudClientReposFuncCall struct {
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 string
|
||||
// Arg3 is the value of the 4th argument passed to this method
|
||||
// invocation.
|
||||
Arg3 *bitbucketcloud.ReposOptions
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []*bitbucketcloud.Repo
|
||||
@ -5171,7 +5571,7 @@ type BitbucketCloudClientReposFuncCall struct {
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c BitbucketCloudClientReposFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
|
||||
1
go.mod
1
go.mod
@ -196,6 +196,7 @@ require (
|
||||
github.com/cloudflare/circl v1.3.0 // indirect
|
||||
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/dghubble/sling v1.4.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@ -620,6 +620,8 @@ github.com/derision-test/glock v1.0.0/go.mod h1:jKtLdBMrF+XQatqvg46wiWdDfDSSDjdh
|
||||
github.com/derision-test/go-mockgen v1.3.7 h1:b/DXAXL2FkaRPpnbYK3ODdZzklmJAwox0tkc6yyXx74=
|
||||
github.com/derision-test/go-mockgen v1.3.7/go.mod h1:/TXUePlhtHmDDCaDAi/a4g6xOHqMDz3Wf0r2NPGskB4=
|
||||
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
|
||||
github.com/dghubble/sling v1.4.1 h1:AxjTubpVyozMvbBCtXcsWEyGGgUZutC5YGrfxPNVOcQ=
|
||||
github.com/dghubble/sling v1.4.1/go.mod h1:QoMB1KL3GAo+7HsD8Itd6S+6tW91who8BGZzuLvpOyc=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
|
||||
@ -57,6 +57,9 @@ const (
|
||||
SecurityEventGitLabAuthSucceeded SecurityEventName = "GitLabAuthSucceeded"
|
||||
SecurityEventGitLabAuthFailed SecurityEventName = "GitLabAuthFailed"
|
||||
|
||||
SecurityEventBitbucketCloudAuthSucceeded SecurityEventName = "BitbucketCloudAuthSucceeded"
|
||||
SecurityEventBitbucketCloudAuthFailed SecurityEventName = "BitbucketCloudAuthFailed"
|
||||
|
||||
SecurityEventOIDCLoginSucceeded SecurityEventName = "SecurityEventOIDCLoginSucceeded"
|
||||
SecurityEventOIDCLoginFailed SecurityEventName = "SecurityEventOIDCLoginFailed"
|
||||
)
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/httpcli"
|
||||
"github.com/sourcegraph/sourcegraph/internal/metrics"
|
||||
"github.com/sourcegraph/sourcegraph/internal/oauthutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace/ot"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -41,10 +42,18 @@ type Client interface {
|
||||
MergePullRequest(ctx context.Context, repo *Repo, id int64, opts MergePullRequestOpts) (*PullRequest, error)
|
||||
|
||||
Repo(ctx context.Context, namespace, slug string) (*Repo, error)
|
||||
Repos(ctx context.Context, pageToken *PageToken, accountName string) ([]*Repo, *PageToken, error)
|
||||
Repos(ctx context.Context, pageToken *PageToken, accountName string, opts *ReposOptions) ([]*Repo, *PageToken, error)
|
||||
ForkRepository(ctx context.Context, upstream *Repo, input ForkInput) (*Repo, error)
|
||||
|
||||
ListExplicitUserPermsForRepo(ctx context.Context, pageToken *PageToken, owner, slug string, opts *RequestOptions) ([]*Account, *PageToken, error)
|
||||
|
||||
CurrentUser(ctx context.Context) (*User, error)
|
||||
CurrentUserEmails(ctx context.Context, pageToken *PageToken) ([]*UserEmail, *PageToken, error)
|
||||
AllCurrentUserEmails(ctx context.Context) ([]*UserEmail, error)
|
||||
}
|
||||
|
||||
type RequestOptions struct {
|
||||
FetchAll bool
|
||||
}
|
||||
|
||||
// client access a Bitbucket Cloud via the REST API 2.0.
|
||||
@ -144,6 +153,21 @@ func (c *client) Ping(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchAll[T any](ctx context.Context, c *client, results []T, next *PageToken, err error) ([]T, error) {
|
||||
var page []T
|
||||
var nextURL *url.URL
|
||||
for err == nil && next.HasMore() {
|
||||
nextURL, err = url.Parse(next.Next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next, err = c.page(ctx, nextURL.Path, nil, next, &page)
|
||||
results = append(results, page...)
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (c *client) page(ctx context.Context, path string, qry url.Values, token *PageToken, results any) (*PageToken, error) {
|
||||
if qry == nil {
|
||||
qry = make(url.Values)
|
||||
@ -200,15 +224,11 @@ func (c *client) do(ctx context.Context, req *http.Request, result any) error {
|
||||
nethttp.ClientTrace(false))
|
||||
defer ht.Finish()
|
||||
|
||||
if err := c.Auth.Authenticate(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.rateLimit.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
resp, err := oauthutil.DoRequest(ctx, nil, c.httpClient, req, c.Auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -254,6 +274,12 @@ func (t *PageToken) Values() url.Values {
|
||||
if t == nil {
|
||||
return v
|
||||
}
|
||||
if t.Next != "" {
|
||||
nextURL, err := url.Parse(t.Next)
|
||||
if err == nil {
|
||||
v = nextURL.Query()
|
||||
}
|
||||
}
|
||||
if t.Pagelen != 0 {
|
||||
v.Set("pagelen", strconv.Itoa(t.Pagelen))
|
||||
}
|
||||
|
||||
50
internal/extsvc/bitbucketcloud/common.go
Normal file
50
internal/extsvc/bitbucketcloud/common.go
Normal file
@ -0,0 +1,50 @@
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/oauthutil"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var MockGetOAuthContext func() *oauthutil.OAuthContext
|
||||
|
||||
func GetOAuthContext(baseURL string) *oauthutil.OAuthContext {
|
||||
if MockGetOAuthContext != nil {
|
||||
return MockGetOAuthContext()
|
||||
}
|
||||
|
||||
for _, authProvider := range conf.SiteConfig().AuthProviders {
|
||||
if authProvider.Bitbucketcloud != nil {
|
||||
p := authProvider.Bitbucketcloud
|
||||
rawURL := p.Url
|
||||
if rawURL == "" {
|
||||
rawURL = "https://bitbucket.org"
|
||||
}
|
||||
rawURL = strings.TrimSuffix(rawURL, "/")
|
||||
if !strings.HasPrefix(baseURL, rawURL) {
|
||||
continue
|
||||
}
|
||||
authURL, err := url.JoinPath(rawURL, "/site/oauth2/authorize")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tokenURL, err := url.JoinPath(rawURL, "/site/oauth2/access_token")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return &oauthutil.OAuthContext{
|
||||
ClientID: p.ClientKey,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authURL,
|
||||
TokenURL: tokenURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
@ -25,24 +26,77 @@ func (c *client) Repo(ctx context.Context, namespace, slug string) (*Repo, error
|
||||
return &repo, nil
|
||||
}
|
||||
|
||||
type ReposOptions struct {
|
||||
RequestOptions
|
||||
Role string `url:"role,omitempty"`
|
||||
}
|
||||
|
||||
// Repos returns a list of repositories that are fetched and populated based on given account
|
||||
// name and pagination criteria. If the account requested is a team, results will be filtered
|
||||
// down to the ones that the app password's user has access to.
|
||||
// If the argument pageToken.Next is not empty, it will be used directly as the URL to make
|
||||
// the request. The PageToken it returns may also contain the URL to the next page for
|
||||
// succeeding requests if any.
|
||||
func (c *client) Repos(ctx context.Context, pageToken *PageToken, accountName string) ([]*Repo, *PageToken, error) {
|
||||
var repos []*Repo
|
||||
var next *PageToken
|
||||
var err error
|
||||
// If the argument accountName is empty, it will return all repositories for
|
||||
// the authenticated user.
|
||||
func (c *client) Repos(ctx context.Context, pageToken *PageToken, accountName string, opts *ReposOptions) (repos []*Repo, next *PageToken, err error) {
|
||||
if pageToken.HasMore() {
|
||||
next, err = c.reqPage(ctx, pageToken.Next, &repos)
|
||||
} else {
|
||||
next, err = c.page(ctx, fmt.Sprintf("/2.0/repositories/%s", accountName), nil, pageToken, &repos)
|
||||
return
|
||||
}
|
||||
|
||||
var reposURL string
|
||||
if accountName == "" {
|
||||
reposURL = "/2.0/repositories"
|
||||
} else {
|
||||
reposURL = fmt.Sprintf("/2.0/repositories/%s", url.PathEscape(accountName))
|
||||
}
|
||||
|
||||
var urlValues url.Values
|
||||
if opts != nil && opts.Role != "" {
|
||||
urlValues = make(url.Values)
|
||||
urlValues.Set("role", opts.Role)
|
||||
}
|
||||
|
||||
next, err = c.page(ctx, reposURL, urlValues, pageToken, &repos)
|
||||
|
||||
if opts != nil && opts.FetchAll {
|
||||
repos, err = fetchAll(ctx, c, repos, next, err)
|
||||
}
|
||||
|
||||
return repos, next, err
|
||||
}
|
||||
|
||||
type ExplicitUserPermsResponse struct {
|
||||
User *Account `json:"user"`
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
func (c *client) ListExplicitUserPermsForRepo(ctx context.Context, pageToken *PageToken, namespace, slug string, opts *RequestOptions) (users []*Account, next *PageToken, err error) {
|
||||
var resp []ExplicitUserPermsResponse
|
||||
if pageToken.HasMore() {
|
||||
next, err = c.reqPage(ctx, pageToken.Next, &resp)
|
||||
} else {
|
||||
userPermsURL := fmt.Sprintf("/2.0/repositories/%s/%s/permissions-config/users", url.PathEscape(namespace), url.PathEscape(slug))
|
||||
next, err = c.page(ctx, userPermsURL, nil, pageToken, &resp)
|
||||
}
|
||||
|
||||
if opts != nil && opts.FetchAll {
|
||||
resp, err = fetchAll(ctx, c, resp, next, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
users = make([]*Account, len(resp))
|
||||
for i, r := range resp {
|
||||
users[i] = r.User
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type ForkInputProject struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
@ -61,6 +61,16 @@ func TestClient_Repos(t *testing.T) {
|
||||
HTML: Link{Href: "https://bitbucket.org/sourcegraph-testing/src-cli"},
|
||||
},
|
||||
ForkPolicy: ForkPolicyNoPublic,
|
||||
Owner: &Account{
|
||||
Links: Links{
|
||||
"avatar": Link{Href: "https://secure.gravatar.com/avatar/f964dc31564db8243e952bdaeabbe884?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FST-2.png"},
|
||||
"html": Link{Href: "https://bitbucket.org/%7B4b85b785-1433-4092-8512-20302f4a03be%7D/"},
|
||||
"self": Link{Href: "https://api.bitbucket.org/2.0/users/%7B4b85b785-1433-4092-8512-20302f4a03be%7D"},
|
||||
},
|
||||
Nickname: "Sourcegraph Testing",
|
||||
DisplayName: "Sourcegraph Testing",
|
||||
UUID: "{4b85b785-1433-4092-8512-20302f4a03be}",
|
||||
},
|
||||
},
|
||||
"sourcegraph": {
|
||||
Slug: "sourcegraph",
|
||||
@ -77,6 +87,16 @@ func TestClient_Repos(t *testing.T) {
|
||||
HTML: Link{Href: "https://bitbucket.org/sourcegraph-testing/sourcegraph"},
|
||||
},
|
||||
ForkPolicy: ForkPolicyNoPublic,
|
||||
Owner: &Account{
|
||||
Links: Links{
|
||||
"avatar": Link{Href: "https://secure.gravatar.com/avatar/f964dc31564db8243e952bdaeabbe884?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FST-2.png"},
|
||||
"html": Link{Href: "https://bitbucket.org/%7B4b85b785-1433-4092-8512-20302f4a03be%7D/"},
|
||||
"self": Link{Href: "https://api.bitbucket.org/2.0/users/%7B4b85b785-1433-4092-8512-20302f4a03be%7D"},
|
||||
},
|
||||
Nickname: "Sourcegraph Testing",
|
||||
DisplayName: "Sourcegraph Testing",
|
||||
UUID: "{4b85b785-1433-4092-8512-20302f4a03be}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -131,7 +151,7 @@ func TestClient_Repos(t *testing.T) {
|
||||
tc.err = "<nil>"
|
||||
}
|
||||
|
||||
repos, next, err := cli.Repos(tc.ctx, tc.page, tc.account)
|
||||
repos, next, err := cli.Repos(tc.ctx, tc.page, tc.account, nil)
|
||||
if have, want := fmt.Sprint(err), tc.err; have != want {
|
||||
t.Errorf("error:\nhave: %q\nwant: %q", have, want)
|
||||
}
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/aharvey-sg/src-cli-testing"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch-fork-00",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/aharvey-sg/src-cli-testing"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch-fork-00",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch-00",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch-00",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch-00",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -20,7 +20,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"is_private": true,
|
||||
"links": {
|
||||
@ -38,5 +39,25 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli-fork-00"
|
||||
}
|
||||
},
|
||||
"fork_policy": "no_public_forks"
|
||||
"fork_policy": "no_public_forks",
|
||||
"owner": {
|
||||
"links": {
|
||||
"avatar": {
|
||||
"href": "https://secure.gravatar.com/avatar/f964dc31564db8243e952bdaeabbe884?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FST-2.png"
|
||||
},
|
||||
"html": {
|
||||
"href": "https://bitbucket.org/%7B4b85b785-1433-4092-8512-20302f4a03be%7D/"
|
||||
},
|
||||
"self": {
|
||||
"href": "https://api.bitbucket.org/2.0/users/%7B4b85b785-1433-4092-8512-20302f4a03be%7D"
|
||||
}
|
||||
},
|
||||
"username": "",
|
||||
"nickname": "Sourcegraph Testing",
|
||||
"account_status": "",
|
||||
"display_name": "Sourcegraph Testing",
|
||||
"website": "",
|
||||
"created_on": "0001-01-01T00:00:00Z",
|
||||
"uuid": "{4b85b785-1433-4092-8512-20302f4a03be}"
|
||||
}
|
||||
}
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "always-open",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "merge-test-01",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -22,5 +22,25 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/sourcegraph"
|
||||
}
|
||||
},
|
||||
"fork_policy": "no_public_forks"
|
||||
"fork_policy": "no_public_forks",
|
||||
"owner": {
|
||||
"links": {
|
||||
"avatar": {
|
||||
"href": "https://secure.gravatar.com/avatar/f964dc31564db8243e952bdaeabbe884?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FST-2.png"
|
||||
},
|
||||
"html": {
|
||||
"href": "https://bitbucket.org/%7B4b85b785-1433-4092-8512-20302f4a03be%7D/"
|
||||
},
|
||||
"self": {
|
||||
"href": "https://api.bitbucket.org/2.0/users/%7B4b85b785-1433-4092-8512-20302f4a03be%7D"
|
||||
}
|
||||
},
|
||||
"username": "",
|
||||
"nickname": "Sourcegraph Testing",
|
||||
"account_status": "",
|
||||
"display_name": "Sourcegraph Testing",
|
||||
"website": "",
|
||||
"created_on": "0001-01-01T00:00:00Z",
|
||||
"uuid": "{4b85b785-1433-4092-8512-20302f4a03be}"
|
||||
}
|
||||
}
|
||||
@ -101,7 +101,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "always-open",
|
||||
@ -128,7 +129,8 @@
|
||||
"href": "https://bitbucket.org/sourcegraph-testing/src-cli"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"branch": {
|
||||
"name": "master",
|
||||
|
||||
@ -194,6 +194,7 @@ type Repo struct {
|
||||
IsPrivate bool `json:"is_private"`
|
||||
Links RepoLinks `json:"links"`
|
||||
ForkPolicy ForkPolicy `json:"fork_policy"`
|
||||
Owner *Account `json:"owner"`
|
||||
}
|
||||
|
||||
func (r *Repo) Namespace() (string, error) {
|
||||
|
||||
47
internal/extsvc/bitbucketcloud/user.go
Normal file
47
internal/extsvc/bitbucketcloud/user.go
Normal file
@ -0,0 +1,47 @@
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/encryption"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
)
|
||||
|
||||
// GetExternalAccountData returns the deserialized user and token from the external account data
|
||||
// JSON blob in a typesafe way.
|
||||
func GetExternalAccountData(ctx context.Context, data *extsvc.AccountData) (usr *Account, tok *oauth2.Token, err error) {
|
||||
if data.Data != nil {
|
||||
usr, err = encryption.DecryptJSON[Account](ctx, data.Data)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if data.AuthData != nil {
|
||||
tok, err = encryption.DecryptJSON[oauth2.Token](ctx, data.AuthData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return usr, tok, nil
|
||||
}
|
||||
|
||||
// SetExternalAccountData sets the user and token into the external account data blob.
|
||||
func SetExternalAccountData(data *extsvc.AccountData, user *Account, token *oauth2.Token) error {
|
||||
serializedUser, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serializedToken, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data.Data = extsvc.NewUnencryptedData(serializedUser)
|
||||
data.AuthData = extsvc.NewUnencryptedData(serializedToken)
|
||||
return nil
|
||||
}
|
||||
@ -27,3 +27,37 @@ type User struct {
|
||||
IsStaff bool `json:"is_staff"`
|
||||
AccountID string `json:"account_id"`
|
||||
}
|
||||
|
||||
type UserEmail struct {
|
||||
Email string `json:"email"`
|
||||
IsConfirmed bool `json:"is_confirmed"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
}
|
||||
|
||||
func (c *client) CurrentUserEmails(ctx context.Context, pageToken *PageToken) (emails []*UserEmail, next *PageToken, err error) {
|
||||
if pageToken.HasMore() {
|
||||
next, err = c.reqPage(ctx, pageToken.Next, &emails)
|
||||
return
|
||||
}
|
||||
|
||||
next, err = c.page(ctx, "/2.0/user/emails", nil, pageToken, &emails)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *client) AllCurrentUserEmails(ctx context.Context) (emails []*UserEmail, err error) {
|
||||
emails, next, err := c.CurrentUserEmails(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for next.HasMore() {
|
||||
var nextEmails []*UserEmail
|
||||
nextEmails, next, err = c.CurrentUserEmails(ctx, next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
emails = append(emails, nextEmails...)
|
||||
}
|
||||
|
||||
return emails, nil
|
||||
}
|
||||
|
||||
@ -179,7 +179,7 @@ func (s *BitbucketCloudSource) listAllRepos(ctx context.Context, results chan So
|
||||
var err error
|
||||
var repos []*bitbucketcloud.Repo
|
||||
for page.HasMore() || page.Page == 0 {
|
||||
if repos, page, err = s.client.Repos(ctx, page, s.config.Username); err != nil {
|
||||
if repos, page, err = s.client.Repos(ctx, page, s.config.Username, nil); err != nil {
|
||||
ch <- batch{err: errors.Wrapf(err, "bitbucketcloud.repos: item=%q, page=%+v", s.config.Username, page)}
|
||||
break
|
||||
}
|
||||
@ -198,7 +198,7 @@ func (s *BitbucketCloudSource) listAllRepos(ctx context.Context, results chan So
|
||||
var err error
|
||||
var repos []*bitbucketcloud.Repo
|
||||
for page.HasMore() || page.Page == 0 {
|
||||
if repos, page, err = s.client.Repos(ctx, page, t); err != nil {
|
||||
if repos, page, err = s.client.Repos(ctx, page, t, nil); err != nil {
|
||||
ch <- batch{err: errors.Wrapf(err, "bitbucketcloud.teams: item=%q, page=%+v", t, page)}
|
||||
break
|
||||
}
|
||||
|
||||
@ -45,7 +45,8 @@
|
||||
"href": "https://bitbucket.org/sg/go-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -94,7 +95,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -141,7 +143,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"is_private": false,
|
||||
"links": {
|
||||
@ -159,7 +162,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver-fork"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -45,7 +45,8 @@
|
||||
"href": "https://bitbucket.org/sg/go-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -94,7 +95,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -141,7 +143,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"is_private": false,
|
||||
"links": {
|
||||
@ -159,7 +162,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver-fork"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -45,7 +45,8 @@
|
||||
"href": "https://bitbucket.org/sg/go-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -94,7 +95,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -141,7 +143,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
},
|
||||
"is_private": false,
|
||||
"links": {
|
||||
@ -159,7 +162,8 @@
|
||||
"href": "https://bitbucket.org/sg/python-langserver-fork"
|
||||
}
|
||||
},
|
||||
"fork_policy": ""
|
||||
"fork_policy": "",
|
||||
"owner": null
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -22,6 +22,12 @@ type GitLabConnection struct {
|
||||
*schema.GitLabConnection
|
||||
}
|
||||
|
||||
type BitbucketCloudConnection struct {
|
||||
// The unique resource identifier of the external service.
|
||||
URN string
|
||||
*schema.BitbucketCloudConnection
|
||||
}
|
||||
|
||||
type PerforceConnection struct {
|
||||
// The unique resource identifier of the external service.
|
||||
URN string
|
||||
|
||||
@ -53,6 +53,17 @@
|
||||
"requestsPerHour": 7200
|
||||
}
|
||||
},
|
||||
"authorization": {
|
||||
"title": "BitbucketCloudAuthorization",
|
||||
"description": "If non-null, enforces Bitbucket Cloud repository permissions. This requires that there is an item in the [site configuration json](https://docs.sourcegraph.com/admin/config/site_config#auth-providers) `auth.providers` field, of type \"bitbucketcloud\" with the same `url` field as specified in this `BitbucketCloudConnection`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityProvider": {
|
||||
"description": "The identity provider to use for user information. If not set, the `url` field is used.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"username": {
|
||||
"description": "The username to use when authenticating to the Bitbucket Cloud. Also set the corresponding \"appPassword\" field.",
|
||||
"type": "string"
|
||||
|
||||
11
schema/bitbucketcloud_util.go
Normal file
11
schema/bitbucketcloud_util.go
Normal file
@ -0,0 +1,11 @@
|
||||
package schema
|
||||
|
||||
var DefaultBitbucketCloudURL = "https://bitbucket.org"
|
||||
|
||||
// GetURL retrieves the configured GitHub URL or a default if one is not set.
|
||||
func (p *BitbucketCloudAuthProvider) GetURL() string {
|
||||
if p != nil && p.Url != "" {
|
||||
return p.Url
|
||||
}
|
||||
return DefaultBitbucketCloudURL
|
||||
}
|
||||
@ -112,12 +112,13 @@ type AuthProviderCommon struct {
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
}
|
||||
type AuthProviders struct {
|
||||
Builtin *BuiltinAuthProvider
|
||||
Saml *SAMLAuthProvider
|
||||
Openidconnect *OpenIDConnectAuthProvider
|
||||
HttpHeader *HTTPHeaderAuthProvider
|
||||
Github *GitHubAuthProvider
|
||||
Gitlab *GitLabAuthProvider
|
||||
Builtin *BuiltinAuthProvider
|
||||
Saml *SAMLAuthProvider
|
||||
Openidconnect *OpenIDConnectAuthProvider
|
||||
HttpHeader *HTTPHeaderAuthProvider
|
||||
Github *GitHubAuthProvider
|
||||
Gitlab *GitLabAuthProvider
|
||||
Bitbucketcloud *BitbucketCloudAuthProvider
|
||||
}
|
||||
|
||||
func (v AuthProviders) MarshalJSON() ([]byte, error) {
|
||||
@ -139,6 +140,9 @@ func (v AuthProviders) MarshalJSON() ([]byte, error) {
|
||||
if v.Gitlab != nil {
|
||||
return json.Marshal(v.Gitlab)
|
||||
}
|
||||
if v.Bitbucketcloud != nil {
|
||||
return json.Marshal(v.Bitbucketcloud)
|
||||
}
|
||||
return nil, errors.New("tagged union type must have exactly 1 non-nil field value")
|
||||
}
|
||||
func (v *AuthProviders) UnmarshalJSON(data []byte) error {
|
||||
@ -149,6 +153,8 @@ func (v *AuthProviders) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
switch d.DiscriminantProperty {
|
||||
case "bitbucketcloud":
|
||||
return json.Unmarshal(data, &v.Bitbucketcloud)
|
||||
case "builtin":
|
||||
return json.Unmarshal(data, &v.Builtin)
|
||||
case "github":
|
||||
@ -162,7 +168,7 @@ func (v *AuthProviders) UnmarshalJSON(data []byte) error {
|
||||
case "saml":
|
||||
return json.Unmarshal(data, &v.Saml)
|
||||
}
|
||||
return fmt.Errorf("tagged union type must have a %q property whose value is one of %s", "type", []string{"builtin", "saml", "openidconnect", "http-header", "github", "gitlab"})
|
||||
return fmt.Errorf("tagged union type must have a %q property whose value is one of %s", "type", []string{"builtin", "saml", "openidconnect", "http-header", "github", "gitlab", "bitbucketcloud"})
|
||||
}
|
||||
|
||||
type BackendInsight struct {
|
||||
@ -213,12 +219,36 @@ type BatchSpec struct {
|
||||
Workspaces []*WorkspaceConfiguration `json:"workspaces,omitempty"`
|
||||
}
|
||||
|
||||
// BitbucketCloudAuthProvider description: Configures the Bitbucket Cloud OAuth authentication provider for SSO. In addition to specifying this configuration object, you must also create a OAuth App on your Bitbucket Cloud workspace: https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/. The application should have account, email, and repository scopes and the callback URL set to the concatenation of your Sourcegraph instance URL and "/.auth/bitbucketcloud/callback".
|
||||
type BitbucketCloudAuthProvider struct {
|
||||
// AllowSignup description: Allows new visitors to sign up for accounts via Bitbucket Cloud authentication. If false, users signing in via Bitbucket Cloud must have an existing Sourcegraph account, which will be linked to their Bitbucket Cloud identity after sign-in.
|
||||
AllowSignup bool `json:"allowSignup,omitempty"`
|
||||
// ApiScope description: The OAuth API scope that should be used
|
||||
ApiScope string `json:"apiScope,omitempty"`
|
||||
// ClientKey description: The Key of the Bitbucket OAuth app.
|
||||
ClientKey string `json:"clientKey"`
|
||||
// ClientSecret description: The Client Secret of the Bitbucket OAuth app.
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Type string `json:"type"`
|
||||
// Url description: URL of the Bitbucket Cloud instance.
|
||||
Url string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// BitbucketCloudAuthorization description: If non-null, enforces Bitbucket Cloud repository permissions. This requires that there is an item in the [site configuration json](https://docs.sourcegraph.com/admin/config/site_config#auth-providers) `auth.providers` field, of type "bitbucketcloud" with the same `url` field as specified in this `BitbucketCloudConnection`.
|
||||
type BitbucketCloudAuthorization struct {
|
||||
// IdentityProvider description: The identity provider to use for user information. If not set, the `url` field is used.
|
||||
IdentityProvider string `json:"identityProvider,omitempty"`
|
||||
}
|
||||
|
||||
// BitbucketCloudConnection description: Configuration for a connection to Bitbucket Cloud.
|
||||
type BitbucketCloudConnection struct {
|
||||
// ApiURL description: The API URL of Bitbucket Cloud, such as https://api.bitbucket.org. Generally, admin should not modify the value of this option because Bitbucket Cloud is a public hosting platform.
|
||||
ApiURL string `json:"apiURL,omitempty"`
|
||||
// AppPassword description: The app password to use when authenticating to the Bitbucket Cloud. Also set the corresponding "username" field.
|
||||
AppPassword string `json:"appPassword"`
|
||||
// Authorization description: If non-null, enforces Bitbucket Cloud repository permissions. This requires that there is an item in the [site configuration json](https://docs.sourcegraph.com/admin/config/site_config#auth-providers) `auth.providers` field, of type "bitbucketcloud" with the same `url` field as specified in this `BitbucketCloudConnection`.
|
||||
Authorization *BitbucketCloudAuthorization `json:"authorization,omitempty"`
|
||||
// Exclude description: A list of repositories to never mirror from Bitbucket Cloud. Takes precedence over "teams" configuration.
|
||||
//
|
||||
// Supports excluding by name ({"name": "myorg/myrepo"}) or by UUID ({"uuid": "{fceb73c7-cef6-4abe-956d-e471281126bd}"}).
|
||||
|
||||
@ -1563,7 +1563,7 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["builtin", "saml", "openidconnect", "http-header", "github", "gitlab"]
|
||||
"enum": ["builtin", "saml", "openidconnect", "http-header", "github", "gitlab", "bitbucketcloud"]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
@ -1572,7 +1572,8 @@
|
||||
{ "$ref": "#/definitions/OpenIDConnectAuthProvider" },
|
||||
{ "$ref": "#/definitions/HTTPHeaderAuthProvider" },
|
||||
{ "$ref": "#/definitions/GitHubAuthProvider" },
|
||||
{ "$ref": "#/definitions/GitLabAuthProvider" }
|
||||
{ "$ref": "#/definitions/GitLabAuthProvider" },
|
||||
{ "$ref": "#/definitions/BitbucketCloudAuthProvider" }
|
||||
],
|
||||
"!go": {
|
||||
"taggedUnionType": true
|
||||
@ -2107,6 +2108,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BitbucketCloudAuthProvider": {
|
||||
"description": "Configures the Bitbucket Cloud OAuth authentication provider for SSO. In addition to specifying this configuration object, you must also create a OAuth App on your Bitbucket Cloud workspace: https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/. The application should have account, email, and repository scopes and the callback URL set to the concatenation of your Sourcegraph instance URL and \"/.auth/bitbucketcloud/callback\".",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "clientKey", "clientSecret"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "bitbucketcloud"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL of the Bitbucket Cloud instance.",
|
||||
"default": "https://bitbucket.org/"
|
||||
},
|
||||
"clientKey": {
|
||||
"type": "string",
|
||||
"description": "The Key of the Bitbucket OAuth app."
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string",
|
||||
"description": "The Client Secret of the Bitbucket OAuth app."
|
||||
},
|
||||
"displayName": { "$ref": "#/definitions/AuthProviderCommon/properties/displayName" },
|
||||
"apiScope": {
|
||||
"type": "string",
|
||||
"description": "The OAuth API scope that should be used",
|
||||
"default": "account,email,repository",
|
||||
"enum": ["account", "email", "repository"]
|
||||
},
|
||||
"allowSignup": {
|
||||
"description": "Allows new visitors to sign up for accounts via Bitbucket Cloud authentication. If false, users signing in via Bitbucket Cloud must have an existing Sourcegraph account, which will be linked to their Bitbucket Cloud identity after sign-in.",
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AuthProviderCommon": {
|
||||
"$comment": "This schema is not used directly. The *AuthProvider schemas refer to its properties directly.",
|
||||
"description": "Common properties for authentication providers.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user