Skip to content

Commit aded7b1

Browse files
authored
feat: implement bitbucket-server external auth defaults (#10520)
* feat: implement bitbucket-server external auth defaults Bitbucket cloud != Bitbucket server Add reasonable defaults for server * change "bitbucket" to "bitbucket-cloud"
1 parent 71153e2 commit aded7b1

File tree

5 files changed

+164
-10
lines changed

5 files changed

+164
-10
lines changed

coderd/externalauth/externalauth.go

+69-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"regexp"
12+
"strings"
1213
"time"
1314

1415
"golang.org/x/oauth2"
@@ -494,7 +495,36 @@ func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([
494495

495496
// applyDefaultsToConfig applies defaults to the config entry.
496497
func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
497-
defaults := defaults[codersdk.EnhancedExternalAuthProvider(config.Type)]
498+
configType := codersdk.EnhancedExternalAuthProvider(config.Type)
499+
if configType == "bitbucket" {
500+
// For backwards compatibility, we need to support the "bitbucket" string.
501+
configType = codersdk.EnhancedExternalAuthProviderBitBucketCloud
502+
defer func() {
503+
// The config type determines the config ID (if unset). So change the legacy
504+
// type to the correct new type after the defaults have been configured.
505+
config.Type = string(codersdk.EnhancedExternalAuthProviderBitBucketCloud)
506+
}()
507+
}
508+
// If static defaults exist, apply them.
509+
if defaults, ok := staticDefaults[configType]; ok {
510+
copyDefaultSettings(config, defaults)
511+
return
512+
}
513+
514+
// Dynamic defaults
515+
switch codersdk.EnhancedExternalAuthProvider(config.Type) {
516+
case codersdk.EnhancedExternalAuthProviderBitBucketServer:
517+
copyDefaultSettings(config, bitbucketServerDefaults(config))
518+
return
519+
default:
520+
// No defaults for this type. We still want to run this apply with
521+
// an empty set of defaults.
522+
copyDefaultSettings(config, codersdk.ExternalAuthConfig{})
523+
return
524+
}
525+
}
526+
527+
func copyDefaultSettings(config *codersdk.ExternalAuthConfig, defaults codersdk.ExternalAuthConfig) {
498528
if config.AuthURL == "" {
499529
config.AuthURL = defaults.AuthURL
500530
}
@@ -542,7 +572,43 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
542572
}
543573
}
544574

545-
var defaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{
575+
func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
576+
defaults := codersdk.ExternalAuthConfig{
577+
DisplayName: "Bitbucket Server",
578+
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
579+
DisplayIcon: "/icon/bitbucket.svg",
580+
}
581+
// Bitbucket servers will have some base url, e.g. https://bitbucket.coder.com.
582+
// We will grab this from the Auth URL. This choice is a bit arbitrary,
583+
// but we need to require at least 1 field to be populated.
584+
if config.AuthURL == "" {
585+
// No auth url, means we cannot guess the urls.
586+
return defaults
587+
}
588+
589+
auth, err := url.Parse(config.AuthURL)
590+
if err != nil {
591+
// We need a valid URL to continue with.
592+
return defaults
593+
}
594+
595+
// Populate Regex, ValidateURL, and TokenURL.
596+
// Default regex should be anything using the same host as the auth url.
597+
defaults.Regex = fmt.Sprintf(`^(https?://)?%s(/.*)?$`, strings.ReplaceAll(auth.Host, ".", `\.`))
598+
599+
tokenURL := auth.ResolveReference(&url.URL{Path: "/rest/oauth2/latest/token"})
600+
defaults.TokenURL = tokenURL.String()
601+
602+
// validate needs to return a 200 when logged in and a 401 when unauthenticated.
603+
// This endpoint returns the count of the number of PR's in the authenticated
604+
// user's inbox. Which will work perfectly for our use case.
605+
validate := auth.ResolveReference(&url.URL{Path: "/rest/api/latest/inbox/pull-requests/count"})
606+
defaults.ValidateURL = validate.String()
607+
608+
return defaults
609+
}
610+
611+
var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{
546612
codersdk.EnhancedExternalAuthProviderAzureDevops: {
547613
AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize",
548614
TokenURL: "https://app.vssps.visualstudio.com/oauth2/token",
@@ -551,7 +617,7 @@ var defaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthCo
551617
Regex: `^(https?://)?dev\.azure\.com(/.*)?$`,
552618
Scopes: []string{"vso.code_write"},
553619
},
554-
codersdk.EnhancedExternalAuthProviderBitBucket: {
620+
codersdk.EnhancedExternalAuthProviderBitBucketCloud: {
555621
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
556622
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
557623
ValidateURL: "https://api.bitbucket.org/2.0/user",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package externalauth
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/v2/codersdk"
9+
)
10+
11+
func Test_bitbucketServerConfigDefaults(t *testing.T) {
12+
t.Parallel()
13+
14+
bbType := string(codersdk.EnhancedExternalAuthProviderBitBucketServer)
15+
tests := []struct {
16+
name string
17+
config *codersdk.ExternalAuthConfig
18+
expected codersdk.ExternalAuthConfig
19+
}{
20+
{
21+
// Very few fields are statically defined for Bitbucket Server.
22+
name: "EmptyBitbucketServer",
23+
config: &codersdk.ExternalAuthConfig{
24+
Type: bbType,
25+
},
26+
expected: codersdk.ExternalAuthConfig{
27+
Type: bbType,
28+
ID: bbType,
29+
DisplayName: "Bitbucket Server",
30+
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
31+
DisplayIcon: "/icon/bitbucket.svg",
32+
},
33+
},
34+
{
35+
// Only the AuthURL is required for defaults to work.
36+
name: "AuthURL",
37+
config: &codersdk.ExternalAuthConfig{
38+
Type: bbType,
39+
AuthURL: "https://bitbucket.example.com/login/oauth/authorize",
40+
},
41+
expected: codersdk.ExternalAuthConfig{
42+
Type: bbType,
43+
ID: bbType,
44+
AuthURL: "https://bitbucket.example.com/login/oauth/authorize",
45+
TokenURL: "https://bitbucket.example.com/rest/oauth2/latest/token",
46+
ValidateURL: "https://bitbucket.example.com/rest/api/latest/inbox/pull-requests/count",
47+
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
48+
Regex: `^(https?://)?bitbucket\.example\.com(/.*)?$`,
49+
DisplayName: "Bitbucket Server",
50+
DisplayIcon: "/icon/bitbucket.svg",
51+
},
52+
},
53+
{
54+
// Ensure backwards compatibility. The type should update to "bitbucket-cloud",
55+
// but the ID and other fields should remain the same.
56+
name: "BitbucketLegacy",
57+
config: &codersdk.ExternalAuthConfig{
58+
Type: "bitbucket",
59+
},
60+
expected: codersdk.ExternalAuthConfig{
61+
Type: string(codersdk.EnhancedExternalAuthProviderBitBucketCloud),
62+
ID: "bitbucket", // Legacy ID remains unchanged
63+
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
64+
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
65+
ValidateURL: "https://api.bitbucket.org/2.0/user",
66+
DisplayName: "BitBucket",
67+
DisplayIcon: "/icon/bitbucket.svg",
68+
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
69+
Scopes: []string{"account", "repository:write"},
70+
},
71+
},
72+
}
73+
for _, tt := range tests {
74+
tt := tt
75+
t.Run(tt.name, func(t *testing.T) {
76+
t.Parallel()
77+
applyDefaultsToConfig(tt.config)
78+
require.Equal(t, tt.expected, *tt.config)
79+
})
80+
}
81+
}

coderd/workspaceagents.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -2452,7 +2452,8 @@ func createExternalAuthResponse(typ, token string, extra pqtype.NullRawMessage)
24522452
Username: "oauth2",
24532453
Password: token,
24542454
}
2455-
case string(codersdk.EnhancedExternalAuthProviderBitBucket):
2455+
case string(codersdk.EnhancedExternalAuthProviderBitBucketCloud), string(codersdk.EnhancedExternalAuthProviderBitBucketServer):
2456+
// The string "bitbucket" was a legacy parameter that needs to still be supported.
24562457
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
24572458
resp = agentsdk.ExternalAuthResponse{
24582459
Username: "x-token-auth",

codersdk/externalauth.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ func (e EnhancedExternalAuthProvider) Git() bool {
2121
switch e {
2222
case EnhancedExternalAuthProviderGitHub,
2323
EnhancedExternalAuthProviderGitLab,
24-
EnhancedExternalAuthProviderBitBucket,
24+
EnhancedExternalAuthProviderBitBucketCloud,
25+
EnhancedExternalAuthProviderBitBucketServer,
2526
EnhancedExternalAuthProviderAzureDevops:
2627
return true
2728
default:
@@ -33,9 +34,12 @@ const (
3334
EnhancedExternalAuthProviderAzureDevops EnhancedExternalAuthProvider = "azure-devops"
3435
EnhancedExternalAuthProviderGitHub EnhancedExternalAuthProvider = "github"
3536
EnhancedExternalAuthProviderGitLab EnhancedExternalAuthProvider = "gitlab"
36-
EnhancedExternalAuthProviderBitBucket EnhancedExternalAuthProvider = "bitbucket"
37-
EnhancedExternalAuthProviderSlack EnhancedExternalAuthProvider = "slack"
38-
EnhancedExternalAuthProviderJFrog EnhancedExternalAuthProvider = "jfrog"
37+
// EnhancedExternalAuthProviderBitBucketCloud is the Bitbucket Cloud provider.
38+
// Not to be confused with the self-hosted 'EnhancedExternalAuthProviderBitBucketServer'
39+
EnhancedExternalAuthProviderBitBucketCloud EnhancedExternalAuthProvider = "bitbucket-cloud"
40+
EnhancedExternalAuthProviderBitBucketServer EnhancedExternalAuthProvider = "bitbucket-server"
41+
EnhancedExternalAuthProviderSlack EnhancedExternalAuthProvider = "slack"
42+
EnhancedExternalAuthProviderJFrog EnhancedExternalAuthProvider = "jfrog"
3943
)
4044

4145
type ExternalAuth struct {

site/src/api/typesGenerated.ts

+4-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)