package providers import ( "context" "net/url" "strings" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" ) // BitbucketProvider represents an Bitbucket based Identity Provider type BitbucketProvider struct { *ProviderData Workspace string Repository string } var _ Provider = (*BitbucketProvider)(nil) const ( bitbucketProviderName = "Bitbucket" bitbucketDefaultScope = "email" ) var ( // Default Login URL for Bitbucket. // Pre-parsed URL of https://bitbucket.org/site/oauth2/authorize. bitbucketDefaultLoginURL = &url.URL{ Scheme: "https", Host: "bitbucket.org", Path: "/site/oauth2/authorize", } // Default Redeem URL for Bitbucket. // Pre-parsed URL of https://bitbucket.org/site/oauth2/access_token. bitbucketDefaultRedeemURL = &url.URL{ Scheme: "https", Host: "bitbucket.org", Path: "/site/oauth2/access_token", } // Default Validation URL for Bitbucket. // This simply returns the email of the authenticated user. // Bitbucket does not have a Profile URL to use. // Pre-parsed URL of https://api.bitbucket.org/2.0/user/emails. bitbucketDefaultValidateURL = &url.URL{ Scheme: "https", Host: "api.bitbucket.org", Path: "/2.0/user/emails", } ) // NewBitbucketProvider initiates a new BitbucketProvider func NewBitbucketProvider(p *ProviderData, opts options.BitbucketOptions) *BitbucketProvider { p.setProviderDefaults(providerDefaults{ name: bitbucketProviderName, loginURL: bitbucketDefaultLoginURL, redeemURL: bitbucketDefaultRedeemURL, profileURL: nil, validateURL: bitbucketDefaultValidateURL, scope: bitbucketDefaultScope, }) provider := &BitbucketProvider{ProviderData: p} if opts.Team != "" { provider.setWorkspace(opts.Team) } if opts.Workspace != "" { provider.setWorkspace(opts.Workspace) } if opts.Repository != "" { provider.setRepository(opts.Repository) } return provider } // setWorkspace defines the Bitbucket workspace the user must be part of func (p *BitbucketProvider) setWorkspace(workspace string) { p.Workspace = workspace if !strings.Contains(p.Scope, "account") { p.Scope += " account" } } // setRepository defines the repository the user must have access to func (p *BitbucketProvider) setRepository(repository string) { p.Repository = repository if !strings.Contains(p.Scope, "repository") { p.Scope += " repository" } } // ValidateSession validates the AccessToken using a Bearer token header func (p *BitbucketProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool { return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken)) } // GetEmailAddress returns the email of the authenticated user func (p *BitbucketProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) { var emails struct { Values []struct { Email string `json:"email"` Primary bool `json:"is_primary"` } } var workspaces struct { Values []struct { Workspace struct { Slug string `json:"slug"` } `json:"workspace"` } } var repositories struct { Values []struct { FullName string `json:"full_name"` } } requestURL := p.ValidateURL.String() err := requests.New(requestURL). WithContext(ctx). WithHeaders(makeOIDCHeader(s.AccessToken)). Do(). UnmarshalInto(&emails) if err != nil { logger.Errorf("failed making request: %v", err) return "", err } if p.Workspace != "" { teamURL := &url.URL{} *teamURL = *p.ValidateURL // /teams api was deprecated in Oct 20, use workspaces instead // https://developer.atlassian.com/cloud/bitbucket/bitbucket-api-teams-deprecation/ // https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-workspaces-get teamURL.Path = "2.0/user/workspaces" requestURL := teamURL.String() err := requests.New(requestURL). WithContext(ctx). WithHeaders(makeOIDCHeader(s.AccessToken)). Do(). UnmarshalInto(&workspaces) logger.Printf("workspaces: %+v", workspaces) if err != nil { logger.Errorf("failed requesting teams membership: %v", err) return "", err } var found = false for _, workspace := range workspaces.Values { if p.Workspace == workspace.Workspace.Slug { found = true break } } if !found { logger.Error("team membership test failed, access denied") return "", nil } } if p.Repository != "" { repositoriesURL := &url.URL{} *repositoriesURL = *p.ValidateURL // split the repository name to get the workspace name, which is the first part of the repository name var repoWorkspace = strings.Split(p.Repository, "/")[0] repositoriesURL.Path = "/2.0/repositories/" + repoWorkspace requestURL := repositoriesURL.String() + "?role=contributor" + "&q=full_name=" + url.QueryEscape("\""+p.Repository+"\"") err := requests.New(requestURL). WithContext(ctx). WithHeaders(makeOIDCHeader(s.AccessToken)). Do(). UnmarshalInto(&repositories) if err != nil { logger.Errorf("failed checking repository access: %v", err) return "", err } var found = false for _, repository := range repositories.Values { if p.Repository == repository.FullName { found = true break } } if !found { logger.Error("repository access test failed, access denied") return "", nil } } for _, email := range emails.Values { if email.Primary { return email.Email, nil } } return "", nil }