diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2019-11-29 18:06:34 +0300 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2019-11-29 18:12:13 +0300 |
commit | af335dc002bde2f71e0a0c2de78b0890d7c07b30 (patch) | |
tree | 6e9fb74faa3a747a46d032d7cf099d139b9219e1 | |
parent | d72bbda34c0e7453337fea9add9276a7fabb4a96 (diff) | |
parent | 29b0a2d8ebc10ef9fa070d5634ab5d18938c935e (diff) |
Merge branch 'master' into feature/gb/gitlab-domains-source
* master:
Improve GitLab client tests
Change GitLab API JWT expire time to 5s
Read the context of api-secret-key file and store it in app config
Improve gitlab client tests
Ensure there is response before defer close it in gitlab.GetVirtualDomain
Document acrguments for gitlab.NewClient
Release 1.12.0
Add HTTP client to consume GitLab internal API for Pages
Add minimal support for the api-secret-key config flag
Conflicts:
internal/source/gitlab/client.go
-rw-r--r-- | CHANGELOG | 6 | ||||
-rw-r--r-- | VERSION | 2 | ||||
-rw-r--r-- | app_config.go | 17 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | internal/source/gitlab/client.go | 143 | ||||
-rw-r--r-- | internal/source/gitlab/client_test.go | 130 | ||||
-rw-r--r-- | internal/source/gitlab/response.go | 21 | ||||
-rw-r--r-- | main.go | 7 |
9 files changed, 315 insertions, 14 deletions
@@ -1,3 +1,9 @@ +v 1.12.0 + +- Add minimal support for the api-secret-key config flag (not yet used) +- Add warnings about secrets given through command-line flags +- Remove Admin gRPC api (was never used) + v 1.11.0 - Refactor domain package and extract disk serving !189 @@ -1 +1 @@ -1.11.0 +1.12.0 diff --git a/app_config.go b/app_config.go index b870626d..f9be6545 100644 --- a/app_config.go +++ b/app_config.go @@ -25,12 +25,13 @@ type appConfig struct { LogFormat string LogVerbose bool - StoreSecret string - GitLabServer string - ClientID string - ClientSecret string - RedirectURI string - SentryDSN string - SentryEnvironment string - CustomHeaders []string + StoreSecret string + GitLabServer string + GitLabAPISecretKey []byte + ClientID string + ClientSecret string + RedirectURI string + SentryDSN string + SentryEnvironment string + CustomHeaders []string } @@ -4,6 +4,7 @@ go 1.12 require ( github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 github.com/getsentry/raven-go v0.1.2 // indirect github.com/golang/mock v1.3.1 @@ -17,6 +17,8 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 h1:roDmqJ4Qes7hrDOsWsMCce0vQHz3xiMPjJ9m4c2eeNs= github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835/go.mod h1:BjL/N0+C+j9uNX+1xcNuM9vdSIcXCZrQZUYbXOFbgN8= diff --git a/internal/source/gitlab/client.go b/internal/source/gitlab/client.go index b2dab32e..9dde7b43 100644 --- a/internal/source/gitlab/client.go +++ b/internal/source/gitlab/client.go @@ -1,9 +1,142 @@ package gitlab -import "context" +import ( + "encoding/json" + "errors" + "net/http" + "net/url" + "time" -// Client is an internal HTTP client used for communication with GitLab -// instance -type Client interface { - Resolve(ctx context.Context, domain string) (*Lookup, int, error) + jwt "github.com/dgrijalva/jwt-go" + + "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport" +) + +// Client is a HTTP client to access Pages internal API +type Client struct { + secretKey []byte + baseURL *url.URL + httpClient *http.Client +} + +var ( + errUnknown = errors.New("Unknown") + errNoContent = errors.New("No Content") + errUnauthorized = errors.New("Unauthorized") + errNotFound = errors.New("Not Found") +) + +// NewClient initializes and returns new Client +// baseUrl is appConfig.GitLabServer +// secretKey is appConfig.GitLabAPISecretKey (not yet implemented) +func NewClient(baseURL string, secretKey []byte) (*Client, error) { + url, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + return &Client{ + secretKey: secretKey, + baseURL: url, + httpClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: httptransport.Transport, + }, + }, nil +} + +// GetVirtualDomain returns VirtualDomain configuration for the given host +func (gc *Client) GetVirtualDomain(host string) (*VirtualDomain, error) { + params := map[string]string{"host": host} + + resp, err := gc.get("/api/v4/internal/pages", params) + if resp != nil { + defer resp.Body.Close() + } + + if err != nil { + return nil, err + } + + var domain VirtualDomain + err = json.NewDecoder(resp.Body).Decode(&domain) + if err != nil { + return nil, err + } + + return &domain, nil +} + +func (gc *Client) get(path string, params map[string]string) (*http.Response, error) { + endpoint, err := gc.endpoint(path, params) + if err != nil { + return nil, err + } + + req, err := gc.request("GET", endpoint) + if err != nil { + return nil, err + } + + resp, err := gc.httpClient.Do(req) + if err != nil { + return nil, err + } + + switch { + case resp.StatusCode == http.StatusOK: + return resp, nil + case resp.StatusCode == http.StatusNoContent: + return resp, errNoContent + case resp.StatusCode == http.StatusUnauthorized: + return resp, errUnauthorized + case resp.StatusCode == http.StatusNotFound: + return resp, errNotFound + default: + return resp, errUnknown + } +} + +func (gc *Client) endpoint(path string, params map[string]string) (*url.URL, error) { + endpoint, err := gc.baseURL.Parse(path) + if err != nil { + return nil, err + } + + values := url.Values{} + for key, value := range params { + values.Add(key, value) + } + endpoint.RawQuery = values.Encode() + + return endpoint, nil +} + +func (gc *Client) request(method string, endpoint *url.URL) (*http.Request, error) { + req, err := http.NewRequest("GET", endpoint.String(), nil) + if err != nil { + return nil, err + } + + token, err := gc.token() + if err != nil { + return nil, err + } + req.Header.Set("Gitlab-Pages-Api-Request", token) + + return req, nil +} + +func (gc *Client) token() (string, error) { + claims := jwt.StandardClaims{ + Issuer: "gitlab-pages", + ExpiresAt: time.Now().Add(5 * time.Second).Unix(), + } + + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(gc.secretKey) + if err != nil { + return "", err + } + + return token, nil } diff --git a/internal/source/gitlab/client_test.go b/internal/source/gitlab/client_test.go new file mode 100644 index 00000000..1d63a590 --- /dev/null +++ b/internal/source/gitlab/client_test.go @@ -0,0 +1,130 @@ +package gitlab + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + jwt "github.com/dgrijalva/jwt-go" +) + +var ( + encodedSecret = "e41rcFh7XBA7sNABWVCe2AZvxMsy6QDtJ8S9Ql1UiN8=" // 32 bytes, base64 encoded +) + +func TestNewValidBaseURL(t *testing.T) { + _, err := NewClient("https://gitlab.com", secretKey()) + require.NoError(t, err) +} + +func TestNewInvalidBaseURL(t *testing.T) { + client, err := NewClient("%", secretKey()) + require.Error(t, err) + require.Nil(t, client) +} + +func TestGetVirtualDomainForErrorResponses(t *testing.T) { + tests := map[int]string{ + http.StatusNoContent: "No Content", + http.StatusUnauthorized: "Unauthorized", + http.StatusNotFound: "Not Found", + } + + for statusCode, expectedError := range tests { + name := fmt.Sprintf("%d %s", statusCode, expectedError) + t.Run(name, func(t *testing.T) { + mux := http.NewServeMux() + + mux.HandleFunc("/api/v4/internal/pages", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, secretKey()) + + actual, err := client.GetVirtualDomain("group.gitlab.io") + + require.EqualError(t, err, expectedError) + require.Nil(t, actual) + }) + } +} + +func TestGetVirtualDomainAuthenticatedRequest(t *testing.T) { + mux := http.NewServeMux() + + mux.HandleFunc("/api/v4/internal/pages", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "GET", r.Method) + require.Equal(t, "group.gitlab.io", r.FormValue("host")) + + validateToken(t, r.Header.Get("Gitlab-Pages-Api-Request")) + + response := `{ + "certificate": "foo", + "key": "bar", + "lookup_paths": [ + { + "project_id": 123, + "access_control": false, + "source": { + "type": "file", + "path": "mygroup/myproject/public/" + }, + "https_only": true, + "prefix": "/myproject/" + } + ] + }` + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, response) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, secretKey()) + + actual, err := client.GetVirtualDomain("group.gitlab.io") + require.NoError(t, err) + + require.Equal(t, "foo", actual.Certificate) + require.Equal(t, "bar", actual.Key) + + lookupPath := actual.LookupPaths[0] + require.Equal(t, 123, lookupPath.ProjectID) + require.Equal(t, false, lookupPath.AccessControl) + require.Equal(t, true, lookupPath.HTTPSOnly) + require.Equal(t, "/myproject/", lookupPath.Prefix) + + require.Equal(t, "file", lookupPath.Source.Type) + require.Equal(t, "mygroup/myproject/public/", lookupPath.Source.Path) +} + +func validateToken(t *testing.T, tokenString string) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return secretKey(), nil + }) + require.NoError(t, err) + + claims, ok := token.Claims.(jwt.MapClaims) + require.True(t, ok) + require.True(t, token.Valid) + require.NotNil(t, claims["exp"]) + require.Equal(t, "gitlab-pages", claims["iss"]) +} + +func secretKey() []byte { + secretKey, _ := base64.StdEncoding.DecodeString(encodedSecret) + return secretKey +} diff --git a/internal/source/gitlab/response.go b/internal/source/gitlab/response.go new file mode 100644 index 00000000..20597362 --- /dev/null +++ b/internal/source/gitlab/response.go @@ -0,0 +1,21 @@ +package gitlab + +// LookupPath represents a lookup path for a GitLab Pages virtual domain +type LookupPath struct { + ProjectID int `json:"project_id,omitempty"` + AccessControl bool `json:"access_control,omitempty"` + HTTPSOnly bool `json:"https_only,omitempty"` + Prefix string `json:"prefix,omitempty"` + Source struct { + Type string `json:"type,omitempty"` + Path string `json:"path,omitempty"` + } +} + +// VirtualDomain represents a GitLab Pages virtual domain +type VirtualDomain struct { + Certificate string `json:"certificate,omitempty"` + Key string `json:"key,omitempty"` + + LookupPaths []LookupPath `json:"lookup_paths"` +} @@ -57,6 +57,7 @@ var ( secret = flag.String("auth-secret", "", "Cookie store hash key, should be at least 32 bytes long.") gitLabAuthServer = flag.String("auth-server", "", "DEPRECATED, use gitlab-server instead. GitLab server, for example https://www.gitlab.com") gitLabServer = flag.String("gitlab-server", "", "GitLab server, for example https://www.gitlab.com") + gitLabAPISecretKey = flag.String("api-secret-key", "", "File with secret key used to authenticate with the GitLab API (NOT YET IMPLEMENTED)") clientID = flag.String("auth-client-id", "", "GitLab application Client ID") clientSecret = flag.String("auth-client-secret", "", "GitLab application Client Secret") redirectURI = flag.String("auth-redirect-uri", "", "GitLab application redirect URI") @@ -143,6 +144,7 @@ func configFromFlags() appConfig { }{ {&config.RootCertificate, *pagesRootCert}, {&config.RootKey, *pagesRootKey}, + {&config.GitLabAPISecretKey, *gitLabAPISecretKey}, } { if file.path != "" { *file.contents = readFile(file.path) @@ -237,9 +239,14 @@ func loadConfig() appConfig { "tls-max-version": *tlsMaxVersion, "use-http-2": config.HTTP2, "gitlab-server": config.GitLabServer, + "api-secret-key": *gitLabAPISecretKey, "auth-redirect-uri": config.RedirectURI, }).Debug("Start daemon with configuration") + if *gitLabAPISecretKey != "" { + log.Warn("api-secret-key parameter is a placeholder for future developments, this option will be ignored.") + } + return config } |