Add support for Bitbucket Server OAuth2 (#64179)

Docs here: https://github.com/sourcegraph/docs/pull/561

This PR adds support for using Bitbucket Server OAuth2 application links
for sign-in and permission syncing.

When used for permission syncing, the user's oauth token is used to
fetch user permissions (and now permissions are fetched via the server).

## Test plan

Tests added and updated.

## Changelog

- Sourcegraph now supports Bitbucket Server OAuth2 application links for
user sign-in and permission syncing.
This commit is contained in:
Petri-Johan Last 2024-08-14 12:24:32 +02:00 committed by GitHub
parent 56e1f11f25
commit d3a3d721d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 1975 additions and 169 deletions

View File

@ -222,6 +222,9 @@ const ProviderIcon: React.FunctionComponent<{ serviceType: AuthProvider['service
case 'bitbucketCloud': {
return <Icon aria-hidden={true} svgPath={mdiBitbucket} />
}
case 'bitbucketServer': {
return <Icon aria-hidden={true} svgPath={mdiBitbucket} />
}
case 'azuredevops': {
return <Icon aria-hidden={true} svgPath={mdiMicrosoftAzureDevops} />
}

View File

@ -226,6 +226,8 @@ export const SignUpForm: React.FunctionComponent<React.PropsWithChildren<SignUpF
<Icon aria-hidden={true} svgPath={mdiGitlab} />
) : provider.serviceType === 'bitbucketCloud' ? (
<Icon aria-hidden={true} svPath={mdiBitbucket} />
) : provider.serviceType === 'bitbucketServer' ? (
<Icon aria-hidden={true} svgPath={mdiBitbucket} />
) : null}{' '}
Continue with {provider.displayName}
</Button>

View File

@ -51,6 +51,10 @@ export const defaultExternalAccounts: Record<ExternalAccountKind, ExternalAccoun
title: 'Bitbucket Cloud',
icon: BitbucketIcon,
},
bitbucketServer: {
title: 'Bitbucket Server',
icon: BitbucketIcon,
},
gerrit: {
title: 'Gerrit',
icon: GerritIcon,

View File

@ -23,6 +23,7 @@ export interface AuthProvider {
| 'github'
| 'gitlab'
| 'bitbucketCloud'
| 'bitbucketServer'
| 'http-header'
| 'openidconnect'
| 'sourcegraph-operator'

View File

@ -34,6 +34,9 @@ export const stringToCodeHostType = (codeHostType: string): CodeHostType => {
case 'bitbucketCloud': {
return CodeHostType.BITBUCKETCLOUD
}
case 'bitbucketServer': {
return CodeHostType.BITBUCKETSERVER
}
case 'gitolite': {
return CodeHostType.GITOLITE
}

View File

@ -73,4 +73,5 @@ export const V2AuthProviderTypes: { [k in AuthProvider['serviceType']]: number }
builtin: 7,
gerrit: 8,
azuredevops: 9,
bitbucketServer: 10,
}

View File

@ -38,6 +38,10 @@ func (m mockAuthnProvider) CachedInfo() *providers.Info {
// return &providers.Info{ServiceID: m.serviceID}
}
func (m mockAuthnProvider) Type() providers.ProviderType {
return providers.ProviderTypeOAuth
}
type mockAuthnProviderUser struct {
Username string `json:"username,omitempty"`
ID int32 `json:"id,omitempty"`

View File

@ -11,6 +11,7 @@ go_library(
"//cmd/frontend/internal/auth/authutil",
"//cmd/frontend/internal/auth/azureoauth",
"//cmd/frontend/internal/auth/bitbucketcloudoauth",
"//cmd/frontend/internal/auth/bitbucketserveroauth",
"//cmd/frontend/internal/auth/gerrit",
"//cmd/frontend/internal/auth/githuboauth",
"//cmd/frontend/internal/auth/gitlaboauth",

View File

@ -7,15 +7,8 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/oauth"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/schema"
)
func init() {
oauth.AddIsOAuth(func(p schema.AuthProviders) bool {
return p.AzureDevOps != nil
})
}
func Middleware(db database.DB) *auth.Middleware {
return &auth.Middleware{
API: func(next http.Handler) http.Handler {

View File

@ -7,17 +7,10 @@ import (
"github.com/sourcegraph/sourcegraph/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 {

View File

@ -0,0 +1,71 @@
load("//dev:go_defs.bzl", "go_test")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "bitbucketserveroauth",
srcs = [
"config.go",
"middleware.go",
"provider.go",
"session.go",
],
importpath = "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/bitbucketserveroauth",
visibility = ["//cmd/frontend:__subpackages__"],
deps = [
"//cmd/frontend/auth",
"//cmd/frontend/hubspot",
"//cmd/frontend/hubspot/hubspotutil",
"//cmd/frontend/internal/auth/oauth",
"//cmd/frontend/internal/auth/providers",
"//cmd/frontend/internal/auth/session",
"//internal/actor",
"//internal/collections",
"//internal/conf",
"//internal/conf/conftypes",
"//internal/database",
"//internal/extsvc",
"//internal/extsvc/auth",
"//internal/extsvc/bitbucketserver",
"//internal/licensing",
"//internal/telemetry/telemetryrecorder",
"//lib/errors",
"//schema",
"@com_github_dghubble_gologin_v2//:gologin",
"@com_github_dghubble_gologin_v2//bitbucket",
"@com_github_dghubble_gologin_v2//oauth2",
"@com_github_sourcegraph_log//:log",
"@org_golang_x_oauth2//:oauth2",
],
)
go_test(
name = "bitbucketserveroauth_test",
srcs = [
"config_test.go",
"middleware_test.go",
"session_test.go",
],
embed = [":bitbucketserveroauth"],
tags = ["requires-network"],
deps = [
"//cmd/frontend/auth",
"//cmd/frontend/internal/auth/oauth",
"//cmd/frontend/internal/auth/providers",
"//cmd/frontend/internal/auth/session",
"//internal/actor",
"//internal/conf",
"//internal/database",
"//internal/database/dbmocks",
"//internal/database/dbtest",
"//internal/extsvc",
"//internal/extsvc/bitbucketserver",
"//internal/httpcli",
"//internal/ratelimit",
"//lib/errors",
"//schema",
"@com_github_google_go_cmp//cmp",
"@com_github_google_go_cmp//cmp/cmpopts",
"@com_github_sourcegraph_log//logtest",
"@org_golang_x_oauth2//:oauth2",
],
)

View File

@ -0,0 +1,77 @@
package bitbucketserveroauth
import (
"fmt"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/providers"
"github.com/sourcegraph/sourcegraph/internal/collections"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/licensing"
"github.com/sourcegraph/sourcegraph/schema"
)
func Init(logger log.Logger, db database.DB) {
const pkgName = "bitbucketserveroauth"
logger = logger.Scoped(pkgName)
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 Server 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.BitbucketServerAuthProvider
providers.Provider
}
func parseConfig(logger log.Logger, cfg conftypes.SiteConfigQuerier, db database.DB) (ps []Provider, problems conf.Problems) {
existingProviders := make(collections.Set[string])
for _, pr := range cfg.SiteConfig().AuthProviders {
if pr.Bitbucketserver == nil {
continue
}
provider, providerProblems := parseProvider(logger, pr.Bitbucketserver, db, pr)
problems = append(problems, conf.NewSiteProblems(providerProblems...)...)
if provider == nil {
continue
}
if existingProviders.Has(provider.CachedInfo().UniqueID()) {
problems = append(problems, conf.NewSiteProblems(fmt.Sprintf(`Cannot have more than one Bitbucket Server auth provider with url %q and client ID %q, only the first one will be used`, provider.ServiceID, provider.CachedInfo().ClientID))...)
continue
}
ps = append(ps, Provider{
BitbucketServerAuthProvider: pr.Bitbucketserver,
Provider: provider,
})
existingProviders.Add(provider.CachedInfo().UniqueID())
}
return ps, problems
}

View File

@ -0,0 +1,224 @@
package bitbucketserveroauth
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/oauth2"
"github.com/sourcegraph/log/logtest"
"github.com/sourcegraph/sourcegraph/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(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 Server config",
args: args{cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{
AuthProviders: []schema.AuthProviders{{
Bitbucketserver: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
}},
}}},
wantProviders: []Provider{
{
BitbucketServerAuthProvider: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
Provider: provider("https://my.bitbucket.org/", oauth2.Config{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
Endpoint: oauth2.Endpoint{
AuthURL: "https://my.bitbucket.org/rest/oauth2/latest/authorize",
TokenURL: "https://my.bitbucket.org/rest/oauth2/latest/token",
},
RedirectURL: "/.auth/bitbucketserver/callback",
Scopes: []string{"REPO_READ"},
}),
},
},
},
{
name: "2 Bitbucket Server configs with the same Url and client IDs",
args: args{cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{
AuthProviders: []schema.AuthProviders{{
Bitbucketserver: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
}, {
Bitbucketserver: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret2",
DisplayName: "Bitbucket Server Duplicate",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
}},
}}},
wantProviders: []Provider{
{
BitbucketServerAuthProvider: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
Provider: provider("https://my.bitbucket.org/", oauth2.Config{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
Endpoint: oauth2.Endpoint{
AuthURL: "https://my.bitbucket.org/rest/oauth2/latest/authorize",
TokenURL: "https://my.bitbucket.org/rest/oauth2/latest/token",
},
RedirectURL: "/.auth/bitbucketserver/callback",
Scopes: []string{"REPO_READ"},
}),
},
},
wantProblems: []string{
`Cannot have more than one Bitbucket Server auth provider with url "https://my.bitbucket.org/" and client ID "myclientid", only the first one will be used`,
},
},
{
name: "2 Bitbucket Server configs with the same Url but different client IDs",
args: args{cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{
AuthProviders: []schema.AuthProviders{{
Bitbucketserver: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
}, {
Bitbucketserver: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid2",
ClientSecret: "myclientsecret2",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
}},
}}},
wantProviders: []Provider{
{
BitbucketServerAuthProvider: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
Provider: provider("https://my.bitbucket.org/", oauth2.Config{
ClientID: "myclientid",
ClientSecret: "myclientsecret",
Endpoint: oauth2.Endpoint{
AuthURL: "https://my.bitbucket.org/rest/oauth2/latest/authorize",
TokenURL: "https://my.bitbucket.org/rest/oauth2/latest/token",
},
RedirectURL: "/.auth/bitbucketserver/callback",
Scopes: []string{"REPO_READ"},
}),
},
{
BitbucketServerAuthProvider: &schema.BitbucketServerAuthProvider{
ClientID: "myclientid2",
ClientSecret: "myclientsecret2",
DisplayName: "Bitbucket Server",
Type: extsvc.TypeBitbucketServer,
Url: "https://my.bitbucket.org",
},
Provider: provider("https://my.bitbucket.org/", oauth2.Config{
ClientID: "myclientid2",
ClientSecret: "myclientsecret2",
Endpoint: oauth2.Endpoint{
AuthURL: "https://my.bitbucket.org/rest/oauth2/latest/authorize",
TokenURL: "https://my.bitbucket.org/rest/oauth2/latest/token",
},
RedirectURL: "/.auth/bitbucketserver/callback",
Scopes: []string{"REPO_READ"},
}),
},
},
},
}
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{Bitbucketserver: p.BitbucketServerAuthProvider}
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, cmpopts.IgnoreUnexported(oauth2.Config{})); 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 },
ServiceID: serviceID,
ServiceType: extsvc.TypeBitbucketServer,
}
return &oauth.Provider{ProviderOp: op}
}

View File

@ -0,0 +1,23 @@
package bitbucketserveroauth
import (
"net/http"
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/oauth"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
)
const authPrefix = auth.AuthURLPrefix + "/bitbucketserver"
func Middleware(db database.DB) *auth.Middleware {
return &auth.Middleware{
API: func(next http.Handler) http.Handler {
return oauth.NewMiddleware(db, extsvc.TypeBitbucketServer, authPrefix, true, next)
},
App: func(next http.Handler) http.Handler {
return oauth.NewMiddleware(db, extsvc.TypeBitbucketServer, authPrefix, false, next)
},
}
}

View File

@ -0,0 +1,268 @@
package bitbucketserveroauth
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/sourcegraph/sourcegraph/internal/conf"
"golang.org/x/oauth2"
"github.com/sourcegraph/log/logtest"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/oauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/providers"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/session"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
"github.com/sourcegraph/sourcegraph/schema"
)
// TestMiddleware exercises the Middleware with requests that simulate the OAuth 2 login flow on
// Bitbucket Server. 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) {
session.ResetMockSessionStore(t)
db := dbmocks.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))
mockBitbucketServer := newMockProvider(t, db, "bitbucketserverclient", "bitbucketserversecret", "https://bitbucket.org/")
providers.MockProviders = []providers.Provider{mockBitbucketServer.Provider}
defer func() { providers.MockProviders = nil }()
doRequest := func(method, urlStr, body string, state string, cookies []*http.Cookie, authed bool) *http.Response {
req := httptest.NewRequest(method, urlStr, bytes.NewBufferString(body))
req.Header.Set("User-Agent", "Mozilla")
for _, cookie := range cookies {
req.AddCookie(cookie)
}
if authed {
req = req.WithContext(actor.WithActor(context.Background(), &actor.Actor{UID: mockUserID}))
}
respRecorder := httptest.NewRecorder()
session.SetData(respRecorder, req, "oauthState", state)
authedHandler.ServeHTTP(respRecorder, req)
return respRecorder.Result()
}
t.Run("unauthenticated homepage visit, sign-out cookie present, access requests enabled -> sg sign-in", func(t *testing.T) {
cookie := &http.Cookie{Name: session.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, access requests disabled -> bitbucket server oauth flow", func(t *testing.T) {
falseVal := false
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
AuthAccessRequest: &schema.AuthAccessRequest{
Enabled: &falseVal,
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
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/bitbucketserver/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, access requests disabled -> bitbucket server oauth flow", func(t *testing.T) {
falseVal := false
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
AuthAccessRequest: &schema.AuthAccessRequest{
Enabled: &falseVal,
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
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/bitbucketserver/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 server auth flow with redirect param", func(t *testing.T) {
resp := doRequest("GET", "http://example.com/.auth/bitbucketserver/login?pc="+mockBitbucketServer.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/rest/oauth2/latest/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"), mockBitbucketServer.Provider.CachedInfo().ClientID; got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := uredirect.Query().Get("scope"), "REPO_READ"; 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, mockBitbucketServer.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 Server OAuth callback with valid state param", func(t *testing.T) {
encodedState, err := oauth.LoginState{
Redirect: "/return-to-url",
ProviderID: mockBitbucketServer.Provider.ConfigID().ID,
CSRF: "csrf-code",
}.Encode()
if err != nil {
t.Fatal(err)
}
resp := doRequest("GET", "http://example.com/.auth/bitbucketserver/callback?code=the-oauth-code&state="+encodedState, "", encodedState, nil, false)
if want := http.StatusFound; resp.StatusCode != want {
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
}
if got, want := mockBitbucketServer.lastCallbackRequestURL, "http://example.com/callback?code=the-oauth-code&state="+encodedState; got == nil || got.String() != want {
t.Errorf("got last bitbucket server callback request url %v, want %v", got, want)
}
mockBitbucketServer.lastCallbackRequestURL = nil
})
t.Run("Bitbucket Server 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)
}
resp := doRequest("GET", "http://example.com/.auth/bitbucketserver/callback?code=the-oauth-code&state="+encodedState, "", encodedState, nil, false)
if want := http.StatusBadRequest; resp.StatusCode != want {
t.Errorf("got response code %v, want %v", resp.StatusCode, want)
}
if mockBitbucketServer.lastCallbackRequestURL != nil {
t.Errorf("got last bitbucket.org callback request url was non-nil: %v", mockBitbucketServer.lastCallbackRequestURL)
}
mockBitbucketServer.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{Bitbucketserver: &schema.BitbucketServerAuthProvider{
Url: baseURL,
ClientSecret: clientSecret,
ClientID: clientID,
}}
mp.Provider, problems = parseProvider(logtest.Scoped(t), cfg.Bitbucketserver, 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
}

View File

@ -0,0 +1,106 @@
package bitbucketserveroauth
import (
"fmt"
"net/http"
"net/url"
"github.com/dghubble/gologin/v2"
"github.com/dghubble/gologin/v2/bitbucket"
oauth2Login "github.com/dghubble/gologin/v2/oauth2"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/oauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/session"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/schema"
"golang.org/x/oauth2"
)
const (
sessionKey = "bitbucketserveroauth@0"
authorizePath = "/rest/oauth2/latest/authorize"
tokenPath = "/rest/oauth2/latest/token"
redirectPath = authPrefix + "/callback"
)
func parseProvider(logger log.Logger, p *schema.BitbucketServerAuthProvider, db database.DB, sourceCfg schema.AuthProviders) (provider *oauth.Provider, messages []string) {
parsedURL, err := url.Parse(p.Url)
if err != nil {
messages = append(messages, fmt.Sprintf("Could not parse Bitbucket Server URL %q. You will not be able to login via Bitbucket Server.", p.Url))
return nil, messages
}
parsedURL = extsvc.NormalizeBaseURL(parsedURL)
extURL, err := url.Parse(conf.ExternalURL())
if err != nil {
messages = append(messages, fmt.Sprintf("Could not parse Sourcegraph external URL %q.", conf.ExternalURL()))
return nil, messages
}
authURL := parsedURL.ResolveReference(&url.URL{Path: authorizePath}).String()
tokenURL := parsedURL.ResolveReference(&url.URL{Path: tokenPath}).String()
redirectURL := extURL.ResolveReference(&url.URL{Path: redirectPath}).String()
return oauth.NewProvider(oauth.ProviderOp{
AuthPrefix: authPrefix,
OAuth2Config: func() oauth2.Config {
return oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{"REPO_READ"},
RedirectURL: redirectURL,
}
},
SourceConfig: sourceCfg,
ServiceID: parsedURL.String(),
ServiceType: extsvc.TypeBitbucketServer,
Login: func(oauth2Cfg oauth2.Config) http.Handler {
return bitbucket.LoginHandler(&oauth2Cfg, nil)
},
Callback: func(oauth2Cfg oauth2.Config) http.Handler {
return oauth2Login.CallbackHandler(
&oauth2Cfg,
oauth.SessionIssuer(logger, db, &sessionIssuerHelper{
logger: logger.Scoped("sessionIssuerHelper"),
baseURL: parsedURL,
db: db,
clientKey: p.ClientID,
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
}
var encodedState string
err := session.GetData(r, "oauthState", &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 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)
}

View File

@ -0,0 +1,126 @@
package bitbucketserveroauth
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/frontend/auth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/hubspot"
"github.com/sourcegraph/sourcegraph/cmd/frontend/hubspot/hubspotutil"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/oauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/providers"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/session"
"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/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/telemetry/telemetryrecorder"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/schema"
"golang.org/x/oauth2"
)
type sessionIssuerHelper struct {
logger log.Logger
baseURL *url.URL
clientKey string
db database.DB
allowSignup bool
client *bitbucketserver.Client
}
func (s *sessionIssuerHelper) AuthSucceededEventName() database.SecurityEventName {
return database.SecurityEventBitbucketServerAuthSucceeded
}
func (s *sessionIssuerHelper) AuthFailedEventName() database.SecurityEventName {
return database.SecurityEventBitbucketServerAuthFailed
}
func (s *sessionIssuerHelper) DeleteStateCookie(w http.ResponseWriter, r *http.Request) {
session.SetData(w, r, "oauthState", "")
}
func (s *sessionIssuerHelper) GetServiceID() string {
return s.baseURL.String()
}
func (s *sessionIssuerHelper) GetOrCreateUser(ctx context.Context, token *oauth2.Token, hubSpotProps *hubspot.ContactProperties) (newUserCreated bool, actr *actor.Actor, safeErrMsg string, err error) {
var client *bitbucketserver.Client
if s.client != nil {
client = s.client
} else {
conf := &schema.BitbucketServerConnection{
Url: s.baseURL.String(),
}
client, err = bitbucketserver.NewClient(s.baseURL.String(), conf, nil)
if err != nil {
return false, nil, "Could not initialize Bitbucket Server 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)
username, err := client.AuthenticatedUsername(ctx)
if err != nil {
return false, nil, "Could not read Bitbucket user from callback request.", errors.Wrap(err, "could not read user from bitbucket")
}
bbUser := bitbucketserver.User{
Slug: username,
}
err = client.LoadUser(ctx, &bbUser)
if err != nil {
return false, nil, "Could not read Bitbucket user from callback request.", errors.Wrap(err, "could not read user from bitbucket")
}
var data extsvc.AccountData
if err := bitbucketserver.SetExternalAccountData(&data, &bbUser, token); err != nil {
return false, nil, "", err
}
recorder := telemetryrecorder.New(s.db)
newUserCreated, userID, safeErrMsg, err := auth.GetAndSaveUser(ctx, s.logger, s.db, recorder, auth.GetAndSaveUserOp{
UserProps: database.NewUser{
Username: bbUser.Name,
Email: bbUser.EmailAddress,
EmailIsVerified: true,
DisplayName: bbUser.DisplayName,
},
ExternalAccount: extsvc.AccountSpec{
ServiceType: extsvc.TypeBitbucketServer,
ServiceID: s.baseURL.String(),
ClientID: s.clientKey,
AccountID: strconv.Itoa(bbUser.ID),
},
ExternalAccountData: data,
CreateIfNotExist: s.allowSignup,
})
if err != nil {
return false, nil, fmt.Sprintf("No Sourcegraph user exists matching the email: %s.\n\nError was: %s", bbUser.EmailAddress, safeErrMsg), err
}
go hubspotutil.SyncUser(bbUser.EmailAddress, hubspotutil.SignupEventID, hubSpotProps)
return newUserCreated, actor.FromUser(userID), "", nil
}
func (s *sessionIssuerHelper) SessionData(token *oauth2.Token) oauth.SessionData {
return oauth.SessionData{
ID: providers.ConfigID{
ID: s.baseURL.String(),
Type: extsvc.TypeBitbucketServer,
},
AccessToken: token.AccessToken,
TokenType: token.Type(),
}
}

View File

@ -0,0 +1,175 @@
package bitbucketserveroauth
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/oauth2"
"github.com/sourcegraph/log/logtest"
"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/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/schema"
)
var (
returnUsername string
returnAccountID int
returnEmail string
)
func createTestServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/rest/api/1.0/users/") {
json.NewEncoder(w).Encode(struct {
Name string `json:"name"`
ID int `json:"id"`
EmailAddress string `json:"emailAddress"`
}{
Name: returnUsername,
ID: returnAccountID,
EmailAddress: returnEmail,
})
return
}
if strings.HasPrefix(r.URL.Path, "/rest/api/1.0/users") {
w.Header().Add("X-Ausername", returnUsername)
return
}
}))
}
func TestSessionIssuerHelper_GetOrCreateUser(t *testing.T) {
ratelimit.SetupForTest(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 *bitbucketserver.User
bbUserEmail string
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: &bitbucketserver.User{Name: "alice"},
bbUserEmail: "alice@example.com",
}},
expActor: &actor.Actor{UID: 1},
expAuthUserOp: &auth.GetAndSaveUserOp{
UserProps: u("alice", "alice@example.com", true),
ExternalAccount: acct(extsvc.TypeBitbucketServer, server.URL+"/", clientID, "1234"),
},
},
}
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.Name
returnAccountID = 1234
}
returnEmail = ci.bbUserEmail
var gotAuthUserOp *auth.GetAndSaveUserOp
auth.MockGetAndSaveUser = func(ctx context.Context, op auth.GetAndSaveUserOp) (newUserCreated bool, 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 false, uid, "", nil
}
return false, 0, "safeErr", errors.New("auth.GetAndSaveUser error")
}
defer func() {
auth.MockGetAndSaveUser = nil
}()
ctx := context.Background()
conf := &schema.BitbucketServerConnection{
Url: server.URL,
}
bbClient, err := bitbucketserver.NewClient(server.URL, conf, httpcli.TestExternalDoer)
if err != nil {
t.Fatal(err)
}
s := &sessionIssuerHelper{
logger: logtest.Scoped(t),
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, nil)
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 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,
}
}

View File

@ -87,6 +87,10 @@ func (p *Provider) CachedInfo() *providers.Info {
}
}
func (p *Provider) Type() providers.ProviderType {
return providers.ProviderTypeGerrit
}
func (p *Provider) ExternalAccountInfo(ctx context.Context, account extsvc.Account) (*extsvc.PublicAccountData, error) {
return gerrit.GetPublicExternalAccountData(ctx, &account.AccountData)
}

View File

@ -7,17 +7,10 @@ import (
"github.com/sourcegraph/sourcegraph/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 + "/github"
func init() {
oauth.AddIsOAuth(func(p schema.AuthProviders) bool {
return p.Github != nil
})
}
func Middleware(db database.DB) *auth.Middleware {
return &auth.Middleware{
API: func(next http.Handler) http.Handler {

View File

@ -7,17 +7,10 @@ import (
"github.com/sourcegraph/sourcegraph/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 + "/gitlab"
func init() {
oauth.AddIsOAuth(func(p schema.AuthProviders) bool {
return p.Gitlab != nil
})
}
func Middleware(db database.DB) *auth.Middleware {
return &auth.Middleware{
API: func(next http.Handler) http.Handler {

View File

@ -34,3 +34,7 @@ func (p *provider) ExternalAccountInfo(ctx context.Context, account extsvc.Accou
DisplayName: account.AccountID,
}, nil
}
func (p *provider) Type() providers.ProviderType {
return providers.ProviderTypeHTTPHeader
}

View File

@ -13,6 +13,7 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/authutil"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/azureoauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/bitbucketcloudoauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/bitbucketserveroauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/gerrit"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/githuboauth"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/gitlaboauth"
@ -32,6 +33,7 @@ func Init(logger log.Logger, db database.DB) {
userpasswd.Init()
azureoauth.Init(logger, db)
bitbucketcloudoauth.Init(logger, db)
bitbucketserveroauth.Init(logger, db)
gerrit.Init()
githuboauth.Init(logger, db)
gitlaboauth.Init(logger, db)
@ -50,6 +52,7 @@ func Init(logger log.Logger, db database.DB) {
githuboauth.Middleware(db),
gitlaboauth.Middleware(db),
bitbucketcloudoauth.Middleware(db),
bitbucketserveroauth.Middleware(db),
azureoauth.Middleware(db),
)
@ -80,6 +83,8 @@ func Init(logger log.Logger, db database.DB) {
name = "GitLab OAuth"
case p.Bitbucketcloud != nil:
name = "Bitbucket Cloud OAuth"
case p.Bitbucketserver != nil:
name = "Bitbucket Server OAuth"
case p.AzureDevOps != nil:
name = "Azure DevOps"
case p.HttpHeader != nil:

View File

@ -25,6 +25,7 @@ go_library(
"//internal/extsvc",
"//internal/extsvc/azuredevops",
"//internal/extsvc/bitbucketcloud",
"//internal/extsvc/bitbucketserver",
"//internal/extsvc/github",
"//internal/extsvc/gitlab",
"//internal/httpcli",

View File

@ -26,7 +26,6 @@ import (
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/trace"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/schema"
)
func NewMiddleware(db database.DB, serviceType, authPrefix string, isAPIHandler bool, next http.Handler) http.Handler {
@ -222,27 +221,12 @@ func getExactlyOneOAuthProvider(skipSoap bool) *Provider {
if !ok {
return nil
}
if !isOAuth(p.Config()) {
if ps[0].Type() != providers.ProviderTypeOAuth {
return nil
}
return p
}
var isOAuths []func(p schema.AuthProviders) bool
func AddIsOAuth(f func(p schema.AuthProviders) bool) {
isOAuths = append(isOAuths, f)
}
func isOAuth(p schema.AuthProviders) bool {
for _, f := range isOAuths {
if f(p) {
return true
}
}
return false
}
// isHuman returns true if the request probably came from a human, rather than a bot. Used to
// prevent unfurling the wrong URL preview.
func isHuman(req *http.Request) bool {

View File

@ -19,6 +19,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
"github.com/sourcegraph/sourcegraph/internal/extsvc/gitlab"
"github.com/sourcegraph/sourcegraph/lib/errors"
@ -51,6 +52,10 @@ func (p *Provider) ConfigID() providers.ConfigID {
}
}
func (p *Provider) Type() providers.ProviderType {
return providers.ProviderTypeOAuth
}
func (p *Provider) Config() schema.AuthProviders {
return p.SourceConfig
}
@ -66,6 +71,8 @@ func (p *Provider) CachedInfo() *providers.Info {
displayName = p.SourceConfig.Gitlab.DisplayName
case p.SourceConfig.Bitbucketcloud != nil && p.SourceConfig.Bitbucketcloud.DisplayName != "":
displayName = p.SourceConfig.Bitbucketcloud.DisplayName
case p.SourceConfig.Bitbucketserver != nil && p.SourceConfig.Bitbucketserver.DisplayName != "":
displayName = p.SourceConfig.Bitbucketserver.DisplayName
}
return &providers.Info{
ServiceID: p.ServiceID,
@ -86,6 +93,8 @@ func (p *Provider) ExternalAccountInfo(ctx context.Context, account extsvc.Accou
return gitlab.GetPublicExternalAccountData(ctx, &account.AccountData)
case extsvc.TypeBitbucketCloud:
return bitbucketcloud.GetPublicExternalAccountData(ctx, &account.AccountData)
case extsvc.TypeBitbucketServer:
return bitbucketserver.GetPublicExternalAccountData(ctx, &account.AccountData, p.ServiceID)
case extsvc.TypeAzureDevOps:
return azuredevops.GetPublicExternalAccountData(ctx, &account.AccountData)
}

View File

@ -49,6 +49,10 @@ func (p *Provider) ConfigID() providers.ConfigID {
}
}
func (p *Provider) Type() providers.ProviderType {
return providers.ProviderTypeOpenIDConnect
}
// Config implements providers.Provider.
func (p *Provider) Config() schema.AuthProviders {
return schema.AuthProviders{Openidconnect: &p.config}

View File

@ -12,12 +12,17 @@ type MockAuthProvider struct {
MockConfig schema.AuthProviderCommon
MockAuthProvidersConfig *schema.AuthProviders
MockPublicAccountData *extsvc.PublicAccountData
MockType ProviderType
}
func (m MockAuthProvider) ConfigID() ConfigID {
return m.MockConfigID
}
func (m MockAuthProvider) Type() ProviderType {
return m.MockType
}
func (m MockAuthProvider) Config() schema.AuthProviders {
if m.MockAuthProvidersConfig != nil {
return *m.MockAuthProvidersConfig

View File

@ -41,8 +41,21 @@ type Provider interface {
// ExternalAccountInfo provides basic external account from this auth provider
ExternalAccountInfo(ctx context.Context, account extsvc.Account) (*extsvc.PublicAccountData, error)
Type() ProviderType
}
type ProviderType = string
const (
ProviderTypeOAuth ProviderType = "oauth"
ProviderTypeSAML ProviderType = "saml"
ProviderTypeOpenIDConnect ProviderType = "openidconnect"
ProviderTypeHTTPHeader ProviderType = "httpheader"
ProviderTypeBuiltin ProviderType = "builtin"
ProviderTypeGerrit ProviderType = "gerrit"
)
// ConfigID identifies a provider config object in the auth.providers site configuration
// array.
//

View File

@ -65,7 +65,7 @@ func authHandler(db database.DB, w http.ResponseWriter, r *http.Request, next ht
// Note: For instances that are conf.AuthPublic(), we don't redirect to sign-in automatically, as that would
// lock out unauthenticated access.
ps := providers.SignInProviders(!r.URL.Query().Has("sourcegraph-operator"))
if !conf.AuthPublic() && len(ps) == 1 && ps[0].Config().Saml != nil && !session.HasSignOutCookie(r) && !isAPIRequest {
if !conf.AuthPublic() && len(ps) == 1 && ps[0].Type() == providers.ProviderTypeSAML && !session.HasSignOutCookie(r) && !isAPIRequest {
p, handled := handleGetProvider(r.Context(), w, ps[0].ConfigID().ID)
if handled {
return

View File

@ -54,6 +54,10 @@ func (p *provider) Config() schema.AuthProviders {
return schema.AuthProviders{Saml: &p.config}
}
func (p *provider) Type() providers.ProviderType {
return providers.ProviderTypeSAML
}
func (p *provider) Refresh(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()

View File

@ -64,3 +64,7 @@ func (p *provider) Config() schema.AuthProviders {
},
}
}
func (p *provider) Type() providers.ProviderType {
return providers.ProviderTypeOpenIDConnect
}

View File

@ -33,3 +33,7 @@ func (p provider) CachedInfo() *providers.Info {
func (p provider) ExternalAccountInfo(ctx context.Context, account extsvc.Account) (*extsvc.PublicAccountData, error) {
return nil, errors.Errorf("not an external account, cannot provide external account info")
}
func (p provider) Type() providers.ProviderType {
return providers.ProviderTypeBuiltin
}

View File

@ -68,6 +68,7 @@ go_test(
"//internal/actor",
"//internal/api",
"//internal/authz",
"//internal/authz/providers/bitbucketserver",
"//internal/authz/providers/github",
"//internal/authz/providers/gitlab",
"//internal/conf",
@ -76,6 +77,7 @@ go_test(
"//internal/database/dbtest",
"//internal/extsvc",
"//internal/extsvc/auth",
"//internal/extsvc/bitbucketserver",
"//internal/extsvc/github",
"//internal/extsvc/gitlab",
"//internal/httptestutil",

View File

@ -19,6 +19,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
authzBitbucketServer "github.com/sourcegraph/sourcegraph/internal/authz/providers/bitbucketserver"
authzGitHub "github.com/sourcegraph/sourcegraph/internal/authz/providers/github"
authzGitLab "github.com/sourcegraph/sourcegraph/internal/authz/providers/gitlab"
"github.com/sourcegraph/sourcegraph/internal/conf"
@ -26,6 +27,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/auth"
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
extsvcGitHub "github.com/sourcegraph/sourcegraph/internal/extsvc/github"
"github.com/sourcegraph/sourcegraph/internal/httptestutil"
@ -581,3 +583,130 @@ func TestIntegration_GitLabPermissions(t *testing.T) {
assertUserPermissions(t, []int32{1, 2})
})
}
func TestIntegration_BitbucketServerPermissions(t *testing.T) {
if testing.Short() {
t.Skip()
}
logger := logtest.Scoped(t)
// BITBUCKETSERVER_TOKEN should be a valid OAuth2 token for the bitbucket server instance
token := os.Getenv("BITBUCKETSERVER_TOKEN")
spec := extsvc.AccountSpec{
ServiceType: extsvc.TypeBitbucketServer,
ServiceID: "https://bitbucket.sgdev.org/",
AccountID: "603",
}
svc := types.ExternalService{
Kind: extsvc.KindBitbucketServer,
Config: extsvc.NewUnencryptedConfig(`{"url": "https://bitbucket.sgdev.org", "authorization": {"oauth2": true}, "token": "abc", "repos": [ "PRIVATE/test-repo" ], "username": "whatever"}`),
}
newUser := database.NewUser{
Email: "sourcegraph-vcr@sourcegraph.com",
Username: "sourcegraph-vcr",
EmailIsVerified: true,
}
// This test requires the "PRIVATE/test-repo" repo to be set up
testRepos := []types.Repo{
{
Name: "bitbucket.sgdev.org/PRIVATE/test-repo",
Private: true,
URI: "bitbucket.sgdev.org/PRIVATE/test-repo",
ExternalRepo: api.ExternalRepoSpec{
ID: "10093",
ServiceType: extsvc.TypeBitbucketServer,
ServiceID: "https://bitbucket.sgdev.org/",
},
Sources: map[string]*types.SourceInfo{
svc.URN(): {
ID: svc.URN(),
},
},
},
}
authData := json.RawMessage(fmt.Sprintf(`{"access_token": "%s"}`, token))
accountData := json.RawMessage(`{"name":"pjlast","emailAddress":"petri.last@sourcegraph.com","id":603,"displayName":"Petri-Johan Last","active":true,"slug":"pjlast","type":"NORMAL"}`)
t.Run("test bitbucket server oauth permissions", func(t *testing.T) {
name := t.Name()
cf, save := httptestutil.NewRecorderFactory(t, update(name), name)
defer save()
doer, err := cf.Doer()
require.NoError(t, err)
testDB := database.NewDB(logger, dbtest.NewDB(t))
ctx := actor.WithInternalActor(context.Background())
reposStore := repos.NewStore(logtest.Scoped(t), testDB)
err = reposStore.ExternalServiceStore().Upsert(ctx, &svc)
require.NoError(t, err)
cfg, err := extsvc.ParseEncryptableConfig(ctx, svc.Kind, svc.Config)
require.NoError(t, err)
conn := &types.BitbucketServerConnection{
URN: svc.URN(),
BitbucketServerConnection: cfg.(*schema.BitbucketServerConnection),
}
cli, err := bitbucketserver.NewClient("https://bitbucket.sgdev.org", conn.BitbucketServerConnection, doer)
require.NoError(t, err)
provider := authzBitbucketServer.NewOAuthProvider(testDB, conn, authzBitbucketServer.ProviderOptions{BitbucketServerClient: cli}, false)
for _, repo := range testRepos {
err = reposStore.RepoStore().Create(ctx, &repo)
require.NoError(t, err)
}
user, err := testDB.Users().CreateWithExternalAccount(ctx, newUser, &extsvc.Account{
AccountSpec: spec,
AccountData: extsvc.AccountData{
AuthData: extsvc.NewUnencryptedData(authData),
Data: extsvc.NewUnencryptedData(accountData),
},
})
require.NoError(t, err)
permsStore := database.Perms(logger, testDB, timeutil.Now)
syncer := newPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now)
syncer.providerFactory = func(context.Context) []authz.Provider {
return []authz.Provider{provider}
}
assertUserPermissions := func(t *testing.T, wantIDs []int32) {
t.Helper()
_, providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{})
require.NoError(t, err)
assert.Equal(t, database.CodeHostStatusesSet{{
ProviderID: "https://bitbucket.sgdev.org/",
ProviderType: "bitbucketServer",
Status: database.CodeHostStatusSuccess,
Message: "FetchUserPerms",
}}, providerStates)
p, err := permsStore.LoadUserPermissions(ctx, user.ID)
require.NoError(t, err)
gotIDs := make([]int32, len(p))
for i, perm := range p {
gotIDs[i] = perm.RepoID
}
if diff := cmp.Diff(wantIDs, gotIDs); diff != "" {
t.Fatalf("IDs mismatch (-want +got):\n%s", diff)
}
}
assertUserPermissions(t, []int32{1})
})
}

View File

@ -449,21 +449,6 @@ func (s *permsSyncerImpl) fetchUserPermsViaExternalAccounts(ctx context.Context,
serviceToAccounts[acct.ServiceType+":"+acct.ServiceID] = acct
}
userEmails, err := s.db.UserEmails().ListByUser(ctx,
database.UserEmailsListOptions{
UserID: user.ID,
OnlyVerified: true,
},
)
if err != nil {
return results, errors.Wrap(err, "list user verified emails")
}
emails := make([]string, len(userEmails))
for i := range userEmails {
emails[i] = userEmails[i].Email
}
byServiceID := s.providersByServiceID(ctx)
accounts := s.db.UserExternalAccounts()
logger := s.logger.Scoped("fetchUserPermsViaExternalAccounts").With(log.Int32("userID", user.ID))
@ -477,7 +462,7 @@ func (s *permsSyncerImpl) fetchUserPermsViaExternalAccounts(ctx context.Context,
continue
}
acct, err := provider.FetchAccount(ctx, user, accts, emails)
acct, err := provider.FetchAccount(ctx, user)
if err != nil {
results.providerStates = append(results.providerStates, database.NewProviderStatus(provider, err, "FetchAccount"))
providerLogger.Error("could not fetch account from authz provider", log.Error(err))

View File

@ -33,12 +33,12 @@ type mockProvider struct {
fetchUserPerms func(context.Context, *extsvc.Account) (*authz.ExternalUserPermissions, error)
fetchUserPermsByToken func(ctx context.Context, token string) (*authz.ExternalUserPermissions, error)
fetchRepoPerms func(ctx context.Context, repo *extsvc.Repository, opts authz.FetchPermsOptions) ([]extsvc.AccountID, error)
fetchAccount func(ctx context.Context, user *types.User, accounts []*extsvc.Account, emails []string) (*extsvc.Account, error)
fetchAccount func(ctx context.Context, user *types.User) (*extsvc.Account, error)
}
func (p *mockProvider) FetchAccount(ctx context.Context, user *types.User, accounts []*extsvc.Account, emails []string) (*extsvc.Account, error) {
func (p *mockProvider) FetchAccount(ctx context.Context, user *types.User) (*extsvc.Account, error) {
if p.fetchAccount != nil {
return p.fetchAccount(ctx, user, accounts, emails)
return p.fetchAccount(ctx, user)
}
return nil, nil
}
@ -363,7 +363,7 @@ func TestPermsSyncer_syncUserPerms_fetchAccount(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.fetchAccountError != nil {
p2.fetchAccount = func(context.Context, *types.User, []*extsvc.Account, []string) (*extsvc.Account, error) {
p2.fetchAccount = func(context.Context, *types.User) (*extsvc.Account, error) {
return nil, test.fetchAccountError
}
}
@ -727,7 +727,6 @@ func TestPermsSyncer_syncUserPermsTemporaryProviderError(t *testing.T) {
// Verify that the UpsertWithSpec (non IP version) was called once
assert.Equal(t, 1, upsertCallCount, "UpsertWithSpec should have been called once")
})
}
func TestPermsSyncer_syncUserPerms_noPerms(t *testing.T) {
@ -1093,7 +1092,7 @@ func TestPermsSyncer_syncUserPerms_subRepoPermissions(t *testing.T) {
},
}, nil
}
p.fetchAccount = func(ctx context.Context, user *types.User, accounts []*extsvc.Account, emails []string) (*extsvc.Account, error) {
p.fetchAccount = func(ctx context.Context, user *types.User) (*extsvc.Account, error) {
return &extsvc.Account{
AccountSpec: extsvc.AccountSpec{
ServiceType: p.ServiceType(),

View File

@ -395,7 +395,7 @@ type mockProvider struct {
extAcct *extsvc.Account
}
func (p *mockProvider) FetchAccount(context.Context, *types.User, []*extsvc.Account, []string) (mine *extsvc.Account, err error) {
func (p *mockProvider) FetchAccount(context.Context, *types.User) (mine *extsvc.Account, err error) {
return p.extAcct, nil
}

View File

@ -130,21 +130,12 @@ type FetchPermsOptions struct {
// In most cases, an authz provider represents a code host, because it is the source of truth for
// repository permissions.
type Provider interface {
// FetchAccount returns the external account that identifies the user to this authz provider,
// taking as input the current list of external accounts associated with the
// user. Implementations should always recompute the returned account (rather than returning an
// element of `current` if it has the correct ServiceID and ServiceType).
//
// Implementations should use only the `user` and `current` parameters to compute the returned
// external account. Specifically, they should not try to get the currently authenticated user
// from the context parameter.
// FetchAccount returns the external account that identifies the user to this authz provider.
// Implementations should always recompute the returned account.
//
// The `user` argument should always be non-nil. If no external account can be computed for the
// provided user, implementations should return nil, nil.
//
// The `verifiedEmails` should only contain a list of verified emails that is
// associated to the `user`.
FetchAccount(ctx context.Context, user *types.User, current []*extsvc.Account, verifiedEmails []string) (mine *extsvc.Account, err error)
FetchAccount(ctx context.Context, user *types.User) (mine *extsvc.Account, err error)
// FetchUserPerms returns a collection of accessible repository/project IDs (on
// code host) that the given account has read access on the code host. The

View File

@ -157,8 +157,8 @@ func ProvidersFromConfig(
initResult := github.NewAuthzProviders(ctx, db, gitHubConns, cfg.SiteConfig().AuthProviders, enableGithubInternalRepoVisibility)
initResult.Append(gitlab.NewAuthzProviders(db, gitLabConns, cfg.SiteConfig().AuthProviders))
initResult.Append(bitbucketserver.NewAuthzProviders(bitbucketServerConns))
initResult.Append(perforce.NewAuthzProviders(perforceConns))
initResult.Append(bitbucketserver.NewAuthzProviders(db, bitbucketServerConns, cfg.SiteConfig().AuthProviders))
initResult.Append(perforce.NewAuthzProviders(db, perforceConns))
initResult.Append(bitbucketcloud.NewAuthzProviders(db, bitbucketCloudConns))
initResult.Append(gerrit.NewAuthzProviders(gerritConns))
initResult.Append(azuredevops.NewAuthzProviders(db, azuredevopsConns, httpcli.ExternalClient))

View File

@ -38,7 +38,7 @@ func (m gitlabAuthzProviderParams) Repos(ctx context.Context, repos []*types.Rep
panic("should never be called")
}
func (m gitlabAuthzProviderParams) FetchAccount(ctx context.Context, user *types.User, current []*extsvc.Account, verifiedEmails []string) (mine *extsvc.Account, err error) {
func (m gitlabAuthzProviderParams) FetchAccount(context.Context, *types.User) (mine *extsvc.Account, err error) {
panic("should never be called")
}
@ -303,12 +303,12 @@ func TestAuthzProvidersFromConfig(t *testing.T) {
bitbucketServerConnections: []*schema.BitbucketServerConnection{
{
Authorization: &schema.BitbucketServerAuthorization{
IdentityProvider: schema.BitbucketServerIdentityProvider{
IdentityProvider: &schema.BitbucketServerIdentityProvider{
Username: &schema.BitbucketServerUsernameIdentity{
Type: "username",
},
},
Oauth: schema.BitbucketServerOAuth{
Oauth: &schema.BitbucketServerOAuth{
ConsumerKey: "sourcegraph",
SigningKey: "Invalid Key",
},
@ -326,12 +326,12 @@ func TestAuthzProvidersFromConfig(t *testing.T) {
bitbucketServerConnections: []*schema.BitbucketServerConnection{
{
Authorization: &schema.BitbucketServerAuthorization{
IdentityProvider: schema.BitbucketServerIdentityProvider{
IdentityProvider: &schema.BitbucketServerIdentityProvider{
Username: &schema.BitbucketServerUsernameIdentity{
Type: "username",
},
},
Oauth: schema.BitbucketServerOAuth{
Oauth: &schema.BitbucketServerOAuth{
ConsumerKey: "sourcegraph",
SigningKey: bogusKey,
},
@ -540,12 +540,12 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) {
bitbucketServerConnections: []*schema.BitbucketServerConnection{
{
Authorization: &schema.BitbucketServerAuthorization{
IdentityProvider: schema.BitbucketServerIdentityProvider{
IdentityProvider: &schema.BitbucketServerIdentityProvider{
Username: &schema.BitbucketServerUsernameIdentity{
Type: "username",
},
},
Oauth: schema.BitbucketServerOAuth{
Oauth: &schema.BitbucketServerOAuth{
ConsumerKey: "sourcegraph",
SigningKey: bogusKey,
},
@ -1283,7 +1283,7 @@ func TestValidateExternalServiceConfig(t *testing.T) {
"authorization": {}
}
`,
assert: includes("authorization: oauth is required"),
assert: includes("authorization: Must validate one and only one schema (oneOf)"),
},
{
kind: extsvc.KindBitbucketServer,
@ -1296,8 +1296,7 @@ func TestValidateExternalServiceConfig(t *testing.T) {
}
`,
assert: includes(
"authorization.oauth: consumerKey is required",
"authorization.oauth: signingKey is required",
"authorization: Must validate one and only one schema (oneOf)",
),
},
{
@ -1324,6 +1323,7 @@ func TestValidateExternalServiceConfig(t *testing.T) {
config: `
{
"authorization": {
"identityProvider": { "type": "username" },
"oauth": {
"consumerKey": "sourcegraph",
"signingKey": "not-base-64-encoded"

View File

@ -105,7 +105,7 @@ type Provider struct {
httpClient *http.Client
}
func (p *Provider) FetchAccount(_ context.Context, _ *types.User, _ []*extsvc.Account, _ []string) (*extsvc.Account, error) {
func (p *Provider) FetchAccount(context.Context, *types.User) (*extsvc.Account, error) {
return nil, nil
}

View File

@ -83,7 +83,7 @@ func (p *Provider) ServiceID() string { return p.codeHost.ServiceID }
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) {
func (p *Provider) FetchAccount(ctx context.Context, user *types.User) (acct *extsvc.Account, err error) {
return nil, nil
}

View File

@ -5,6 +5,7 @@ go_library(
name = "bitbucketserver",
srcs = [
"authz.go",
"oauthprovider.go",
"provider.go",
],
importpath = "github.com/sourcegraph/sourcegraph/internal/authz/providers/bitbucketserver",
@ -13,10 +14,14 @@ go_library(
deps = [
"//internal/authz",
"//internal/authz/types",
"//internal/database",
"//internal/encryption",
"//internal/extsvc",
"//internal/extsvc/auth",
"//internal/extsvc/bitbucketserver",
"//internal/httpcli",
"//internal/licensing",
"//internal/oauthtoken",
"//internal/trace",
"//internal/types",
"//lib/errors",

View File

@ -1,8 +1,11 @@
package bitbucketserver
import (
"net/url"
"github.com/sourcegraph/sourcegraph/internal/authz"
atypes "github.com/sourcegraph/sourcegraph/internal/authz/types"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/licensing"
@ -21,13 +24,33 @@ import (
// desired, callers should use `(*Provider).ValidateConnection` directly to get warnings related
// to connection issues.
func NewAuthzProviders(
db database.DB,
conns []*types.BitbucketServerConnection,
authProviders []schema.AuthProviders,
) *atypes.ProviderInitResult {
initResults := &atypes.ProviderInitResult{}
oauthProviders := make(map[string]*schema.BitbucketServerAuthProvider)
for _, p := range authProviders {
if p.Bitbucketserver != nil {
var id string
bbURL, err := url.Parse(p.Bitbucketserver.Url)
if err != nil {
// error reporting for this should happen elsewhere, for now just use what is given
id = p.Bitbucketserver.Url
} else {
// use codehost normalized URL as ID
ch := extsvc.NewCodeHost(bbURL, p.Bitbucketserver.Type)
id = ch.ServiceID
}
oauthProviders[id] = p.Bitbucketserver
}
}
// Authorization (i.e., permissions) providers
for _, c := range conns {
pluginPerm := c.Plugin != nil && c.Plugin.Permissions == "enabled"
p, err := newAuthzProvider(c, pluginPerm)
p, err := newAuthzProvider(db, c, pluginPerm)
if err != nil {
initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeBitbucketServer)
initResults.Problems = append(initResults.Problems, err.Error())
@ -40,6 +63,7 @@ func NewAuthzProviders(
}
func newAuthzProvider(
db database.DB,
c *types.BitbucketServerConnection,
pluginPerm bool,
) (authz.Provider, error) {
@ -47,32 +71,34 @@ func newAuthzProvider(
return nil, nil
}
if errLicense := licensing.Check(licensing.FeatureACLs); errLicense != nil {
return nil, errLicense
if !c.Authorization.Oauth2 && c.Authorization.IdentityProvider == nil {
return nil, errors.New("authorization is set without oauth2 or identityProvider")
}
var errs error
if err := licensing.Check(licensing.FeatureACLs); err != nil {
return nil, err
}
cli, err := bitbucketserver.NewClient(c.URN, c.BitbucketServerConnection, nil)
if err != nil {
errs = errors.Append(errs, err)
return nil, errs
return nil, err
}
var p authz.Provider
switch idp := c.Authorization.IdentityProvider; {
case idp.Username != nil:
p = NewProvider(cli, c.URN, pluginPerm)
default:
errs = errors.Append(errs, errors.Errorf("No identityProvider was specified"))
if c.Authorization.Oauth2 {
return NewOAuthProvider(db, c, ProviderOptions{BitbucketServerClient: cli}, pluginPerm), nil
} else {
switch idp := c.Authorization.IdentityProvider; {
case idp.Username != nil:
return NewProvider(cli, c.URN, pluginPerm), nil
default:
return nil, errors.Errorf("No identityProvider was specified")
}
}
return p, errs
}
// ValidateAuthz validates the authorization fields of the given BitbucketServer external
// service config.
func ValidateAuthz(c *schema.BitbucketServerConnection) error {
_, err := newAuthzProvider(&types.BitbucketServerConnection{BitbucketServerConnection: c}, false)
_, err := newAuthzProvider(nil, &types.BitbucketServerConnection{BitbucketServerConnection: c}, false)
return err
}

View File

@ -0,0 +1,239 @@
package bitbucketserver
import (
"context"
"fmt"
"net/url"
"strconv"
"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/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/oauthtoken"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type OAuth2Provider struct {
urn string
codeHost *extsvc.CodeHost
client *bitbucketserver.Client
pageSize int // Page size to use in paginated requests.
db database.DB
pluginPerm bool
}
type ProviderOptions struct {
BitbucketServerClient *bitbucketserver.Client
}
var _ authz.Provider = (*OAuth2Provider)(nil)
// NewProvider returns a new Bitbucket Server authorization provider that uses
// the given bitbucket.Client to talk to the Bitbucket Server API that is
// the source of truth for permissions. Sourcegraph users will need a valid
// Bitbucket Server external account for permissions to sync correctly.
func NewOAuthProvider(db database.DB, conn *types.BitbucketServerConnection, opts ProviderOptions, pluginPerm bool) *OAuth2Provider {
baseURL, err := url.Parse(conn.Url)
if err != nil {
return nil
}
if opts.BitbucketServerClient == nil {
opts.BitbucketServerClient, err = bitbucketserver.NewClient(conn.URN, conn.BitbucketServerConnection, httpcli.ExternalClient)
if err != nil {
fmt.Printf("Error creating Bitbucket Server client: %s\n", err)
return nil
}
}
return &OAuth2Provider{
urn: conn.URN,
codeHost: extsvc.NewCodeHost(baseURL, extsvc.TypeBitbucketServer),
client: opts.BitbucketServerClient,
pageSize: 1000,
db: db,
pluginPerm: pluginPerm,
}
}
// ValidateConnection validates that the Provider has access to the Bitbucket Server API
// with the credentials it was configured with.
//
// Credentials are verified by querying the "rest/api/1.0/repositories" endpoint.
// This validates that the credentials have the `REPO_READ` scope.
func (p *OAuth2Provider) ValidateConnection(ctx context.Context) error {
// We don't care about the contents returned, only whether or not an error occurred
_, _, err := p.client.Repos(ctx, nil)
return err
}
func (p *OAuth2Provider) URN() string {
return p.urn
}
// ServiceID returns the absolute URL that identifies the Bitbucket Server instance
// this provider is configured with.
func (p *OAuth2Provider) ServiceID() string { return p.codeHost.ServiceID }
// ServiceType returns the type of this Provider, namely, "bitbucketServer".
func (p *OAuth2Provider) ServiceType() string { return p.codeHost.ServiceType }
// FetchAccount satisfies the authz.Provider interface.
func (p *OAuth2Provider) FetchAccount(ctx context.Context, user *types.User) (acct *extsvc.Account, err error) {
// OAuth2 accounts are created via user sign-in
return nil, nil
}
type accountSuspendedError struct{}
func (e accountSuspendedError) Error() string {
return "account suspended"
}
func (e accountSuspendedError) AccountSuspended() bool {
return true
}
// 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 *OAuth2Provider) 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 := bitbucketserver.GetExternalAccountData(ctx, &account.AccountData)
if err != nil {
return nil, err
}
// if tok is nil, this is most likely an OAuth1 account and should no
// longer be used.
if tok == nil {
return nil, accountSuspendedError{}
}
oauthToken := &auth.OAuthBearerToken{
Token: tok.AccessToken,
RefreshToken: tok.RefreshToken,
Expiry: tok.Expiry,
NeedsRefreshBuffer: 5,
}
oauthToken.RefreshFunc = oauthtoken.GetAccountRefreshAndStoreOAuthTokenFunc(p.db.UserExternalAccounts(), account.ID, bitbucketserver.GetOAuthContext(p.codeHost.BaseURL.String()))
client := p.client.WithAuthenticator(oauthToken)
ids, err := p.repoIDs(ctx, client)
extIDs := make([]extsvc.RepoID, 0, len(ids))
for _, id := range ids {
extIDs = append(extIDs, extsvc.RepoID(strconv.FormatUint(uint64(id), 10)))
}
return &authz.ExternalUserPermissions{
Exacts: extIDs,
}, err
}
func (p *OAuth2Provider) repoIDs(ctx context.Context, client *bitbucketserver.Client) ([]uint32, error) {
if p.pluginPerm {
return p.repoIDsFromPlugin(ctx, client)
}
return repoIDsFromAPI(ctx, p.pageSize, client)
}
func (p *OAuth2Provider) repoIDsFromPlugin(ctx context.Context, client *bitbucketserver.Client) (ids []uint32, err error) {
return client.RepoIDs(ctx, "read")
}
// repoIDsFromAPI returns all repositories for which the given user has the permission to read from
// the Bitbucket Server API. when no username is given, only public repos are returned.
func repoIDsFromAPI(ctx context.Context, pageSize int, client *bitbucketserver.Client) (ids []uint32, err error) {
t := &bitbucketserver.PageToken{Limit: pageSize}
var filters []string
filters = append(filters, "?visibility=private")
for t.HasMore() {
repos, next, err := client.Repos(ctx, t, filters...)
if err != nil {
return ids, err
}
for _, r := range repos {
ids = append(ids, uint32(r.ID))
}
t = next
}
if len(ids) == 0 {
return nil, errNoResults
}
return ids, nil
}
// 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 *OAuth2Provider) FetchRepoPerms(ctx context.Context, repo *extsvc.Repository, opts authz.FetchPermsOptions) ([]extsvc.AccountID, error) {
switch {
case repo == nil:
return nil, errors.New("no repo provided")
case !extsvc.IsHostOfRepo(p.codeHost, &repo.ExternalRepoSpec):
return nil, errors.Errorf("not a code host of the repo: want %q but have %q",
p.codeHost.ServiceID, repo.ServiceID)
}
ids, err := p.userIDs(ctx, repo.ID)
extIDs := make([]extsvc.AccountID, 0, len(ids))
for _, id := range ids {
extIDs = append(extIDs, extsvc.AccountID(strconv.FormatInt(int64(id), 10)))
}
return extIDs, err
}
func (p *OAuth2Provider) userIDs(ctx context.Context, repoID string) (ids []int, err error) {
t := &bitbucketserver.PageToken{Limit: p.pageSize}
f := bitbucketserver.UserFilter{Permission: bitbucketserver.PermissionFilter{
Root: "REPO_READ",
RepositoryID: repoID,
}}
for t.HasMore() {
users, next, err := p.client.Users(ctx, t, f)
if err != nil {
return ids, err
}
for _, u := range users {
ids = append(ids, u.ID)
}
t = next
}
return ids, nil
}

View File

@ -78,7 +78,7 @@ func (p *Provider) ServiceID() string { return p.codeHost.ServiceID }
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) {
func (p *Provider) FetchAccount(ctx context.Context, user *types.User) (acct *extsvc.Account, err error) {
if user == nil {
return nil, nil
}

View File

@ -109,7 +109,7 @@ func testProviderFetchAccount(f *fixtures, cli *bitbucketserver.Client) func(*te
tc.err = "<nil>"
}
acct, err := p.FetchAccount(tc.ctx, tc.user, nil, nil)
acct, err := p.FetchAccount(tc.ctx, tc.user)
if have, want := fmt.Sprint(err), tc.err; have != want {
t.Errorf("error:\nhave: %q\nwant: %q", have, want)

View File

@ -43,7 +43,7 @@ func NewProvider(conn *types.GerritConnection) (*Provider, error) {
// FetchAccount is unused for Gerrit. Users need to provide their own account
// credentials instead.
func (p Provider) FetchAccount(ctx context.Context, user *types.User, current []*extsvc.Account, verifiedEmails []string) (*extsvc.Account, error) {
func (p Provider) FetchAccount(context.Context, *types.User) (*extsvc.Account, error) {
return nil, nil
}

View File

@ -93,7 +93,7 @@ var _ authz.Provider = (*Provider)(nil)
// FetchAccount implements the authz.Provider interface. It always returns nil, because the GitHub
// API doesn't currently provide a way to fetch user by external SSO account.
func (p *Provider) FetchAccount(context.Context, *types.User, []*extsvc.Account, []string) (mine *extsvc.Account, err error) {
func (p *Provider) FetchAccount(context.Context, *types.User) (mine *extsvc.Account, err error) {
return nil, nil
}

View File

@ -85,7 +85,7 @@ func (p *OAuthProvider) ServiceType() string {
return p.codeHost.ServiceType
}
func (p *OAuthProvider) FetchAccount(context.Context, *types.User, []*extsvc.Account, []string) (mine *extsvc.Account, err error) {
func (p *OAuthProvider) FetchAccount(context.Context, *types.User) (mine *extsvc.Account, err error) {
return nil, nil
}

View File

@ -87,10 +87,8 @@ func (p *SudoProvider) ServiceType() string {
return p.codeHost.ServiceType
}
// FetchAccount satisfies the authz.Provider interface. It iterates through the current list of
// linked external accounts, find the one (if it exists) that matches the authn provider specified
// in the SudoProvider struct, and fetches the user account from the GitLab API using that identity.
func (p *SudoProvider) FetchAccount(ctx context.Context, user *types.User, current []*extsvc.Account, _ []string) (mine *extsvc.Account, err error) {
// FetchAccount satisfies the authz.Provider interface.
func (p *SudoProvider) FetchAccount(ctx context.Context, user *types.User) (mine *extsvc.Account, err error) {
if user == nil {
return nil, nil
}

View File

@ -29,8 +29,7 @@ func Test_GitLab_FetchAccount(t *testing.T) {
type call struct {
description string
user *types.User
current []*extsvc.Account
user *types.User
expMine *extsvc.Account
}
@ -98,7 +97,7 @@ func Test_GitLab_FetchAccount(t *testing.T) {
authzProvider := newSudoProvider(test.op, nil)
for _, c := range test.calls {
t.Run(c.description, func(t *testing.T) {
acct, err := authzProvider.FetchAccount(ctx, c.user, c.current, nil)
acct, err := authzProvider.FetchAccount(ctx, c.user)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

View File

@ -15,6 +15,7 @@ go_library(
deps = [
"//internal/authz",
"//internal/authz/types",
"//internal/database",
"//internal/extsvc",
"//internal/extsvc/perforce",
"//internal/gitserver",
@ -49,6 +50,8 @@ go_test(
"//internal/authz",
"//internal/authz/subrepoperms",
"//internal/conf",
"//internal/database",
"//internal/database/dbmocks",
"//internal/encryption/testing",
"//internal/extsvc",
"//internal/extsvc/perforce",

View File

@ -5,6 +5,7 @@ import (
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/licensing"
@ -24,10 +25,10 @@ import (
// 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(conns []*types.PerforceConnection) *atypes.ProviderInitResult {
func NewAuthzProviders(db database.DB, conns []*types.PerforceConnection) *atypes.ProviderInitResult {
initResults := &atypes.ProviderInitResult{}
for _, c := range conns {
p, err := newAuthzProvider(c.URN, c.Authorization, c.P4Port, c.P4User, c.P4Passwd, c.Depots)
p, err := newAuthzProvider(c.URN, db, c.Authorization, c.P4Port, c.P4User, c.P4Passwd, c.Depots)
if err != nil {
initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypePerforce)
initResults.Problems = append(initResults.Problems, err.Error())
@ -41,6 +42,7 @@ func NewAuthzProviders(conns []*types.PerforceConnection) *atypes.ProviderInitRe
func newAuthzProvider(
urn string,
db database.DB,
a *schema.PerforceAuthorization,
host, user, password string,
depots []string,
@ -68,7 +70,7 @@ func newAuthzProvider(
}
}
return NewProvider(logger, gitserver.NewClient("authz.perforce"), urn, host, user, password, depotIDs, a.IgnoreRulesWithHost), nil
return NewProvider(logger, db, gitserver.NewClient("authz.perforce"), urn, host, user, password, depotIDs, a.IgnoreRulesWithHost), nil
}
// ValidateAuthz validates the authorization fields of the given Perforce

View File

@ -12,6 +12,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
@ -28,6 +29,7 @@ const cacheTTL = time.Hour
// Provider implements authz.Provider for Perforce depot permissions.
type Provider struct {
logger log.Logger
db database.DB
urn string
codeHost *extsvc.CodeHost
@ -57,9 +59,10 @@ func cacheIsUpToDate(lastUpdate time.Time) bool {
// host, user and password to talk to a Perforce Server that is the source of
// truth for permissions. It assumes emails of Sourcegraph accounts match 1-1
// with emails of Perforce Server users.
func NewProvider(logger log.Logger, gitserverClient gitserver.Client, urn, host, user, password string, depots []extsvc.RepoID, ignoreRulesWithHost bool) *Provider {
func NewProvider(logger log.Logger, db database.DB, gitserverClient gitserver.Client, urn, host, user, password string, depots []extsvc.RepoID, ignoreRulesWithHost bool) *Provider {
baseURL, _ := url.Parse(host)
return &Provider{
db: db,
logger: logger,
urn: urn,
codeHost: extsvc.NewCodeHost(baseURL, extsvc.TypePerforce),
@ -76,7 +79,7 @@ func NewProvider(logger log.Logger, gitserverClient gitserver.Client, urn, host,
// FetchAccount uses given user's verified emails to match users on the Perforce
// Server. It returns when any of the verified email has matched and the match
// result is not deterministic.
func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*extsvc.Account, verifiedEmails []string) (_ *extsvc.Account, err error) {
func (p *Provider) FetchAccount(ctx context.Context, user *types.User) (_ *extsvc.Account, err error) {
if user == nil {
return nil, nil
}
@ -94,9 +97,18 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
tr.End()
}()
emailSet := make(map[string]struct{}, len(verifiedEmails))
for _, email := range verifiedEmails {
emailSet[strings.ToLower(email)] = struct{}{}
userEmails, err := p.db.UserEmails().ListByUser(ctx,
database.UserEmailsListOptions{
UserID: user.ID,
OnlyVerified: true,
})
if err != nil {
return nil, errors.Wrap(err, "could not fetch user emails")
}
emailSet := make(map[string]struct{}, len(userEmails))
for _, email := range userEmails {
emailSet[strings.ToLower(email.Email)] = struct{}{}
}
users, err := p.gitserverClient.PerforceUsers(ctx, protocol.PerforceConnectionDetails{

View File

@ -12,6 +12,8 @@ import (
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
et "github.com/sourcegraph/sourcegraph/internal/encryption/testing"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
@ -30,6 +32,10 @@ func TestProvider_FetchAccount(t *testing.T) {
Username: "alice",
}
db := dbmocks.NewMockDB()
mockUserEmails := dbmocks.NewMockUserEmailsStore()
db.UserEmailsFunc.SetDefaultReturn(mockUserEmails)
gitserverClient := gitserver.NewStrictMockClient()
gitserverClient.PerforceUsersFunc.SetDefaultReturn([]*p4types.User{
{Username: "alice", Email: "Alice@Example.com"},
@ -37,8 +43,9 @@ func TestProvider_FetchAccount(t *testing.T) {
}, nil)
t.Run("no matching account", func(t *testing.T) {
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchAccount(ctx, user, nil, []string{"bob@example.com"})
mockUserEmails.ListByUserFunc.SetDefaultReturn([]*database.UserEmail{{Email: "bob@example.com"}}, nil)
p := NewProvider(logger, db, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchAccount(ctx, user)
if err != nil {
t.Fatal(err)
}
@ -49,8 +56,9 @@ func TestProvider_FetchAccount(t *testing.T) {
})
t.Run("found matching account", func(t *testing.T) {
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchAccount(ctx, user, nil, []string{"alice@example.com"})
p := NewProvider(logger, db, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
mockUserEmails.ListByUserFunc.SetDefaultReturn([]*database.UserEmail{{Email: "alice@example.com"}}, nil)
got, err := p.FetchAccount(ctx, user)
if err != nil {
t.Fatal(err)
}
@ -82,8 +90,9 @@ func TestProvider_FetchAccount(t *testing.T) {
})
t.Run("found matching account case insensitive", func(t *testing.T) {
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchAccount(ctx, user, nil, []string{"Alice@Example.com"})
p := NewProvider(logger, db, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
mockUserEmails.ListByUserFunc.SetDefaultReturn([]*database.UserEmail{{Email: "Alice@example.com"}}, nil)
got, err := p.FetchAccount(ctx, user)
if err != nil {
t.Fatal(err)
}
@ -118,9 +127,11 @@ func TestProvider_FetchAccount(t *testing.T) {
func TestProvider_FetchUserPerms(t *testing.T) {
ctx := context.Background()
db := dbmocks.NewMockDB()
t.Run("nil account", func(t *testing.T) {
logger := logtest.Scoped(t)
p := NewProvider(logger, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
_, err := p.FetchUserPerms(ctx, nil, authz.FetchPermsOptions{})
want := "no account provided"
got := fmt.Sprintf("%v", err)
@ -131,7 +142,7 @@ func TestProvider_FetchUserPerms(t *testing.T) {
t.Run("not the code host of the account", func(t *testing.T) {
logger := logtest.Scoped(t)
p := NewProvider(logger, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
_, err := p.FetchUserPerms(context.Background(),
&extsvc.Account{
AccountSpec: extsvc.AccountSpec{
@ -150,7 +161,7 @@ func TestProvider_FetchUserPerms(t *testing.T) {
t.Run("no user found in account data", func(t *testing.T) {
logger := logtest.Scoped(t)
p := NewProvider(logger, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
_, err := p.FetchUserPerms(ctx,
&extsvc.Account{
AccountSpec: extsvc.AccountSpec{
@ -300,7 +311,7 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
gc := gitserver.NewStrictMockClient()
gc.PerforceProtectsForUserFunc.SetDefaultReturn(test.protects, nil)
p := NewProvider(logger, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchUserPerms(ctx,
&extsvc.Account{
AccountSpec: extsvc.AccountSpec{
@ -376,7 +387,7 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
gitserverClient.PerforceProtectsForDepotFunc.SetDefaultReturn(test.input, nil)
gitserverClient.PerforceProtectsForUserFunc.SetDefaultReturn(test.input, nil)
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = append(p.depots, "//Sourcegraph/")
got, err := p.FetchUserPerms(ctx,
@ -406,9 +417,10 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
func TestProvider_FetchRepoPerms(t *testing.T) {
logger := logtest.Scoped(t)
ctx := context.Background()
db := dbmocks.NewMockDB()
t.Run("nil repository", func(t *testing.T) {
p := NewProvider(logger, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
_, err := p.FetchRepoPerms(ctx, nil, authz.FetchPermsOptions{})
want := "no repository provided"
got := fmt.Sprintf("%v", err)
@ -418,7 +430,7 @@ func TestProvider_FetchRepoPerms(t *testing.T) {
})
t.Run("not the code host of the repository", func(t *testing.T) {
p := NewProvider(logger, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserver.NewTestClient(t), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
_, err := p.FetchRepoPerms(ctx,
&extsvc.Repository{
URI: "gitlab.com/user/repo",
@ -464,7 +476,7 @@ func TestProvider_FetchRepoPerms(t *testing.T) {
{Level: "list", EntityType: "user", EntityName: "david", Host: "*", Match: "//Sourcegraph/..."}, // "list" can't grant read access
}, nil)
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p := NewProvider(logger, db, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchRepoPerms(ctx,
&extsvc.Repository{
URI: "gitlab.com/user/repo",

View File

@ -18,6 +18,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/authz"
srp "github.com/sourcegraph/sourcegraph/internal/authz/subrepoperms"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
@ -231,7 +232,9 @@ func TestScanFullRepoPermissions(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
db := dbmocks.NewMockDB()
p := NewProvider(logger, db, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/main/",
"//depot/training/",
@ -312,7 +315,9 @@ func TestScanIPPermissions(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
db := dbmocks.NewMockDB()
p := NewProvider(logger, db, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/src/",
"//depot/project1/",
@ -404,7 +409,9 @@ func TestScanFullRepoPermissionsWithWildcardMatchingDepot(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
db := dbmocks.NewMockDB()
p := NewProvider(logger, db, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/main/base/",
}
@ -726,7 +733,9 @@ read group Dev1 * //depot/main/.../*.go
}
})
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
db := dbmocks.NewMockDB()
p := NewProvider(logger, db, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
extsvc.RepoID(tc.depot),
}
@ -787,7 +796,9 @@ func TestFullScanWildcardDepotMatching(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
db := dbmocks.NewMockDB()
p := NewProvider(logger, db, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/654/deploy/base/",
}
@ -934,7 +945,10 @@ func TestScanAllUsers(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
gc := gitserver.NewStrictMockClient()
gc.PerforceGroupMembersFunc.SetDefaultReturn(nil, nil)
p := NewProvider(logger, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
db := dbmocks.NewMockDB()
p := NewProvider(logger, db, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.cachedGroupMembers = map[string][]string{
"dev": {"user1", "user2"},
}

View File

@ -70,6 +70,9 @@ const (
SecurityEventBitbucketCloudAuthSucceeded SecurityEventName = "BitbucketCloudAuthSucceeded"
SecurityEventBitbucketCloudAuthFailed SecurityEventName = "BitbucketCloudAuthFailed"
SecurityEventBitbucketServerAuthSucceeded SecurityEventName = "BitbucketServerAuthSucceeded"
SecurityEventBitbucketServerAuthFailed SecurityEventName = "BitbucketServerAuthFailed"
SecurityEventAzureDevOpsAuthSucceeded SecurityEventName = "AzureDevOpsAuthSucceeded"
SecurityEventAzureDevOpsAuthFailed SecurityEventName = "AzureDevOpsAuthFailed"

View File

@ -6,19 +6,25 @@ go_library(
srcs = [
"auth.go",
"client.go",
"common.go",
"events.go",
"testing.go",
"user.go",
],
importpath = "github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketserver",
tags = [TAG_PLATFORM_SOURCE],
visibility = ["//:__subpackages__"],
deps = [
"//internal/conf",
"//internal/encryption",
"//internal/errcode",
"//internal/extsvc",
"//internal/extsvc/auth",
"//internal/httpcli",
"//internal/httptestutil",
"//internal/lazyregexp",
"//internal/metrics",
"//internal/oauthutil",
"//internal/ratelimit",
"//internal/trace",
"//lib/errors",
@ -29,6 +35,7 @@ go_library(
"@com_github_roaringbitmap_roaring//:roaring",
"@com_github_segmentio_fasthash//fnv1",
"@com_github_sourcegraph_log//:log",
"@org_golang_x_oauth2//:oauth2",
"@org_golang_x_time//rate",
],
)

View File

@ -69,7 +69,7 @@ func NewClient(urn string, config *schema.BitbucketServerConnection, httpClient
return nil, err
}
if config.Authorization == nil {
if config.Authorization == nil || config.Authorization.Oauth2 {
if config.Token != "" {
client.Auth = &auth.OAuthBearerToken{Token: config.Token}
} else {
@ -78,7 +78,7 @@ func NewClient(urn string, config *schema.BitbucketServerConnection, httpClient
Password: config.Password,
}
}
} else {
} else if config.Authorization.Oauth != nil {
err := client.SetOAuth(
config.Authorization.Oauth.ConsumerKey,
config.Authorization.Oauth.SigningKey,
@ -582,7 +582,6 @@ func (c *Client) CreatePullRequest(ctx context.Context, pr *PullRequest) error {
)
resp, err := c.send(ctx, "POST", path, nil, payload, pr)
if err != nil {
var code int
if resp != nil {
@ -946,7 +945,6 @@ func (c *Client) page(ctx context.Context, path string, qry url.Values, token *P
PageToken: &next,
Values: results,
})
if err != nil {
return nil, err
}

View File

@ -1150,7 +1150,7 @@ func TestAuth(t *testing.T) {
if _, err := NewClient("urn", &schema.BitbucketServerConnection{
Url: "http://example.com/",
Authorization: &schema.BitbucketServerAuthorization{
Oauth: schema.BitbucketServerOAuth{
Oauth: &schema.BitbucketServerOAuth{
ConsumerKey: "foo",
SigningKey: "this is an invalid key",
},
@ -1174,7 +1174,7 @@ func TestAuth(t *testing.T) {
client, err := NewClient("urn", &schema.BitbucketServerConnection{
Url: "http://example.com/",
Authorization: &schema.BitbucketServerAuthorization{
Oauth: schema.BitbucketServerOAuth{
Oauth: &schema.BitbucketServerOAuth{
ConsumerKey: "foo",
SigningKey: signingKey,
},

View File

@ -0,0 +1,51 @@
package bitbucketserver
import (
"net/url"
"strings"
"golang.org/x/oauth2"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/oauthutil"
)
var MockGetOAuthContext func() *oauthutil.OAuthContext
func GetOAuthContext(baseURL string) *oauthutil.OAuthContext {
if MockGetOAuthContext != nil {
return MockGetOAuthContext()
}
for _, authProvider := range conf.SiteConfig().AuthProviders {
if authProvider.Bitbucketserver != nil {
p := authProvider.Bitbucketserver
rawURL := p.Url
rawURL = strings.TrimSuffix(rawURL, "/")
if !strings.HasPrefix(baseURL, rawURL) {
continue
}
authURL, err := url.JoinPath(rawURL, "/rest/oauth2/latest/authorize")
if err != nil {
continue
}
tokenURL, err := url.JoinPath(rawURL, "/rest/oauth2/latest/token")
if err != nil {
continue
}
scopes := []string{"REPO_READ"}
return &oauthutil.OAuthContext{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Scopes: scopes,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
}
}
}
return nil
}

View File

@ -0,0 +1,66 @@
package bitbucketserver
import (
"context"
"encoding/json"
"net/url"
"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 *User, tok *oauth2.Token, err error) {
if data.Data != nil {
usr, err = encryption.DecryptJSON[User](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
}
func GetPublicExternalAccountData(ctx context.Context, accountData *extsvc.AccountData, serverURL string) (*extsvc.PublicAccountData, error) {
data, _, err := GetExternalAccountData(ctx, accountData)
if err != nil {
return nil, err
}
url, err := url.JoinPath(serverURL, "profile")
if err != nil {
url = serverURL
}
return &extsvc.PublicAccountData{
DisplayName: data.DisplayName,
Login: data.Name,
URL: url,
}, nil
}
// SetExternalAccountData sets the user and token into the external account data blob.
func SetExternalAccountData(data *extsvc.AccountData, user *User, 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
}

View File

@ -209,8 +209,18 @@
"description": "If non-null, enforces Bitbucket Server / Bitbucket Data Center repository permissions.",
"type": "object",
"additionalProperties": false,
"required": ["identityProvider", "oauth"],
"oneOf": [
{
"required": ["identityProvider", "oauth"]
},
{
"required": ["oauth2"]
}
],
"properties": {
"oauth2": {
"type": "boolean"
},
"identityProvider": {
"description": "The source of identity to use when computing permissions. This defines how to compute the Bitbucket Server / Bitbucket Data Center identity to use for a given Sourcegraph user. When 'username' is used, Sourcegraph assumes usernames are identical in Sourcegraph and Bitbucket Server / Bitbucket Data Center accounts and `auth.enableUsernameChanges` must be set to false for security reasons.",
"title": "BitbucketServerIdentityProvider",

View File

@ -158,15 +158,16 @@ type AuthProviderCommon struct {
Order int `json:"order,omitempty"`
}
type AuthProviders struct {
AzureDevOps *AzureDevOpsAuthProvider
Bitbucketcloud *BitbucketCloudAuthProvider
Builtin *BuiltinAuthProvider
Gerrit *GerritAuthProvider
Github *GitHubAuthProvider
Gitlab *GitLabAuthProvider
HttpHeader *HTTPHeaderAuthProvider
Openidconnect *OpenIDConnectAuthProvider
Saml *SAMLAuthProvider
AzureDevOps *AzureDevOpsAuthProvider
Bitbucketcloud *BitbucketCloudAuthProvider
Bitbucketserver *BitbucketServerAuthProvider
Builtin *BuiltinAuthProvider
Gerrit *GerritAuthProvider
Github *GitHubAuthProvider
Gitlab *GitLabAuthProvider
HttpHeader *HTTPHeaderAuthProvider
Openidconnect *OpenIDConnectAuthProvider
Saml *SAMLAuthProvider
}
func (v AuthProviders) MarshalJSON() ([]byte, error) {
@ -176,6 +177,9 @@ func (v AuthProviders) MarshalJSON() ([]byte, error) {
if v.Bitbucketcloud != nil {
return json.Marshal(v.Bitbucketcloud)
}
if v.Bitbucketserver != nil {
return json.Marshal(v.Bitbucketserver)
}
if v.Builtin != nil {
return json.Marshal(v.Builtin)
}
@ -211,6 +215,8 @@ func (v *AuthProviders) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &v.AzureDevOps)
case "bitbucketcloud":
return json.Unmarshal(data, &v.Bitbucketcloud)
case "bitbucketserver":
return json.Unmarshal(data, &v.Bitbucketserver)
case "builtin":
return json.Unmarshal(data, &v.Builtin)
case "gerrit":
@ -226,7 +232,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{"azureDevOps", "bitbucketcloud", "builtin", "gerrit", "github", "gitlab", "http-header", "openidconnect", "saml"})
return fmt.Errorf("tagged union type must have a %q property whose value is one of %s", "type", []string{"azureDevOps", "bitbucketcloud", "bitbucketserver", "builtin", "gerrit", "github", "gitlab", "http-header", "openidconnect", "saml"})
}
// AzureDevOpsAuthProvider description: Azure auth provider for dev.azure.com
@ -405,12 +411,31 @@ type BitbucketCloudRateLimit struct {
RequestsPerHour float64 `json:"requestsPerHour"`
}
// BitbucketServerAuthProvider description: Configures the Bitbucket Server OAuth authentication provider for SSO. In addition to specifying this configuration object, you must also create a OAuth App on your Bitbucket Server instance: https://confluence.atlassian.com/bitbucketserver0720/configure-an-incoming-link-1116282013.html. The application should have the repository read permission and the callback URL set to the concatenation of your Sourcegraph instance URL and "/.auth/bitbucketserver/callback".
type BitbucketServerAuthProvider struct {
// AllowSignup description: Allows new visitors to sign up for accounts via Bitbucket Server OAuth. If false, users signing in via Bitbucket Server must have an existing Sourcegraph account, which will be linked to their Bitbucket Server identity after sign-in.
AllowSignup bool `json:"allowSignup,omitempty"`
// ClientID description: The ID of the Bitbucket OAuth app.
ClientID string `json:"clientID"`
// ClientSecret description: The Client Secret of the Bitbucket OAuth app.
ClientSecret string `json:"clientSecret"`
DisplayName string `json:"displayName,omitempty"`
DisplayPrefix *string `json:"displayPrefix,omitempty"`
Hidden bool `json:"hidden,omitempty"`
NoSignIn bool `json:"noSignIn,omitempty"`
Order int `json:"order,omitempty"`
Type string `json:"type"`
// Url description: URL of the Bitbucket Server instance.
Url string `json:"url,omitempty"`
}
// BitbucketServerAuthorization description: If non-null, enforces Bitbucket Server / Bitbucket Data Center repository permissions.
type BitbucketServerAuthorization struct {
// IdentityProvider description: The source of identity to use when computing permissions. This defines how to compute the Bitbucket Server / Bitbucket Data Center identity to use for a given Sourcegraph user. When 'username' is used, Sourcegraph assumes usernames are identical in Sourcegraph and Bitbucket Server / Bitbucket Data Center accounts and `auth.enableUsernameChanges` must be set to false for security reasons.
IdentityProvider BitbucketServerIdentityProvider `json:"identityProvider"`
IdentityProvider *BitbucketServerIdentityProvider `json:"identityProvider,omitempty"`
// Oauth description: OAuth configuration specified when creating the Bitbucket Server / Bitbucket Data Center Application Link with incoming authentication. Two Legged OAuth with 'ExecuteAs=admin' must be enabled as well as user impersonation.
Oauth BitbucketServerOAuth `json:"oauth"`
Oauth *BitbucketServerOAuth `json:"oauth,omitempty"`
Oauth2 bool `json:"oauth2,omitempty"`
}
// BitbucketServerConnection description: Configuration for a connection to Bitbucket Server / Bitbucket Data Center.

View File

@ -2424,6 +2424,7 @@
"enum": [
"azureDevOps",
"bitbucketcloud",
"bitbucketserver",
"builtin",
"gerrit",
"github",
@ -2441,6 +2442,9 @@
{
"$ref": "#/definitions/BitbucketCloudAuthProvider"
},
{
"$ref": "#/definitions/BitbucketServerAuthProvider"
},
{
"$ref": "#/definitions/BuiltinAuthProvider"
},
@ -4399,6 +4403,54 @@
}
}
},
"BitbucketServerAuthProvider": {
"description": "Configures the Bitbucket Server OAuth authentication provider for SSO. In addition to specifying this configuration object, you must also create a OAuth App on your Bitbucket Server instance: https://confluence.atlassian.com/bitbucketserver0720/configure-an-incoming-link-1116282013.html. The application should have the repository read permission and the callback URL set to the concatenation of your Sourcegraph instance URL and \"/.auth/bitbucketserver/callback\".",
"type": "object",
"additionalProperties": false,
"required": ["type", "clientID", "clientSecret"],
"properties": {
"type": {
"type": "string",
"const": "bitbucketserver"
},
"url": {
"type": "string",
"description": "URL of the Bitbucket Server instance.",
"example": "https://bitbucket.example.org/"
},
"clientID": {
"type": "string",
"description": "The ID of the Bitbucket OAuth app."
},
"clientSecret": {
"type": "string",
"description": "The Client Secret of the Bitbucket OAuth app."
},
"displayName": {
"$ref": "#/definitions/AuthProviderCommon/properties/displayName"
},
"displayPrefix": {
"$ref": "#/definitions/AuthProviderCommon/properties/displayPrefix",
"!go": {
"pointer": true
}
},
"hidden": {
"$ref": "#/definitions/AuthProviderCommon/properties/hidden"
},
"noSignIn": {
"$ref": "#/definitions/AuthProviderCommon/properties/noSignIn"
},
"order": {
"$ref": "#/definitions/AuthProviderCommon/properties/order"
},
"allowSignup": {
"description": "Allows new visitors to sign up for accounts via Bitbucket Server OAuth. If false, users signing in via Bitbucket Server must have an existing Sourcegraph account, which will be linked to their Bitbucket Server identity after sign-in.",
"default": true,
"type": "boolean"
}
}
},
"GerritAuthProvider": {
"description": "Gerrit auth provider",
"type": "object",