mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 16:51:55 +00:00
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:
parent
56e1f11f25
commit
d3a3d721d3
@ -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} />
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -23,6 +23,7 @@ export interface AuthProvider {
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'bitbucketCloud'
|
||||
| 'bitbucketServer'
|
||||
| 'http-header'
|
||||
| 'openidconnect'
|
||||
| 'sourcegraph-operator'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -73,4 +73,5 @@ export const V2AuthProviderTypes: { [k in AuthProvider['serviceType']]: number }
|
||||
builtin: 7,
|
||||
gerrit: 8,
|
||||
azuredevops: 9,
|
||||
bitbucketServer: 10,
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
71
cmd/frontend/internal/auth/bitbucketserveroauth/BUILD.bazel
Normal file
71
cmd/frontend/internal/auth/bitbucketserveroauth/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
||||
77
cmd/frontend/internal/auth/bitbucketserveroauth/config.go
Normal file
77
cmd/frontend/internal/auth/bitbucketserveroauth/config.go
Normal 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
|
||||
}
|
||||
224
cmd/frontend/internal/auth/bitbucketserveroauth/config_test.go
Normal file
224
cmd/frontend/internal/auth/bitbucketserveroauth/config_test.go
Normal 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}
|
||||
}
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
106
cmd/frontend/internal/auth/bitbucketserveroauth/provider.go
Normal file
106
cmd/frontend/internal/auth/bitbucketserveroauth/provider.go
Normal 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)
|
||||
}
|
||||
126
cmd/frontend/internal/auth/bitbucketserveroauth/session.go
Normal file
126
cmd/frontend/internal/auth/bitbucketserveroauth/session.go
Normal 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(),
|
||||
}
|
||||
}
|
||||
175
cmd/frontend/internal/auth/bitbucketserveroauth/session_test.go
Normal file
175
cmd/frontend/internal/auth/bitbucketserveroauth/session_test.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
//
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -64,3 +64,7 @@ func (p *provider) Config() schema.AuthProviders {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) Type() providers.ProviderType {
|
||||
return providers.ProviderTypeOpenIDConnect
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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})
|
||||
})
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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(),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
239
internal/authz/providers/bitbucketserver/oauthprovider.go
Normal file
239
internal/authz/providers/bitbucketserver/oauthprovider.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"},
|
||||
}
|
||||
|
||||
@ -70,6 +70,9 @@ const (
|
||||
SecurityEventBitbucketCloudAuthSucceeded SecurityEventName = "BitbucketCloudAuthSucceeded"
|
||||
SecurityEventBitbucketCloudAuthFailed SecurityEventName = "BitbucketCloudAuthFailed"
|
||||
|
||||
SecurityEventBitbucketServerAuthSucceeded SecurityEventName = "BitbucketServerAuthSucceeded"
|
||||
SecurityEventBitbucketServerAuthFailed SecurityEventName = "BitbucketServerAuthFailed"
|
||||
|
||||
SecurityEventAzureDevOpsAuthSucceeded SecurityEventName = "AzureDevOpsAuthSucceeded"
|
||||
SecurityEventAzureDevOpsAuthFailed SecurityEventName = "AzureDevOpsAuthFailed"
|
||||
|
||||
|
||||
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
51
internal/extsvc/bitbucketserver/common.go
Normal file
51
internal/extsvc/bitbucketserver/common.go
Normal 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
|
||||
}
|
||||
66
internal/extsvc/bitbucketserver/user.go
Normal file
66
internal/extsvc/bitbucketserver/user.go
Normal 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
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user