diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 04d8dae9bc99f..6eb30c835b31b 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "time" "golang.org/x/oauth2" @@ -494,7 +495,36 @@ func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([ // applyDefaultsToConfig applies defaults to the config entry. func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) { - defaults := defaults[codersdk.EnhancedExternalAuthProvider(config.Type)] + configType := codersdk.EnhancedExternalAuthProvider(config.Type) + if configType == "bitbucket" { + // For backwards compatibility, we need to support the "bitbucket" string. + configType = codersdk.EnhancedExternalAuthProviderBitBucketCloud + defer func() { + // The config type determines the config ID (if unset). So change the legacy + // type to the correct new type after the defaults have been configured. + config.Type = string(codersdk.EnhancedExternalAuthProviderBitBucketCloud) + }() + } + // If static defaults exist, apply them. + if defaults, ok := staticDefaults[configType]; ok { + copyDefaultSettings(config, defaults) + return + } + + // Dynamic defaults + switch codersdk.EnhancedExternalAuthProvider(config.Type) { + case codersdk.EnhancedExternalAuthProviderBitBucketServer: + copyDefaultSettings(config, bitbucketServerDefaults(config)) + return + default: + // No defaults for this type. We still want to run this apply with + // an empty set of defaults. + copyDefaultSettings(config, codersdk.ExternalAuthConfig{}) + return + } +} + +func copyDefaultSettings(config *codersdk.ExternalAuthConfig, defaults codersdk.ExternalAuthConfig) { if config.AuthURL == "" { config.AuthURL = defaults.AuthURL } @@ -542,7 +572,43 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) { } } -var defaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{ +func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig { + defaults := codersdk.ExternalAuthConfig{ + DisplayName: "Bitbucket Server", + Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"}, + DisplayIcon: "/icon/bitbucket.svg", + } + // Bitbucket servers will have some base url, e.g. https://bitbucket.coder.com. + // We will grab this from the Auth URL. This choice is a bit arbitrary, + // but we need to require at least 1 field to be populated. + if config.AuthURL == "" { + // No auth url, means we cannot guess the urls. + return defaults + } + + auth, err := url.Parse(config.AuthURL) + if err != nil { + // We need a valid URL to continue with. + return defaults + } + + // Populate Regex, ValidateURL, and TokenURL. + // Default regex should be anything using the same host as the auth url. + defaults.Regex = fmt.Sprintf(`^(https?://)?%s(/.*)?$`, strings.ReplaceAll(auth.Host, ".", `\.`)) + + tokenURL := auth.ResolveReference(&url.URL{Path: "/rest/oauth2/latest/token"}) + defaults.TokenURL = tokenURL.String() + + // validate needs to return a 200 when logged in and a 401 when unauthenticated. + // This endpoint returns the count of the number of PR's in the authenticated + // user's inbox. Which will work perfectly for our use case. + validate := auth.ResolveReference(&url.URL{Path: "/rest/api/latest/inbox/pull-requests/count"}) + defaults.ValidateURL = validate.String() + + return defaults +} + +var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{ codersdk.EnhancedExternalAuthProviderAzureDevops: { AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize", TokenURL: "https://app.vssps.visualstudio.com/oauth2/token", @@ -551,7 +617,7 @@ var defaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthCo Regex: `^(https?://)?dev\.azure\.com(/.*)?$`, Scopes: []string{"vso.code_write"}, }, - codersdk.EnhancedExternalAuthProviderBitBucket: { + codersdk.EnhancedExternalAuthProviderBitBucketCloud: { AuthURL: "https://bitbucket.org/site/oauth2/authorize", TokenURL: "https://bitbucket.org/site/oauth2/access_token", ValidateURL: "https://api.bitbucket.org/2.0/user", diff --git a/coderd/externalauth/externalauth_internal_test.go b/coderd/externalauth/externalauth_internal_test.go new file mode 100644 index 0000000000000..5bb14a4c8f697 --- /dev/null +++ b/coderd/externalauth/externalauth_internal_test.go @@ -0,0 +1,81 @@ +package externalauth + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +func Test_bitbucketServerConfigDefaults(t *testing.T) { + t.Parallel() + + bbType := string(codersdk.EnhancedExternalAuthProviderBitBucketServer) + tests := []struct { + name string + config *codersdk.ExternalAuthConfig + expected codersdk.ExternalAuthConfig + }{ + { + // Very few fields are statically defined for Bitbucket Server. + name: "EmptyBitbucketServer", + config: &codersdk.ExternalAuthConfig{ + Type: bbType, + }, + expected: codersdk.ExternalAuthConfig{ + Type: bbType, + ID: bbType, + DisplayName: "Bitbucket Server", + Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"}, + DisplayIcon: "/icon/bitbucket.svg", + }, + }, + { + // Only the AuthURL is required for defaults to work. + name: "AuthURL", + config: &codersdk.ExternalAuthConfig{ + Type: bbType, + AuthURL: "https://bitbucket.example.com/login/oauth/authorize", + }, + expected: codersdk.ExternalAuthConfig{ + Type: bbType, + ID: bbType, + AuthURL: "https://bitbucket.example.com/login/oauth/authorize", + TokenURL: "https://bitbucket.example.com/rest/oauth2/latest/token", + ValidateURL: "https://bitbucket.example.com/rest/api/latest/inbox/pull-requests/count", + Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"}, + Regex: `^(https?://)?bitbucket\.example\.com(/.*)?$`, + DisplayName: "Bitbucket Server", + DisplayIcon: "/icon/bitbucket.svg", + }, + }, + { + // Ensure backwards compatibility. The type should update to "bitbucket-cloud", + // but the ID and other fields should remain the same. + name: "BitbucketLegacy", + config: &codersdk.ExternalAuthConfig{ + Type: "bitbucket", + }, + expected: codersdk.ExternalAuthConfig{ + Type: string(codersdk.EnhancedExternalAuthProviderBitBucketCloud), + ID: "bitbucket", // Legacy ID remains unchanged + AuthURL: "https://bitbucket.org/site/oauth2/authorize", + TokenURL: "https://bitbucket.org/site/oauth2/access_token", + ValidateURL: "https://api.bitbucket.org/2.0/user", + DisplayName: "BitBucket", + DisplayIcon: "/icon/bitbucket.svg", + Regex: `^(https?://)?bitbucket\.org(/.*)?$`, + Scopes: []string{"account", "repository:write"}, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + applyDefaultsToConfig(tt.config) + require.Equal(t, tt.expected, *tt.config) + }) + } +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index dc2d240384475..bfc92d92a0849 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -2452,7 +2452,8 @@ func createExternalAuthResponse(typ, token string, extra pqtype.NullRawMessage) Username: "oauth2", Password: token, } - case string(codersdk.EnhancedExternalAuthProviderBitBucket): + case string(codersdk.EnhancedExternalAuthProviderBitBucketCloud), string(codersdk.EnhancedExternalAuthProviderBitBucketServer): + // The string "bitbucket" was a legacy parameter that needs to still be supported. // https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token resp = agentsdk.ExternalAuthResponse{ Username: "x-token-auth", diff --git a/codersdk/externalauth.go b/codersdk/externalauth.go index 60800af7195de..8d858670eed3d 100644 --- a/codersdk/externalauth.go +++ b/codersdk/externalauth.go @@ -21,7 +21,8 @@ func (e EnhancedExternalAuthProvider) Git() bool { switch e { case EnhancedExternalAuthProviderGitHub, EnhancedExternalAuthProviderGitLab, - EnhancedExternalAuthProviderBitBucket, + EnhancedExternalAuthProviderBitBucketCloud, + EnhancedExternalAuthProviderBitBucketServer, EnhancedExternalAuthProviderAzureDevops: return true default: @@ -33,9 +34,12 @@ const ( EnhancedExternalAuthProviderAzureDevops EnhancedExternalAuthProvider = "azure-devops" EnhancedExternalAuthProviderGitHub EnhancedExternalAuthProvider = "github" EnhancedExternalAuthProviderGitLab EnhancedExternalAuthProvider = "gitlab" - EnhancedExternalAuthProviderBitBucket EnhancedExternalAuthProvider = "bitbucket" - EnhancedExternalAuthProviderSlack EnhancedExternalAuthProvider = "slack" - EnhancedExternalAuthProviderJFrog EnhancedExternalAuthProvider = "jfrog" + // EnhancedExternalAuthProviderBitBucketCloud is the Bitbucket Cloud provider. + // Not to be confused with the self-hosted 'EnhancedExternalAuthProviderBitBucketServer' + EnhancedExternalAuthProviderBitBucketCloud EnhancedExternalAuthProvider = "bitbucket-cloud" + EnhancedExternalAuthProviderBitBucketServer EnhancedExternalAuthProvider = "bitbucket-server" + EnhancedExternalAuthProviderSlack EnhancedExternalAuthProvider = "slack" + EnhancedExternalAuthProviderJFrog EnhancedExternalAuthProvider = "jfrog" ) type ExternalAuth struct { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1485121342a32..3940ecfe83aea 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1681,14 +1681,16 @@ export const DisplayApps: DisplayApp[] = [ // From codersdk/externalauth.go export type EnhancedExternalAuthProvider = | "azure-devops" - | "bitbucket" + | "bitbucket-cloud" + | "bitbucket-server" | "github" | "gitlab" | "jfrog" | "slack"; export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = [ "azure-devops", - "bitbucket", + "bitbucket-cloud", + "bitbucket-server", "github", "gitlab", "jfrog",