Skip to content

Commit f160a66

Browse files
committed
Git Gateway support for BitBucket
1 parent a9002bf commit f160a66

File tree

4 files changed

+207
-10
lines changed

4 files changed

+207
-10
lines changed

api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
7777
}
7878
r.With(api.requireAuthentication).Mount("/github", NewGitHubGateway())
7979
r.With(api.requireAuthentication).Mount("/gitlab", NewGitLabGateway())
80+
r.With(api.requireAuthentication).Mount("/bitbucket", NewBitBucketGateway())
8081
r.With(api.requireAuthentication).Get("/settings", api.Settings)
8182
})
8283

api/bitbucket.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"net/http/httputil"
8+
"net/url"
9+
"regexp"
10+
"sync"
11+
12+
"golang.org/x/oauth2"
13+
"golang.org/x/oauth2/bitbucket"
14+
)
15+
16+
type BitBucketGateway struct {
17+
proxy *httputil.ReverseProxy
18+
}
19+
20+
func NewBitBucketGateway() *BitBucketGateway {
21+
return &BitBucketGateway{
22+
proxy: &httputil.ReverseProxy{
23+
Director: bitbucketDirector,
24+
Transport: &BitBucketTransport{},
25+
},
26+
}
27+
}
28+
29+
var bitbucketPathRegexp = regexp.MustCompile("^/bitbucket/?")
30+
var bitbucketAllowedRegexp = regexp.MustCompile("^/bitbucket/src/?")
31+
32+
var bitbucketTokenExpirationMessageRegexp = regexp.MustCompile("(?i)^access token expired")
33+
var currentAccessToken *oauth2.Token
34+
35+
type notifyRefreshTokenSource struct {
36+
new oauth2.TokenSource
37+
mu sync.Mutex // guards t
38+
t *oauth2.Token
39+
}
40+
41+
func (s *notifyRefreshTokenSource) Token() (*oauth2.Token, error) {
42+
s.mu.Lock()
43+
defer s.mu.Unlock()
44+
if s.t.Valid() {
45+
return s.t, nil
46+
}
47+
t, err := s.new.Token()
48+
if err != nil {
49+
return nil, err
50+
}
51+
s.t = t
52+
return t, nil
53+
}
54+
55+
var currentTokenSource *notifyRefreshTokenSource
56+
57+
func getTokenSource(ctx context.Context) *notifyRefreshTokenSource {
58+
config := getConfig(ctx)
59+
if currentTokenSource != nil {
60+
return currentTokenSource
61+
}
62+
63+
blankToken := &oauth2.Token{
64+
AccessToken: "",
65+
RefreshToken: config.BitBucket.RefreshToken,
66+
}
67+
oauthConfig := &oauth2.Config{
68+
ClientID: config.BitBucket.ClientID,
69+
ClientSecret: config.BitBucket.ClientSecret,
70+
Endpoint: bitbucket.Endpoint,
71+
}
72+
tokenSource := &notifyRefreshTokenSource{
73+
new: oauthConfig.TokenSource(ctx, blankToken),
74+
t: blankToken,
75+
}
76+
currentTokenSource = tokenSource
77+
return tokenSource
78+
}
79+
80+
func bitbucketDirector(r *http.Request) {
81+
ctx := r.Context()
82+
target := getProxyTarget(ctx)
83+
accessToken := getAccessToken(ctx)
84+
85+
targetQuery := target.RawQuery
86+
r.Host = target.Host
87+
r.URL.Scheme = target.Scheme
88+
r.URL.Host = target.Host
89+
r.URL.Path = singleJoiningSlash(target.Path, bitbucketPathRegexp.ReplaceAllString(r.URL.Path, "/"))
90+
if targetQuery == "" || r.URL.RawQuery == "" {
91+
r.URL.RawQuery = targetQuery + r.URL.RawQuery
92+
} else {
93+
r.URL.RawQuery = targetQuery + "&" + r.URL.RawQuery
94+
}
95+
if _, ok := r.Header["User-Agent"]; !ok {
96+
r.Header.Set("User-Agent", "")
97+
}
98+
99+
if r.Method != http.MethodOptions {
100+
r.Header.Set("Authorization", "Bearer "+accessToken)
101+
}
102+
103+
log := getLogEntry(r)
104+
log.Infof("Proxying to BitBucket: %v", r.URL.String(), r.Header.Get("Authorization"))
105+
}
106+
107+
func (bb *BitBucketGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
108+
ctx := r.Context()
109+
config := getConfig(ctx)
110+
if config == nil || config.BitBucket.RefreshToken == "" {
111+
handleError(notFoundError("No BitBucket Settings Configured"), w, r)
112+
return
113+
}
114+
115+
if err := bb.authenticate(w, r); err != nil {
116+
handleError(unauthorizedError(err.Error()), w, r)
117+
return
118+
}
119+
120+
endpoint := config.BitBucket.Endpoint
121+
apiURL := singleJoiningSlash(endpoint, "/repositories/"+config.BitBucket.Repo)
122+
target, err := url.Parse(apiURL)
123+
if err != nil {
124+
handleError(internalServerError("Unable to process BitBucket endpoint"), w, r)
125+
return
126+
}
127+
128+
tokenSource := getTokenSource(ctx)
129+
token, err := tokenSource.Token()
130+
if err != nil {
131+
handleError(internalServerError("Unable to process BitBucket endpoint"), w, r)
132+
}
133+
134+
ctx = withProxyTarget(ctx, target)
135+
ctx = withAccessToken(ctx, token.AccessToken)
136+
bb.proxy.ServeHTTP(w, r.WithContext(ctx))
137+
}
138+
139+
func (bb *BitBucketGateway) authenticate(w http.ResponseWriter, r *http.Request) error {
140+
ctx := r.Context()
141+
claims := getClaims(ctx)
142+
config := getConfig(ctx)
143+
144+
if claims == nil {
145+
return errors.New("Access to endpoint not allowed: no claims found in Bearer token")
146+
}
147+
148+
if !bitbucketAllowedRegexp.MatchString(r.URL.Path) {
149+
return errors.New("Access to endpoint not allowed: this part of BitBucket's API has been restricted")
150+
}
151+
152+
if len(config.Roles) == 0 {
153+
return nil
154+
}
155+
156+
roles, ok := claims.AppMetaData["roles"]
157+
if ok {
158+
roleStrings, _ := roles.([]interface{})
159+
for _, data := range roleStrings {
160+
role, _ := data.(string)
161+
for _, adminRole := range config.Roles {
162+
if role == adminRole {
163+
return nil
164+
}
165+
}
166+
}
167+
}
168+
169+
return errors.New("Access to endpoint not allowed: your role doesn't allow access")
170+
}
171+
172+
type BitBucketTransport struct{}
173+
174+
func (t *BitBucketTransport) RoundTrip(r *http.Request) (*http.Response, error) {
175+
resp, err := http.DefaultTransport.RoundTrip(r)
176+
if err == nil {
177+
// remove CORS headers from BitBucket and use our own
178+
resp.Header.Del("Access-Control-Allow-Origin")
179+
}
180+
return resp, err
181+
}

api/settings.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@ package api
33
import "net/http"
44

55
type Settings struct {
6-
GitHub bool `json:"github_enabled"`
7-
GitLab bool `json:"gitlab_enabled"`
8-
Roles []string `json:"roles"`
6+
GitHub bool `json:"github_enabled"`
7+
GitLab bool `json:"gitlab_enabled"`
8+
BitBucket bool `json:"bitbucket_enabled"`
9+
Roles []string `json:"roles"`
910
}
1011

1112
func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
1213
ctx := r.Context()
1314
config := getConfig(ctx)
1415

1516
settings := Settings{
16-
GitHub: config.GitHub.Repo != "",
17-
GitLab: config.GitLab.Repo != "",
18-
Roles: config.Roles,
17+
GitHub: config.GitHub.Repo != "",
18+
GitLab: config.GitLab.Repo != "",
19+
BitBucket: config.BitBucket.Repo != "",
20+
Roles: config.Roles,
1921
}
2022

2123
return sendJSON(w, http.StatusOK, &settings)

conf/configuration.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
const DefaultGitHubEndpoint = "https://api.github.com"
1212
const DefaultGitLabEndpoint = "https://gitlab.com/api/v4"
1313
const DefaultGitLabTokenType = "oauth"
14+
const DefaultBitBucketEndpoint = "https://api.bitbucket.org/2.0"
1415

1516
type GitHubConfig struct {
1617
AccessToken string `envconfig:"ACCESS_TOKEN" json:"access_token,omitempty"`
@@ -25,6 +26,14 @@ type GitLabConfig struct {
2526
Repo string `envconfig:"REPO" json:"repo"` // Should be "owner/repo" format
2627
}
2728

29+
type BitBucketConfig struct {
30+
RefreshToken string `envconfig:"REFRESH_TOKEN" json:"refresh_token,omitempty"`
31+
ClientID string `envconfig:"CLIENT_ID" json:"client_id,omitempty"`
32+
ClientSecret string `envconfig:"CLIENT_SECRET" json:"client_secret,omitempty"`
33+
Endpoint string `envconfig:"ENDPOINT" json:"endpoint"`
34+
Repo string `envconfig:"REPO" json:"repo"`
35+
}
36+
2837
// DBConfiguration holds all the database related configuration.
2938
type DBConfiguration struct {
3039
Dialect string `json:"dialect"`
@@ -54,10 +63,11 @@ type GlobalConfiguration struct {
5463

5564
// Configuration holds all the per-instance configuration.
5665
type Configuration struct {
57-
JWT JWTConfiguration `json:"jwt"`
58-
GitHub GitHubConfig `envconfig:"GITHUB" json:"github"`
59-
GitLab GitLabConfig `envconfig:"GITLAB" json:"gitlab"`
60-
Roles []string `envconfig:"ROLES" json:"roles"`
66+
JWT JWTConfiguration `json:"jwt"`
67+
GitHub GitHubConfig `envconfig:"GITHUB" json:"github"`
68+
GitLab GitLabConfig `envconfig:"GITLAB" json:"gitlab"`
69+
BitBucket BitBucketConfig `envconfig:"BITBUCKET" json:"bitbucket"`
70+
Roles []string `envconfig:"ROLES" json:"roles"`
6171
}
6272

6373
func loadEnvironment(filename string) error {
@@ -115,4 +125,7 @@ func (config *Configuration) ApplyDefaults() {
115125
if config.GitLab.AccessTokenType == "" {
116126
config.GitLab.AccessTokenType = DefaultGitLabTokenType
117127
}
128+
if config.BitBucket.Endpoint == "" {
129+
config.BitBucket.Endpoint = DefaultBitBucketEndpoint
130+
}
118131
}

0 commit comments

Comments
 (0)