Add Bitbucket provider. (#201)
Add a new provider for Bitbucket, can be configured from the options specifying team and/or repository that the user must be part/have access to in order to grant login.
This commit is contained in:
parent
91184c58c7
commit
81f15258a8
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -10,3 +10,7 @@
|
|||||||
# or the public devops channel at https://chat.18f.gov/).
|
# or the public devops channel at https://chat.18f.gov/).
|
||||||
providers/logingov.go @timothy-spencer
|
providers/logingov.go @timothy-spencer
|
||||||
providers/logingov_test.go @timothy-spencer
|
providers/logingov_test.go @timothy-spencer
|
||||||
|
|
||||||
|
# Bitbucket provider
|
||||||
|
providers/bitbucket.go @aledeganopix4d
|
||||||
|
providers/bitbucket_test.go @aledeganopix4d
|
||||||
|
@ -96,6 +96,10 @@
|
|||||||
- [#198](https://github.com/pusher/oauth2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
|
- [#198](https://github.com/pusher/oauth2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
|
||||||
- [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email`
|
- [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email`
|
||||||
- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
|
- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
|
||||||
|
- [#201](https://github.com/pusher/oauth2_proxy/pull/201) Add Bitbucket as new OAuth2 provider, accepts email, team and repository permissions to determine authorization (@aledeganopix4d)
|
||||||
|
- Implement flags to enable Bitbucket authentication:
|
||||||
|
- `-bitbucket-repository` Restrict authorization to users that can access this repository
|
||||||
|
- `-bitbucket-team` Restrict authorization to users that are part of this Bitbucket team
|
||||||
- [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore)
|
- [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore)
|
||||||
- [#145](https://github.com/pusher/oauth2_proxy/pull/145) Add support for OIDC UserInfo endpoint email verification (@rtluckie)
|
- [#145](https://github.com/pusher/oauth2_proxy/pull/145) Add support for OIDC UserInfo endpoint email verification (@rtluckie)
|
||||||
|
|
||||||
|
2
main.go
2
main.go
@ -56,6 +56,8 @@ func main() {
|
|||||||
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
||||||
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
|
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
|
||||||
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
|
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
|
||||||
|
flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
|
||||||
|
flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
|
||||||
flagSet.String("github-org", "", "restrict logins to members of this organisation")
|
flagSet.String("github-org", "", "restrict logins to members of this organisation")
|
||||||
flagSet.String("github-team", "", "restrict logins to members of this team")
|
flagSet.String("github-team", "", "restrict logins to members of this team")
|
||||||
flagSet.String("gitlab-group", "", "restrict logins to members of this group")
|
flagSet.String("gitlab-group", "", "restrict logins to members of this group")
|
||||||
|
@ -42,6 +42,8 @@ type Options struct {
|
|||||||
|
|
||||||
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
|
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
|
||||||
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
|
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
|
||||||
|
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"`
|
||||||
|
BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"`
|
||||||
EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
|
EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
|
||||||
WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
|
WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
|
||||||
GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
|
GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
|
||||||
@ -405,6 +407,9 @@ func parseProviderInfo(o *Options, msgs []string) []string {
|
|||||||
p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
|
p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case *providers.BitbucketProvider:
|
||||||
|
p.SetTeam(o.BitbucketTeam)
|
||||||
|
p.SetRepository(o.BitbucketRepository)
|
||||||
case *providers.OIDCProvider:
|
case *providers.OIDCProvider:
|
||||||
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
|
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
|
||||||
if o.oidcVerifier == nil {
|
if o.oidcVerifier == nil {
|
||||||
|
163
providers/bitbucket.go
Normal file
163
providers/bitbucket.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||||
|
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||||
|
"github.com/pusher/oauth2_proxy/pkg/requests"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitbucketProvider represents an Bitbucket based Identity Provider
|
||||||
|
type BitbucketProvider struct {
|
||||||
|
*ProviderData
|
||||||
|
Team string
|
||||||
|
Repository string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitbucketProvider initiates a new BitbucketProvider
|
||||||
|
func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
|
||||||
|
p.ProviderName = "Bitbucket"
|
||||||
|
if p.LoginURL == nil || p.LoginURL.String() == "" {
|
||||||
|
p.LoginURL = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "bitbucket.org",
|
||||||
|
Path: "/site/oauth2/authorize",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
|
||||||
|
p.RedeemURL = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "bitbucket.org",
|
||||||
|
Path: "/site/oauth2/access_token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
|
||||||
|
p.ValidateURL = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "api.bitbucket.org",
|
||||||
|
Path: "/2.0/user/emails",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.Scope == "" {
|
||||||
|
p.Scope = "email"
|
||||||
|
}
|
||||||
|
return &BitbucketProvider{ProviderData: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTeam defines the Bitbucket team the user must be part of
|
||||||
|
func (p *BitbucketProvider) SetTeam(team string) {
|
||||||
|
p.Team = team
|
||||||
|
if !strings.Contains(p.Scope, "team") {
|
||||||
|
p.Scope += " team"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmailAddress returns the email of the authenticated user
|
||||||
|
func (p *BitbucketProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET",
|
||||||
|
p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed building request %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = requests.RequestJSON(req, &emails)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed making request %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Team != "" {
|
||||||
|
teamURL := &url.URL{}
|
||||||
|
*teamURL = *p.ValidateURL
|
||||||
|
teamURL.Path = "/2.0/teams"
|
||||||
|
req, err = http.NewRequest("GET",
|
||||||
|
teamURL.String()+"?role=member&access_token="+s.AccessToken, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed building request %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = requests.RequestJSON(req, &teams)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed requesting teams membership %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var found = false
|
||||||
|
for _, team := range teams.Values {
|
||||||
|
if p.Team == team.Name {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found != true {
|
||||||
|
logger.Print("team membership test failed, access denied")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Repository != "" {
|
||||||
|
repositoriesURL := &url.URL{}
|
||||||
|
*repositoriesURL = *p.ValidateURL
|
||||||
|
repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0]
|
||||||
|
req, err = http.NewRequest("GET",
|
||||||
|
repositoriesURL.String()+"?role=contributor"+
|
||||||
|
"&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+
|
||||||
|
"&access_token="+s.AccessToken,
|
||||||
|
nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed building request %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = requests.RequestJSON(req, &repositories)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed checking repository access %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var found = false
|
||||||
|
for _, repository := range repositories.Values {
|
||||||
|
if p.Repository == repository.FullName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found != true {
|
||||||
|
logger.Print("repository access test failed, access denied")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, email := range emails.Values {
|
||||||
|
if email.Primary {
|
||||||
|
return email.Email, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
170
providers/bitbucket_test.go
Normal file
170
providers/bitbucket_test.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testBitbucketProvider(hostname, team string, repository string) *BitbucketProvider {
|
||||||
|
p := NewBitbucketProvider(
|
||||||
|
&ProviderData{
|
||||||
|
ProviderName: "",
|
||||||
|
LoginURL: &url.URL{},
|
||||||
|
RedeemURL: &url.URL{},
|
||||||
|
ProfileURL: &url.URL{},
|
||||||
|
ValidateURL: &url.URL{},
|
||||||
|
Scope: ""})
|
||||||
|
|
||||||
|
if team != "" {
|
||||||
|
p.SetTeam(team)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repository != "" {
|
||||||
|
p.SetRepository(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostname != "" {
|
||||||
|
updateURL(p.Data().LoginURL, hostname)
|
||||||
|
updateURL(p.Data().RedeemURL, hostname)
|
||||||
|
updateURL(p.Data().ProfileURL, hostname)
|
||||||
|
updateURL(p.Data().ValidateURL, hostname)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBitbucketBackend(payload string) *httptest.Server {
|
||||||
|
paths := map[string]bool{
|
||||||
|
"/2.0/user/emails": true,
|
||||||
|
"/2.0/teams": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
url := r.URL
|
||||||
|
if !paths[url.Path] {
|
||||||
|
log.Printf("%s not in %+v\n", url.Path, paths)
|
||||||
|
w.WriteHeader(404)
|
||||||
|
} else if r.URL.Query().Get("access_token") != "imaginary_access_token" {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(payload))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderDefaults(t *testing.T) {
|
||||||
|
p := testBitbucketProvider("", "", "")
|
||||||
|
assert.NotEqual(t, nil, p)
|
||||||
|
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/site/oauth2/authorize",
|
||||||
|
p.Data().LoginURL.String())
|
||||||
|
assert.Equal(t, "https://bitbucket.org/site/oauth2/access_token",
|
||||||
|
p.Data().RedeemURL.String())
|
||||||
|
assert.Equal(t, "https://api.bitbucket.org/2.0/user/emails",
|
||||||
|
p.Data().ValidateURL.String())
|
||||||
|
assert.Equal(t, "email", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderScopeAdjustForTeam(t *testing.T) {
|
||||||
|
p := testBitbucketProvider("", "test-team", "")
|
||||||
|
assert.NotEqual(t, nil, p)
|
||||||
|
assert.Equal(t, "email team", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderScopeAdjustForRepository(t *testing.T) {
|
||||||
|
p := testBitbucketProvider("", "", "rest-repo")
|
||||||
|
assert.NotEqual(t, nil, p)
|
||||||
|
assert.Equal(t, "email repository", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderOverrides(t *testing.T) {
|
||||||
|
p := NewBitbucketProvider(
|
||||||
|
&ProviderData{
|
||||||
|
LoginURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/oauth/auth"},
|
||||||
|
RedeemURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/oauth/token"},
|
||||||
|
ValidateURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/api/v3/user"},
|
||||||
|
Scope: "profile"})
|
||||||
|
assert.NotEqual(t, nil, p)
|
||||||
|
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
|
||||||
|
assert.Equal(t, "https://example.com/oauth/auth",
|
||||||
|
p.Data().LoginURL.String())
|
||||||
|
assert.Equal(t, "https://example.com/oauth/token",
|
||||||
|
p.Data().RedeemURL.String())
|
||||||
|
assert.Equal(t, "https://example.com/api/v3/user",
|
||||||
|
p.Data().ValidateURL.String())
|
||||||
|
assert.Equal(t, "profile", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderGetEmailAddress(t *testing.T) {
|
||||||
|
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true } ] }")
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
bURL, _ := url.Parse(b.URL)
|
||||||
|
p := testBitbucketProvider(bURL.Host, "", "")
|
||||||
|
|
||||||
|
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||||
|
email, err := p.GetEmailAddress(session)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) {
|
||||||
|
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true, \"username\": \"bioinformatics\" } ] }")
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
bURL, _ := url.Parse(b.URL)
|
||||||
|
p := testBitbucketProvider(bURL.Host, "bioinformatics", "")
|
||||||
|
|
||||||
|
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||||
|
email, err := p.GetEmailAddress(session)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that trying to trigger the "failed building request" case is not
|
||||||
|
// practical, since the only way it can fail is if the URL fails to parse.
|
||||||
|
func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) {
|
||||||
|
b := testBitbucketBackend("unused payload")
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
bURL, _ := url.Parse(b.URL)
|
||||||
|
p := testBitbucketProvider(bURL.Host, "", "")
|
||||||
|
|
||||||
|
// We'll trigger a request failure by using an unexpected access
|
||||||
|
// token. Alternatively, we could allow the parsing of the payload as
|
||||||
|
// JSON to fail.
|
||||||
|
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
|
||||||
|
email, err := p.GetEmailAddress(session)
|
||||||
|
assert.NotEqual(t, nil, err)
|
||||||
|
assert.Equal(t, "", email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
|
||||||
|
b := testBitbucketBackend("{\"foo\": \"bar\"}")
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
bURL, _ := url.Parse(b.URL)
|
||||||
|
p := testBitbucketProvider(bURL.Host, "", "")
|
||||||
|
|
||||||
|
session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
|
||||||
|
email, err := p.GetEmailAddress(session)
|
||||||
|
assert.Equal(t, "", email)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
}
|
@ -36,6 +36,8 @@ func New(provider string, p *ProviderData) Provider {
|
|||||||
return NewOIDCProvider(p)
|
return NewOIDCProvider(p)
|
||||||
case "login.gov":
|
case "login.gov":
|
||||||
return NewLoginGovProvider(p)
|
return NewLoginGovProvider(p)
|
||||||
|
case "bitbucket":
|
||||||
|
return NewBitbucketProvider(p)
|
||||||
default:
|
default:
|
||||||
return NewGoogleProvider(p)
|
return NewGoogleProvider(p)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user