2019-08-16 15:53:22 +02:00
|
|
|
package providers
|
|
|
|
|
|
|
|
import (
|
2020-05-05 17:53:33 +02:00
|
|
|
"context"
|
2019-08-16 15:53:22 +02:00
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
|
2022-02-15 13:18:32 +02:00
|
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
2020-09-29 18:44:42 +02:00
|
|
|
"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"
|
2019-08-16 15:53:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// BitbucketProvider represents an Bitbucket based Identity Provider
|
|
|
|
type BitbucketProvider struct {
|
|
|
|
*ProviderData
|
|
|
|
Team string
|
|
|
|
Repository string
|
|
|
|
}
|
|
|
|
|
2020-05-05 17:53:33 +02:00
|
|
|
var _ Provider = (*BitbucketProvider)(nil)
|
|
|
|
|
2020-05-25 14:08:04 +02:00
|
|
|
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",
|
2019-08-16 15:53:22 +02:00
|
|
|
}
|
2020-05-25 14:08:04 +02:00
|
|
|
|
|
|
|
// 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",
|
2019-08-16 15:53:22 +02:00
|
|
|
}
|
2020-05-25 14:08:04 +02:00
|
|
|
|
|
|
|
// 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",
|
2019-08-16 15:53:22 +02:00
|
|
|
}
|
2020-05-25 14:08:04 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// NewBitbucketProvider initiates a new BitbucketProvider
|
2022-02-15 13:18:32 +02:00
|
|
|
func NewBitbucketProvider(p *ProviderData, opts options.BitbucketOptions) *BitbucketProvider {
|
2020-05-25 14:08:04 +02:00
|
|
|
p.setProviderDefaults(providerDefaults{
|
|
|
|
name: bitbucketProviderName,
|
|
|
|
loginURL: bitbucketDefaultLoginURL,
|
|
|
|
redeemURL: bitbucketDefaultRedeemURL,
|
|
|
|
profileURL: nil,
|
|
|
|
validateURL: bitbucketDefaultValidateURL,
|
|
|
|
scope: bitbucketDefaultScope,
|
|
|
|
})
|
2022-02-15 13:18:32 +02:00
|
|
|
|
|
|
|
provider := &BitbucketProvider{ProviderData: p}
|
|
|
|
|
|
|
|
if opts.Team != "" {
|
|
|
|
provider.setTeam(opts.Team)
|
|
|
|
}
|
|
|
|
if opts.Repository != "" {
|
|
|
|
provider.setRepository(opts.Repository)
|
|
|
|
}
|
|
|
|
return provider
|
2019-08-16 15:53:22 +02:00
|
|
|
}
|
|
|
|
|
2022-02-15 13:18:32 +02:00
|
|
|
// setTeam defines the Bitbucket team the user must be part of
|
|
|
|
func (p *BitbucketProvider) setTeam(team string) {
|
2019-08-16 15:53:22 +02:00
|
|
|
p.Team = team
|
|
|
|
if !strings.Contains(p.Scope, "team") {
|
|
|
|
p.Scope += " team"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 13:18:32 +02:00
|
|
|
// setRepository defines the repository the user must have access to
|
|
|
|
func (p *BitbucketProvider) setRepository(repository string) {
|
2019-08-16 15:53:22 +02:00
|
|
|
p.Repository = repository
|
|
|
|
if !strings.Contains(p.Scope, "repository") {
|
|
|
|
p.Scope += " repository"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetEmailAddress returns the email of the authenticated user
|
2020-05-05 17:53:33 +02:00
|
|
|
func (p *BitbucketProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
|
2019-08-16 15:53:22 +02:00
|
|
|
|
|
|
|
var emails struct {
|
|
|
|
Values []struct {
|
|
|
|
Email string `json:"email"`
|
|
|
|
Primary bool `json:"is_primary"`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var teams struct {
|
|
|
|
Values []struct {
|
|
|
|
Name string `json:"username"`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var repositories struct {
|
|
|
|
Values []struct {
|
|
|
|
FullName string `json:"full_name"`
|
|
|
|
}
|
|
|
|
}
|
2020-07-03 20:27:25 +02:00
|
|
|
|
|
|
|
requestURL := p.ValidateURL.String() + "?access_token=" + s.AccessToken
|
|
|
|
err := requests.New(requestURL).
|
|
|
|
WithContext(ctx).
|
2020-07-06 18:42:26 +02:00
|
|
|
Do().
|
2020-07-03 20:27:25 +02:00
|
|
|
UnmarshalInto(&emails)
|
2019-08-16 15:53:22 +02:00
|
|
|
if err != nil {
|
2020-08-10 12:44:08 +02:00
|
|
|
logger.Errorf("failed making request: %v", err)
|
2019-08-16 15:53:22 +02:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Team != "" {
|
|
|
|
teamURL := &url.URL{}
|
|
|
|
*teamURL = *p.ValidateURL
|
|
|
|
teamURL.Path = "/2.0/teams"
|
2020-07-03 20:27:25 +02:00
|
|
|
|
|
|
|
requestURL := teamURL.String() + "?role=member&access_token=" + s.AccessToken
|
|
|
|
|
|
|
|
err := requests.New(requestURL).
|
|
|
|
WithContext(ctx).
|
2020-07-06 18:42:26 +02:00
|
|
|
Do().
|
2020-07-03 20:27:25 +02:00
|
|
|
UnmarshalInto(&teams)
|
2019-08-16 15:53:22 +02:00
|
|
|
if err != nil {
|
2020-08-10 12:44:08 +02:00
|
|
|
logger.Errorf("failed requesting teams membership: %v", err)
|
2019-08-16 15:53:22 +02:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
var found = false
|
|
|
|
for _, team := range teams.Values {
|
|
|
|
if p.Team == team.Name {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2020-04-14 10:36:44 +02:00
|
|
|
if !found {
|
2020-08-10 12:44:08 +02:00
|
|
|
logger.Error("team membership test failed, access denied")
|
2019-08-16 15:53:22 +02:00
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Repository != "" {
|
|
|
|
repositoriesURL := &url.URL{}
|
|
|
|
*repositoriesURL = *p.ValidateURL
|
|
|
|
repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0]
|
2020-07-03 20:27:25 +02:00
|
|
|
|
|
|
|
requestURL := repositoriesURL.String() + "?role=contributor" +
|
|
|
|
"&q=full_name=" + url.QueryEscape("\""+p.Repository+"\"") +
|
|
|
|
"&access_token=" + s.AccessToken
|
|
|
|
|
|
|
|
err := requests.New(requestURL).
|
|
|
|
WithContext(ctx).
|
2020-07-06 18:42:26 +02:00
|
|
|
Do().
|
2020-07-03 20:27:25 +02:00
|
|
|
UnmarshalInto(&repositories)
|
2019-08-16 15:53:22 +02:00
|
|
|
if err != nil {
|
2020-08-10 12:44:08 +02:00
|
|
|
logger.Errorf("failed checking repository access: %v", err)
|
2019-08-16 15:53:22 +02:00
|
|
|
return "", err
|
|
|
|
}
|
2020-07-03 20:27:25 +02:00
|
|
|
|
2019-08-16 15:53:22 +02:00
|
|
|
var found = false
|
|
|
|
for _, repository := range repositories.Values {
|
|
|
|
if p.Repository == repository.FullName {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2020-04-14 10:36:44 +02:00
|
|
|
if !found {
|
2020-08-10 12:44:08 +02:00
|
|
|
logger.Error("repository access test failed, access denied")
|
2019-08-16 15:53:22 +02:00
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, email := range emails.Values {
|
|
|
|
if email.Primary {
|
|
|
|
return email.Email, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", nil
|
|
|
|
}
|