Skip to content

Commit 73c75b2

Browse files
authored
Merge pull request netlify#10 from netlify/gitlab-support
Add GitLab support
2 parents 7a3f4e8 + 0958bdd commit 73c75b2

File tree

4 files changed

+153
-0
lines changed

4 files changed

+153
-0
lines changed

api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
7676
r.Use(api.loadInstanceConfig)
7777
}
7878
r.With(api.requireAuthentication).Mount("/github", NewGitHubGateway())
79+
r.With(api.requireAuthentication).Mount("/gitlab", NewGitLabGateway())
80+
r.With(api.requireAuthentication).Get("/settings", api.Settings)
7981
})
8082

8183
if globalConfig.MultiInstanceMode {

api/gitlab.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package api
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httputil"
7+
"net/url"
8+
"regexp"
9+
)
10+
11+
// GitLabGateway acts as a proxy to Gitlab
12+
type GitLabGateway struct {
13+
proxy *httputil.ReverseProxy
14+
}
15+
16+
var gitlabPathRegexp = regexp.MustCompile("^/gitlab/?")
17+
var gitlabAllowedRegexp = regexp.MustCompile("^/gitlab/repository/(files|commits|tree)/?")
18+
19+
func NewGitLabGateway() *GitLabGateway {
20+
return &GitLabGateway{
21+
proxy: &httputil.ReverseProxy{
22+
Director: gitlabDirector,
23+
Transport: &GitLabTransport{},
24+
},
25+
}
26+
}
27+
28+
func gitlabDirector(r *http.Request) {
29+
ctx := r.Context()
30+
target := getProxyTarget(ctx)
31+
accessToken := getAccessToken(ctx)
32+
33+
targetQuery := target.RawQuery
34+
r.Host = target.Host
35+
r.URL.Scheme = target.Scheme
36+
r.URL.Host = target.Host
37+
r.URL.Path = singleJoiningSlash(target.Path, gitlabPathRegexp.ReplaceAllString(r.URL.Path, "/"))
38+
if targetQuery == "" || r.URL.RawQuery == "" {
39+
r.URL.RawQuery = targetQuery + r.URL.RawQuery
40+
} else {
41+
r.URL.RawQuery = targetQuery + "&" + r.URL.RawQuery
42+
}
43+
if _, ok := r.Header["User-Agent"]; !ok {
44+
// explicitly disable User-Agent so it's not set to default value
45+
r.Header.Set("User-Agent", "")
46+
}
47+
if r.Method != http.MethodOptions {
48+
r.Header.Set("Authorization", "Bearer "+accessToken)
49+
}
50+
51+
log := getLogEntry(r)
52+
log.Infof("Proxying to GitLab: %v", r.URL.String())
53+
}
54+
55+
func (gl *GitLabGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
56+
ctx := r.Context()
57+
config := getConfig(ctx)
58+
if config == nil || config.GitLab.AccessToken == "" {
59+
handleError(notFoundError("No GitLab Settings Configured"), w, r)
60+
return
61+
}
62+
63+
if err := gl.authenticate(w, r); err != nil {
64+
handleError(unauthorizedError(err.Error()), w, r)
65+
return
66+
}
67+
68+
endpoint := config.GitLab.Endpoint
69+
apiURL := singleJoiningSlash(endpoint, "/repos/"+config.GitLab.Repo)
70+
target, err := url.Parse(apiURL)
71+
if err != nil {
72+
handleError(internalServerError("Unable to process GitLab endpoint"), w, r)
73+
return
74+
}
75+
ctx = withProxyTarget(ctx, target)
76+
ctx = withAccessToken(ctx, config.GitLab.AccessToken)
77+
gl.proxy.ServeHTTP(w, r.WithContext(ctx))
78+
}
79+
80+
func (gl *GitLabGateway) authenticate(w http.ResponseWriter, r *http.Request) error {
81+
ctx := r.Context()
82+
claims := getClaims(ctx)
83+
config := getConfig(ctx)
84+
85+
if claims == nil {
86+
return errors.New("Access to endpoint not allowed: no claims found in Bearer token")
87+
}
88+
89+
if !gitlabAllowedRegexp.MatchString(r.URL.Path) {
90+
return errors.New("Access to endpoint not allowed: this part of GitLab's API has been restricted")
91+
}
92+
93+
if len(config.Roles) == 0 {
94+
return nil
95+
}
96+
97+
roles, ok := claims.AppMetaData["roles"]
98+
if ok {
99+
roleStrings, _ := roles.([]interface{})
100+
for _, data := range roleStrings {
101+
role, _ := data.(string)
102+
for _, adminRole := range config.Roles {
103+
if role == adminRole {
104+
return nil
105+
}
106+
}
107+
}
108+
}
109+
110+
return errors.New("Access to endpoint not allowed: your role doesn't allow access")
111+
}
112+
113+
type GitLabTransport struct{}
114+
115+
func (t *GitLabTransport) RoundTrip(r *http.Request) (*http.Response, error) {
116+
resp, err := http.DefaultTransport.RoundTrip(r)
117+
if err == nil {
118+
// remove CORS headers from GitHub and use our own
119+
resp.Header.Del("Access-Control-Allow-Origin")
120+
}
121+
return resp, err
122+
}

api/settings.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package api
2+
3+
import "net/http"
4+
5+
type Settings struct {
6+
GitHub bool `json:"github_enabled"`
7+
GitLab bool `json:"gitlab_enabled"`
8+
Roles []string `json:"roles"`
9+
}
10+
11+
func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
12+
ctx := r.Context()
13+
config := getConfig(ctx)
14+
15+
settings := Settings{
16+
GitHub: config.GitHub.Repo != "",
17+
GitLab: config.GitLab.Repo != "",
18+
Roles: config.Roles,
19+
}
20+
21+
return sendJSON(w, http.StatusOK, &settings)
22+
}

conf/configuration.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ type GitHubConfig struct {
1616
Repo string `envconfig:"REPO" json:"repo"` // Should be "owner/repo" format
1717
}
1818

19+
type GitLabConfig struct {
20+
AccessToken string `envconfig:"ACCESS_TOKEN" json:"access_token,omitempty"`
21+
Endpoint string `envconfig:"ENDPOINT" json:"endpoint"`
22+
Repo string `envconfig:"REPO" json:"repo"` // Should be "owner/repo" format
23+
}
24+
1925
// DBConfiguration holds all the database related configuration.
2026
type DBConfiguration struct {
2127
Dialect string `json:"dialect"`
@@ -47,6 +53,7 @@ type GlobalConfiguration struct {
4753
type Configuration struct {
4854
JWT JWTConfiguration `json:"jwt"`
4955
GitHub GitHubConfig `envconfig:"GITHUB" json:"github"`
56+
GitLab GitLabConfig `envconfig:"GITLAB" json:"gitlab"`
5057
Roles []string `envconfig:"ROLES" json:"roles"`
5158
}
5259

0 commit comments

Comments
 (0)