From 9cf40354085f4b4446f06d4d03926dcaa6ab9565 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Fri, 6 Apr 2018 18:23:58 +0300 Subject: [PATCH 01/39] Add support for private projects and authentication with GitLab API --- README.md | 23 + acceptance_test.go | 291 ++++++++ app.go | 18 + app_config.go | 6 + helpers_test.go | 14 + internal/auth/auth.go | 279 ++++++++ internal/auth/auth_test.go | 224 ++++++ internal/domain/domain.go | 51 +- internal/domain/domain_config.go | 7 +- internal/domain/domain_test.go | 1 + internal/domain/map.go | 11 +- internal/httperrors/httperrors.go | 12 + internal/httperrors/httperrors_test.go | 12 + main.go | 51 ++ server.go | 3 +- .../pages/group/private.project/config.json | 1 + .../group/private.project/public/index.html | 1 + vendor/github.com/gorilla/context/LICENSE | 27 + vendor/github.com/gorilla/context/README.md | 10 + vendor/github.com/gorilla/context/context.go | 143 ++++ vendor/github.com/gorilla/context/doc.go | 88 +++ .../github.com/gorilla/securecookie/LICENSE | 27 + .../github.com/gorilla/securecookie/README.md | 80 +++ vendor/github.com/gorilla/securecookie/doc.go | 61 ++ .../github.com/gorilla/securecookie/fuzz.go | 25 + .../gorilla/securecookie/securecookie.go | 646 ++++++++++++++++++ vendor/github.com/gorilla/sessions/LICENSE | 27 + vendor/github.com/gorilla/sessions/README.md | 92 +++ vendor/github.com/gorilla/sessions/doc.go | 198 ++++++ vendor/github.com/gorilla/sessions/go.mod | 6 + vendor/github.com/gorilla/sessions/lex.go | 102 +++ .../github.com/gorilla/sessions/sessions.go | 243 +++++++ vendor/github.com/gorilla/sessions/store.go | 295 ++++++++ vendor/vendor.json | 18 + 34 files changed, 3080 insertions(+), 13 deletions(-) create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/auth_test.go create mode 100644 shared/pages/group/private.project/config.json create mode 100644 shared/pages/group/private.project/public/index.html create mode 100644 vendor/github.com/gorilla/context/LICENSE create mode 100644 vendor/github.com/gorilla/context/README.md create mode 100644 vendor/github.com/gorilla/context/context.go create mode 100644 vendor/github.com/gorilla/context/doc.go create mode 100644 vendor/github.com/gorilla/securecookie/LICENSE create mode 100644 vendor/github.com/gorilla/securecookie/README.md create mode 100644 vendor/github.com/gorilla/securecookie/doc.go create mode 100644 vendor/github.com/gorilla/securecookie/fuzz.go create mode 100644 vendor/github.com/gorilla/securecookie/securecookie.go create mode 100644 vendor/github.com/gorilla/sessions/LICENSE create mode 100644 vendor/github.com/gorilla/sessions/README.md create mode 100644 vendor/github.com/gorilla/sessions/doc.go create mode 100644 vendor/github.com/gorilla/sessions/go.mod create mode 100644 vendor/github.com/gorilla/sessions/lex.go create mode 100644 vendor/github.com/gorilla/sessions/sessions.go create mode 100644 vendor/github.com/gorilla/sessions/store.go diff --git a/README.md b/README.md index 7e2c3cbfe..8ae6b0e4d 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,29 @@ $ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pa This is most useful in dual-stack environments (IPv4+IPv6) where both Gitlab Pages and another HTTP server have to co-exist on the same server. +### GitLab access control + +GitLab access control is configured with properties `auth-client-id`, `auth-client-secret`, `auth-redirect-uri`, `auth-server` and `auth-secret`. Client ID, secret and redirect uri are configured in the GitLab and should match. `auth-server` points to a GitLab instance used for authentication. `auth-redirect-uri` should be `http(s)://pages-domain/auth`. Using HTTPS is _strongly_ encouraged. `auth-secret` is used to encrypt the session cookie, and it should be strong enough. + +Example: +``` +$ make +$ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -auth-client-id -auth-client-secret -auth-redirect-uri https://example.com/auth -auth-secret something-very-secret -auth-server https://gitlab.com +``` + +#### How it works + +1. GitLab pages looks for `access_control`, `private` and `id` fields in `config.json` files + in `pages-root/group/project` directories. +2. For projects that have `access_control` and `private` set to `true` pages will require user to authenticate. +3. When user accesses a project that requires authentication, user will be redirected + to GitLab to log in and grant access for GitLab pages. +4. When user grant's access to GitLab pages, pages will use the OAuth2 `code` to get an access + token which is stored in the user session cookie. +5. Pages will now check user's access to a project with a access token stored in the user + session cookie. This is done via a request to GitLab API with the user's access token. +6. If token is invalidated, user will be redirected again to GitLab to authorize pages again. + ### Enable Prometheus Metrics For monitoring purposes, you can pass the `-metrics-address` flag when starting. diff --git a/acceptance_test.go b/acceptance_test.go index 361cba68e..654fcb698 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "testing" "time" @@ -573,3 +574,293 @@ func TestKnownHostInReverseProxySetupReturns200(t *testing.T) { assert.Equal(t, http.StatusOK, rsp.StatusCode) } } + +func TestWhenAuthIsDisabledPrivateIsPublic(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "") + defer teardown() + + rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + rsp.Body.Close() + assert.Equal(t, http.StatusOK, rsp.StatusCode) +} + +func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server=https://gitlab-example.com", + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + defer rsp.Body.Close() + + assert.Equal(t, http.StatusFound, rsp.StatusCode) + assert.Equal(t, 1, len(rsp.Header["Location"])) + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + assert.Equal(t, "https", url.Scheme) + assert.Equal(t, "gitlab-example.com", url.Host) + assert.Equal(t, "/oauth/authorize", url.Path) + assert.Equal(t, "1", url.Query().Get("client_id")) + assert.Equal(t, "https://gitlab-example.com/auth", url.Query().Get("redirect_uri")) + assert.NotEqual(t, "", url.Query().Get("state")) +} + +func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server=https://gitlab-example.com", + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetPageFromListener(t, httpsListener, "gitlab-example.com", "/auth?error=access_denied") + + require.NoError(t, err) + defer rsp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, rsp.StatusCode) +} +func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server=https://gitlab-example.com", + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + defer rsp.Body.Close() + + // Go to auth page with wrong state will cause failure + authrsp, err := GetPageFromListener(t, httpsListener, "gitlab-example.com", "/auth?code=0&state=0") + + require.NoError(t, err) + defer authrsp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, authrsp.StatusCode) +} + +func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server=https://gitlab-example.com", + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + defer rsp.Body.Close() + + cookie := rsp.Header.Get("Set-Cookie") + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + // Go to auth page with correct state will cause fetching the token + authrsp, err := GetPageFromListenerWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + url.Query().Get("state"), cookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + // Will cause 503 because token endpoint is not available + assert.Equal(t, http.StatusServiceUnavailable, authrsp.StatusCode) +} + +func TestWhenLoginCallbackWithCorrectStateWithEndpointAndAccess(t *testing.T) { + skipUnlessEnabled(t) + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + assert.Equal(t, "POST", r.Method) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "{\"access_token\":\"abc\"}") + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusOK) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + testServer.Start() + defer testServer.Close() + + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server="+testServer.URL, + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + defer rsp.Body.Close() + + cookie := rsp.Header.Get("Set-Cookie") + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + // Go to auth page with correct state will cause fetching the token + authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + url.Query().Get("state"), cookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + // server returns the ticket, user will be redirected to the project page + assert.Equal(t, http.StatusFound, authrsp.StatusCode) + cookie = authrsp.Header.Get("Set-Cookie") + rsp, err = GetRedirectPageWithCookie(t, httpsListener, "group.gitlab-example.com", "private.project/", cookie) + + require.NoError(t, err) + defer rsp.Body.Close() + + // server returns user has access, status will be success + assert.Equal(t, http.StatusOK, rsp.StatusCode) +} + +func TestWhenLoginCallbackWithCorrectStateWithEndpointAndNoAccess(t *testing.T) { + skipUnlessEnabled(t) + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + assert.Equal(t, "POST", r.Method) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "{\"access_token\":\"abc\"}") + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusUnauthorized) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + testServer.Start() + defer testServer.Close() + + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server="+testServer.URL, + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + defer rsp.Body.Close() + + cookie := rsp.Header.Get("Set-Cookie") + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + // Go to auth page with correct state will cause fetching the token + authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + url.Query().Get("state"), cookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + // server returns the ticket, user will be redirected to the project page + assert.Equal(t, http.StatusFound, authrsp.StatusCode) + cookie = authrsp.Header.Get("Set-Cookie") + rsp, err = GetRedirectPageWithCookie(t, httpsListener, "group.gitlab-example.com", "private.project/", cookie) + + require.NoError(t, err) + defer rsp.Body.Close() + + // server returns user has NO access, status will be success + assert.Equal(t, http.StatusUnauthorized, rsp.StatusCode) +} + +func TestWhenLoginCallbackWithCorrectStateWithEndpointButTokenIsInvalid(t *testing.T) { + skipUnlessEnabled(t) + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + assert.Equal(t, "POST", r.Method) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "{\"access_token\":\"abc\"}") + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "{\"error\":\"invalid_token\"}") + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + testServer.Start() + defer testServer.Close() + + teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server="+testServer.URL, + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") + defer teardown() + + rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + + require.NoError(t, err) + defer rsp.Body.Close() + + cookie := rsp.Header.Get("Set-Cookie") + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + // Go to auth page with correct state will cause fetching the token + authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + url.Query().Get("state"), cookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + // server returns the ticket, user will be redirected to the project page + assert.Equal(t, http.StatusFound, authrsp.StatusCode) + cookie = authrsp.Header.Get("Set-Cookie") + rsp, err = GetRedirectPageWithCookie(t, httpsListener, "group.gitlab-example.com", "private.project/", cookie) + + require.NoError(t, err) + defer rsp.Body.Close() + + // server returns token is invalid and removes token from cookie and redirects user back to be redirected for new token + assert.Equal(t, http.StatusFound, rsp.StatusCode) + url, err = url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + assert.Equal(t, "https", url.Scheme) + assert.Equal(t, "group.gitlab-example.com", url.Host) + assert.Equal(t, "/private.project/", url.Path) +} diff --git a/app.go b/app.go index 5527d1f75..68b3d85b7 100644 --- a/app.go +++ b/app.go @@ -19,6 +19,7 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/admin" "gitlab.com/gitlab-org/gitlab-pages/internal/artifact" + "gitlab.com/gitlab-org/gitlab-pages/internal/auth" "gitlab.com/gitlab-org/gitlab-pages/internal/domain" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" "gitlab.com/gitlab-org/gitlab-pages/metrics" @@ -39,6 +40,7 @@ type theApp struct { dm domain.Map lock sync.RWMutex Artifact *artifact.Artifact + Auth *auth.Auth } func (a *theApp) isReady() bool { @@ -138,10 +140,21 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo host, domain := a.getHostAndDomain(r) + if a.Auth.TryAuthenticate(&w, r) { + return + } + if a.tryAuxiliaryHandlers(&w, r, https, host, domain) { return } + // Only for private domains that have access control enabled + if domain.IsAccessControlEnabled(r) && domain.IsPrivate(r) { + if a.Auth.CheckAuthentication(&w, r, domain.GetID(r)) { + return + } + } + // Serve static file, applying CORS headers if necessary if a.DisableCrossOriginRequests { domain.ServeHTTP(&w, r) @@ -291,6 +304,11 @@ func runApp(config appConfig) { a.Artifact = artifact.New(config.ArtifactsServer, config.ArtifactsServerTimeout, config.Domain) } + if config.ClientID != "" { + a.Auth = auth.New(config.Domain, config.StoreSecret, config.ClientID, config.ClientSecret, + config.RedirectURI, config.GitLabServer) + } + configureLogging(config.LogFormat, config.LogVerbose) if err := mimedb.LoadTypes(); err != nil { diff --git a/app_config.go b/app_config.go index f2aa90cdd..ab8cc264c 100644 --- a/app_config.go +++ b/app_config.go @@ -25,4 +25,10 @@ type appConfig struct { LogFormat string LogVerbose bool + + StoreSecret string + GitLabServer string + ClientID string + ClientSecret string + RedirectURI string } diff --git a/helpers_test.go b/helpers_test.go index ccbbb6e31..1de79a352 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -248,11 +248,18 @@ func getPagesDaemonArgs(t *testing.T) []string { // Does a HTTP(S) GET against the listener specified, setting a fake // Host: and constructing the URL from the listener and the URL suffix. func GetPageFromListener(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) { + return GetPageFromListenerWithCookie(t, spec, host, urlsuffix, "") +} + +func GetPageFromListenerWithCookie(t *testing.T, spec ListenSpec, host, urlsuffix string, cookie string) (*http.Response, error) { url := spec.URL(urlsuffix) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } + if cookie != "" { + req.Header.Set("Cookie", cookie) + } req.Host = host @@ -279,11 +286,18 @@ func DoPagesRequest(t *testing.T, req *http.Request) (*http.Response, error) { } func GetRedirectPage(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) { + return GetRedirectPageWithCookie(t, spec, host, urlsuffix, "") +} + +func GetRedirectPageWithCookie(t *testing.T, spec ListenSpec, host, urlsuffix string, cookie string) (*http.Response, error) { url := spec.URL(urlsuffix) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } + if cookie != "" { + req.Header.Set("Cookie", cookie) + } req.Host = host diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 000000000..d5600d323 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,279 @@ +package auth + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" +) + +const ( + apiURLTemplate = "%s/api/v4/projects/%d?access_token=%s" + authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" + tokenURLTemplate = "%s/oauth/token" + tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" + callbackPath = "/auth" +) + +// Auth handles authenticating users with GitLab API +type Auth struct { + clientID string + clientSecret string + redirectURI string + gitLabServer string + store *sessions.CookieStore + apiClient *http.Client +} + +type tokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` +} + +type errorResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) bool { + + // Create or get session + session, err := a.store.Get(r, "gitlab-pages") + + if err != nil { + // Save cookie again + session.Save(r, w) + http.Redirect(w, r, getRequestAddress(r), 302) + return true + } + + return false +} + +func (a *Auth) getSession(r *http.Request) *sessions.Session { + session, _ := a.store.Get(r, "gitlab-pages") + return session +} + +// TryAuthenticate tries to authenticate user and fetch access token if request is a callback to auth +func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { + + if a == nil { + return false + } + + if a.checkSession(w, r) { + return true + } + + session := a.getSession(r) + + // If callback from authentication and the state matches + if r.URL.Path != callbackPath { + return false + } + + // If callback is not successful + errorParam := r.URL.Query().Get("error") + if errorParam != "" { + httperrors.Serve401(w) + return true + } + + if verifyCodeAndStateGiven(r) { + + if !validateState(r, session) { + // State is NOT ok + httperrors.Serve401(w) + return true + } + + // Fetch access token with authorization code + token, err := a.fetchAccessToken(r.URL.Query().Get("code")) + + // Fetching token not OK + if err != nil { + httperrors.Serve503(w) + return true + } + + // Store access token + session.Values["access_token"] = token.AccessToken + session.Save(r, w) + + // Redirect back to requested URI + http.Redirect(w, r, session.Values["uri"].(string), 302) + + return true + } + + return false +} + +func getRequestAddress(r *http.Request) string { + if r.TLS != nil { + return "https://" + r.Host + r.RequestURI + } + return "http://" + r.Host + r.RequestURI +} + +func validateState(r *http.Request, session *sessions.Session) bool { + state := r.URL.Query().Get("state") + if state == "" { + // No state param + return false + } + + // Check state + if session.Values["state"] == nil || session.Values["state"].(string) != state { + // State does not match + return false + } + + // State ok + return true +} + +func verifyCodeAndStateGiven(r *http.Request) bool { + return r.URL.Query().Get("code") != "" && r.URL.Query().Get("state") != "" +} + +func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { + token := tokenResponse{} + + // Prepare request + url := fmt.Sprintf(tokenURLTemplate, a.gitLabServer) + content := fmt.Sprintf(tokenContentTemplate, a.clientID, a.clientSecret, code, a.redirectURI) + req, err := http.NewRequest("POST", url, strings.NewReader(content)) + + if err != nil { + return token, err + } + + // Request token + resp, err := a.apiClient.Do(req) + + if err != nil { + return token, err + } + + if resp.StatusCode != 200 { + return token, errors.New("response was not OK") + } + + // Parse response + body, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + err = json.Unmarshal(body, &token) + if err != nil { + return token, err + } + + return token, nil +} + +// CheckAuthentication checks if user is authenticated and has access to the project +func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID int) bool { + + if a == nil { + return false + } + + if a.checkSession(w, r) { + return true + } + + session := a.getSession(r) + + // If no access token redirect to OAuth login page + if session.Values["access_token"] == nil { + + // Generate state hash and store requested address + state := base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(16)) + session.Values["state"] = state + session.Values["uri"] = getRequestAddress(r) + session.Save(r, w) + + // Redirect to OAuth login + url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state) + http.Redirect(w, r, url, 302) + + return true + } + + // Access token exists, authorize request + url := fmt.Sprintf(apiURLTemplate, a.gitLabServer, projectID, session.Values["access_token"].(string)) + resp, err := a.apiClient.Get(url) + + if checkResponseForInvalidToken(resp, err) { + + // Invalidate access token and redirect back for refreshing and re-authenticating + delete(session.Values, "access_token") + session.Save(r, w) + + http.Redirect(w, r, getRequestAddress(r), 302) + + return true + } + + if err != nil || resp.StatusCode != 200 { + httperrors.Serve401(w) + return true + } + + return false +} + +func checkResponseForInvalidToken(resp *http.Response, err error) bool { + if err == nil && resp.StatusCode == 401 { + errResp := errorResponse{} + + // Parse response + body, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + err = json.Unmarshal(body, &errResp) + if err != nil { + return false + } + + if errResp.Error == "invalid_token" { + // Token is invalid + return true + } + } + + return false +} + +// New when authentication supported this will be used to create authentication handler +func New(pagesDomain string, storeSecret string, clientID string, clientSecret string, + redirectURI string, gitLabServer string) *Auth { + + store := sessions.NewCookieStore([]byte(storeSecret)) + + store.Options = &sessions.Options{ + Path: "/", + Domain: pagesDomain, + } + + return &Auth{ + clientID: clientID, + clientSecret: clientSecret, + redirectURI: redirectURI, + gitLabServer: strings.TrimRight(gitLabServer, "/"), + store: store, + apiClient: &http.Client{Timeout: 5 * time.Second}, + } +} diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 000000000..e8a956628 --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,224 @@ +package auth_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gorilla/sessions" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-pages/internal/auth" +) + +func TestTryAuthenticate(t *testing.T) { + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + "http://gitlab-example.com") + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/something/else") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + assert.Equal(t, false, auth.TryAuthenticate(result, r)) +} + +func TestTryAuthenticateWithError(t *testing.T) { + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + "http://gitlab-example.com") + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?error=access_denied") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + assert.Equal(t, true, auth.TryAuthenticate(result, r)) + assert.Equal(t, 401, result.Code) +} + +func TestTryAuthenticateWithCodeButInvalidState(t *testing.T) { + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + "http://gitlab-example.com") + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=invalid") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["state"] = "state" + session.Save(r, result) + + assert.Equal(t, true, auth.TryAuthenticate(result, r)) + assert.Equal(t, 401, result.Code) +} + +func TestTryAuthenticateWithCodeAndState(t *testing.T) { + apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + assert.Equal(t, "POST", r.Method) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "{\"access_token\":\"abc\"}") + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusOK) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + apiServer.Start() + defer apiServer.Close() + + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + apiServer.URL) + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["uri"] = "http://pages.gitlab-example.com/project/" + session.Values["state"] = "state" + session.Save(r, result) + + assert.Equal(t, true, auth.TryAuthenticate(result, r)) + assert.Equal(t, 302, result.Code) + assert.Equal(t, "http://pages.gitlab-example.com/project/", result.Header().Get("Location")) +} + +func TestCheckAuthenticationWhenAccess(t *testing.T) { + apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusOK) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + apiServer.Start() + defer apiServer.Close() + + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + apiServer.URL) + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["access_token"] = "abc" + session.Save(r, result) + + assert.Equal(t, false, auth.CheckAuthentication(result, r, 1000)) + assert.Equal(t, 200, result.Code) +} + +func TestCheckAuthenticationWhenNoAccess(t *testing.T) { + apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusUnauthorized) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + apiServer.Start() + defer apiServer.Close() + + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + apiServer.URL) + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["access_token"] = "abc" + session.Save(r, result) + + assert.Equal(t, true, auth.CheckAuthentication(result, r, 1000)) + assert.Equal(t, 401, result.Code) +} + +func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { + apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v4/projects/1000": + assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "{\"error\":\"invalid_token\"}") + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + apiServer.Start() + defer apiServer.Close() + + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + apiServer.URL) + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["access_token"] = "abc" + session.Save(r, result) + + assert.Equal(t, true, auth.CheckAuthentication(result, r, 1000)) + assert.Equal(t, 302, result.Code) +} diff --git a/internal/domain/domain.go b/internal/domain/domain.go index f50dacd8a..3aef96f52 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -24,7 +24,10 @@ type locationDirectoryError struct { } type project struct { - HTTPSOnly bool + HTTPSOnly bool + Private bool + AccessControl bool + ID int } type projects map[string]*project @@ -97,6 +100,14 @@ func setContentType(w http.ResponseWriter, fullPath string) { } } +func (d *D) getProject(r *http.Request) *project { + split := strings.SplitN(r.URL.Path, "/", 3) + if len(split) < 2 { + return nil + } + return d.projects[split[1]] +} + // IsHTTPSOnly figures out if the request should be handled with HTTPS // only by looking at group and project level config. func (d *D) IsHTTPSOnly(r *http.Request) bool { @@ -104,20 +115,48 @@ func (d *D) IsHTTPSOnly(r *http.Request) bool { return d.config.HTTPSOnly } - split := strings.SplitN(r.URL.Path, "/", 3) - if len(split) < 2 { - return false + project := d.getProject(r) + + if project != nil { + return project.HTTPSOnly + } + + return false +} + +// IsAccessControlEnabled figures out if the request is to a project that has access control enabled +func (d *D) IsAccessControlEnabled(r *http.Request) bool { + project := d.getProject(r) + + if project != nil { + return project.AccessControl } - project := d.projects[split[1]] + return false +} + +// IsPrivate figures out if the request is to a project that needs user to sign in +func (d *D) IsPrivate(r *http.Request) bool { + project := d.getProject(r) if project != nil { - return project.HTTPSOnly + return project.Private } return false } +// GetID figures out what is the ID of the project user tries to access +func (d *D) GetID(r *http.Request) int { + project := d.getProject(r) + + if project != nil { + return project.ID + } + + return -1 +} + func (d *D) serveFile(w http.ResponseWriter, r *http.Request, origPath string) error { fullPath := handleGZip(w, r, origPath) diff --git a/internal/domain/domain_config.go b/internal/domain/domain_config.go index ed7e0820f..1acb3461a 100644 --- a/internal/domain/domain_config.go +++ b/internal/domain/domain_config.go @@ -15,8 +15,11 @@ type domainConfig struct { } type domainsConfig struct { - Domains []domainConfig - HTTPSOnly bool `json:"https_only"` + Domains []domainConfig + HTTPSOnly bool `json:"https_only"` + Private bool `json:"private"` + ID int `json:"id"` + AccessControl bool `json:"access_control"` } func (c *domainConfig) Valid(rootDomain string) bool { diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index 130501d61..4cbc6cc20 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -38,6 +38,7 @@ func TestGroupServeHTTP(t *testing.T) { assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir/", nil, "project-subsubdir") assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project2/", nil, "project2-main") assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project2/index.html", nil, "project2-main") + assert.HTTPRedirect(t, testGroup.ServeHTTP, "GET", "http://group.test.io/private.project/", nil) assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io//about.gitlab.com/%2e%2e", nil) assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink", nil) assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink/index.html", nil) diff --git a/internal/domain/map.go b/internal/domain/map.go index 943f5c20b..a6537bda6 100644 --- a/internal/domain/map.go +++ b/internal/domain/map.go @@ -32,7 +32,7 @@ func (dm Map) addDomain(rootDomain, group, projectName string, config *domainCon dm[domainName] = newDomain } -func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool) { +func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool, private bool, accessControl bool, id int) { domainName := strings.ToLower(group + "." + rootDomain) groupDomain := dm[domainName] @@ -44,7 +44,10 @@ func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly } groupDomain.projects[projectName] = &project{ - HTTPSOnly: httpsOnly, + HTTPSOnly: httpsOnly, + Private: private, + AccessControl: accessControl, + ID: id, } dm[domainName] = groupDomain @@ -55,11 +58,11 @@ func (dm Map) readProjectConfig(rootDomain string, group, projectName string, co // This is necessary to preserve the previous behaviour where a // group domain is created even if no config.json files are // loaded successfully. Is it safe to remove this? - dm.updateGroupDomain(rootDomain, group, projectName, false) + dm.updateGroupDomain(rootDomain, group, projectName, false, false, false, -1) return } - dm.updateGroupDomain(rootDomain, group, projectName, config.HTTPSOnly) + dm.updateGroupDomain(rootDomain, group, projectName, config.HTTPSOnly, config.Private, config.AccessControl, config.ID) for _, domainConfig := range config.Domains { config := domainConfig // domainConfig is reused for each loop iteration diff --git a/internal/httperrors/httperrors.go b/internal/httperrors/httperrors.go index 92413e07b..1ae5224b0 100644 --- a/internal/httperrors/httperrors.go +++ b/internal/httperrors/httperrors.go @@ -14,6 +14,13 @@ type content struct { } var ( + content401 = content{ + http.StatusUnauthorized, + "Unauthorized (401)", + "401", + "You don't have permission to access the resource.", + `

The resource that you are attempting to access is protected and you don't have the necessary permissions to view it.

`, + } content404 = content{ http.StatusNotFound, "The page you're looking for could not be found (404)", @@ -155,6 +162,11 @@ func serveErrorPage(w http.ResponseWriter, c content) { fmt.Fprintln(w, generateErrorHTML(c)) } +// Serve401 returns a 401 error response / HTML page to the http.ResponseWriter +func Serve401(w http.ResponseWriter) { + serveErrorPage(w, content401) +} + // Serve404 returns a 404 error response / HTML page to the http.ResponseWriter func Serve404(w http.ResponseWriter) { serveErrorPage(w, content404) diff --git a/internal/httperrors/httperrors_test.go b/internal/httperrors/httperrors_test.go index 1a79d8506..be532dfee 100644 --- a/internal/httperrors/httperrors_test.go +++ b/internal/httperrors/httperrors_test.go @@ -68,6 +68,18 @@ func TestServeErrorPage(t *testing.T) { assert.Equal(t, w.Status(), testingContent.status) } +func TestServe401(t *testing.T) { + w := newTestResponseWriter(httptest.NewRecorder()) + Serve401(w) + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") + assert.Equal(t, w.Header().Get("X-Content-Type-Options"), "nosniff") + assert.Equal(t, w.Status(), content401.status) + assert.Contains(t, w.Content(), content401.title) + assert.Contains(t, w.Content(), content401.statusString) + assert.Contains(t, w.Content(), content401.header) + assert.Contains(t, w.Content(), content401.subHeader) +} + func TestServe404(t *testing.T) { w := newTestResponseWriter(httptest.NewRecorder()) Serve404(w) diff --git a/main.go b/main.go index 30f594ead..20979aa86 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,11 @@ var ( adminHTTPSListener = flag.String("admin-https-listener", "", "The listen address for the admin API HTTPS listener (optional)") adminHTTPSCert = flag.String("admin-https-cert", "", "The path to the certificate file for the admin API (optional)") adminHTTPSKey = flag.String("admin-https-key", "", "The path to the key file for the admin API (optional)") + secret = flag.String("auth-secret", "", "Cookie store hash key, should be at least 32 bytes long.") + gitLabServer = flag.String("auth-server", "", "GitLab server, for example https://www.gitlab.com") + 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") disableCrossOriginRequests = flag.Bool("disable-cross-origin-requests", false, "Disable cross-origin requests") @@ -58,6 +63,12 @@ var ( var ( errArtifactSchemaUnsupported = errors.New("artifacts-server scheme must be either http:// or https://") errArtifactsServerTimeoutValue = errors.New("artifacts-server-timeout must be greater than or equal to 1") + + errSecretNotDefined = errors.New("auth-secret must be defined if authentication is supported") + errClientIDNotDefined = errors.New("auth-client-id must be defined if authentication is supported") + errClientSecretNotDefined = errors.New("auth-client-secret must be defined if authentication is supported") + errGitLabServerNotDefined = errors.New("auth-server must be defined if authentication is supported") + errRedirectURINotDefined = errors.New("auth-redirect-uri must be defined if authentication is supported") ) func configFromFlags() appConfig { @@ -107,9 +118,44 @@ func configFromFlags() appConfig { config.ArtifactsServerTimeout = *artifactsServerTimeout config.ArtifactsServer = *artifactsServer } + + checkAuthenticationConfig(config) + + config.StoreSecret = *secret + config.ClientID = *clientID + config.ClientSecret = *clientSecret + config.GitLabServer = *gitLabServer + config.RedirectURI = *redirectURI + return config } +func checkAuthenticationConfig(config appConfig) { + if *secret != "" || *clientID != "" || *clientSecret != "" || + *gitLabServer != "" || *redirectURI != "" { + // Check all auth params are valid + assertAuthConfig() + } +} + +func assertAuthConfig() { + if *secret == "" { + log.Fatal(errSecretNotDefined) + } + if *clientID == "" { + log.Fatal(errClientIDNotDefined) + } + if *clientSecret == "" { + log.Fatal(errClientSecretNotDefined) + } + if *gitLabServer == "" { + log.Fatal(errGitLabServerNotDefined) + } + if *redirectURI == "" { + log.Fatal(errRedirectURINotDefined) + } +} + func appMain() { var showVersion = flag.Bool("version", false, "Show version") @@ -160,6 +206,11 @@ func appMain() { "root-key": *pagesRootCert, "status_path": config.StatusPath, "use-http-2": config.HTTP2, + "auth-secret": config.StoreSecret, + "auth-server": config.GitLabServer, + "auth-client-id": config.ClientID, + "auth-client-secret": config.ClientSecret, + "auth-redirect-uri": config.RedirectURI, }).Debug("Start daemon with configuration") for _, cs := range [][]io.Closer{ diff --git a/server.go b/server.go index cfd6b9939..120088b02 100644 --- a/server.go +++ b/server.go @@ -8,6 +8,7 @@ import ( "os" "time" + "github.com/gorilla/context" "golang.org/x/net/http2" ) @@ -29,7 +30,7 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { func listenAndServe(fd uintptr, handler http.HandlerFunc, useHTTP2 bool, tlsConfig *tls.Config) error { // create server - server := &http.Server{Handler: handler, TLSConfig: tlsConfig} + server := &http.Server{Handler: context.ClearHandler(handler), TLSConfig: tlsConfig} if useHTTP2 { err := http2.ConfigureServer(server, &http2.Server{}) diff --git a/shared/pages/group/private.project/config.json b/shared/pages/group/private.project/config.json new file mode 100644 index 000000000..9b9b3f151 --- /dev/null +++ b/shared/pages/group/private.project/config.json @@ -0,0 +1 @@ +{ "domains": [], "id": 1000, "private": true, "access_control": true } diff --git a/shared/pages/group/private.project/public/index.html b/shared/pages/group/private.project/public/index.html new file mode 100644 index 000000000..c8c6761a5 --- /dev/null +++ b/shared/pages/group/private.project/public/index.html @@ -0,0 +1 @@ +private \ No newline at end of file diff --git a/vendor/github.com/gorilla/context/LICENSE b/vendor/github.com/gorilla/context/LICENSE new file mode 100644 index 000000000..0e5fb8728 --- /dev/null +++ b/vendor/github.com/gorilla/context/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/context/README.md b/vendor/github.com/gorilla/context/README.md new file mode 100644 index 000000000..08f86693b --- /dev/null +++ b/vendor/github.com/gorilla/context/README.md @@ -0,0 +1,10 @@ +context +======= +[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) + +gorilla/context is a general purpose registry for global request variables. + +> Note: gorilla/context, having been born well before `context.Context` existed, does not play well +> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`. + +Read the full documentation here: http://www.gorillatoolkit.org/pkg/context diff --git a/vendor/github.com/gorilla/context/context.go b/vendor/github.com/gorilla/context/context.go new file mode 100644 index 000000000..81cb128b1 --- /dev/null +++ b/vendor/github.com/gorilla/context/context.go @@ -0,0 +1,143 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package context + +import ( + "net/http" + "sync" + "time" +) + +var ( + mutex sync.RWMutex + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) +) + +// Set stores a value for a given key in a given request. +func Set(r *http.Request, key, val interface{}) { + mutex.Lock() + if data[r] == nil { + data[r] = make(map[interface{}]interface{}) + datat[r] = time.Now().Unix() + } + data[r][key] = val + mutex.Unlock() +} + +// Get returns a value stored for a given key in a given request. +func Get(r *http.Request, key interface{}) interface{} { + mutex.RLock() + if ctx := data[r]; ctx != nil { + value := ctx[key] + mutex.RUnlock() + return value + } + mutex.RUnlock() + return nil +} + +// GetOk returns stored value and presence state like multi-value return of map access. +func GetOk(r *http.Request, key interface{}) (interface{}, bool) { + mutex.RLock() + if _, ok := data[r]; ok { + value, ok := data[r][key] + mutex.RUnlock() + return value, ok + } + mutex.RUnlock() + return nil, false +} + +// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests. +func GetAll(r *http.Request) map[interface{}]interface{} { + mutex.RLock() + if context, ok := data[r]; ok { + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result + } + mutex.RUnlock() + return nil +} + +// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if +// the request was registered. +func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { + mutex.RLock() + context, ok := data[r] + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result, ok +} + +// Delete removes a value stored for a given key in a given request. +func Delete(r *http.Request, key interface{}) { + mutex.Lock() + if data[r] != nil { + delete(data[r], key) + } + mutex.Unlock() +} + +// Clear removes all values stored for a given request. +// +// This is usually called by a handler wrapper to clean up request +// variables at the end of a request lifetime. See ClearHandler(). +func Clear(r *http.Request) { + mutex.Lock() + clear(r) + mutex.Unlock() +} + +// clear is Clear without the lock. +func clear(r *http.Request) { + delete(data, r) + delete(datat, r) +} + +// Purge removes request data stored for longer than maxAge, in seconds. +// It returns the amount of requests removed. +// +// If maxAge <= 0, all request data is removed. +// +// This is only used for sanity check: in case context cleaning was not +// properly set some request data can be kept forever, consuming an increasing +// amount of memory. In case this is detected, Purge() must be called +// periodically until the problem is fixed. +func Purge(maxAge int) int { + mutex.Lock() + count := 0 + if maxAge <= 0 { + count = len(data) + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) + } else { + min := time.Now().Unix() - int64(maxAge) + for r := range data { + if datat[r] < min { + clear(r) + count++ + } + } + } + mutex.Unlock() + return count +} + +// ClearHandler wraps an http.Handler and clears request values at the end +// of a request lifetime. +func ClearHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer Clear(r) + h.ServeHTTP(w, r) + }) +} diff --git a/vendor/github.com/gorilla/context/doc.go b/vendor/github.com/gorilla/context/doc.go new file mode 100644 index 000000000..448d1bfca --- /dev/null +++ b/vendor/github.com/gorilla/context/doc.go @@ -0,0 +1,88 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package context stores values shared during a request lifetime. + +Note: gorilla/context, having been born well before `context.Context` existed, +does not play well > with the shallow copying of the request that +[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) +(added to net/http Go 1.7 onwards) performs. You should either use *just* +gorilla/context, or moving forward, the new `http.Request.Context()`. + +For example, a router can set variables extracted from the URL and later +application handlers can access those values, or it can be used to store +sessions values to be saved at the end of a request. There are several +others common uses. + +The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: + + http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 + +Here's the basic usage: first define the keys that you will need. The key +type is interface{} so a key can be of any type that supports equality. +Here we define a key using a custom int type to avoid name collisions: + + package foo + + import ( + "github.com/gorilla/context" + ) + + type key int + + const MyKey key = 0 + +Then set a variable. Variables are bound to an http.Request object, so you +need a request instance to set a value: + + context.Set(r, MyKey, "bar") + +The application can later access the variable using the same key you provided: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // val is "bar". + val := context.Get(r, foo.MyKey) + + // returns ("bar", true) + val, ok := context.GetOk(r, foo.MyKey) + // ... + } + +And that's all about the basic usage. We discuss some other ideas below. + +Any type can be stored in the context. To enforce a given type, make the key +private and wrap Get() and Set() to accept and return values of a specific +type: + + type key int + + const mykey key = 0 + + // GetMyKey returns a value for this package from the request values. + func GetMyKey(r *http.Request) SomeType { + if rv := context.Get(r, mykey); rv != nil { + return rv.(SomeType) + } + return nil + } + + // SetMyKey sets a value for this package in the request values. + func SetMyKey(r *http.Request, val SomeType) { + context.Set(r, mykey, val) + } + +Variables must be cleared at the end of a request, to remove all values +that were stored. This can be done in an http.Handler, after a request was +served. Just call Clear() passing the request: + + context.Clear(r) + +...or use ClearHandler(), which conveniently wraps an http.Handler to clear +variables at the end of a request lifetime. + +The Routers from the packages gorilla/mux and gorilla/pat call Clear() +so if you are using either of them you don't need to clear the context manually. +*/ +package context diff --git a/vendor/github.com/gorilla/securecookie/LICENSE b/vendor/github.com/gorilla/securecookie/LICENSE new file mode 100644 index 000000000..0e5fb8728 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md new file mode 100644 index 000000000..aa7bd1a5b --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/README.md @@ -0,0 +1,80 @@ +securecookie +============ +[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge) + + +securecookie encodes and decodes authenticated and optionally encrypted +cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. It is still +recommended that sensitive data not be stored in cookies, and that HTTPS be used +to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). + +## Examples + +To use it, first create a new SecureCookie instance: + +```go +// Hash keys should be at least 32 bytes long +var hashKey = []byte("very-secret") +// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. +// Shorter keys may weaken the encryption used. +var blockKey = []byte("a-lot-secret") +var s = securecookie.New(hashKey, blockKey) +``` + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + +```go +func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + Secure: true, + HttpOnly: true, + } + http.SetCookie(w, cookie) + } +} +``` + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + +```go +func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } +} +``` + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using `encoding/gob`. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. An optional JSON encoder that uses `encoding/json` is +available for types compatible with JSON. + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/securecookie/doc.go b/vendor/github.com/gorilla/securecookie/doc.go new file mode 100644 index 000000000..ae89408d9 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/doc.go @@ -0,0 +1,61 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package securecookie encodes and decodes authenticated and optionally +encrypted cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. + +To use it, first create a new SecureCookie instance: + + var hashKey = []byte("very-secret") + var blockKey = []byte("a-lot-secret") + var s = securecookie.New(hashKey, blockKey) + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + + func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + } + } + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + + func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } + } + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using encoding/gob. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. +*/ +package securecookie diff --git a/vendor/github.com/gorilla/securecookie/fuzz.go b/vendor/github.com/gorilla/securecookie/fuzz.go new file mode 100644 index 000000000..e4d0534e4 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/fuzz.go @@ -0,0 +1,25 @@ +// +build gofuzz + +package securecookie + +var hashKey = []byte("very-secret12345") +var blockKey = []byte("a-lot-secret1234") +var s = New(hashKey, blockKey) + +type Cookie struct { + B bool + I int + S string +} + +func Fuzz(data []byte) int { + datas := string(data) + var c Cookie + if err := s.Decode("fuzz", datas, &c); err != nil { + return 0 + } + if _, err := s.Encode("fuzz", c); err != nil { + panic(err) + } + return 1 +} diff --git a/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go new file mode 100644 index 000000000..cd4e0976d --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/securecookie.go @@ -0,0 +1,646 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package securecookie + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/gob" + "encoding/json" + "fmt" + "hash" + "io" + "strconv" + "strings" + "time" +) + +// Error is the interface of all errors returned by functions in this library. +type Error interface { + error + + // IsUsage returns true for errors indicating the client code probably + // uses this library incorrectly. For example, the client may have + // failed to provide a valid hash key, or may have failed to configure + // the Serializer adequately for encoding value. + IsUsage() bool + + // IsDecode returns true for errors indicating that a cookie could not + // be decoded and validated. Since cookies are usually untrusted + // user-provided input, errors of this type should be expected. + // Usually, the proper action is simply to reject the request. + IsDecode() bool + + // IsInternal returns true for unexpected errors occurring in the + // securecookie implementation. + IsInternal() bool + + // Cause, if it returns a non-nil value, indicates that this error was + // propagated from some underlying library. If this method returns nil, + // this error was raised directly by this library. + // + // Cause is provided principally for debugging/logging purposes; it is + // rare that application logic should perform meaningfully different + // logic based on Cause. See, for example, the caveats described on + // (MultiError).Cause(). + Cause() error +} + +// errorType is a bitmask giving the error type(s) of an cookieError value. +type errorType int + +const ( + usageError = errorType(1 << iota) + decodeError + internalError +) + +type cookieError struct { + typ errorType + msg string + cause error +} + +func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } +func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } +func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } + +func (e cookieError) Cause() error { return e.cause } + +func (e cookieError) Error() string { + parts := []string{"securecookie: "} + if e.msg == "" { + parts = append(parts, "error") + } else { + parts = append(parts, e.msg) + } + if c := e.Cause(); c != nil { + parts = append(parts, " - caused by: ", c.Error()) + } + return strings.Join(parts, "") +} + +var ( + errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} + + errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} + errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} + errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} + errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} + + errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} + errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} + errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} + errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} + errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} + errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} + errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} + + // ErrMacInvalid indicates that cookie decoding failed because the HMAC + // could not be extracted and verified. Direct use of this error + // variable is deprecated; it is public only for legacy compatibility, + // and may be privatized in the future, as it is rarely useful to + // distinguish between this error and other Error implementations. + ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} +) + +// Codec defines an interface to encode and decode cookie values. +type Codec interface { + Encode(name string, value interface{}) (string, error) + Decode(name, value string, dst interface{}) error +} + +// New returns a new SecureCookie. +// +// hashKey is required, used to authenticate values using HMAC. Create it using +// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes. +// +// blockKey is optional, used to encrypt values. Create it using +// GenerateRandomKey(). The key length must correspond to the block size +// of the encryption algorithm. For AES, used by default, valid lengths are +// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. +// The default encoder used for cookie serialization is encoding/gob. +// +// Note that keys created using GenerateRandomKey() are not automatically +// persisted. New keys will be created when the application is restarted, and +// previously issued cookies will not be able to be decoded. +func New(hashKey, blockKey []byte) *SecureCookie { + s := &SecureCookie{ + hashKey: hashKey, + blockKey: blockKey, + hashFunc: sha256.New, + maxAge: 86400 * 30, + maxLength: 4096, + sz: GobEncoder{}, + } + if hashKey == nil { + s.err = errHashKeyNotSet + } + if blockKey != nil { + s.BlockFunc(aes.NewCipher) + } + return s +} + +// SecureCookie encodes and decodes authenticated and optionally encrypted +// cookie values. +type SecureCookie struct { + hashKey []byte + hashFunc func() hash.Hash + blockKey []byte + block cipher.Block + maxLength int + maxAge int64 + minAge int64 + err error + sz Serializer + // For testing purposes, the function that returns the current timestamp. + // If not set, it will use time.Now().UTC().Unix(). + timeFunc func() int64 +} + +// Serializer provides an interface for providing custom serializers for cookie +// values. +type Serializer interface { + Serialize(src interface{}) ([]byte, error) + Deserialize(src []byte, dst interface{}) error +} + +// GobEncoder encodes cookie values using encoding/gob. This is the simplest +// encoder and can handle complex types via gob.Register. +type GobEncoder struct{} + +// JSONEncoder encodes cookie values using encoding/json. Users who wish to +// encode complex types need to satisfy the json.Marshaller and +// json.Unmarshaller interfaces. +type JSONEncoder struct{} + +// NopEncoder does not encode cookie values, and instead simply accepts a []byte +// (as an interface{}) and returns a []byte. This is particularly useful when +// you encoding an object upstream and do not wish to re-encode it. +type NopEncoder struct{} + +// MaxLength restricts the maximum length, in bytes, for the cookie value. +// +// Default is 4096, which is the maximum value accepted by Internet Explorer. +func (s *SecureCookie) MaxLength(value int) *SecureCookie { + s.maxLength = value + return s +} + +// MaxAge restricts the maximum age, in seconds, for the cookie value. +// +// Default is 86400 * 30. Set it to 0 for no restriction. +func (s *SecureCookie) MaxAge(value int) *SecureCookie { + s.maxAge = int64(value) + return s +} + +// MinAge restricts the minimum age, in seconds, for the cookie value. +// +// Default is 0 (no restriction). +func (s *SecureCookie) MinAge(value int) *SecureCookie { + s.minAge = int64(value) + return s +} + +// HashFunc sets the hash function used to create HMAC. +// +// Default is crypto/sha256.New. +func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { + s.hashFunc = f + return s +} + +// BlockFunc sets the encryption function used to create a cipher.Block. +// +// Default is crypto/aes.New. +func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { + if s.blockKey == nil { + s.err = errBlockKeyNotSet + } else if block, err := f(s.blockKey); err == nil { + s.block = block + } else { + s.err = cookieError{cause: err, typ: usageError} + } + return s +} + +// Encoding sets the encoding/serialization method for cookies. +// +// Default is encoding/gob. To encode special structures using encoding/gob, +// they must be registered first using gob.Register(). +func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { + s.sz = sz + + return s +} + +// Encode encodes a cookie value. +// +// It serializes, optionally encrypts, signs with a message authentication code, +// and finally encodes the value. +// +// The name argument is the cookie name. It is stored with the encoded value. +// The value argument is the value to be encoded. It can be any value that can +// be encoded using the currently selected serializer; see SetSerializer(). +// +// It is the client's responsibility to ensure that value, when encoded using +// the current serialization/encryption settings on s and then base64-encoded, +// is shorter than the maximum permissible length. +func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { + if s.err != nil { + return "", s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return "", s.err + } + var err error + var b []byte + // 1. Serialize. + if b, err = s.sz.Serialize(value); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + // 2. Encrypt (optional). + if s.block != nil { + if b, err = encrypt(s.block, b); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + } + b = encode(b) + // 3. Create MAC for "name|date|value". Extra pipe to be used later. + b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) + mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) + // Append mac, remove name. + b = append(b, mac...)[len(name)+1:] + // 4. Encode to base64. + b = encode(b) + // 5. Check length. + if s.maxLength != 0 && len(b) > s.maxLength { + return "", errEncodedValueTooLong + } + // Done. + return string(b), nil +} + +// Decode decodes a cookie value. +// +// It decodes, verifies a message authentication code, optionally decrypts and +// finally deserializes the value. +// +// The name argument is the cookie name. It must be the same name used when +// it was stored. The value argument is the encoded cookie value. The dst +// argument is where the cookie will be decoded. It must be a pointer. +func (s *SecureCookie) Decode(name, value string, dst interface{}) error { + if s.err != nil { + return s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return s.err + } + // 1. Check length. + if s.maxLength != 0 && len(value) > s.maxLength { + return errValueToDecodeTooLong + } + // 2. Decode from base64. + b, err := decode([]byte(value)) + if err != nil { + return err + } + // 3. Verify MAC. Value is "date|value|mac". + parts := bytes.SplitN(b, []byte("|"), 3) + if len(parts) != 3 { + return ErrMacInvalid + } + h := hmac.New(s.hashFunc, s.hashKey) + b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) + if err = verifyMac(h, b, parts[2]); err != nil { + return err + } + // 4. Verify date ranges. + var t1 int64 + if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { + return errTimestampInvalid + } + t2 := s.timestamp() + if s.minAge != 0 && t1 > t2-s.minAge { + return errTimestampTooNew + } + if s.maxAge != 0 && t1 < t2-s.maxAge { + return errTimestampExpired + } + // 5. Decrypt (optional). + b, err = decode(parts[1]) + if err != nil { + return err + } + if s.block != nil { + if b, err = decrypt(s.block, b); err != nil { + return err + } + } + // 6. Deserialize. + if err = s.sz.Deserialize(b, dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + // Done. + return nil +} + +// timestamp returns the current timestamp, in seconds. +// +// For testing purposes, the function that generates the timestamp can be +// overridden. If not set, it will return time.Now().UTC().Unix(). +func (s *SecureCookie) timestamp() int64 { + if s.timeFunc == nil { + return time.Now().UTC().Unix() + } + return s.timeFunc() +} + +// Authentication ------------------------------------------------------------- + +// createMac creates a message authentication code (MAC). +func createMac(h hash.Hash, value []byte) []byte { + h.Write(value) + return h.Sum(nil) +} + +// verifyMac verifies that a message authentication code (MAC) is valid. +func verifyMac(h hash.Hash, value []byte, mac []byte) error { + mac2 := createMac(h, value) + // Check that both MACs are of equal length, as subtle.ConstantTimeCompare + // does not do this prior to Go 1.4. + if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { + return nil + } + return ErrMacInvalid +} + +// Encryption ----------------------------------------------------------------- + +// encrypt encrypts a value using the given block in counter mode. +// +// A random initialization vector (http://goo.gl/zF67k) with the length of the +// block size is prepended to the resulting ciphertext. +func encrypt(block cipher.Block, value []byte) ([]byte, error) { + iv := GenerateRandomKey(block.BlockSize()) + if iv == nil { + return nil, errGeneratingIV + } + // Encrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + // Return iv + ciphertext. + return append(iv, value...), nil +} + +// decrypt decrypts a value using the given block in counter mode. +// +// The value to be decrypted must be prepended by a initialization vector +// (http://goo.gl/zF67k) with the length of the block size. +func decrypt(block cipher.Block, value []byte) ([]byte, error) { + size := block.BlockSize() + if len(value) > size { + // Extract iv. + iv := value[:size] + // Extract ciphertext. + value = value[size:] + // Decrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + return value, nil + } + return nil, errDecryptionFailed +} + +// Serialization -------------------------------------------------------------- + +// Serialize encodes a value using gob. +func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize encodes a value using encoding/json. +func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using encoding/json. +func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { + dec := json.NewDecoder(bytes.NewReader(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize passes a []byte through as-is. +func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { + if b, ok := src.([]byte); ok { + return b, nil + } + + return nil, errValueNotByte +} + +// Deserialize passes a []byte through as-is. +func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { + if dat, ok := dst.(*[]byte); ok { + *dat = src + return nil + } + return errValueNotBytePtr +} + +// Encoding ------------------------------------------------------------------- + +// encode encodes a value using base64. +func encode(value []byte) []byte { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) + base64.URLEncoding.Encode(encoded, value) + return encoded +} + +// decode decodes a cookie using base64. +func decode(value []byte) ([]byte, error) { + decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) + b, err := base64.URLEncoding.Decode(decoded, value) + if err != nil { + return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} + } + return decoded[:b], nil +} + +// Helpers -------------------------------------------------------------------- + +// GenerateRandomKey creates a random key with the given length in bytes. +// On failure, returns nil. +// +// Callers should explicitly check for the possibility of a nil return, treat +// it as a failure of the system random number generator, and not continue. +func GenerateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +// CodecsFromPairs returns a slice of SecureCookie instances. +// +// It is a convenience function to create a list of codecs for key rotation. Note +// that the generated Codecs will have the default options applied: callers +// should iterate over each Codec and type-assert the underlying *SecureCookie to +// change these. +// +// Example: +// +// codecs := securecookie.CodecsFromPairs( +// []byte("new-hash-key"), +// []byte("new-block-key"), +// []byte("old-hash-key"), +// []byte("old-block-key"), +// ) +// +// // Modify each instance. +// for _, s := range codecs { +// if cookie, ok := s.(*securecookie.SecureCookie); ok { +// cookie.MaxAge(86400 * 7) +// cookie.SetSerializer(securecookie.JSONEncoder{}) +// cookie.HashFunc(sha512.New512_256) +// } +// } +// +func CodecsFromPairs(keyPairs ...[]byte) []Codec { + codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) + for i := 0; i < len(keyPairs); i += 2 { + var blockKey []byte + if i+1 < len(keyPairs) { + blockKey = keyPairs[i+1] + } + codecs[i/2] = New(keyPairs[i], blockKey) + } + return codecs +} + +// EncodeMulti encodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { + if len(codecs) == 0 { + return "", errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + encoded, err := codec.Encode(name, value) + if err == nil { + return encoded, nil + } + errors = append(errors, err) + } + return "", errors +} + +// DecodeMulti decodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { + if len(codecs) == 0 { + return errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + err := codec.Decode(name, value, dst) + if err == nil { + return nil + } + errors = append(errors, err) + } + return errors +} + +// MultiError groups multiple errors. +type MultiError []error + +func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } +func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } +func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } + +// Cause returns nil for MultiError; there is no unique underlying cause in the +// general case. +// +// Note: we could conceivably return a non-nil Cause only when there is exactly +// one child error with a Cause. However, it would be brittle for client code +// to rely on the arity of causes inside a MultiError, so we have opted not to +// provide this functionality. Clients which really wish to access the Causes +// of the underlying errors are free to iterate through the errors themselves. +func (m MultiError) Cause() error { return nil } + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} + +// any returns true if any element of m is an Error for which pred returns true. +func (m MultiError) any(pred func(Error) bool) bool { + for _, e := range m { + if ourErr, ok := e.(Error); ok && pred(ourErr) { + return true + } + } + return false +} diff --git a/vendor/github.com/gorilla/sessions/LICENSE b/vendor/github.com/gorilla/sessions/LICENSE new file mode 100644 index 000000000..0e5fb8728 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/sessions/README.md b/vendor/github.com/gorilla/sessions/README.md new file mode 100644 index 000000000..c9e0e92c7 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/README.md @@ -0,0 +1,92 @@ +sessions +======== +[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.svg?branch=master)](https://travis-ci.org/gorilla/sessions) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/sessions/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/sessions?badge) + + +gorilla/sessions provides cookie and filesystem sessions and infrastructure for +custom session backends. + +The key features are: + +* Simple API: use it as an easy way to set signed (and optionally + encrypted) cookies. +* Built-in backends to store sessions in cookies or the filesystem. +* Flash messages: session values that last until read. +* Convenient way to switch session persistency (aka "remember me") and set + other attributes. +* Mechanism to rotate authentication and encryption keys. +* Multiple sessions per request, even using different backends. +* Interfaces and infrastructure for custom session backends: sessions from + different stores can be retrieved and batch-saved using a common API. + +Let's start with an example that shows the sessions API in a nutshell: + +```go + import ( + "net/http" + "github.com/gorilla/sessions" + ) + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. We're ignoring the error resulted from decoding an + // existing session: Get() always returns a session, even if empty. + session, _ := store.Get(r, "session-name") + // Set some session values. + session.Values["foo"] = "bar" + session.Values[42] = 43 + // Save it before we write to the response/return from the handler. + session.Save(r, w) + } +``` + +First we initialize a session store calling `NewCookieStore()` and passing a +secret key used to authenticate the session. Inside the handler, we call +`store.Get()` to retrieve an existing session or create a new one. Then we set +some session values in session.Values, which is a `map[interface{}]interface{}`. +And finally we call `session.Save()` to save the session in the response. + +Important Note: If you aren't using gorilla/mux, you need to wrap your handlers +with +[`context.ClearHandler`](http://www.gorillatoolkit.org/pkg/context#ClearHandler) +or else you will leak memory! An easy way to do this is to wrap the top-level +mux when calling http.ListenAndServe: + +```go + http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux)) +``` + +The ClearHandler function is provided by the gorilla/context package. + +More examples are available [on the Gorilla +website](http://www.gorillatoolkit.org/pkg/sessions). + +## Store Implementations + +Other implementations of the `sessions.Store` interface: + +* [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB +* [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt +* [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase +* [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS +* [github.com/savaki/dynastore](https://github.com/savaki/dynastore) - DynamoDB on AWS (Official AWS library) +* [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache +* [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine +* [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB +* [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL +* [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster +* [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL +* [github.com/boj/redistore](https://github.com/boj/redistore) - Redis +* [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB +* [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak +* [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite +* [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite) +* [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql +* [github.com/quasoft/memstore](https://github.com/quasoft/memstore) - In-memory implementation for use in unit tests +* [github.com/lafriks/xormstore](https://github.com/lafriks/xormstore) - XORM (MySQL, PostgreSQL, SQLite, Microsoft SQL Server, TiDB) + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/sessions/doc.go b/vendor/github.com/gorilla/sessions/doc.go new file mode 100644 index 000000000..57a529177 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/doc.go @@ -0,0 +1,198 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package sessions provides cookie and filesystem sessions and +infrastructure for custom session backends. + +The key features are: + + * Simple API: use it as an easy way to set signed (and optionally + encrypted) cookies. + * Built-in backends to store sessions in cookies or the filesystem. + * Flash messages: session values that last until read. + * Convenient way to switch session persistency (aka "remember me") and set + other attributes. + * Mechanism to rotate authentication and encryption keys. + * Multiple sessions per request, even using different backends. + * Interfaces and infrastructure for custom session backends: sessions from + different stores can be retrieved and batch-saved using a common API. + +Let's start with an example that shows the sessions API in a nutshell: + + import ( + "net/http" + "github.com/gorilla/sessions" + ) + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. Get() always returns a session, even if empty. + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Set some session values. + session.Values["foo"] = "bar" + session.Values[42] = 43 + // Save it before we write to the response/return from the handler. + session.Save(r, w) + } + +First we initialize a session store calling NewCookieStore() and passing a +secret key used to authenticate the session. Inside the handler, we call +store.Get() to retrieve an existing session or a new one. Then we set some +session values in session.Values, which is a map[interface{}]interface{}. +And finally we call session.Save() to save the session in the response. + +Note that in production code, we should check for errors when calling +session.Save(r, w), and either display an error message or otherwise handle it. + +Save must be called before writing to the response, otherwise the session +cookie will not be sent to the client. + +Important Note: If you aren't using gorilla/mux, you need to wrap your handlers +with context.ClearHandler as or else you will leak memory! An easy way to do this +is to wrap the top-level mux when calling http.ListenAndServe: + + http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux)) + +The ClearHandler function is provided by the gorilla/context package. + +That's all you need to know for the basic usage. Let's take a look at other +options, starting with flash messages. + +Flash messages are session values that last until read. The term appeared with +Ruby On Rails a few years back. When we request a flash message, it is removed +from the session. To add a flash, call session.AddFlash(), and to get all +flashes, call session.Flashes(). Here is an example: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get the previous flashes, if any. + if flashes := session.Flashes(); len(flashes) > 0 { + // Use the flash values. + } else { + // Set a new flash. + session.AddFlash("Hello, flash messages world!") + } + session.Save(r, w) + } + +Flash messages are useful to set information to be read after a redirection, +like after form submissions. + +There may also be cases where you want to store a complex datatype within a +session, such as a struct. Sessions are serialised using the encoding/gob package, +so it is easy to register new datatypes for storage in sessions: + + import( + "encoding/gob" + "github.com/gorilla/sessions" + ) + + type Person struct { + FirstName string + LastName string + Email string + Age int + } + + type M map[string]interface{} + + func init() { + + gob.Register(&Person{}) + gob.Register(&M{}) + } + +As it's not possible to pass a raw type as a parameter to a function, gob.Register() +relies on us passing it a value of the desired type. In the example above we've passed +it a pointer to a struct and a pointer to a custom type representing a +map[string]interface. (We could have passed non-pointer values if we wished.) This will +then allow us to serialise/deserialise values of those types to and from our sessions. + +Note that because session values are stored in a map[string]interface{}, there's +a need to type-assert data when retrieving it. We'll use the Person struct we registered above: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Retrieve our struct and type-assert it + val := session.Values["person"] + var person = &Person{} + if person, ok := val.(*Person); !ok { + // Handle the case that it's not an expected type + } + + // Now we can use our person object + } + +By default, session cookies last for a month. This is probably too long for +some cases, but it is easy to change this and other attributes during +runtime. Sessions can be configured individually or the store can be +configured and then all sessions saved using it will use that configuration. +We access session.Options or store.Options to set a new configuration. The +fields are basically a subset of http.Cookie fields. Let's change the +maximum age of a session to one week: + + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400 * 7, + HttpOnly: true, + } + +Sometimes we may want to change authentication and/or encryption keys without +breaking existing sessions. The CookieStore supports key rotation, and to use +it you just need to set multiple authentication and encryption keys, in pairs, +to be tested in order: + + var store = sessions.NewCookieStore( + []byte("new-authentication-key"), + []byte("new-encryption-key"), + []byte("old-authentication-key"), + []byte("old-encryption-key"), + ) + +New sessions will be saved using the first pair. Old sessions can still be +read because the first pair will fail, and the second will be tested. This +makes it easy to "rotate" secret keys and still be able to validate existing +sessions. Note: for all pairs the encryption key is optional; set it to nil +or omit it and and encryption won't be used. + +Multiple sessions can be used in the same request, even with different +session backends. When this happens, calling Save() on each session +individually would be cumbersome, so we have a way to save all sessions +at once: it's sessions.Save(). Here's an example: + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session and set a value. + session1, _ := store.Get(r, "session-one") + session1.Values["foo"] = "bar" + // Get another session and set another value. + session2, _ := store.Get(r, "session-two") + session2.Values[42] = 43 + // Save all sessions. + sessions.Save(r, w) + } + +This is possible because when we call Get() from a session store, it adds the +session to a common registry. Save() uses it to save all registered sessions. +*/ +package sessions diff --git a/vendor/github.com/gorilla/sessions/go.mod b/vendor/github.com/gorilla/sessions/go.mod new file mode 100644 index 000000000..bb9ad35e4 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/go.mod @@ -0,0 +1,6 @@ +module "github.com/gorilla/sessions" + +require ( + "github.com/gorilla/context" v1.1 + "github.com/gorilla/securecookie" v1.1 +) diff --git a/vendor/github.com/gorilla/sessions/lex.go b/vendor/github.com/gorilla/sessions/lex.go new file mode 100644 index 000000000..4bbbe1096 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/lex.go @@ -0,0 +1,102 @@ +// This file contains code adapted from the Go standard library +// https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go + +package sessions + +import "strings" + +var isTokenTable = [127]bool{ + '!': true, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '*': true, + '+': true, + '-': true, + '.': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'W': true, + 'V': true, + 'X': true, + 'Y': true, + 'Z': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '|': true, + '~': true, +} + +func isToken(r rune) bool { + i := int(r) + return i < len(isTokenTable) && isTokenTable[i] +} + +func isNotToken(r rune) bool { + return !isToken(r) +} + +func isCookieNameValid(raw string) bool { + if raw == "" { + return false + } + return strings.IndexFunc(raw, isNotToken) < 0 +} diff --git a/vendor/github.com/gorilla/sessions/sessions.go b/vendor/github.com/gorilla/sessions/sessions.go new file mode 100644 index 000000000..9870e3101 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/sessions.go @@ -0,0 +1,243 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sessions + +import ( + "encoding/gob" + "fmt" + "net/http" + "time" + + "github.com/gorilla/context" +) + +// Default flashes key. +const flashesKey = "_flash" + +// Options -------------------------------------------------------------------- + +// Options stores configuration for a session or session store. +// +// Fields are a subset of http.Cookie fields. +type Options struct { + Path string + Domain string + // MaxAge=0 means no Max-Age attribute specified and the cookie will be + // deleted after the browser session ends. + // MaxAge<0 means delete cookie immediately. + // MaxAge>0 means Max-Age attribute present and given in seconds. + MaxAge int + Secure bool + HttpOnly bool +} + +// Session -------------------------------------------------------------------- + +// NewSession is called by session stores to create a new session instance. +func NewSession(store Store, name string) *Session { + return &Session{ + Values: make(map[interface{}]interface{}), + store: store, + name: name, + Options: new(Options), + } +} + +// Session stores the values and optional configuration for a session. +type Session struct { + // The ID of the session, generated by stores. It should not be used for + // user data. + ID string + // Values contains the user-data for the session. + Values map[interface{}]interface{} + Options *Options + IsNew bool + store Store + name string +} + +// Flashes returns a slice of flash messages from the session. +// +// A single variadic argument is accepted, and it is optional: it defines +// the flash key. If not defined "_flash" is used by default. +func (s *Session) Flashes(vars ...string) []interface{} { + var flashes []interface{} + key := flashesKey + if len(vars) > 0 { + key = vars[0] + } + if v, ok := s.Values[key]; ok { + // Drop the flashes and return it. + delete(s.Values, key) + flashes = v.([]interface{}) + } + return flashes +} + +// AddFlash adds a flash message to the session. +// +// A single variadic argument is accepted, and it is optional: it defines +// the flash key. If not defined "_flash" is used by default. +func (s *Session) AddFlash(value interface{}, vars ...string) { + key := flashesKey + if len(vars) > 0 { + key = vars[0] + } + var flashes []interface{} + if v, ok := s.Values[key]; ok { + flashes = v.([]interface{}) + } + s.Values[key] = append(flashes, value) +} + +// Save is a convenience method to save this session. It is the same as calling +// store.Save(request, response, session). You should call Save before writing to +// the response or returning from the handler. +func (s *Session) Save(r *http.Request, w http.ResponseWriter) error { + return s.store.Save(r, w, s) +} + +// Name returns the name used to register the session. +func (s *Session) Name() string { + return s.name +} + +// Store returns the session store used to register the session. +func (s *Session) Store() Store { + return s.store +} + +// Registry ------------------------------------------------------------------- + +// sessionInfo stores a session tracked by the registry. +type sessionInfo struct { + s *Session + e error +} + +// contextKey is the type used to store the registry in the context. +type contextKey int + +// registryKey is the key used to store the registry in the context. +const registryKey contextKey = 0 + +// GetRegistry returns a registry instance for the current request. +func GetRegistry(r *http.Request) *Registry { + registry := context.Get(r, registryKey) + if registry != nil { + return registry.(*Registry) + } + newRegistry := &Registry{ + request: r, + sessions: make(map[string]sessionInfo), + } + context.Set(r, registryKey, newRegistry) + return newRegistry +} + +// Registry stores sessions used during a request. +type Registry struct { + request *http.Request + sessions map[string]sessionInfo +} + +// Get registers and returns a session for the given name and session store. +// +// It returns a new session if there are no sessions registered for the name. +func (s *Registry) Get(store Store, name string) (session *Session, err error) { + if !isCookieNameValid(name) { + return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name) + } + if info, ok := s.sessions[name]; ok { + session, err = info.s, info.e + } else { + session, err = store.New(s.request, name) + session.name = name + s.sessions[name] = sessionInfo{s: session, e: err} + } + session.store = store + return +} + +// Save saves all sessions registered for the current request. +func (s *Registry) Save(w http.ResponseWriter) error { + var errMulti MultiError + for name, info := range s.sessions { + session := info.s + if session.store == nil { + errMulti = append(errMulti, fmt.Errorf( + "sessions: missing store for session %q", name)) + } else if err := session.store.Save(s.request, w, session); err != nil { + errMulti = append(errMulti, fmt.Errorf( + "sessions: error saving session %q -- %v", name, err)) + } + } + if errMulti != nil { + return errMulti + } + return nil +} + +// Helpers -------------------------------------------------------------------- + +func init() { + gob.Register([]interface{}{}) +} + +// Save saves all sessions used during the current request. +func Save(r *http.Request, w http.ResponseWriter) error { + return GetRegistry(r).Save(w) +} + +// NewCookie returns an http.Cookie with the options set. It also sets +// the Expires field calculated based on the MaxAge value, for Internet +// Explorer compatibility. +func NewCookie(name, value string, options *Options) *http.Cookie { + cookie := &http.Cookie{ + Name: name, + Value: value, + Path: options.Path, + Domain: options.Domain, + MaxAge: options.MaxAge, + Secure: options.Secure, + HttpOnly: options.HttpOnly, + } + if options.MaxAge > 0 { + d := time.Duration(options.MaxAge) * time.Second + cookie.Expires = time.Now().Add(d) + } else if options.MaxAge < 0 { + // Set it to the past to expire now. + cookie.Expires = time.Unix(1, 0) + } + return cookie +} + +// Error ---------------------------------------------------------------------- + +// MultiError stores multiple errors. +// +// Borrowed from the App Engine SDK. +type MultiError []error + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} diff --git a/vendor/github.com/gorilla/sessions/store.go b/vendor/github.com/gorilla/sessions/store.go new file mode 100644 index 000000000..4ff6b6c32 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/store.go @@ -0,0 +1,295 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sessions + +import ( + "encoding/base32" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/gorilla/securecookie" +) + +// Store is an interface for custom session stores. +// +// See CookieStore and FilesystemStore for examples. +type Store interface { + // Get should return a cached session. + Get(r *http.Request, name string) (*Session, error) + + // New should create and return a new session. + // + // Note that New should never return a nil session, even in the case of + // an error if using the Registry infrastructure to cache the session. + New(r *http.Request, name string) (*Session, error) + + // Save should persist session to the underlying store implementation. + Save(r *http.Request, w http.ResponseWriter, s *Session) error +} + +// CookieStore ---------------------------------------------------------------- + +// NewCookieStore returns a new CookieStore. +// +// Keys are defined in pairs to allow key rotation, but the common case is +// to set a single authentication key and optionally an encryption key. +// +// The first key in a pair is used for authentication and the second for +// encryption. The encryption key can be set to nil or omitted in the last +// pair, but the authentication key is required in all pairs. +// +// It is recommended to use an authentication key with 32 or 64 bytes. +// The encryption key, if set, must be either 16, 24, or 32 bytes to select +// AES-128, AES-192, or AES-256 modes. +// +// Use the convenience function securecookie.GenerateRandomKey() to create +// strong keys. +func NewCookieStore(keyPairs ...[]byte) *CookieStore { + cs := &CookieStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + } + + cs.MaxAge(cs.Options.MaxAge) + return cs +} + +// CookieStore stores sessions using secure cookies. +type CookieStore struct { + Codecs []securecookie.Codec + Options *Options // default configuration +} + +// Get returns a session for the given name after adding it to the registry. +// +// It returns a new session if the sessions doesn't exist. Access IsNew on +// the session to check if it is an existing session or a new one. +// +// It returns a new session and an error if the session exists but could +// not be decoded. +func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { + return GetRegistry(r).Get(s, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// The difference between New() and Get() is that calling New() twice will +// decode the session data twice, while Get() registers and reuses the same +// decoded session after the first call. +func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.Values, + s.Codecs...) + if err == nil { + session.IsNew = false + } + } + return session, err +} + +// Save adds a single session to the response. +func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, + session *Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting Options.MaxAge +// = -1 for that session. +func (s *CookieStore) MaxAge(age int) { + s.Options.MaxAge = age + + // Set the maxAge for each securecookie instance. + for _, codec := range s.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// FilesystemStore ------------------------------------------------------------ + +var fileMutex sync.RWMutex + +// NewFilesystemStore returns a new FilesystemStore. +// +// The path argument is the directory where sessions will be saved. If empty +// it will use os.TempDir(). +// +// See NewCookieStore() for a description of the other parameters. +func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore { + if path == "" { + path = os.TempDir() + } + fs := &FilesystemStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + path: path, + } + + fs.MaxAge(fs.Options.MaxAge) + return fs +} + +// FilesystemStore stores sessions in the filesystem. +// +// It also serves as a reference for custom stores. +// +// This store is still experimental and not well tested. Feedback is welcome. +type FilesystemStore struct { + Codecs []securecookie.Codec + Options *Options // default configuration + path string +} + +// MaxLength restricts the maximum length of new sessions to l. +// If l is 0 there is no limit to the size of a session, use with caution. +// The default for a new FilesystemStore is 4096. +func (s *FilesystemStore) MaxLength(l int) { + for _, c := range s.Codecs { + if codec, ok := c.(*securecookie.SecureCookie); ok { + codec.MaxLength(l) + } + } +} + +// Get returns a session for the given name after adding it to the registry. +// +// See CookieStore.Get(). +func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { + return GetRegistry(r).Get(s, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// See CookieStore.New(). +func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) + if err == nil { + err = s.load(session) + if err == nil { + session.IsNew = false + } + } + } + return session, err +} + +// Save adds a single session to the response. +// +// If the Options.MaxAge of the session is <= 0 then the session file will be +// deleted from the store path. With this process it enforces the properly +// session cookie handling so no need to trust in the cookie management in the +// web browser. +func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter, + session *Session) error { + // Delete if max-age is <= 0 + if session.Options.MaxAge <= 0 { + if err := s.erase(session); err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), "", session.Options)) + return nil + } + + if session.ID == "" { + // Because the ID is used in the filename, encode it to + // use alphanumeric characters only. + session.ID = strings.TrimRight( + base32.StdEncoding.EncodeToString( + securecookie.GenerateRandomKey(32)), "=") + } + if err := s.save(session); err != nil { + return err + } + encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, + s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting Options.MaxAge +// = -1 for that session. +func (s *FilesystemStore) MaxAge(age int) { + s.Options.MaxAge = age + + // Set the maxAge for each securecookie instance. + for _, codec := range s.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// save writes encoded session.Values to a file. +func (s *FilesystemStore) save(session *Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + s.Codecs...) + if err != nil { + return err + } + filename := filepath.Join(s.path, "session_"+session.ID) + fileMutex.Lock() + defer fileMutex.Unlock() + return ioutil.WriteFile(filename, []byte(encoded), 0600) +} + +// load reads a file and decodes its content into session.Values. +func (s *FilesystemStore) load(session *Session) error { + filename := filepath.Join(s.path, "session_"+session.ID) + fileMutex.RLock() + defer fileMutex.RUnlock() + fdata, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + if err = securecookie.DecodeMulti(session.Name(), string(fdata), + &session.Values, s.Codecs...); err != nil { + return err + } + return nil +} + +// delete session file +func (s *FilesystemStore) erase(session *Session) error { + filename := filepath.Join(s.path, "session_"+session.ID) + + fileMutex.RLock() + defer fileMutex.RUnlock() + + err := os.Remove(filename) + return err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index e138abcce..5045e83ca 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -54,6 +54,24 @@ "revision": "e09c5db296004fbe3f74490e84dcd62c3c5ddb1b", "revisionTime": "2018-03-28T16:31:53Z" }, + { + "checksumSHA1": "g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=", + "path": "github.com/gorilla/context", + "revision": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42", + "revisionTime": "2016-08-17T18:46:32Z" + }, + { + "checksumSHA1": "ucTBCc7dDRKLGPsYfAzu/Gq63qA=", + "path": "github.com/gorilla/securecookie", + "revision": "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983", + "revisionTime": "2017-02-24T19:38:04Z" + }, + { + "checksumSHA1": "/jOLCzAcN8p1GakgUYaWOs4tjw8=", + "path": "github.com/gorilla/sessions", + "revision": "a2f2a3de9a4a575047f73e3e36bc85ecc3546391", + "revisionTime": "2018-03-21T16:38:55Z" + }, { "checksumSHA1": "xtOnCs1i5rmga0KIQ9BZUHGRfL8=", "path": "github.com/grpc-ecosystem/go-grpc-middleware", -- GitLab From 301dd4675e9df8ccc8ad47fe7f2c40c03428ddb2 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sun, 17 Jun 2018 23:34:04 +0300 Subject: [PATCH 02/39] Fix logic to return failure instead of making private pages public --- acceptance_test.go | 4 ++-- internal/auth/auth.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 654fcb698..78247f33a 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -575,7 +575,7 @@ func TestKnownHostInReverseProxySetupReturns200(t *testing.T) { } } -func TestWhenAuthIsDisabledPrivateIsPublic(t *testing.T) { +func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "") defer teardown() @@ -584,7 +584,7 @@ func TestWhenAuthIsDisabledPrivateIsPublic(t *testing.T) { require.NoError(t, err) rsp.Body.Close() - assert.Equal(t, http.StatusOK, rsp.StatusCode) + assert.Equal(t, http.StatusInternalServerError, rsp.StatusCode) } func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d5600d323..d64a40533 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -187,7 +187,8 @@ func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID int) bool { if a == nil { - return false + httperrors.Serve500(w) + return true } if a.checkSession(w, r) { -- GitLab From 5828ffbece3a751198a2f9a0c7c8b144d13179c4 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 18 Jun 2018 00:20:20 +0300 Subject: [PATCH 03/39] Change project id to uint64 --- internal/auth/auth.go | 2 +- internal/domain/domain.go | 6 +++--- internal/domain/domain_config.go | 8 ++++---- internal/domain/map.go | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d64a40533..a3d44dc6a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -184,7 +184,7 @@ func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { } // CheckAuthentication checks if user is authenticated and has access to the project -func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID int) bool { +func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { if a == nil { httperrors.Serve500(w) diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 3aef96f52..aecbb93da 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -27,7 +27,7 @@ type project struct { HTTPSOnly bool Private bool AccessControl bool - ID int + ID uint64 } type projects map[string]*project @@ -147,14 +147,14 @@ func (d *D) IsPrivate(r *http.Request) bool { } // GetID figures out what is the ID of the project user tries to access -func (d *D) GetID(r *http.Request) int { +func (d *D) GetID(r *http.Request) uint64 { project := d.getProject(r) if project != nil { return project.ID } - return -1 + return 0 } func (d *D) serveFile(w http.ResponseWriter, r *http.Request, origPath string) error { diff --git a/internal/domain/domain_config.go b/internal/domain/domain_config.go index 1acb3461a..3b1c94195 100644 --- a/internal/domain/domain_config.go +++ b/internal/domain/domain_config.go @@ -16,10 +16,10 @@ type domainConfig struct { type domainsConfig struct { Domains []domainConfig - HTTPSOnly bool `json:"https_only"` - Private bool `json:"private"` - ID int `json:"id"` - AccessControl bool `json:"access_control"` + HTTPSOnly bool `json:"https_only"` + Private bool `json:"private"` + ID uint64 `json:"id"` + AccessControl bool `json:"access_control"` } func (c *domainConfig) Valid(rootDomain string) bool { diff --git a/internal/domain/map.go b/internal/domain/map.go index a6537bda6..3b72009f3 100644 --- a/internal/domain/map.go +++ b/internal/domain/map.go @@ -32,7 +32,7 @@ func (dm Map) addDomain(rootDomain, group, projectName string, config *domainCon dm[domainName] = newDomain } -func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool, private bool, accessControl bool, id int) { +func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool, private bool, accessControl bool, id uint64) { domainName := strings.ToLower(group + "." + rootDomain) groupDomain := dm[domainName] @@ -58,7 +58,7 @@ func (dm Map) readProjectConfig(rootDomain string, group, projectName string, co // This is necessary to preserve the previous behaviour where a // group domain is created even if no config.json files are // loaded successfully. Is it safe to remove this? - dm.updateGroupDomain(rootDomain, group, projectName, false, false, false, -1) + dm.updateGroupDomain(rootDomain, group, projectName, false, false, false, 0) return } -- GitLab From 1b2c9bec53272e1f757015dcb28c835492b25ad0 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 18 Jun 2018 19:43:44 +0300 Subject: [PATCH 04/39] Use header authentication instead of query parameter --- acceptance_test.go | 12 ++++++------ internal/auth/auth.go | 14 +++++++++++--- internal/auth/auth_test.go | 8 ++++---- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 78247f33a..3e46a5f03 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -619,7 +619,7 @@ func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", "-auth-client-secret=1", - "-auth-server=https://gitlab-example.com", + "-auth-server=https://gitlab-auth.com", "-auth-redirect-uri=https://gitlab-example.com/auth", "-auth-secret=something-very-secret") defer teardown() @@ -635,7 +635,7 @@ func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", "-auth-client-secret=1", - "-auth-server=https://gitlab-example.com", + "-auth-server=https://gitlab-auth.com", "-auth-redirect-uri=https://gitlab-example.com/auth", "-auth-secret=something-very-secret") defer teardown() @@ -658,7 +658,7 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", "-auth-client-secret=1", - "-auth-server=https://gitlab-example.com", + "-auth-server=https://gitlab-auth.com", "-auth-redirect-uri=https://gitlab-example.com/auth", "-auth-secret=something-very-secret") defer teardown() @@ -694,7 +694,7 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointAndAccess(t *testing.T) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) default: t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) @@ -752,7 +752,7 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointAndNoAccess(t *testing.T) w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) default: t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) @@ -810,7 +810,7 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointButTokenIsInvalid(t *testi w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "{\"error\":\"invalid_token\"}") default: diff --git a/internal/auth/auth.go b/internal/auth/auth.go index a3d44dc6a..407de0c3a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -16,7 +16,7 @@ import ( ) const ( - apiURLTemplate = "%s/api/v4/projects/%d?access_token=%s" + apiURLTemplate = "%s/api/v4/projects/%d" authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" tokenURLTemplate = "%s/oauth/token" tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" @@ -214,8 +214,16 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje } // Access token exists, authorize request - url := fmt.Sprintf(apiURLTemplate, a.gitLabServer, projectID, session.Values["access_token"].(string)) - resp, err := a.apiClient.Get(url) + url := fmt.Sprintf(apiURLTemplate, a.gitLabServer, projectID) + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + httperrors.Serve500(w) + return true + } + + req.Header.Add("Authorization", "Bearer "+session.Values["access_token"].(string)) + resp, err := a.apiClient.Do(req) if checkResponseForInvalidToken(resp, err) { diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index e8a956628..60ff62231 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -76,7 +76,7 @@ func TestTryAuthenticateWithCodeAndState(t *testing.T) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) default: t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) @@ -115,7 +115,7 @@ func TestCheckAuthenticationWhenAccess(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) default: t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) @@ -152,7 +152,7 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) default: t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) @@ -189,7 +189,7 @@ func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v4/projects/1000": - assert.Equal(t, "abc", r.URL.Query().Get("access_token")) + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "{\"error\":\"invalid_token\"}") default: -- GitLab From c3dc6f13f26ca57862afc9a7e1be6d4392021f87 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 18 Jun 2018 20:21:11 +0300 Subject: [PATCH 05/39] Use json decoder directly --- internal/auth/auth.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 407de0c3a..0278dcbe7 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" "strings" "time" @@ -172,10 +171,8 @@ func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { } // Parse response - body, _ := ioutil.ReadAll(resp.Body) defer resp.Body.Close() - - err = json.Unmarshal(body, &token) + err = json.NewDecoder(resp.Body).Decode(&token) if err != nil { return token, err } @@ -249,10 +246,8 @@ func checkResponseForInvalidToken(resp *http.Response, err error) bool { errResp := errorResponse{} // Parse response - body, _ := ioutil.ReadAll(resp.Body) defer resp.Body.Close() - - err = json.Unmarshal(body, &errResp) + err := json.NewDecoder(resp.Body).Decode(&errResp) if err != nil { return false } -- GitLab From 50d85895b5a742671bdf90adc8ab2d9c37e8a709 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 18 Jun 2018 20:21:29 +0300 Subject: [PATCH 06/39] Add transport to auth as well --- internal/auth/auth.go | 5 +++- internal/auth/transport.go | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 internal/auth/transport.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 0278dcbe7..c8022f2c4 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -278,6 +278,9 @@ func New(pagesDomain string, storeSecret string, clientID string, clientSecret s redirectURI: redirectURI, gitLabServer: strings.TrimRight(gitLabServer, "/"), store: store, - apiClient: &http.Client{Timeout: 5 * time.Second}, + apiClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: transport, + }, } } diff --git a/internal/auth/transport.go b/internal/auth/transport.go new file mode 100644 index 000000000..c8682ba2f --- /dev/null +++ b/internal/auth/transport.go @@ -0,0 +1,55 @@ +package auth + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net" + "net/http" + "os" + "sync" + + log "github.com/sirupsen/logrus" +) + +var ( + sysPoolOnce = &sync.Once{} + sysPool *x509.CertPool + + transport = &http.Transport{ + DialTLS: func(network, addr string) (net.Conn, error) { + return tls.Dial(network, addr, &tls.Config{RootCAs: pool()}) + }, + } +) + +// This is here because macOS does not support the SSL_CERT_FILE +// environment variable. We have arrange things to read SSL_CERT_FILE as +// late as possible to avoid conflicts with file descriptor passing at +// startup. +func pool() *x509.CertPool { + sysPoolOnce.Do(loadPool) + return sysPool +} + +func loadPool() { + sslCertFile := os.Getenv("SSL_CERT_FILE") + if sslCertFile == "" { + return + } + + var err error + sysPool, err = x509.SystemCertPool() + if err != nil { + log.WithError(err).Error("failed to load system cert pool for artifacts client") + return + } + + certPem, err := ioutil.ReadFile(sslCertFile) + if err != nil { + log.WithError(err).Error("failed to read SSL_CERT_FILE") + return + } + + sysPool.AppendCertsFromPEM(certPem) +} -- GitLab From 4d1f6523968b96351bcf0060ad736a3ec13326e7 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Wed, 20 Jun 2018 20:26:53 +0300 Subject: [PATCH 07/39] Deduplicate running pages with auth --- acceptance_test.go | 44 ++++++++------------------------------------ helpers_test.go | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 3e46a5f03..6eba0a84c 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -589,11 +589,7 @@ func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { skipUnlessEnabled(t) - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server=https://gitlab-example.com", - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") @@ -608,7 +604,7 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { require.NoError(t, err) assert.Equal(t, "https", url.Scheme) - assert.Equal(t, "gitlab-example.com", url.Host) + assert.Equal(t, "gitlab-auth.com", url.Host) assert.Equal(t, "/oauth/authorize", url.Path) assert.Equal(t, "1", url.Query().Get("client_id")) assert.Equal(t, "https://gitlab-example.com/auth", url.Query().Get("redirect_uri")) @@ -617,11 +613,7 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) { skipUnlessEnabled(t) - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server=https://gitlab-auth.com", - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetPageFromListener(t, httpsListener, "gitlab-example.com", "/auth?error=access_denied") @@ -633,11 +625,7 @@ func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) { } func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { skipUnlessEnabled(t) - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server=https://gitlab-auth.com", - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") @@ -656,11 +644,7 @@ func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { skipUnlessEnabled(t) - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server=https://gitlab-auth.com", - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") @@ -706,11 +690,7 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointAndAccess(t *testing.T) { testServer.Start() defer testServer.Close() - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server="+testServer.URL, - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) defer teardown() rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") @@ -764,11 +744,7 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointAndNoAccess(t *testing.T) testServer.Start() defer testServer.Close() - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server="+testServer.URL, - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) defer teardown() rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") @@ -823,11 +799,7 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointButTokenIsInvalid(t *testi testServer.Start() defer testServer.Close() - teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-auth-client-id=1", - "-auth-client-secret=1", - "-auth-server="+testServer.URL, - "-auth-redirect-uri=https://gitlab-example.com/auth", - "-auth-secret=something-very-secret") + teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) defer teardown() rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") diff --git a/helpers_test.go b/helpers_test.go index 1de79a352..f19dd3e8d 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -144,6 +144,22 @@ func RunPagesProcessWithSSLCertFile(t *testing.T, pagesPath string, listeners [] return runPagesProcess(t, true, pagesPath, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, extraArgs...) } +func RunPagesProcessWithAuth(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string) (teardown func()) { + return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server=https://gitlab-auth.com", + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") +} + +func RunPagesProcessWithAuthServer(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string, authServer string) (teardown func()) { + return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server="+authServer, + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") +} + func runPagesProcess(t *testing.T, wait bool, pagesPath string, listeners []ListenSpec, promPort string, extraEnv []string, extraArgs ...string) (teardown func()) { _, err := os.Stat(pagesPath) require.NoError(t, err) -- GitLab From a74388ede02f148bb4c39feaed0aff11821ae517 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Wed, 20 Jun 2018 21:01:44 +0300 Subject: [PATCH 08/39] Convert access control acceptance tests to table based --- acceptance_test.go | 143 +++++++++--------- .../pages/group/private.project.1/config.json | 1 + .../group/private.project.1/public/index.html | 1 + .../pages/group/private.project.2/config.json | 1 + .../group/private.project.2/public/index.html | 1 + 5 files changed, 74 insertions(+), 73 deletions(-) create mode 100644 shared/pages/group/private.project.1/config.json create mode 100644 shared/pages/group/private.project.1/public/index.html create mode 100644 shared/pages/group/private.project.2/config.json create mode 100644 shared/pages/group/private.project.2/public/index.html diff --git a/acceptance_test.go b/acceptance_test.go index 6eba0a84c..680eae5a0 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -470,6 +470,7 @@ func TestArtifactProxyRequest(t *testing.T) { t.Log("Artifact server URL", artifactServerURL) for _, c := range cases { + t.Run(fmt.Sprintf("Proxy Request Test: %s", c.Description), func(t *testing.T) { teardown := RunPagesProcessWithSSLCertFile( t, @@ -668,7 +669,7 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { assert.Equal(t, http.StatusServiceUnavailable, authrsp.StatusCode) } -func TestWhenLoginCallbackWithCorrectStateWithEndpointAndAccess(t *testing.T) { +func TestAccessControl(t *testing.T) { skipUnlessEnabled(t) testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -680,6 +681,13 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointAndAccess(t *testing.T) { case "/api/v4/projects/1000": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) + case "/api/v4/projects/2000": + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) + w.WriteHeader(http.StatusUnauthorized) + case "/api/v4/projects/3000": + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "{\"error\":\"invalid_token\"}") default: t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -687,93 +695,82 @@ func TestWhenLoginCallbackWithCorrectStateWithEndpointAndAccess(t *testing.T) { } })) + cases := []struct { + Host string + Path string + Status int + RedirectBack bool + Description string + }{ + { + "group.gitlab-example.com", + "/private.project/", + http.StatusOK, + false, + "project with access", + }, + { + "group.gitlab-example.com", + "/private.project.1/", + http.StatusUnauthorized, + false, + "project without access", + }, + { + "group.gitlab-example.com", + "/private.project.2/", + http.StatusFound, + true, + "invalid token test should redirect back", + }, + } + testServer.Start() defer testServer.Close() - teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) - defer teardown() - - rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") - - require.NoError(t, err) - defer rsp.Body.Close() - - cookie := rsp.Header.Get("Set-Cookie") - - url, err := url.Parse(rsp.Header.Get("Location")) - require.NoError(t, err) - - // Go to auth page with correct state will cause fetching the token - authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ - url.Query().Get("state"), cookie) - - require.NoError(t, err) - defer authrsp.Body.Close() - - // server returns the ticket, user will be redirected to the project page - assert.Equal(t, http.StatusFound, authrsp.StatusCode) - cookie = authrsp.Header.Get("Set-Cookie") - rsp, err = GetRedirectPageWithCookie(t, httpsListener, "group.gitlab-example.com", "private.project/", cookie) - - require.NoError(t, err) - defer rsp.Body.Close() - - // server returns user has access, status will be success - assert.Equal(t, http.StatusOK, rsp.StatusCode) -} + for _, c := range cases { -func TestWhenLoginCallbackWithCorrectStateWithEndpointAndNoAccess(t *testing.T) { - skipUnlessEnabled(t) + t.Run(fmt.Sprintf("Access Control Test: %s", c.Description), func(t *testing.T) { + teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) + defer teardown() - testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/oauth/token": - assert.Equal(t, "POST", r.Method) - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "{\"access_token\":\"abc\"}") - case "/api/v4/projects/1000": - assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) - w.WriteHeader(http.StatusUnauthorized) - default: - t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusNotFound) - } - })) + rsp, err := GetRedirectPage(t, httpsListener, c.Host, c.Path) - testServer.Start() - defer testServer.Close() + require.NoError(t, err) + defer rsp.Body.Close() - teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) - defer teardown() + cookie := rsp.Header.Get("Set-Cookie") - rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) - require.NoError(t, err) - defer rsp.Body.Close() + // Go to auth page with correct state will cause fetching the token + authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + url.Query().Get("state"), cookie) - cookie := rsp.Header.Get("Set-Cookie") - - url, err := url.Parse(rsp.Header.Get("Location")) - require.NoError(t, err) + require.NoError(t, err) + defer authrsp.Body.Close() - // Go to auth page with correct state will cause fetching the token - authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ - url.Query().Get("state"), cookie) + // server returns the ticket, user will be redirected to the project page + assert.Equal(t, http.StatusFound, authrsp.StatusCode) + cookie = authrsp.Header.Get("Set-Cookie") + rsp, err = GetRedirectPageWithCookie(t, httpsListener, c.Host, c.Path, cookie) - require.NoError(t, err) - defer authrsp.Body.Close() + require.NoError(t, err) + defer rsp.Body.Close() - // server returns the ticket, user will be redirected to the project page - assert.Equal(t, http.StatusFound, authrsp.StatusCode) - cookie = authrsp.Header.Get("Set-Cookie") - rsp, err = GetRedirectPageWithCookie(t, httpsListener, "group.gitlab-example.com", "private.project/", cookie) + assert.Equal(t, c.Status, rsp.StatusCode) - require.NoError(t, err) - defer rsp.Body.Close() + if c.RedirectBack { + url, err = url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) - // server returns user has NO access, status will be success - assert.Equal(t, http.StatusUnauthorized, rsp.StatusCode) + assert.Equal(t, "https", url.Scheme) + assert.Equal(t, c.Host, url.Host) + assert.Equal(t, c.Path, url.Path) + } + }) + } } func TestWhenLoginCallbackWithCorrectStateWithEndpointButTokenIsInvalid(t *testing.T) { diff --git a/shared/pages/group/private.project.1/config.json b/shared/pages/group/private.project.1/config.json new file mode 100644 index 000000000..f0fa2e8a4 --- /dev/null +++ b/shared/pages/group/private.project.1/config.json @@ -0,0 +1 @@ +{ "domains": [], "id": 2000, "private": true, "access_control": true } diff --git a/shared/pages/group/private.project.1/public/index.html b/shared/pages/group/private.project.1/public/index.html new file mode 100644 index 000000000..c8c6761a5 --- /dev/null +++ b/shared/pages/group/private.project.1/public/index.html @@ -0,0 +1 @@ +private \ No newline at end of file diff --git a/shared/pages/group/private.project.2/config.json b/shared/pages/group/private.project.2/config.json new file mode 100644 index 000000000..218a41bda --- /dev/null +++ b/shared/pages/group/private.project.2/config.json @@ -0,0 +1 @@ +{ "domains": [], "id": 3000, "private": true, "access_control": true } diff --git a/shared/pages/group/private.project.2/public/index.html b/shared/pages/group/private.project.2/public/index.html new file mode 100644 index 000000000..c8c6761a5 --- /dev/null +++ b/shared/pages/group/private.project.2/public/index.html @@ -0,0 +1 @@ +private \ No newline at end of file -- GitLab From 01be853119e87fe56e25901e0c95d92e869f8d52 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Wed, 20 Jun 2018 22:05:46 +0300 Subject: [PATCH 09/39] Refactor logic to avoid existence leak --- acceptance_test.go | 12 ++++- app.go | 16 +++++- internal/auth/auth.go | 105 ++++++++++++++++++++++++++++--------- internal/auth/auth_test.go | 99 ++++++++++++++++++++++++++++------ 4 files changed, 189 insertions(+), 43 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 680eae5a0..06fab9508 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -678,6 +678,9 @@ func TestAccessControl(t *testing.T) { assert.Equal(t, "POST", r.Method) w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") + case "/api/v4/projects": + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) + w.WriteHeader(http.StatusOK) case "/api/v4/projects/1000": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) @@ -712,7 +715,7 @@ func TestAccessControl(t *testing.T) { { "group.gitlab-example.com", "/private.project.1/", - http.StatusUnauthorized, + http.StatusNotFound, // Do not expose project existed false, "project without access", }, @@ -723,6 +726,13 @@ func TestAccessControl(t *testing.T) { true, "invalid token test should redirect back", }, + { + "group.gitlab-example.com", + "/nonexistent/", + http.StatusNotFound, + false, + "no project should redirect to login and then return 404", + }, } testServer.Start() diff --git a/app.go b/app.go index 68b3d85b7..284a420cb 100644 --- a/app.go +++ b/app.go @@ -94,6 +94,19 @@ func (a *theApp) getHostAndDomain(r *http.Request) (host string, domain *domain. return host, a.domain(host) } +func (a *theApp) checkAuthenticationIfNotExists(domain *domain.D, w http.ResponseWriter, r *http.Request) bool { + if domain == nil { + // To avoid user knowing if pages exist, we will force user to login and authorize pages + if a.Auth.CheckAuthenticationWithoutProject(w, r) { + return true + } + // User is authenticated, show the 404 + httperrors.Serve404(w) + return true + } + return false +} + func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, https bool, host string, domain *domain.D) bool { // short circuit content serving to check for a status page if r.RequestURI == a.appConfig.StatusPath { @@ -118,8 +131,7 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht return true } - if domain == nil { - httperrors.Serve404(w) + if a.checkAuthenticationIfNotExists(domain, w, r) { return true } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index c8022f2c4..483b471de 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -15,11 +15,12 @@ import ( ) const ( - apiURLTemplate = "%s/api/v4/projects/%d" - authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" - tokenURLTemplate = "%s/oauth/token" - tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" - callbackPath = "/auth" + apiURLProjectsTemplate = "%s/api/v4/projects" + apiURLProjectTemplate = "%s/api/v4/projects/%d" + authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" + tokenURLTemplate = "%s/oauth/token" + tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" + callbackPath = "/auth" ) // Auth handles authenticating users with GitLab API @@ -180,20 +181,7 @@ func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { return token, nil } -// CheckAuthentication checks if user is authenticated and has access to the project -func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { - - if a == nil { - httperrors.Serve500(w) - return true - } - - if a.checkSession(w, r) { - return true - } - - session := a.getSession(r) - +func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool { // If no access token redirect to OAuth login page if session.Values["access_token"] == nil { @@ -209,9 +197,37 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje return true } + return false +} + +func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) { + // Invalidate access token and redirect back for refreshing and re-authenticating + delete(session.Values, "access_token") + session.Save(r, w) + + http.Redirect(w, r, getRequestAddress(r), 302) +} + +// CheckAuthenticationWithoutProject checks if user is authenticated and has a valid token +func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request) bool { + + if a == nil { + // No auth supported + return false + } + + if a.checkSession(w, r) { + return true + } + + session := a.getSession(r) + + if a.checkTokenExists(session, w, r) { + return true + } // Access token exists, authorize request - url := fmt.Sprintf(apiURLTemplate, a.gitLabServer, projectID) + url := fmt.Sprintf(apiURLProjectsTemplate, a.gitLabServer) req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -223,18 +239,57 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje resp, err := a.apiClient.Do(req) if checkResponseForInvalidToken(resp, err) { + destroySession(session, w, r) + return true + } - // Invalidate access token and redirect back for refreshing and re-authenticating - delete(session.Values, "access_token") - session.Save(r, w) + if err != nil || resp.StatusCode != 200 { + // We return 404 if for some reason token is not valid to avoid (not) existence leak + httperrors.Serve404(w) + return true + } - http.Redirect(w, r, getRequestAddress(r), 302) + return false +} + +// CheckAuthentication checks if user is authenticated and has access to the project +func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { + + if a == nil { + httperrors.Serve500(w) + return true + } + + if a.checkSession(w, r) { + return true + } + session := a.getSession(r) + + if a.checkTokenExists(session, w, r) { + return true + } + + // Access token exists, authorize request + url := fmt.Sprintf(apiURLProjectTemplate, a.gitLabServer, projectID) + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + httperrors.Serve500(w) + return true + } + + req.Header.Add("Authorization", "Bearer "+session.Values["access_token"].(string)) + resp, err := a.apiClient.Do(req) + + if checkResponseForInvalidToken(resp, err) { + destroySession(session, w, r) return true } if err != nil || resp.StatusCode != 200 { - httperrors.Serve401(w) + // We return 404 if user has no access to avoid user knowing if the pages really existed or not + httperrors.Serve404(w) return true } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 60ff62231..69f1d7313 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -13,13 +13,17 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/auth" ) -func TestTryAuthenticate(t *testing.T) { - auth := auth.New("pages.gitlab-example.com", +func createAuth(t *testing.T) *auth.Auth { + return auth.New("pages.gitlab-example.com", "something-very-secret", "id", "secret", "http://pages.gitlab-example.com/auth", "http://gitlab-example.com") +} + +func TestTryAuthenticate(t *testing.T) { + auth := createAuth(t) result := httptest.NewRecorder() reqURL, err := url.Parse("/something/else") @@ -30,12 +34,7 @@ func TestTryAuthenticate(t *testing.T) { } func TestTryAuthenticateWithError(t *testing.T) { - auth := auth.New("pages.gitlab-example.com", - "something-very-secret", - "id", - "secret", - "http://pages.gitlab-example.com/auth", - "http://gitlab-example.com") + auth := createAuth(t) result := httptest.NewRecorder() reqURL, err := url.Parse("/auth?error=access_denied") @@ -48,12 +47,7 @@ func TestTryAuthenticateWithError(t *testing.T) { func TestTryAuthenticateWithCodeButInvalidState(t *testing.T) { store := sessions.NewCookieStore([]byte("something-very-secret")) - auth := auth.New("pages.gitlab-example.com", - "something-very-secret", - "id", - "secret", - "http://pages.gitlab-example.com/auth", - "http://gitlab-example.com") + auth := createAuth(t) result := httptest.NewRecorder() reqURL, err := url.Parse("/auth?code=1&state=invalid") @@ -182,7 +176,7 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) { session.Save(r, result) assert.Equal(t, true, auth.CheckAuthentication(result, r, 1000)) - assert.Equal(t, 401, result.Code) + assert.Equal(t, 404, result.Code) } func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { @@ -222,3 +216,78 @@ func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { assert.Equal(t, true, auth.CheckAuthentication(result, r, 1000)) assert.Equal(t, 302, result.Code) } + +func TestCheckAuthenticationWithoutProject(t *testing.T) { + apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v4/projects": + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) + w.WriteHeader(http.StatusOK) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + apiServer.Start() + defer apiServer.Close() + + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + apiServer.URL) + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["access_token"] = "abc" + session.Save(r, result) + + assert.Equal(t, false, auth.CheckAuthenticationWithoutProject(result, r)) + assert.Equal(t, 200, result.Code) +} + +func TestCheckAuthenticationWithoutProjectWhenInvalidToken(t *testing.T) { + apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v4/projects": + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "{\"error\":\"invalid_token\"}") + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + + apiServer.Start() + defer apiServer.Close() + + store := sessions.NewCookieStore([]byte("something-very-secret")) + auth := auth.New("pages.gitlab-example.com", + "something-very-secret", + "id", + "secret", + "http://pages.gitlab-example.com/auth", + apiServer.URL) + + result := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") + require.NoError(t, err) + r := &http.Request{URL: reqURL} + + session, _ := store.Get(r, "gitlab-pages") + session.Values["access_token"] = "abc" + session.Save(r, result) + + assert.Equal(t, true, auth.CheckAuthenticationWithoutProject(result, r)) + assert.Equal(t, 302, result.Code) +} -- GitLab From a0ba5ba1c3397987be7159c7952dfa38da219ab7 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Thu, 28 Jun 2018 21:03:38 +0300 Subject: [PATCH 10/39] Add SSL cert file to access control test --- acceptance_test.go | 21 +++++++++++++++++---- helpers_test.go | 8 ++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 06fab9508..ad22d9d94 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -672,6 +672,12 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { func TestAccessControl(t *testing.T) { skipUnlessEnabled(t) + transport := (TestHTTPSClient.Transport).(*http.Transport) + defer func(t time.Duration) { + transport.ResponseHeaderTimeout = t + }(transport.ResponseHeaderTimeout) + transport.ResponseHeaderTimeout = 5 * time.Second + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/oauth/token": @@ -698,6 +704,16 @@ func TestAccessControl(t *testing.T) { } })) + keyFile, certFile := CreateHTTPSFixtureFiles(t) + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + require.NoError(t, err) + defer os.Remove(keyFile) + defer os.Remove(certFile) + + testServer.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} + testServer.StartTLS() + defer testServer.Close() + cases := []struct { Host string Path string @@ -735,13 +751,10 @@ func TestAccessControl(t *testing.T) { }, } - testServer.Start() - defer testServer.Close() - for _, c := range cases { t.Run(fmt.Sprintf("Access Control Test: %s", c.Description), func(t *testing.T) { - teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) + teardown := RunPagesProcessWithAuthServerWithSSL(t, *pagesBinary, listeners, "", certFile, testServer.URL) defer teardown() rsp, err := GetRedirectPage(t, httpsListener, c.Host, c.Path) diff --git a/helpers_test.go b/helpers_test.go index f19dd3e8d..8ee27d0a5 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -160,6 +160,14 @@ func RunPagesProcessWithAuthServer(t *testing.T, pagesPath string, listeners []L "-auth-secret=something-very-secret") } +func RunPagesProcessWithAuthServerWithSSL(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string, sslCertFile string, authServer string) (teardown func()) { + return runPagesProcess(t, true, pagesPath, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, "-auth-client-id=1", + "-auth-client-secret=1", + "-auth-server="+authServer, + "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-secret=something-very-secret") +} + func runPagesProcess(t *testing.T, wait bool, pagesPath string, listeners []ListenSpec, promPort string, extraEnv []string, extraArgs ...string) (teardown func()) { _, err := os.Stat(pagesPath) require.NoError(t, err) -- GitLab From a57640fd330cc854b0d48a3b685dd61bec9cf57d Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Thu, 28 Jun 2018 21:50:02 +0300 Subject: [PATCH 11/39] Fix not exposing project existence when group is found but project is not --- acceptance_test.go | 2 ++ app.go | 22 +++++++++++++++++----- internal/auth/auth.go | 8 ++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index ad22d9d94..e21d0435e 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -762,6 +762,8 @@ func TestAccessControl(t *testing.T) { require.NoError(t, err) defer rsp.Body.Close() + assert.Equal(t, http.StatusFound, rsp.StatusCode) + cookie := rsp.Header.Get("Set-Cookie") url, err := url.Parse(rsp.Header.Get("Location")) diff --git a/app.go b/app.go index 284a420cb..af4d57442 100644 --- a/app.go +++ b/app.go @@ -95,15 +95,27 @@ func (a *theApp) getHostAndDomain(r *http.Request) (host string, domain *domain. } func (a *theApp) checkAuthenticationIfNotExists(domain *domain.D, w http.ResponseWriter, r *http.Request) bool { - if domain == nil { - // To avoid user knowing if pages exist, we will force user to login and authorize pages - if a.Auth.CheckAuthenticationWithoutProject(w, r) { + if domain == nil || domain.GetID(r) == 0 { + + // Only if auth is supported + if a.Auth.IsAuthSupported() { + + // To avoid user knowing if pages exist, we will force user to login and authorize pages + if a.Auth.CheckAuthenticationWithoutProject(w, r) { + return true + } + + // User is authenticated, show the 404 + httperrors.Serve404(w) return true } - // User is authenticated, show the 404 + } + + // Without auth, fall back to 404 + if domain == nil { httperrors.Serve404(w) - return true } + return false } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 483b471de..b4e693fba 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -208,6 +208,14 @@ func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Re http.Redirect(w, r, getRequestAddress(r), 302) } +// IsAuthSupported checks if pages is running with the authentication support +func (a *Auth) IsAuthSupported() bool { + if a == nil { + return false + } + return true +} + // CheckAuthenticationWithoutProject checks if user is authenticated and has a valid token func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request) bool { -- GitLab From 9681f91e84e78035f48f85c320ae1a3aa4b6ee07 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 30 Jun 2018 22:31:06 +0300 Subject: [PATCH 12/39] Combine private boolean with the access_control flag --- app.go | 4 ++-- internal/domain/domain.go | 12 ------------ internal/domain/domain_config.go | 1 - internal/domain/map.go | 7 +++---- shared/pages/group/private.project.1/config.json | 2 +- shared/pages/group/private.project.2/config.json | 2 +- shared/pages/group/private.project/config.json | 2 +- 7 files changed, 8 insertions(+), 22 deletions(-) diff --git a/app.go b/app.go index af4d57442..596725890 100644 --- a/app.go +++ b/app.go @@ -172,8 +172,8 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo return } - // Only for private domains that have access control enabled - if domain.IsAccessControlEnabled(r) && domain.IsPrivate(r) { + // Only for projects that have access control enabled + if domain.IsAccessControlEnabled(r) { if a.Auth.CheckAuthentication(&w, r, domain.GetID(r)) { return } diff --git a/internal/domain/domain.go b/internal/domain/domain.go index aecbb93da..bf0129c8a 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -25,7 +25,6 @@ type locationDirectoryError struct { type project struct { HTTPSOnly bool - Private bool AccessControl bool ID uint64 } @@ -135,17 +134,6 @@ func (d *D) IsAccessControlEnabled(r *http.Request) bool { return false } -// IsPrivate figures out if the request is to a project that needs user to sign in -func (d *D) IsPrivate(r *http.Request) bool { - project := d.getProject(r) - - if project != nil { - return project.Private - } - - return false -} - // GetID figures out what is the ID of the project user tries to access func (d *D) GetID(r *http.Request) uint64 { project := d.getProject(r) diff --git a/internal/domain/domain_config.go b/internal/domain/domain_config.go index 3b1c94195..672f939ca 100644 --- a/internal/domain/domain_config.go +++ b/internal/domain/domain_config.go @@ -17,7 +17,6 @@ type domainConfig struct { type domainsConfig struct { Domains []domainConfig HTTPSOnly bool `json:"https_only"` - Private bool `json:"private"` ID uint64 `json:"id"` AccessControl bool `json:"access_control"` } diff --git a/internal/domain/map.go b/internal/domain/map.go index 3b72009f3..5c6d5f5d4 100644 --- a/internal/domain/map.go +++ b/internal/domain/map.go @@ -32,7 +32,7 @@ func (dm Map) addDomain(rootDomain, group, projectName string, config *domainCon dm[domainName] = newDomain } -func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool, private bool, accessControl bool, id uint64) { +func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly bool, accessControl bool, id uint64) { domainName := strings.ToLower(group + "." + rootDomain) groupDomain := dm[domainName] @@ -45,7 +45,6 @@ func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly groupDomain.projects[projectName] = &project{ HTTPSOnly: httpsOnly, - Private: private, AccessControl: accessControl, ID: id, } @@ -58,11 +57,11 @@ func (dm Map) readProjectConfig(rootDomain string, group, projectName string, co // This is necessary to preserve the previous behaviour where a // group domain is created even if no config.json files are // loaded successfully. Is it safe to remove this? - dm.updateGroupDomain(rootDomain, group, projectName, false, false, false, 0) + dm.updateGroupDomain(rootDomain, group, projectName, false, false, 0) return } - dm.updateGroupDomain(rootDomain, group, projectName, config.HTTPSOnly, config.Private, config.AccessControl, config.ID) + dm.updateGroupDomain(rootDomain, group, projectName, config.HTTPSOnly, config.AccessControl, config.ID) for _, domainConfig := range config.Domains { config := domainConfig // domainConfig is reused for each loop iteration diff --git a/shared/pages/group/private.project.1/config.json b/shared/pages/group/private.project.1/config.json index f0fa2e8a4..dbff776fe 100644 --- a/shared/pages/group/private.project.1/config.json +++ b/shared/pages/group/private.project.1/config.json @@ -1 +1 @@ -{ "domains": [], "id": 2000, "private": true, "access_control": true } +{ "domains": [], "id": 2000, "access_control": true } diff --git a/shared/pages/group/private.project.2/config.json b/shared/pages/group/private.project.2/config.json index 218a41bda..6c5952195 100644 --- a/shared/pages/group/private.project.2/config.json +++ b/shared/pages/group/private.project.2/config.json @@ -1 +1 @@ -{ "domains": [], "id": 3000, "private": true, "access_control": true } +{ "domains": [], "id": 3000, "access_control": true } diff --git a/shared/pages/group/private.project/config.json b/shared/pages/group/private.project/config.json index 9b9b3f151..292ba6730 100644 --- a/shared/pages/group/private.project/config.json +++ b/shared/pages/group/private.project/config.json @@ -1 +1 @@ -{ "domains": [], "id": 1000, "private": true, "access_control": true } +{ "domains": [], "id": 1000, "access_control": true } -- GitLab From e9253d07ddad509465d64ca58400e527bc378da9 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sun, 1 Jul 2018 21:57:49 +0300 Subject: [PATCH 13/39] Add debug logging --- acceptance_test.go | 9 ++++++++- app.go | 4 +++- internal/auth/auth.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index e21d0435e..c095b4ba0 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -670,7 +670,7 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { } func TestAccessControl(t *testing.T) { - skipUnlessEnabled(t) + skipUnlessEnabled(t, "not-inplace-chroot") transport := (TestHTTPSClient.Transport).(*http.Transport) defer func(t time.Duration) { @@ -749,6 +749,13 @@ func TestAccessControl(t *testing.T) { false, "no project should redirect to login and then return 404", }, + { + "nonexistent.gitlab-example.com", + "/nonexistent/", + http.StatusNotFound, + false, + "no project should redirect to login and then return 404", + }, } for _, c := range cases { diff --git a/app.go b/app.go index 596725890..047e7ed93 100644 --- a/app.go +++ b/app.go @@ -11,11 +11,11 @@ import ( "sync" "time" - mimedb "gitlab.com/lupine/go-mimedb" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/cors" log "github.com/sirupsen/logrus" + mimedb "gitlab.com/lupine/go-mimedb" "gitlab.com/gitlab-org/gitlab-pages/internal/admin" "gitlab.com/gitlab-org/gitlab-pages/internal/artifact" @@ -174,6 +174,8 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo // Only for projects that have access control enabled if domain.IsAccessControlEnabled(r) { + log.Debug("Authenticate request") + if a.Auth.CheckAuthentication(&w, r, domain.GetID(r)) { return } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b4e693fba..334a38db4 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/securecookie" "github.com/gorilla/sessions" + log "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" ) @@ -76,6 +77,8 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { return true } + log.Debug("Authentication callback") + session := a.getSession(r) // If callback from authentication and the state matches @@ -86,6 +89,8 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { // If callback is not successful errorParam := r.URL.Query().Get("error") if errorParam != "" { + log.WithField("error", errorParam).Debug("OAuth endpoint returned error") + httperrors.Serve401(w) return true } @@ -94,6 +99,8 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { if !validateState(r, session) { // State is NOT ok + log.Debug("Authentication state did not match expected") + httperrors.Serve401(w) return true } @@ -103,6 +110,8 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { // Fetching token not OK if err != nil { + log.WithError(err).Debug("Fetching access token failed") + httperrors.Serve503(w) return true } @@ -112,6 +121,8 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { session.Save(r, w) // Redirect back to requested URI + log.Debug("Authentication was successful, redirecting user back to requested page") + http.Redirect(w, r, session.Values["uri"].(string), 302) return true @@ -184,6 +195,7 @@ func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool { // If no access token redirect to OAuth login page if session.Values["access_token"] == nil { + log.Debug("No access token exists, redirecting user to OAuth2 login") // Generate state hash and store requested address state := base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(16)) @@ -201,6 +213,8 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter } func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) { + log.Debug("Destroying session") + // Invalidate access token and redirect back for refreshing and re-authenticating delete(session.Values, "access_token") session.Save(r, w) @@ -239,6 +253,8 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. req, err := http.NewRequest("GET", url, nil) if err != nil { + log.WithError(err).Debug("Failed to authenticate request") + httperrors.Serve500(w) return true } @@ -247,12 +263,18 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. resp, err := a.apiClient.Do(req) if checkResponseForInvalidToken(resp, err) { + log.Debug("Access token was invalid, destroying session") + destroySession(session, w, r) return true } if err != nil || resp.StatusCode != 200 { // We return 404 if for some reason token is not valid to avoid (not) existence leak + if err != nil { + log.WithError(err).Debug("Failed to retrieve info with token") + } + httperrors.Serve404(w) return true } @@ -291,11 +313,17 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje resp, err := a.apiClient.Do(req) if checkResponseForInvalidToken(resp, err) { + log.Debug("Access token was invalid, destroying session") + destroySession(session, w, r) return true } if err != nil || resp.StatusCode != 200 { + if err != nil { + log.WithError(err).Debug("Failed to retrieve info with token") + } + // We return 404 if user has no access to avoid user knowing if the pages really existed or not httperrors.Serve404(w) return true -- GitLab From 415f4224b04c631588fba386687e69b47cae9be4 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 2 Jul 2018 18:14:40 +0300 Subject: [PATCH 14/39] Get rid of panic serving errors --- internal/domain/domain.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/domain/domain.go b/internal/domain/domain.go index bf0129c8a..e32ba097f 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -110,6 +110,10 @@ func (d *D) getProject(r *http.Request) *project { // IsHTTPSOnly figures out if the request should be handled with HTTPS // only by looking at group and project level config. func (d *D) IsHTTPSOnly(r *http.Request) bool { + if d == nil { + return false + } + if d.config != nil { return d.config.HTTPSOnly } @@ -125,6 +129,10 @@ func (d *D) IsHTTPSOnly(r *http.Request) bool { // IsAccessControlEnabled figures out if the request is to a project that has access control enabled func (d *D) IsAccessControlEnabled(r *http.Request) bool { + if d == nil { + return false + } + project := d.getProject(r) if project != nil { @@ -337,6 +345,11 @@ func (d *D) EnsureCertificate() (*tls.Certificate, error) { // ServeHTTP implements http.Handler. func (d *D) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if d == nil { + httperrors.Serve404(w) + return + } + if d.config != nil { d.serveFromConfig(w, r) } else { -- GitLab From 5021d635a0a37f296467fa78122273ad5f1f8128 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 2 Jul 2018 18:25:48 +0300 Subject: [PATCH 15/39] Remove duplicate test --- acceptance_test.go | 61 ---------------------------------------------- 1 file changed, 61 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index c095b4ba0..4a9c2f22e 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -804,64 +804,3 @@ func TestAccessControl(t *testing.T) { }) } } - -func TestWhenLoginCallbackWithCorrectStateWithEndpointButTokenIsInvalid(t *testing.T) { - skipUnlessEnabled(t) - - testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/oauth/token": - assert.Equal(t, "POST", r.Method) - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "{\"access_token\":\"abc\"}") - case "/api/v4/projects/1000": - assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) - w.WriteHeader(http.StatusUnauthorized) - fmt.Fprint(w, "{\"error\":\"invalid_token\"}") - default: - t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusNotFound) - } - })) - - testServer.Start() - defer testServer.Close() - - teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) - defer teardown() - - rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") - - require.NoError(t, err) - defer rsp.Body.Close() - - cookie := rsp.Header.Get("Set-Cookie") - - url, err := url.Parse(rsp.Header.Get("Location")) - require.NoError(t, err) - - // Go to auth page with correct state will cause fetching the token - authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ - url.Query().Get("state"), cookie) - - require.NoError(t, err) - defer authrsp.Body.Close() - - // server returns the ticket, user will be redirected to the project page - assert.Equal(t, http.StatusFound, authrsp.StatusCode) - cookie = authrsp.Header.Get("Set-Cookie") - rsp, err = GetRedirectPageWithCookie(t, httpsListener, "group.gitlab-example.com", "private.project/", cookie) - - require.NoError(t, err) - defer rsp.Body.Close() - - // server returns token is invalid and removes token from cookie and redirects user back to be redirected for new token - assert.Equal(t, http.StatusFound, rsp.StatusCode) - url, err = url.Parse(rsp.Header.Get("Location")) - require.NoError(t, err) - - assert.Equal(t, "https", url.Scheme) - assert.Equal(t, "group.gitlab-example.com", url.Host) - assert.Equal(t, "/private.project/", url.Path) -} -- GitLab From bcadf897da62964c8448add086bc03d6352109f8 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Tue, 3 Jul 2018 21:23:41 +0300 Subject: [PATCH 16/39] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ae6b0e4d..fc0fc2bc0 100644 --- a/README.md +++ b/README.md @@ -172,9 +172,9 @@ $ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pa #### How it works -1. GitLab pages looks for `access_control`, `private` and `id` fields in `config.json` files +1. GitLab pages looks for `access_control` and `id` fields in `config.json` files in `pages-root/group/project` directories. -2. For projects that have `access_control` and `private` set to `true` pages will require user to authenticate. +2. For projects that have `access_control` set to `true` pages will require user to authenticate. 3. When user accesses a project that requires authentication, user will be redirected to GitLab to log in and grant access for GitLab pages. 4. When user grant's access to GitLab pages, pages will use the OAuth2 `code` to get an access -- GitLab From 2666c24dacb27efd22ad78044d4f321beed63772 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 9 Jul 2018 21:49:40 +0300 Subject: [PATCH 17/39] Update to new endpoint and switch to better (user) endpoint when checking for token validity --- acceptance_test.go | 8 ++++---- internal/auth/auth.go | 14 +++++++------- internal/auth/auth_test.go | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 4a9c2f22e..483f4295a 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -684,16 +684,16 @@ func TestAccessControl(t *testing.T) { assert.Equal(t, "POST", r.Method) w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") - case "/api/v4/projects": + case "/api/v4/user": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) - case "/api/v4/projects/1000": + case "/api/v4/projects/1000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) - case "/api/v4/projects/2000": + case "/api/v4/projects/2000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) - case "/api/v4/projects/3000": + case "/api/v4/projects/3000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "{\"error\":\"invalid_token\"}") diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 334a38db4..dedb93413 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -16,12 +16,12 @@ import ( ) const ( - apiURLProjectsTemplate = "%s/api/v4/projects" - apiURLProjectTemplate = "%s/api/v4/projects/%d" - authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" - tokenURLTemplate = "%s/oauth/token" - tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" - callbackPath = "/auth" + apiURLUserTemplate = "%s/api/v4/user" + apiURLProjectTemplate = "%s/api/v4/projects/%d/pages_access" + authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" + tokenURLTemplate = "%s/oauth/token" + tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" + callbackPath = "/auth" ) // Auth handles authenticating users with GitLab API @@ -249,7 +249,7 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. } // Access token exists, authorize request - url := fmt.Sprintf(apiURLProjectsTemplate, a.gitLabServer) + url := fmt.Sprintf(apiURLUserTemplate, a.gitLabServer) req, err := http.NewRequest("GET", url, nil) if err != nil { diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 69f1d7313..f95583b33 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -69,7 +69,7 @@ func TestTryAuthenticateWithCodeAndState(t *testing.T) { assert.Equal(t, "POST", r.Method) w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{\"access_token\":\"abc\"}") - case "/api/v4/projects/1000": + case "/api/v4/projects/1000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) default: @@ -108,7 +108,7 @@ func TestTryAuthenticateWithCodeAndState(t *testing.T) { func TestCheckAuthenticationWhenAccess(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v4/projects/1000": + case "/api/v4/projects/1000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) default: @@ -145,7 +145,7 @@ func TestCheckAuthenticationWhenAccess(t *testing.T) { func TestCheckAuthenticationWhenNoAccess(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v4/projects/1000": + case "/api/v4/projects/1000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) default: @@ -182,7 +182,7 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) { func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v4/projects/1000": + case "/api/v4/projects/1000/pages_access": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "{\"error\":\"invalid_token\"}") @@ -220,7 +220,7 @@ func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { func TestCheckAuthenticationWithoutProject(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v4/projects": + case "/api/v4/user": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusOK) default: @@ -257,7 +257,7 @@ func TestCheckAuthenticationWithoutProject(t *testing.T) { func TestCheckAuthenticationWithoutProjectWhenInvalidToken(t *testing.T) { apiServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v4/projects": + case "/api/v4/user": assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "{\"error\":\"invalid_token\"}") -- GitLab From 90690a9d77b673df5845f05d626ff8f6e75529c7 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Tue, 7 Aug 2018 20:16:26 +0300 Subject: [PATCH 18/39] Make private pages public if gitlab and pages is ran without access control, add support for custom domains for which auth is proxied via gitlab pages domain --- acceptance_test.go | 85 ++++++++++- internal/auth/auth.go | 143 ++++++++++++++---- internal/domain/domain.go | 12 ++ internal/domain/domain_config.go | 10 +- internal/domain/map_test.go | 1 + .../pages/group/private.project/config.json | 11 +- 6 files changed, 228 insertions(+), 34 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 483f4295a..8a8422908 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -298,7 +298,7 @@ func TestPrometheusMetricsCanBeScraped(t *testing.T) { body, _ := ioutil.ReadAll(resp.Body) assert.Contains(t, string(body), "gitlab_pages_http_sessions_active 0") - assert.Contains(t, string(body), "gitlab_pages_domains_served_total 11") + assert.Contains(t, string(body), "gitlab_pages_domains_served_total 12") } } @@ -576,7 +576,7 @@ func TestKnownHostInReverseProxySetupReturns200(t *testing.T) { } } -func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { +func TestWhenAuthIsDisabledPrivateIsAccessible(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "") defer teardown() @@ -585,7 +585,7 @@ func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { require.NoError(t, err) rsp.Body.Close() - assert.Equal(t, http.StatusInternalServerError, rsp.StatusCode) + assert.Equal(t, http.StatusOK, rsp.StatusCode) } func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { @@ -669,6 +669,85 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { assert.Equal(t, http.StatusServiceUnavailable, authrsp.StatusCode) } +func TestAccessControlUnderCustomDomain(t *testing.T) { + skipUnlessEnabled(t, "not-inplace-chroot") + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + assert.Equal(t, "POST", r.Method) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "{\"access_token\":\"abc\"}") + case "/api/v4/projects/1000/pages_access": + assert.Equal(t, "Bearer abc", r.Header.Get("Authorization")) + w.WriteHeader(http.StatusOK) + default: + t.Logf("Unexpected r.URL.RawPath: %q", r.URL.Path) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + } + })) + testServer.Start() + defer testServer.Close() + + teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) + defer teardown() + + rsp, err := GetRedirectPage(t, httpListener, "private.domain.com", "/") + require.NoError(t, err) + defer rsp.Body.Close() + + cookie := rsp.Header.Get("Set-Cookie") + + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + + state := url.Query().Get("state") + assert.Equal(t, url.Query().Get("domain"), "private.domain.com") + + pagesrsp, err := GetRedirectPage(t, httpListener, url.Host, url.Path+"?"+url.RawQuery) + require.NoError(t, err) + defer pagesrsp.Body.Close() + + pagescookie := pagesrsp.Header.Get("Set-Cookie") + + // Go to auth page with correct state will cause fetching the token + authrsp, err := GetRedirectPageWithCookie(t, httpListener, "gitlab-example.com", "/auth?code=1&state="+ + state, pagescookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + url, err = url.Parse(authrsp.Header.Get("Location")) + require.NoError(t, err) + + // Will redirect to custom domain + assert.Equal(t, "private.domain.com", url.Host) + assert.Equal(t, "1", url.Query().Get("code")) + assert.Equal(t, state, url.Query().Get("state")) + + // Run auth callback in custom domain + authrsp, err = GetRedirectPageWithCookie(t, httpListener, "private.domain.com", "/auth?code=1&state="+ + state, cookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + // Will redirect to the page + cookie = authrsp.Header.Get("Set-Cookie") + assert.Equal(t, http.StatusFound, authrsp.StatusCode) + + url, err = url.Parse(authrsp.Header.Get("Location")) + require.NoError(t, err) + + // Will redirect to custom domain + assert.Equal(t, "http://private.domain.com/", url.String()) + + // Fetch page in custom domain + authrsp, err = GetRedirectPageWithCookie(t, httpListener, "private.domain.com", "/", cookie) + assert.Equal(t, http.StatusOK, authrsp.StatusCode) +} + func TestAccessControl(t *testing.T) { skipUnlessEnabled(t, "not-inplace-chroot") diff --git a/internal/auth/auth.go b/internal/auth/auth.go index dedb93413..ea185ea25 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -16,21 +16,23 @@ import ( ) const ( - apiURLUserTemplate = "%s/api/v4/user" - apiURLProjectTemplate = "%s/api/v4/projects/%d/pages_access" - authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" - tokenURLTemplate = "%s/oauth/token" - tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" - callbackPath = "/auth" + apiURLUserTemplate = "%s/api/v4/user" + apiURLProjectTemplate = "%s/api/v4/projects/%d/pages_access" + authorizeURLTemplate = "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s" + tokenURLTemplate = "%s/oauth/token" + tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" + callbackPath = "/auth" + authorizeProxyTemplate = "%s/auth?domain=%s&state=%s" ) // Auth handles authenticating users with GitLab API type Auth struct { + pagesDomain string clientID string clientSecret string redirectURI string gitLabServer string - store *sessions.CookieStore + storeSecret string apiClient *http.Client } @@ -46,10 +48,30 @@ type errorResponse struct { ErrorDescription string `json:"error_description"` } +func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { + store := sessions.NewCookieStore([]byte(a.storeSecret)) + + if strings.HasSuffix(r.Host, a.pagesDomain) { + // GitLab pages wide cookie + store.Options = &sessions.Options{ + Path: "/", + Domain: a.pagesDomain, + } + } else { + // Cookie just for this domain + store.Options = &sessions.Options{ + Path: "/", + Domain: r.Host, + } + } + + return store.Get(r, "gitlab-pages") +} + func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) bool { // Create or get session - session, err := a.store.Get(r, "gitlab-pages") + session, err := a.getSessionFromStore(r) if err != nil { // Save cookie again @@ -62,7 +84,7 @@ func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) bool { } func (a *Auth) getSession(r *http.Request) *sessions.Session { - session, _ := a.store.Get(r, "gitlab-pages") + session, _ := a.getSessionFromStore(r) return session } @@ -77,15 +99,19 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { return true } - log.Debug("Authentication callback") - session := a.getSession(r) - // If callback from authentication and the state matches + // Request is for auth if r.URL.Path != callbackPath { return false } + log.Debug("Authentication callback") + + if a.handleProxyingAuth(session, w, r) { + return true + } + // If callback is not successful errorParam := r.URL.Query().Get("error") if errorParam != "" { @@ -131,6 +157,47 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { return false } +func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool { + // If request is for authenticating via custom domain + if shouldProxyAuth(r) { + customDomain := r.URL.Query().Get("domain") + state := r.URL.Query().Get("state") + log.WithField("domain", customDomain).Debug("User is authenticating via custom domain") + + if r.TLS != nil { + session.Values["proxy_auth_domain"] = "https://" + customDomain + } else { + session.Values["proxy_auth_domain"] = "http://" + customDomain + } + session.Save(r, w) + + url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state) + http.Redirect(w, r, url, 302) + + return true + } + + // If auth request callback should be proxied to custom domain + if shouldProxyCallbackToCustomDomain(r, session) { + // Auth request is from custom domain, proxy callback there + log.Debug("Redirecting auth callback to custom domain") + + // Store access token + proxyDomain := session.Values["proxy_auth_domain"].(string) + + // Clear proxying from session + delete(session.Values, "proxy_auth_domain") + session.Save(r, w) + + // Redirect pages under custom domain + http.Redirect(w, r, proxyDomain+r.URL.Path+"?"+r.URL.RawQuery, 302) + + return true + } + + return false +} + func getRequestAddress(r *http.Request) string { if r.TLS != nil { return "https://" + r.Host + r.RequestURI @@ -138,6 +205,21 @@ func getRequestAddress(r *http.Request) string { return "http://" + r.Host + r.RequestURI } +func getRequestDomain(r *http.Request) string { + if r.TLS != nil { + return "https://" + r.Host + } + return "http://" + r.Host +} + +func shouldProxyAuth(r *http.Request) bool { + return r.URL.Query().Get("domain") != "" && r.URL.Query().Get("state") != "" +} + +func shouldProxyCallbackToCustomDomain(r *http.Request, session *sessions.Session) bool { + return session.Values["proxy_auth_domain"] != nil +} + func validateState(r *http.Request, session *sessions.Session) bool { state := r.URL.Query().Get("state") if state == "" { @@ -201,17 +283,33 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter state := base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(16)) session.Values["state"] = state session.Values["uri"] = getRequestAddress(r) + + // Clear possible proxying + delete(session.Values, "proxy_auth_domain") + session.Save(r, w) - // Redirect to OAuth login - url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state) - http.Redirect(w, r, url, 302) + // If we are in custom domain, redirect to pages domain to trigger authorization flow + if !strings.HasSuffix(r.Host, a.pagesDomain) { + http.Redirect(w, r, a.getProxyAddress(r, state), 302) + } else { + // Otherwise just redirect to OAuth login + url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state) + http.Redirect(w, r, url, 302) + } return true } return false } +func (a *Auth) getProxyAddress(r *http.Request, state string) string { + if r.TLS != nil { + return fmt.Sprintf(authorizeProxyTemplate, "https://"+a.pagesDomain, r.Host, state) + } + return fmt.Sprintf(authorizeProxyTemplate, "http://"+a.pagesDomain, r.Host, state) +} + func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) { log.Debug("Destroying session") @@ -286,8 +384,8 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { if a == nil { - httperrors.Serve500(w) - return true + log.Warn("Authentication is disabled, falling back to PUBLIC pages") + return false } if a.checkSession(w, r) { @@ -355,20 +453,13 @@ func checkResponseForInvalidToken(resp *http.Response, err error) bool { // New when authentication supported this will be used to create authentication handler func New(pagesDomain string, storeSecret string, clientID string, clientSecret string, redirectURI string, gitLabServer string) *Auth { - - store := sessions.NewCookieStore([]byte(storeSecret)) - - store.Options = &sessions.Options{ - Path: "/", - Domain: pagesDomain, - } - return &Auth{ + pagesDomain: pagesDomain, clientID: clientID, clientSecret: clientSecret, redirectURI: redirectURI, gitLabServer: strings.TrimRight(gitLabServer, "/"), - store: store, + storeSecret: storeSecret, apiClient: &http.Client{ Timeout: 5 * time.Second, Transport: transport, diff --git a/internal/domain/domain.go b/internal/domain/domain.go index e32ba097f..774293728 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -133,6 +133,10 @@ func (d *D) IsAccessControlEnabled(r *http.Request) bool { return false } + if d.config != nil { + return d.config.AccessControl + } + project := d.getProject(r) if project != nil { @@ -144,6 +148,14 @@ func (d *D) IsAccessControlEnabled(r *http.Request) bool { // GetID figures out what is the ID of the project user tries to access func (d *D) GetID(r *http.Request) uint64 { + if d == nil { + return 0 + } + + if d.config != nil { + return d.config.ID + } + project := d.getProject(r) if project != nil { diff --git a/internal/domain/domain_config.go b/internal/domain/domain_config.go index 672f939ca..2ab2ce6cb 100644 --- a/internal/domain/domain_config.go +++ b/internal/domain/domain_config.go @@ -8,10 +8,12 @@ import ( ) type domainConfig struct { - Domain string - Certificate string - Key string - HTTPSOnly bool `json:"https_only"` + Domain string + Certificate string + Key string + HTTPSOnly bool `json:"https_only"` + ID uint64 `json:"id"` + AccessControl bool `json:"access_control"` } type domainsConfig struct { diff --git a/internal/domain/map_test.go b/internal/domain/map_test.go index f20f98bd5..45658e957 100644 --- a/internal/domain/map_test.go +++ b/internal/domain/map_test.go @@ -35,6 +35,7 @@ func TestReadProjects(t *testing.T) { "test.my-domain.com", "test2.my-domain.com", "no.cert.com", + "private.domain.com", } for _, expected := range domains { diff --git a/shared/pages/group/private.project/config.json b/shared/pages/group/private.project/config.json index 292ba6730..e7d754a04 100644 --- a/shared/pages/group/private.project/config.json +++ b/shared/pages/group/private.project/config.json @@ -1 +1,10 @@ -{ "domains": [], "id": 1000, "access_control": true } +{ "domains": [ + { + "domain": "private.domain.com", + "id": 1000, + "access_control": true + } + ], + "id": 1000, + "access_control": true +} -- GitLab From b30197c907c86e38740df5640642f2a5ea739c69 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Wed, 8 Aug 2018 21:59:37 +0300 Subject: [PATCH 19/39] Fix problem with the public suffix listed pages domain --- acceptance_test.go | 32 +++++++++++++++++++++++++++++--- internal/auth/auth.go | 35 +++++++++++------------------------ 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 8a8422908..516b90d22 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -600,9 +600,15 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { assert.Equal(t, http.StatusFound, rsp.StatusCode) assert.Equal(t, 1, len(rsp.Header["Location"])) - url, err := url.Parse(rsp.Header.Get("Location")) require.NoError(t, err) + rsp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery) + + assert.Equal(t, http.StatusFound, rsp.StatusCode) + assert.Equal(t, 1, len(rsp.Header["Location"])) + + url, err = url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) assert.Equal(t, "https", url.Scheme) assert.Equal(t, "gitlab-auth.com", url.Host) @@ -849,19 +855,39 @@ func TestAccessControl(t *testing.T) { defer rsp.Body.Close() assert.Equal(t, http.StatusFound, rsp.StatusCode) - cookie := rsp.Header.Get("Set-Cookie") + // Redirects to the gitlab pages root domain for authentication flow url, err := url.Parse(rsp.Header.Get("Location")) require.NoError(t, err) + assert.Equal(t, "gitlab-example.com", url.Host) + assert.Equal(t, "/auth", url.Path) + state := url.Query().Get("state") + + rsp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery) + + require.NoError(t, err) + defer rsp.Body.Close() + + assert.Equal(t, http.StatusFound, rsp.StatusCode) + pagesDomainCookie := rsp.Header.Get("Set-Cookie") // Go to auth page with correct state will cause fetching the token authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ - url.Query().Get("state"), cookie) + state, pagesDomainCookie) require.NoError(t, err) defer authrsp.Body.Close() + // Will redirect auth callback to correct host + url, err = url.Parse(authrsp.Header.Get("Location")) + require.NoError(t, err) + assert.Equal(t, c.Host, url.Host) + assert.Equal(t, "/auth", url.Path) + + // Request auth callback in project domain + authrsp, err = GetRedirectPageWithCookie(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery, cookie) + // server returns the ticket, user will be redirected to the project page assert.Equal(t, http.StatusFound, authrsp.StatusCode) cookie = authrsp.Header.Get("Set-Cookie") diff --git a/internal/auth/auth.go b/internal/auth/auth.go index ea185ea25..e88cf7a2d 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -51,18 +51,10 @@ type errorResponse struct { func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { store := sessions.NewCookieStore([]byte(a.storeSecret)) - if strings.HasSuffix(r.Host, a.pagesDomain) { - // GitLab pages wide cookie - store.Options = &sessions.Options{ - Path: "/", - Domain: a.pagesDomain, - } - } else { - // Cookie just for this domain - store.Options = &sessions.Options{ - Path: "/", - Domain: r.Host, - } + // Cookie just for this domain + store.Options = &sessions.Options{ + Path: "/", + Domain: r.Host, } return store.Get(r, "gitlab-pages") @@ -160,14 +152,14 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool { // If request is for authenticating via custom domain if shouldProxyAuth(r) { - customDomain := r.URL.Query().Get("domain") + domain := r.URL.Query().Get("domain") state := r.URL.Query().Get("state") - log.WithField("domain", customDomain).Debug("User is authenticating via custom domain") + log.WithField("domain", domain).Debug("User is authenticating via domain") if r.TLS != nil { - session.Values["proxy_auth_domain"] = "https://" + customDomain + session.Values["proxy_auth_domain"] = "https://" + domain } else { - session.Values["proxy_auth_domain"] = "http://" + customDomain + session.Values["proxy_auth_domain"] = "http://" + domain } session.Save(r, w) @@ -289,14 +281,9 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter session.Save(r, w) - // If we are in custom domain, redirect to pages domain to trigger authorization flow - if !strings.HasSuffix(r.Host, a.pagesDomain) { - http.Redirect(w, r, a.getProxyAddress(r, state), 302) - } else { - // Otherwise just redirect to OAuth login - url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state) - http.Redirect(w, r, url, 302) - } + // Because the pages domain might be in public suffix list, we have to + // redirect to pages domain to trigger authorization flow + http.Redirect(w, r, a.getProxyAddress(r, state), 302) return true } -- GitLab From 3425634584820837fd88d14b944bbdc391823936 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Fri, 10 Aug 2018 00:06:43 +0300 Subject: [PATCH 20/39] Allow auth proxying only for configured domains and everything under pages domain --- app.go | 2 +- internal/auth/auth.go | 22 +++++++++++++++++++--- internal/auth/auth_test.go | 10 ++++++---- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 047e7ed93..3ffe9ad94 100644 --- a/app.go +++ b/app.go @@ -164,7 +164,7 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo host, domain := a.getHostAndDomain(r) - if a.Auth.TryAuthenticate(&w, r) { + if a.Auth.TryAuthenticate(&w, r, a.dm, &a.lock) { return } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index e88cf7a2d..d37012079 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -7,11 +7,13 @@ import ( "fmt" "net/http" "strings" + "sync" "time" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" log "github.com/sirupsen/logrus" + "gitlab.com/gitlab-org/gitlab-pages/internal/domain" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" ) @@ -81,7 +83,7 @@ func (a *Auth) getSession(r *http.Request) *sessions.Session { } // TryAuthenticate tries to authenticate user and fetch access token if request is a callback to auth -func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { +func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain.Map, lock *sync.RWMutex) bool { if a == nil { return false @@ -100,7 +102,7 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { log.Debug("Authentication callback") - if a.handleProxyingAuth(session, w, r) { + if a.handleProxyingAuth(session, w, r, dm, lock) { return true } @@ -149,11 +151,25 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request) bool { return false } -func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool { +func (a *Auth) domainAllowed(domain string, dm domain.Map, lock *sync.RWMutex) bool { + lock.RLock() + defer lock.RUnlock() + _, present := dm[domain] + return strings.HasSuffix(strings.ToLower(domain), a.pagesDomain) || present +} + +func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request, dm domain.Map, lock *sync.RWMutex) bool { // If request is for authenticating via custom domain if shouldProxyAuth(r) { domain := r.URL.Query().Get("domain") state := r.URL.Query().Get("state") + + if !a.domainAllowed(domain, dm, lock) { + log.WithField("domain", domain).Debug("Domain is not configured") + httperrors.Serve401(w) + return true + } + log.WithField("domain", domain).Debug("User is authenticating via domain") if r.TLS != nil { diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index f95583b33..4973ce015 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -5,12 +5,14 @@ import ( "net/http" "net/http/httptest" "net/url" + "sync" "testing" "github.com/gorilla/sessions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/auth" + "gitlab.com/gitlab-org/gitlab-pages/internal/domain" ) func createAuth(t *testing.T) *auth.Auth { @@ -30,7 +32,7 @@ func TestTryAuthenticate(t *testing.T) { require.NoError(t, err) r := &http.Request{URL: reqURL} - assert.Equal(t, false, auth.TryAuthenticate(result, r)) + assert.Equal(t, false, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{})) } func TestTryAuthenticateWithError(t *testing.T) { @@ -41,7 +43,7 @@ func TestTryAuthenticateWithError(t *testing.T) { require.NoError(t, err) r := &http.Request{URL: reqURL} - assert.Equal(t, true, auth.TryAuthenticate(result, r)) + assert.Equal(t, true, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{})) assert.Equal(t, 401, result.Code) } @@ -58,7 +60,7 @@ func TestTryAuthenticateWithCodeButInvalidState(t *testing.T) { session.Values["state"] = "state" session.Save(r, result) - assert.Equal(t, true, auth.TryAuthenticate(result, r)) + assert.Equal(t, true, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{})) assert.Equal(t, 401, result.Code) } @@ -100,7 +102,7 @@ func TestTryAuthenticateWithCodeAndState(t *testing.T) { session.Values["state"] = "state" session.Save(r, result) - assert.Equal(t, true, auth.TryAuthenticate(result, r)) + assert.Equal(t, true, auth.TryAuthenticate(result, r, make(domain.Map), &sync.RWMutex{})) assert.Equal(t, 302, result.Code) assert.Equal(t, "http://pages.gitlab-example.com/project/", result.Header().Get("Location")) } -- GitLab From 1966ab9bf8592a32a7ff88e1d1439ed80f7f65bb Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sun, 12 Aug 2018 12:36:10 +0300 Subject: [PATCH 21/39] Use reserved namespace 'projects' for the redirect uri to handle situation where root pages domain is not handled with pages daemon --- README.md | 4 ++-- acceptance_test.go | 16 ++++++++-------- helpers_test.go | 6 +++--- internal/auth/auth.go | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fc0fc2bc0..3040e3029 100644 --- a/README.md +++ b/README.md @@ -162,12 +162,12 @@ Pages and another HTTP server have to co-exist on the same server. ### GitLab access control -GitLab access control is configured with properties `auth-client-id`, `auth-client-secret`, `auth-redirect-uri`, `auth-server` and `auth-secret`. Client ID, secret and redirect uri are configured in the GitLab and should match. `auth-server` points to a GitLab instance used for authentication. `auth-redirect-uri` should be `http(s)://pages-domain/auth`. Using HTTPS is _strongly_ encouraged. `auth-secret` is used to encrypt the session cookie, and it should be strong enough. +GitLab access control is configured with properties `auth-client-id`, `auth-client-secret`, `auth-redirect-uri`, `auth-server` and `auth-secret`. Client ID, secret and redirect uri are configured in the GitLab and should match. `auth-server` points to a GitLab instance used for authentication. `auth-redirect-uri` should be `http(s)://pages-domain/auth`. Note that if the pages-domain is not handled by GitLab pages, then the `auth-redirect-uri` should use some reserved namespace prefix (such as `http(s)://projects.pages-domain/auth`). Using HTTPS is _strongly_ encouraged. `auth-secret` is used to encrypt the session cookie, and it should be strong enough. Example: ``` $ make -$ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -auth-client-id -auth-client-secret -auth-redirect-uri https://example.com/auth -auth-secret something-very-secret -auth-server https://gitlab.com +$ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -auth-client-id -auth-client-secret -auth-redirect-uri https://projects.example.com/auth -auth-secret something-very-secret -auth-server https://gitlab.com ``` #### How it works diff --git a/acceptance_test.go b/acceptance_test.go index 516b90d22..98db02037 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -614,7 +614,7 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { assert.Equal(t, "gitlab-auth.com", url.Host) assert.Equal(t, "/oauth/authorize", url.Path) assert.Equal(t, "1", url.Query().Get("client_id")) - assert.Equal(t, "https://gitlab-example.com/auth", url.Query().Get("redirect_uri")) + assert.Equal(t, "https://projects.gitlab-example.com/auth", url.Query().Get("redirect_uri")) assert.NotEqual(t, "", url.Query().Get("state")) } @@ -623,7 +623,7 @@ func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) { teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() - rsp, err := GetPageFromListener(t, httpsListener, "gitlab-example.com", "/auth?error=access_denied") + rsp, err := GetPageFromListener(t, httpsListener, "projects.gitlab-example.com", "/auth?error=access_denied") require.NoError(t, err) defer rsp.Body.Close() @@ -641,7 +641,7 @@ func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { defer rsp.Body.Close() // Go to auth page with wrong state will cause failure - authrsp, err := GetPageFromListener(t, httpsListener, "gitlab-example.com", "/auth?code=0&state=0") + authrsp, err := GetPageFromListener(t, httpsListener, "projects.gitlab-example.com", "/auth?code=0&state=0") require.NoError(t, err) defer authrsp.Body.Close() @@ -665,7 +665,7 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { require.NoError(t, err) // Go to auth page with correct state will cause fetching the token - authrsp, err := GetPageFromListenerWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + authrsp, err := GetPageFromListenerWithCookie(t, httpsListener, "projects.gitlab-example.com", "/auth?code=1&state="+ url.Query().Get("state"), cookie) require.NoError(t, err) @@ -718,7 +718,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { pagescookie := pagesrsp.Header.Get("Set-Cookie") // Go to auth page with correct state will cause fetching the token - authrsp, err := GetRedirectPageWithCookie(t, httpListener, "gitlab-example.com", "/auth?code=1&state="+ + authrsp, err := GetRedirectPageWithCookie(t, httpListener, "projects.gitlab-example.com", "/auth?code=1&state="+ state, pagescookie) require.NoError(t, err) @@ -857,10 +857,10 @@ func TestAccessControl(t *testing.T) { assert.Equal(t, http.StatusFound, rsp.StatusCode) cookie := rsp.Header.Get("Set-Cookie") - // Redirects to the gitlab pages root domain for authentication flow + // Redirects to the projects under gitlab pages domain for authentication flow url, err := url.Parse(rsp.Header.Get("Location")) require.NoError(t, err) - assert.Equal(t, "gitlab-example.com", url.Host) + assert.Equal(t, "projects.gitlab-example.com", url.Host) assert.Equal(t, "/auth", url.Path) state := url.Query().Get("state") @@ -873,7 +873,7 @@ func TestAccessControl(t *testing.T) { pagesDomainCookie := rsp.Header.Get("Set-Cookie") // Go to auth page with correct state will cause fetching the token - authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "gitlab-example.com", "/auth?code=1&state="+ + authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "projects.gitlab-example.com", "/auth?code=1&state="+ state, pagesDomainCookie) require.NoError(t, err) diff --git a/helpers_test.go b/helpers_test.go index 8ee27d0a5..831074880 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -148,7 +148,7 @@ func RunPagesProcessWithAuth(t *testing.T, pagesPath string, listeners []ListenS return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1", "-auth-client-secret=1", "-auth-server=https://gitlab-auth.com", - "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-redirect-uri=https://projects.gitlab-example.com/auth", "-auth-secret=something-very-secret") } @@ -156,7 +156,7 @@ func RunPagesProcessWithAuthServer(t *testing.T, pagesPath string, listeners []L return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1", "-auth-client-secret=1", "-auth-server="+authServer, - "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-redirect-uri=https://projects.gitlab-example.com/auth", "-auth-secret=something-very-secret") } @@ -164,7 +164,7 @@ func RunPagesProcessWithAuthServerWithSSL(t *testing.T, pagesPath string, listen return runPagesProcess(t, true, pagesPath, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, "-auth-client-id=1", "-auth-client-secret=1", "-auth-server="+authServer, - "-auth-redirect-uri=https://gitlab-example.com/auth", + "-auth-redirect-uri=https://projects.gitlab-example.com/auth", "-auth-secret=something-very-secret") } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d37012079..936754cc5 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -24,7 +24,7 @@ const ( tokenURLTemplate = "%s/oauth/token" tokenContentTemplate = "client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s" callbackPath = "/auth" - authorizeProxyTemplate = "%s/auth?domain=%s&state=%s" + authorizeProxyTemplate = "%s?domain=%s&state=%s" ) // Auth handles authenticating users with GitLab API @@ -308,9 +308,9 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter func (a *Auth) getProxyAddress(r *http.Request, state string) string { if r.TLS != nil { - return fmt.Sprintf(authorizeProxyTemplate, "https://"+a.pagesDomain, r.Host, state) + return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, r.Host, state) } - return fmt.Sprintf(authorizeProxyTemplate, "http://"+a.pagesDomain, r.Host, state) + return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, r.Host, state) } func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) { -- GitLab From 82335177a418afc9aeaab9cf117c94f5063fb1b7 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 18 Aug 2018 09:56:12 +0300 Subject: [PATCH 22/39] Combine transports to one package --- internal/artifact/artifact.go | 3 +- internal/auth/auth.go | 3 +- internal/auth/transport.go | 55 ------------------- .../{artifact => httptransport}/transport.go | 5 +- 4 files changed, 7 insertions(+), 59 deletions(-) delete mode 100644 internal/auth/transport.go rename internal/{artifact => httptransport}/transport.go (89%) diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index 9a23e269f..5050b4269 100644 --- a/internal/artifact/artifact.go +++ b/internal/artifact/artifact.go @@ -12,6 +12,7 @@ import ( "time" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" + "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport" ) const ( @@ -43,7 +44,7 @@ func New(server string, timeoutSeconds int, pagesDomain string) *Artifact { suffix: "." + strings.ToLower(pagesDomain), client: &http.Client{ Timeout: time.Second * time.Duration(timeoutSeconds), - Transport: transport, + Transport: httptransport.Transport, }, } } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 936754cc5..da6789dc7 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -15,6 +15,7 @@ import ( log "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-pages/internal/domain" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" + "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport" ) const ( @@ -465,7 +466,7 @@ func New(pagesDomain string, storeSecret string, clientID string, clientSecret s storeSecret: storeSecret, apiClient: &http.Client{ Timeout: 5 * time.Second, - Transport: transport, + Transport: httptransport.Transport, }, } } diff --git a/internal/auth/transport.go b/internal/auth/transport.go deleted file mode 100644 index c8682ba2f..000000000 --- a/internal/auth/transport.go +++ /dev/null @@ -1,55 +0,0 @@ -package auth - -import ( - "crypto/tls" - "crypto/x509" - "io/ioutil" - "net" - "net/http" - "os" - "sync" - - log "github.com/sirupsen/logrus" -) - -var ( - sysPoolOnce = &sync.Once{} - sysPool *x509.CertPool - - transport = &http.Transport{ - DialTLS: func(network, addr string) (net.Conn, error) { - return tls.Dial(network, addr, &tls.Config{RootCAs: pool()}) - }, - } -) - -// This is here because macOS does not support the SSL_CERT_FILE -// environment variable. We have arrange things to read SSL_CERT_FILE as -// late as possible to avoid conflicts with file descriptor passing at -// startup. -func pool() *x509.CertPool { - sysPoolOnce.Do(loadPool) - return sysPool -} - -func loadPool() { - sslCertFile := os.Getenv("SSL_CERT_FILE") - if sslCertFile == "" { - return - } - - var err error - sysPool, err = x509.SystemCertPool() - if err != nil { - log.WithError(err).Error("failed to load system cert pool for artifacts client") - return - } - - certPem, err := ioutil.ReadFile(sslCertFile) - if err != nil { - log.WithError(err).Error("failed to read SSL_CERT_FILE") - return - } - - sysPool.AppendCertsFromPEM(certPem) -} diff --git a/internal/artifact/transport.go b/internal/httptransport/transport.go similarity index 89% rename from internal/artifact/transport.go rename to internal/httptransport/transport.go index da182df60..207531f4e 100644 --- a/internal/artifact/transport.go +++ b/internal/httptransport/transport.go @@ -1,4 +1,4 @@ -package artifact +package httptransport import ( "crypto/tls" @@ -16,7 +16,8 @@ var ( sysPoolOnce = &sync.Once{} sysPool *x509.CertPool - transport = &http.Transport{ + // Transport can be used with httpclient with TLS and certificates + Transport = &http.Transport{ DialTLS: func(network, addr string) (net.Conn, error) { return tls.Dial(network, addr, &tls.Config{RootCAs: pool()}) }, -- GitLab From 240293f844ff22e19924644aa27815b6ca54735e Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 18 Aug 2018 10:00:42 +0300 Subject: [PATCH 23/39] Add missing return call --- app.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app.go b/app.go index 3ffe9ad94..32ce53a03 100644 --- a/app.go +++ b/app.go @@ -114,6 +114,7 @@ func (a *theApp) checkAuthenticationIfNotExists(domain *domain.D, w http.Respons // Without auth, fall back to 404 if domain == nil { httperrors.Serve404(w) + return true } return false -- GitLab From af8b9cd5df9bf6331b9494149d2e402d30bcea81 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 18 Aug 2018 10:08:58 +0300 Subject: [PATCH 24/39] Fix the cookie domain by using the SplitHostAndPort --- internal/auth/auth.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index da6789dc7..f2f21537d 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/http" "strings" "sync" @@ -54,10 +55,15 @@ type errorResponse struct { func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { store := sessions.NewCookieStore([]byte(a.storeSecret)) + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } + // Cookie just for this domain store.Options = &sessions.Options{ Path: "/", - Domain: r.Host, + Domain: host, } return store.Get(r, "gitlab-pages") -- GitLab From f6edf4e90517c8ba0ffa3190f0b9db537f5f0e1b Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 18 Aug 2018 21:49:47 +0300 Subject: [PATCH 25/39] Added checks for errors, refactored a bit to avoid method complexity increasing, fixed to work with custom ports and TLS enabled or not --- acceptance_test.go | 2 +- internal/auth/auth.go | 143 +++++++++++++++++++++++++----------------- 2 files changed, 88 insertions(+), 57 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 98db02037..31f4e3e5c 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -709,7 +709,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { require.NoError(t, err) state := url.Query().Get("state") - assert.Equal(t, url.Query().Get("domain"), "private.domain.com") + assert.Equal(t, url.Query().Get("domain"), "http://private.domain.com") pagesrsp, err := GetRedirectPage(t, httpListener, url.Host, url.Path+"?"+url.RawQuery) require.NoError(t, err) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f2f21537d..f85244050 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "net/url" "strings" "sync" "time" @@ -69,24 +70,25 @@ func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { return store.Get(r, "gitlab-pages") } -func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) bool { +func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { // Create or get session session, err := a.getSessionFromStore(r) if err != nil { // Save cookie again - session.Save(r, w) + err := session.Save(r, w) + if err != nil { + log.WithError(err).Error("Failed to save the session") + httperrors.Serve500(w) + return nil, err + } + http.Redirect(w, r, getRequestAddress(r), 302) - return true + return nil, err } - return false -} - -func (a *Auth) getSession(r *http.Request) *sessions.Session { - session, _ := a.getSessionFromStore(r) - return session + return session, nil } // TryAuthenticate tries to authenticate user and fetch access token if request is a callback to auth @@ -96,12 +98,11 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain return false } - if a.checkSession(w, r) { + session, err := a.checkSession(w, r) + if err != nil { return true } - session := a.getSession(r) - // Request is for auth if r.URL.Path != callbackPath { return false @@ -123,39 +124,47 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain } if verifyCodeAndStateGiven(r) { + a.checkAuthenticationResponse(session, w, r) + return true + } - if !validateState(r, session) { - // State is NOT ok - log.Debug("Authentication state did not match expected") - - httperrors.Serve401(w) - return true - } + return false +} - // Fetch access token with authorization code - token, err := a.fetchAccessToken(r.URL.Query().Get("code")) +func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.ResponseWriter, r *http.Request) { - // Fetching token not OK - if err != nil { - log.WithError(err).Debug("Fetching access token failed") + if !validateState(r, session) { + // State is NOT ok + log.Debug("Authentication state did not match expected") - httperrors.Serve503(w) - return true - } + httperrors.Serve401(w) + return + } - // Store access token - session.Values["access_token"] = token.AccessToken - session.Save(r, w) + // Fetch access token with authorization code + token, err := a.fetchAccessToken(r.URL.Query().Get("code")) - // Redirect back to requested URI - log.Debug("Authentication was successful, redirecting user back to requested page") + // Fetching token not OK + if err != nil { + log.WithError(err).Debug("Fetching access token failed") - http.Redirect(w, r, session.Values["uri"].(string), 302) + httperrors.Serve503(w) + return + } - return true + // Store access token + session.Values["access_token"] = token.AccessToken + err = session.Save(r, w) + if err != nil { + log.WithError(err).Error("Failed to save the session") + httperrors.Serve500(w) + return } - return false + // Redirect back to requested URI + log.Debug("Authentication was successful, redirecting user back to requested page") + + http.Redirect(w, r, session.Values["uri"].(string), 302) } func (a *Auth) domainAllowed(domain string, dm domain.Map, lock *sync.RWMutex) bool { @@ -171,20 +180,33 @@ func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWrit domain := r.URL.Query().Get("domain") state := r.URL.Query().Get("state") - if !a.domainAllowed(domain, dm, lock) { - log.WithField("domain", domain).Debug("Domain is not configured") + proxyurl, err := url.Parse(domain) + if err != nil { + log.WithField("domain", domain).Error("Failed to parse domain query parameter") + httperrors.Serve500(w) + return true + } + host, _, err := net.SplitHostPort(proxyurl.Host) + if err != nil { + host = proxyurl.Host + } + + if !a.domainAllowed(host, dm, lock) { + log.WithField("domain", host).Debug("Domain is not configured") httperrors.Serve401(w) return true } log.WithField("domain", domain).Debug("User is authenticating via domain") - if r.TLS != nil { - session.Values["proxy_auth_domain"] = "https://" + domain - } else { - session.Values["proxy_auth_domain"] = "http://" + domain + session.Values["proxy_auth_domain"] = domain + + err = session.Save(r, w) + if err != nil { + log.WithError(err).Error("Failed to save the session") + httperrors.Serve500(w) + return true } - session.Save(r, w) url := fmt.Sprintf(authorizeURLTemplate, a.gitLabServer, a.clientID, a.redirectURI, state) http.Redirect(w, r, url, 302) @@ -202,7 +224,12 @@ func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWrit // Clear proxying from session delete(session.Values, "proxy_auth_domain") - session.Save(r, w) + err := session.Save(r, w) + if err != nil { + log.WithError(err).Error("Failed to save the session") + httperrors.Serve500(w) + return true + } // Redirect pages under custom domain http.Redirect(w, r, proxyDomain+r.URL.Path+"?"+r.URL.RawQuery, 302) @@ -302,7 +329,12 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter // Clear possible proxying delete(session.Values, "proxy_auth_domain") - session.Save(r, w) + err := session.Save(r, w) + if err != nil { + log.WithError(err).Error("Failed to save the session") + httperrors.Serve500(w) + return true + } // Because the pages domain might be in public suffix list, we have to // redirect to pages domain to trigger authorization flow @@ -314,10 +346,7 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter } func (a *Auth) getProxyAddress(r *http.Request, state string) string { - if r.TLS != nil { - return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, r.Host, state) - } - return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, r.Host, state) + return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, getRequestDomain(r), state) } func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) { @@ -325,7 +354,12 @@ func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Re // Invalidate access token and redirect back for refreshing and re-authenticating delete(session.Values, "access_token") - session.Save(r, w) + err := session.Save(r, w) + if err != nil { + log.WithError(err).Error("Failed to save the session") + httperrors.Serve500(w) + return + } http.Redirect(w, r, getRequestAddress(r), 302) } @@ -346,12 +380,11 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. return false } - if a.checkSession(w, r) { + session, err := a.checkSession(w, r) + if err != nil { return true } - session := a.getSession(r) - if a.checkTokenExists(session, w, r) { return true } @@ -394,16 +427,14 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { if a == nil { - log.Warn("Authentication is disabled, falling back to PUBLIC pages") return false } - if a.checkSession(w, r) { + session, err := a.checkSession(w, r) + if err != nil { return true } - session := a.getSession(r) - if a.checkTokenExists(session, w, r) { return true } -- GitLab From 036f5bd5f519d54a502ae44e966e6c5dbcefc315 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Tue, 21 Aug 2018 22:22:11 +0300 Subject: [PATCH 26/39] Make private projects not accessible if auth is not configured --- acceptance_test.go | 4 ++-- internal/auth/auth.go | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 31f4e3e5c..23abad5d7 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -576,7 +576,7 @@ func TestKnownHostInReverseProxySetupReturns200(t *testing.T) { } } -func TestWhenAuthIsDisabledPrivateIsAccessible(t *testing.T) { +func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "") defer teardown() @@ -585,7 +585,7 @@ func TestWhenAuthIsDisabledPrivateIsAccessible(t *testing.T) { require.NoError(t, err) rsp.Body.Close() - assert.Equal(t, http.StatusOK, rsp.StatusCode) + assert.Equal(t, http.StatusInternalServerError, rsp.StatusCode) } func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f85244050..8b0396d4b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -427,7 +427,9 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { if a == nil { - return false + log.Debug("Authentication is not configured") + httperrors.Serve500(w) + return true } session, err := a.checkSession(w, r) -- GitLab From 9977423ac9ac8b6a58d4602f808dccc3fa08428b Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 10 Sep 2018 13:00:11 +0300 Subject: [PATCH 27/39] Fix handling the projects with not updated configuration --- app.go | 2 +- internal/domain/domain.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 32ce53a03..6edf0ae53 100644 --- a/app.go +++ b/app.go @@ -95,7 +95,7 @@ func (a *theApp) getHostAndDomain(r *http.Request) (host string, domain *domain. } func (a *theApp) checkAuthenticationIfNotExists(domain *domain.D, w http.ResponseWriter, r *http.Request) bool { - if domain == nil || domain.GetID(r) == 0 { + if domain == nil || !domain.HasProject(r) { // Only if auth is supported if a.Auth.IsAuthSupported() { diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 1ea008c4b..2b6e83bd1 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -173,6 +173,25 @@ func (d *D) GetID(r *http.Request) uint64 { return 0 } +// HasProject figures out if the project exists that the user tries to access +func (d *D) HasProject(r *http.Request) bool { + if d == nil { + return false + } + + if d.config != nil { + return true + } + + project := d.getProject(r) + + if project != nil { + return true + } + + return false +} + func (d *D) serveFile(w http.ResponseWriter, r *http.Request, origPath string) error { fullPath := handleGZip(w, r, origPath) -- GitLab From ce5d10b13b433220a9e8a9c1f91b430098382b4e Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 10 Sep 2018 15:37:22 +0300 Subject: [PATCH 28/39] Fix panic serving errors --- internal/auth/auth.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 8b0396d4b..f1fe03c08 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -73,19 +73,19 @@ func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { // Create or get session - session, err := a.getSessionFromStore(r) + session, errsession := a.getSessionFromStore(r) - if err != nil { + if errsession != nil { // Save cookie again - err := session.Save(r, w) - if err != nil { - log.WithError(err).Error("Failed to save the session") + errsave := session.Save(r, w) + if errsave != nil { + log.WithError(errsave).Error("Failed to save the session") httperrors.Serve500(w) - return nil, err + return nil, errsave } http.Redirect(w, r, getRequestAddress(r), 302) - return nil, err + return nil, errsession } return session, nil -- GitLab From 82028462a4c14b947bd897598aa12f16796ea6c3 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 10 Sep 2018 15:55:32 +0300 Subject: [PATCH 29/39] Refactor to use the same store --- internal/auth/auth.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f1fe03c08..ab661d3db 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -37,8 +37,8 @@ type Auth struct { clientSecret string redirectURI string gitLabServer string - storeSecret string apiClient *http.Client + store sessions.Store } type tokenResponse struct { @@ -54,20 +54,22 @@ type errorResponse struct { } func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { - store := sessions.NewCookieStore([]byte(a.storeSecret)) - host, _, err := net.SplitHostPort(r.Host) if err != nil { host = r.Host } - // Cookie just for this domain - store.Options = &sessions.Options{ - Path: "/", - Domain: host, + session, err := a.store.Get(r, "gitlab-pages") + + if session != nil { + // Cookie just for this domain + session.Options = &sessions.Options{ + Path: "/", + Domain: host, + } } - return store.Get(r, "gitlab-pages") + return session, err } func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { @@ -502,10 +504,10 @@ func New(pagesDomain string, storeSecret string, clientID string, clientSecret s clientSecret: clientSecret, redirectURI: redirectURI, gitLabServer: strings.TrimRight(gitLabServer, "/"), - storeSecret: storeSecret, apiClient: &http.Client{ Timeout: 5 * time.Second, Transport: httptransport.Transport, }, + store: sessions.NewCookieStore([]byte(storeSecret)), } } -- GitLab From 2ef34395a5106e46009a3261b1c8a8a2a224b032 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 10 Sep 2018 16:07:08 +0300 Subject: [PATCH 30/39] Add file to make an example of the project conflict issue --- .../group/group.gitlab-example.com/public/project/index.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 shared/pages/group/group.gitlab-example.com/public/project/index.html diff --git a/shared/pages/group/group.gitlab-example.com/public/project/index.html b/shared/pages/group/group.gitlab-example.com/public/project/index.html new file mode 100644 index 000000000..7c9933f70 --- /dev/null +++ b/shared/pages/group/group.gitlab-example.com/public/project/index.html @@ -0,0 +1 @@ +domain project subdirectory -- GitLab From 551ec02c4a7d0d4da3936f0e1fccf6269ac07921 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Mon, 10 Sep 2018 18:49:17 +0300 Subject: [PATCH 31/39] Move access controlled projects under a group without default domain project --- acceptance_test.go | 18 +++++++++--------- internal/domain/map_test.go | 1 + .../private.project.1/config.json | 0 .../private.project.1/public/index.html | 0 .../private.project.2/config.json | 0 .../private.project.2/public/index.html | 0 .../private.project/config.json | 0 .../private.project/public/index.html | 0 8 files changed, 10 insertions(+), 9 deletions(-) rename shared/pages/{group => group.auth}/private.project.1/config.json (100%) rename shared/pages/{group => group.auth}/private.project.1/public/index.html (100%) rename shared/pages/{group => group.auth}/private.project.2/config.json (100%) rename shared/pages/{group => group.auth}/private.project.2/public/index.html (100%) rename shared/pages/{group => group.auth}/private.project/config.json (100%) rename shared/pages/{group => group.auth}/private.project/public/index.html (100%) diff --git a/acceptance_test.go b/acceptance_test.go index 23abad5d7..19d1c4c90 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -298,7 +298,7 @@ func TestPrometheusMetricsCanBeScraped(t *testing.T) { body, _ := ioutil.ReadAll(resp.Body) assert.Contains(t, string(body), "gitlab_pages_http_sessions_active 0") - assert.Contains(t, string(body), "gitlab_pages_domains_served_total 12") + assert.Contains(t, string(body), "gitlab_pages_domains_served_total 13") } } @@ -581,7 +581,7 @@ func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "") defer teardown() - rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "private.project/") + rsp, err := GetPageFromListener(t, httpListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) rsp.Body.Close() @@ -593,7 +593,7 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() - rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) defer rsp.Body.Close() @@ -635,7 +635,7 @@ func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() - rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) defer rsp.Body.Close() @@ -654,7 +654,7 @@ func TestWhenLoginCallbackWithCorrectStateWithoutEndpoint(t *testing.T) { teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") defer teardown() - rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "private.project/") + rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) defer rsp.Body.Close() @@ -807,28 +807,28 @@ func TestAccessControl(t *testing.T) { Description string }{ { - "group.gitlab-example.com", + "group.auth.gitlab-example.com", "/private.project/", http.StatusOK, false, "project with access", }, { - "group.gitlab-example.com", + "group.auth.gitlab-example.com", "/private.project.1/", http.StatusNotFound, // Do not expose project existed false, "project without access", }, { - "group.gitlab-example.com", + "group.auth.gitlab-example.com", "/private.project.2/", http.StatusFound, true, "invalid token test should redirect back", }, { - "group.gitlab-example.com", + "group.auth.gitlab-example.com", "/nonexistent/", http.StatusNotFound, false, diff --git a/internal/domain/map_test.go b/internal/domain/map_test.go index 88b406bf2..31ebc0162 100644 --- a/internal/domain/map_test.go +++ b/internal/domain/map_test.go @@ -52,6 +52,7 @@ func TestReadProjects(t *testing.T) { "test2.my-domain.com", "no.cert.com", "private.domain.com", + "group.auth.test.io", } for _, expected := range domains { diff --git a/shared/pages/group/private.project.1/config.json b/shared/pages/group.auth/private.project.1/config.json similarity index 100% rename from shared/pages/group/private.project.1/config.json rename to shared/pages/group.auth/private.project.1/config.json diff --git a/shared/pages/group/private.project.1/public/index.html b/shared/pages/group.auth/private.project.1/public/index.html similarity index 100% rename from shared/pages/group/private.project.1/public/index.html rename to shared/pages/group.auth/private.project.1/public/index.html diff --git a/shared/pages/group/private.project.2/config.json b/shared/pages/group.auth/private.project.2/config.json similarity index 100% rename from shared/pages/group/private.project.2/config.json rename to shared/pages/group.auth/private.project.2/config.json diff --git a/shared/pages/group/private.project.2/public/index.html b/shared/pages/group.auth/private.project.2/public/index.html similarity index 100% rename from shared/pages/group/private.project.2/public/index.html rename to shared/pages/group.auth/private.project.2/public/index.html diff --git a/shared/pages/group/private.project/config.json b/shared/pages/group.auth/private.project/config.json similarity index 100% rename from shared/pages/group/private.project/config.json rename to shared/pages/group.auth/private.project/config.json diff --git a/shared/pages/group/private.project/public/index.html b/shared/pages/group.auth/private.project/public/index.html similarity index 100% rename from shared/pages/group/private.project/public/index.html rename to shared/pages/group.auth/private.project/public/index.html -- GitLab From 7a47e860f3e31b02eacfff2bc32bef25e52f27b7 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Tue, 11 Sep 2018 16:03:19 +0000 Subject: [PATCH 32/39] Do not set domain to cookie to avoid wildcard cookie --- internal/auth/auth.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index ab661d3db..5890feaee 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -65,7 +65,6 @@ func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { // Cookie just for this domain session.Options = &sessions.Options{ Path: "/", - Domain: host, } } -- GitLab From cdf67e05dc6de54790d899d350acf863ac4eceb1 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Tue, 11 Sep 2018 17:17:58 +0000 Subject: [PATCH 33/39] Remove not used host --- internal/auth/auth.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 5890feaee..4f4427ff9 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -54,11 +54,6 @@ type errorResponse struct { } func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - host = r.Host - } - session, err := a.store.Get(r, "gitlab-pages") if session != nil { -- GitLab From 2c766aba4f008c4a80e328a4eabbaae186276fc9 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Wed, 12 Sep 2018 16:51:54 +0300 Subject: [PATCH 34/39] Avoid caching if project is access controlled --- acceptance_test.go | 1 + internal/domain/domain.go | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/acceptance_test.go b/acceptance_test.go index 19d1c4c90..f0cdc749f 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -897,6 +897,7 @@ func TestAccessControl(t *testing.T) { defer rsp.Body.Close() assert.Equal(t, c.Status, rsp.StatusCode) + assert.Equal(t, "", rsp.Header.Get("Cache-Control")) if c.RedirectBack { url, err = url.Parse(rsp.Header.Get("Location")) diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 2b6e83bd1..d98a0b913 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -206,9 +206,11 @@ func (d *D) serveFile(w http.ResponseWriter, r *http.Request, origPath string) e return err } - // Set caching headers - w.Header().Set("Cache-Control", "max-age=600") - w.Header().Set("Expires", time.Now().Add(10*time.Minute).Format(time.RFC1123)) + if !d.IsAccessControlEnabled(r) { + // Set caching headers + w.Header().Set("Cache-Control", "max-age=600") + w.Header().Set("Expires", time.Now().Add(10*time.Minute).Format(time.RFC1123)) + } // ServeContent sets Content-Type for us http.ServeContent(w, r, origPath, fi.ModTime(), file) -- GitLab From 0cb2e9714fc66486b8deaeeb06c60ae7b701698f Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 22 Sep 2018 14:52:47 +0300 Subject: [PATCH 35/39] Add special handling for namespace projects to avoid existence leak --- acceptance_test.go | 25 ++++++ app.go | 27 +++++- internal/domain/domain.go | 72 +++++++++++++--- internal/domain/domain_test.go | 86 ++++++++++--------- internal/domain/map.go | 7 +- .../public/index.html | 1 + .../public/private.project/index.html | 1 + 7 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html create mode 100644 shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html diff --git a/acceptance_test.go b/acceptance_test.go index f0cdc749f..17eb7cb42 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -754,6 +754,31 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { assert.Equal(t, http.StatusOK, authrsp.StatusCode) } +func TestAccessControlGroupDomain404RedirectsAuth(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") + defer teardown() + + rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "/nonexistent/") + require.NoError(t, err) + defer rsp.Body.Close() + assert.Equal(t, http.StatusFound, rsp.StatusCode) + // Redirects to the projects under gitlab pages domain for authentication flow + url, err := url.Parse(rsp.Header.Get("Location")) + require.NoError(t, err) + assert.Equal(t, "projects.gitlab-example.com", url.Host) + assert.Equal(t, "/auth", url.Path) +} +func TestAccessControlProject404DoesNotRedirect(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcessWithAuth(t, *pagesBinary, listeners, "") + defer teardown() + + rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "/project/nonexistent/") + require.NoError(t, err) + defer rsp.Body.Close() + assert.Equal(t, http.StatusNotFound, rsp.StatusCode) +} func TestAccessControl(t *testing.T) { skipUnlessEnabled(t, "not-inplace-chroot") diff --git a/app.go b/app.go index 6edf0ae53..295bc7c8f 100644 --- a/app.go +++ b/app.go @@ -184,14 +184,37 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo // Serve static file, applying CORS headers if necessary if a.DisableCrossOriginRequests { - domain.ServeHTTP(&w, r) + a.serveFileOrNotFound(domain, &w, r) } else { - corsHandler.ServeHTTP(&w, r, domain.ServeHTTP) + corsHandler.ServeHTTP(&w, r, a.serveFileOrNotFound(domain, &w, r)) } metrics.ProcessedRequests.WithLabelValues(strconv.Itoa(w.status), r.Method).Inc() } +func (a *theApp) serveFileOrNotFound(domain *domain.D, ww http.ResponseWriter, r *http.Request) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + fileServed := domain.ServeFileHTTP(w, r) + + if !fileServed { + // We need to trigger authentication flow here if file does not exist to prevent exposing possibly private project existence, + // because the projects override the paths of the namespace project and they might be private even though + // namespace project is public. + if domain.IsNamespaceProject(r) { + + if a.Auth.CheckAuthenticationWithoutProject(ww, r) { + return + } + + httperrors.Serve404(ww) + return + } + + domain.ServeNotFoundHTTP(w, r) + } + } +} + func (a *theApp) ServeHTTP(ww http.ResponseWriter, r *http.Request) { https := r.TLS != nil a.serveContent(ww, r, https) diff --git a/internal/domain/domain.go b/internal/domain/domain.go index d98a0b913..01ee1a469 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -24,9 +24,10 @@ type locationDirectoryError struct { } type project struct { - HTTPSOnly bool - AccessControl bool - ID uint64 + NamespaceProject bool + HTTPSOnly bool + AccessControl bool + ID uint64 } type projects map[string]*project @@ -154,6 +155,26 @@ func (d *D) IsAccessControlEnabled(r *http.Request) bool { return false } +// IsNamespaceProject figures out if the request is to a namespace project +func (d *D) IsNamespaceProject(r *http.Request) bool { + if d == nil { + return false + } + + // If request is to a custom domain, we do not handle it as a namespace project + // as there can't be multiple projects under the same custom domain + if d.config != nil { + return false + } + + // Check projects served under the group domain, including the default one + if project := d.getProject(r); project != nil { + return project.NamespaceProject + } + + return false +} + // GetID figures out what is the ID of the project user tries to access func (d *D) GetID(r *http.Request) uint64 { if d == nil { @@ -324,20 +345,27 @@ func (d *D) tryFile(w http.ResponseWriter, r *http.Request, projectName, pathSuf return d.serveFile(w, r, fullPath) } -func (d *D) serveFromGroup(w http.ResponseWriter, r *http.Request) { +func (d *D) serveFileFromGroup(w http.ResponseWriter, r *http.Request) bool { // The Path always contains "/" at the beginning split := strings.SplitN(r.URL.Path, "/", 3) // Try to serve file for http://group.example.com/subpath/... => /group/subpath/... if len(split) >= 2 && d.tryFile(w, r, split[1], split[1], split[2:]...) == nil { - return + return true } // Try to serve file for http://group.example.com/... => /group/group.example.com/... if r.Host != "" && d.tryFile(w, r, strings.ToLower(r.Host), "", r.URL.Path) == nil { - return + return true } + return false +} + +func (d *D) serveNotFoundFromGroup(w http.ResponseWriter, r *http.Request) { + // The Path always contains "/" at the beginning + split := strings.SplitN(r.URL.Path, "/", 3) + // Try serving not found page for http://group.example.com/subpath/ => /group/subpath/404.html if len(split) >= 2 && d.tryNotFound(w, r, split[1]) == nil { return @@ -352,12 +380,16 @@ func (d *D) serveFromGroup(w http.ResponseWriter, r *http.Request) { httperrors.Serve404(w) } -func (d *D) serveFromConfig(w http.ResponseWriter, r *http.Request) { +func (d *D) serveFileFromConfig(w http.ResponseWriter, r *http.Request) bool { // Try to serve file for http://host/... => /group/project/... if d.tryFile(w, r, d.projectName, "", r.URL.Path) == nil { - return + return true } + return false +} + +func (d *D) serveNotFoundFromConfig(w http.ResponseWriter, r *http.Request) { // Try serving not found page for http://host/ => /group/project/404.html if d.tryNotFound(w, r, d.projectName) == nil { return @@ -384,18 +416,32 @@ func (d *D) EnsureCertificate() (*tls.Certificate, error) { return d.certificate, d.certificateError } -// ServeHTTP implements http.Handler. -func (d *D) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// ServeFileHTTP implements http.Handler. Returns true if something was served, false if not. +func (d *D) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool { + if d == nil { + httperrors.Serve404(w) + return true + } + + if d.config != nil { + return d.serveFileFromConfig(w, r) + } + + return d.serveFileFromGroup(w, r) +} + +// ServeNotFoundHTTP implements http.Handler. Serves the not found pages from the projects. +func (d *D) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { if d == nil { httperrors.Serve404(w) return } if d.config != nil { - d.serveFromConfig(w, r) - } else { - d.serveFromGroup(w, r) + d.serveNotFoundFromConfig(w, r) } + + d.serveNotFoundFromGroup(w, r) } func endsWithSlash(path string) bool { diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index 64d11a29c..4c08aee5d 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -17,6 +17,14 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/fixture" ) +func serveFileOrNotFound(domain *D) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !domain.ServeFileHTTP(w, r) { + domain.ServeNotFoundHTTP(w, r) + } + } +} + func TestGroupServeHTTP(t *testing.T) { setUpTests() @@ -25,27 +33,27 @@ func TestGroupServeHTTP(t *testing.T) { projectName: "", } - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/", nil, "main-dir") - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/index.html", nil, "main-dir") - assert.HTTPRedirect(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project", nil) - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project", nil, + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/", nil, "main-dir") + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/index.html", nil, "main-dir") + assert.HTTPRedirect(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project", nil) + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project", nil, `Found`) - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/", nil, "project-subdir") - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/index.html", nil, "project-subdir") - assert.HTTPRedirect(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir", nil) - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir", nil, + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/", nil, "project-subdir") + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/index.html", nil, "project-subdir") + assert.HTTPRedirect(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/subdir", nil) + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/subdir", nil, `Found`) - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/subdir/", nil, "project-subsubdir") - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project2/", nil, "project2-main") - assert.HTTPBodyContains(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project2/index.html", nil, "project2-main") - assert.HTTPRedirect(t, testGroup.ServeHTTP, "GET", "http://group.test.io/private.project/", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io//about.gitlab.com/%2e%2e", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink/index.html", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/symlink/subdir/", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project/fifo", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/not-existing-file", nil) - assert.HTTPError(t, testGroup.ServeHTTP, "GET", "http://group.test.io/project//about.gitlab.com/%2e%2e", nil) + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/subdir/", nil, "project-subsubdir") + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project2/", nil, "project2-main") + assert.HTTPBodyContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project2/index.html", nil, "project2-main") + assert.HTTPRedirect(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/private.project/", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io//about.gitlab.com/%2e%2e", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/symlink", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/symlink/index.html", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/symlink/subdir/", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project/fifo", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/not-existing-file", nil) + assert.HTTPError(t, serveFileOrNotFound(testGroup), "GET", "http://group.test.io/project//about.gitlab.com/%2e%2e", nil) } func TestDomainServeHTTP(t *testing.T) { @@ -59,15 +67,15 @@ func TestDomainServeHTTP(t *testing.T) { }, } - assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/", nil, "project2-main") - assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/index.html", nil, "project2-main") - assert.HTTPRedirect(t, testDomain.ServeHTTP, "GET", "/subdir", nil) - assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/subdir", nil, + assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/", nil, "project2-main") + assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/index.html", nil, "project2-main") + assert.HTTPRedirect(t, serveFileOrNotFound(testDomain), "GET", "/subdir", nil) + assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir", nil, `Found`) - assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/subdir/", nil, "project2-subdir") - assert.HTTPBodyContains(t, testDomain.ServeHTTP, "GET", "/subdir/index.html", nil, "project2-subdir") - assert.HTTPError(t, testDomain.ServeHTTP, "GET", "//about.gitlab.com/%2e%2e", nil) - assert.HTTPError(t, testDomain.ServeHTTP, "GET", "/not-existing-file", nil) + assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir/", nil, "project2-subdir") + assert.HTTPBodyContains(t, serveFileOrNotFound(testDomain), "GET", "/subdir/index.html", nil, "project2-subdir") + assert.HTTPError(t, serveFileOrNotFound(testDomain), "GET", "//about.gitlab.com/%2e%2e", nil) + assert.HTTPError(t, serveFileOrNotFound(testDomain), "GET", "/not-existing-file", nil) } func TestIsHTTPSOnly(t *testing.T) { @@ -238,7 +246,7 @@ func TestGroupServeHTTPGzip(t *testing.T) { } for _, tt := range testSet { - testHTTPGzip(t, testGroup.ServeHTTP, tt.mode, tt.url, tt.params, tt.acceptEncoding, tt.body, tt.ungzip) + testHTTPGzip(t, serveFileOrNotFound(testGroup), tt.mode, tt.url, tt.params, tt.acceptEncoding, tt.body, tt.ungzip) } } @@ -262,13 +270,13 @@ func TestGroup404ServeHTTP(t *testing.T) { projectName: "", } - testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.404/not/existing-file", nil, "Custom 404 project page") - testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.404/", nil, "Custom 404 project page") - testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.no.404/not/existing-file", nil, "Custom 404 group page") - testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/not/existing-file", nil, "Custom 404 group page") - testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") - testHTTP404(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/", nil, "Custom 404 group page") - assert.HTTPBodyNotContains(t, testGroup.ServeHTTP, "GET", "http://group.404.test.io/project.404.symlink/not/existing-file", nil, "Custom 404 project page") + testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/not/existing-file", nil, "Custom 404 project page") + testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404/", nil, "Custom 404 project page") + testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.no.404/not/existing-file", nil, "Custom 404 group page") + testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not/existing-file", nil, "Custom 404 group page") + testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") + testHTTP404(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/", nil, "Custom 404 group page") + assert.HTTPBodyNotContains(t, serveFileOrNotFound(testGroup), "GET", "http://group.404.test.io/project.404.symlink/not/existing-file", nil, "Custom 404 project page") } func TestDomain404ServeHTTP(t *testing.T) { @@ -282,8 +290,8 @@ func TestDomain404ServeHTTP(t *testing.T) { }, } - testHTTP404(t, testDomain.ServeHTTP, "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") - testHTTP404(t, testDomain.ServeHTTP, "GET", "http://group.404.test.io/", nil, "Custom 404 group page") + testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") + testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom 404 group page") } func TestPredefined404ServeHTTP(t *testing.T) { @@ -293,7 +301,7 @@ func TestPredefined404ServeHTTP(t *testing.T) { group: "group", } - testHTTP404(t, testDomain.ServeHTTP, "GET", "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found") + testHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found") } func TestGroupCertificate(t *testing.T) { @@ -348,7 +356,7 @@ func TestCacheControlHeaders(t *testing.T) { require.NoError(t, err) now := time.Now() - testGroup.ServeHTTP(w, req) + serveFileOrNotFound(testGroup)(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "max-age=600", w.Header().Get("Cache-Control")) diff --git a/internal/domain/map.go b/internal/domain/map.go index b832ffb3b..d2e7c74f9 100644 --- a/internal/domain/map.go +++ b/internal/domain/map.go @@ -58,9 +58,10 @@ func (dm Map) updateGroupDomain(rootDomain, group, projectName string, httpsOnly } groupDomain.projects[strings.ToLower(projectName)] = &project{ - HTTPSOnly: httpsOnly, - AccessControl: accessControl, - ID: id, + NamespaceProject: domainName == strings.ToLower(projectName), + HTTPSOnly: httpsOnly, + AccessControl: accessControl, + ID: id, } dm[domainName] = groupDomain diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html b/shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html new file mode 100644 index 000000000..d86bac9de --- /dev/null +++ b/shared/pages/group.auth/group.auth.gitlab-example.com/public/index.html @@ -0,0 +1 @@ +OK diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html b/shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html new file mode 100644 index 000000000..7c9933f70 --- /dev/null +++ b/shared/pages/group.auth/group.auth.gitlab-example.com/public/private.project/index.html @@ -0,0 +1 @@ +domain project subdirectory -- GitLab From cee3464d6fe99efebac5f8d1b569f1301babdade Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sat, 22 Sep 2018 15:06:01 +0300 Subject: [PATCH 36/39] Copied the fixed getProject function from the !111 to make tests pass --- internal/domain/domain.go | 48 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 01ee1a469..4ac431002 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "mime" + "net" "net/http" "os" "path/filepath" @@ -100,20 +101,37 @@ func setContentType(w http.ResponseWriter, fullPath string) { } } -func (d *D) getProject(r *http.Request) *project { - // Check default domain config (e.g. http://mydomain.gitlab.io) - if groupProject := d.projects[strings.ToLower(r.Host)]; groupProject != nil { - return groupProject +// Look up a project inside the domain based on the host and path. Returns the +// project and its name (if applicable) +func (d *D) getProject(r *http.Request) (*project, string) { + // Check for a project specified in the URL: http://group.gitlab.io/projectA + // If present, these projects shadow the group domain. + split := strings.SplitN(r.URL.Path, "/", 3) + if len(split) >= 2 { + if project := d.projects[split[1]]; project != nil { + return project, split[1] + } } - // Check URLs with multiple projects for a group - // (e.g. http://group.gitlab.io/projectA and http://group.gitlab.io/projectB) - split := strings.SplitN(r.URL.Path, "/", 3) - if len(split) < 2 { - return nil + // Since the URL doesn't specify a project (e.g. http://mydomain.gitlab.io), + // return the group project if it exists. + if host := getHost(r); host != "" { + if groupProject := d.projects[host]; groupProject != nil { + return groupProject, host + } + } + + return nil, "" +} + +func getHost(r *http.Request) string { + host := strings.ToLower(r.Host) + + if splitHost, _, err := net.SplitHostPort(host); err == nil { + host = splitHost } - return d.projects[split[1]] + return host } // IsHTTPSOnly figures out if the request should be handled with HTTPS @@ -129,7 +147,7 @@ func (d *D) IsHTTPSOnly(r *http.Request) bool { } // Check projects served under the group domain, including the default one - if project := d.getProject(r); project != nil { + if project, _ := d.getProject(r); project != nil { return project.HTTPSOnly } @@ -148,7 +166,7 @@ func (d *D) IsAccessControlEnabled(r *http.Request) bool { } // Check projects served under the group domain, including the default one - if project := d.getProject(r); project != nil { + if project, _ := d.getProject(r); project != nil { return project.AccessControl } @@ -168,7 +186,7 @@ func (d *D) IsNamespaceProject(r *http.Request) bool { } // Check projects served under the group domain, including the default one - if project := d.getProject(r); project != nil { + if project, _ := d.getProject(r); project != nil { return project.NamespaceProject } @@ -185,7 +203,7 @@ func (d *D) GetID(r *http.Request) uint64 { return d.config.ID } - project := d.getProject(r) + project, _ := d.getProject(r) if project != nil { return project.ID @@ -204,7 +222,7 @@ func (d *D) HasProject(r *http.Request) bool { return true } - project := d.getProject(r) + project, _ := d.getProject(r) if project != nil { return true -- GitLab From c84c03d5fecb2214fe8fb43ad740ccf37a967990 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Sun, 30 Sep 2018 16:02:58 +0300 Subject: [PATCH 37/39] Fix returning and calling function --- app.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 295bc7c8f..a8810da3b 100644 --- a/app.go +++ b/app.go @@ -184,15 +184,15 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo // Serve static file, applying CORS headers if necessary if a.DisableCrossOriginRequests { - a.serveFileOrNotFound(domain, &w, r) + a.serveFileOrNotFound(domain)(&w, r) } else { - corsHandler.ServeHTTP(&w, r, a.serveFileOrNotFound(domain, &w, r)) + corsHandler.ServeHTTP(&w, r, a.serveFileOrNotFound(domain)) } metrics.ProcessedRequests.WithLabelValues(strconv.Itoa(w.status), r.Method).Inc() } -func (a *theApp) serveFileOrNotFound(domain *domain.D, ww http.ResponseWriter, r *http.Request) http.HandlerFunc { +func (a *theApp) serveFileOrNotFound(domain *domain.D) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fileServed := domain.ServeFileHTTP(w, r) @@ -202,11 +202,11 @@ func (a *theApp) serveFileOrNotFound(domain *domain.D, ww http.ResponseWriter, r // namespace project is public. if domain.IsNamespaceProject(r) { - if a.Auth.CheckAuthenticationWithoutProject(ww, r) { + if a.Auth.CheckAuthenticationWithoutProject(w, r) { return } - httperrors.Serve404(ww) + httperrors.Serve404(w) return } -- GitLab From 934846801003e59e006d8ca47d985c6102bc93aa Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Tue, 2 Oct 2018 19:16:34 +0300 Subject: [PATCH 38/39] Fix comparing the domain and log request information as well. Removed invalid comment and fixed one else case. --- app.go | 5 +++- internal/auth/auth.go | 58 ++++++++++++++++++++++----------------- internal/domain/domain.go | 5 ++-- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/app.go b/app.go index a8810da3b..862a18943 100644 --- a/app.go +++ b/app.go @@ -175,7 +175,10 @@ func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https boo // Only for projects that have access control enabled if domain.IsAccessControlEnabled(r) { - log.Debug("Authenticate request") + log.WithFields(log.Fields{ + "host": r.Host, + "path": r.RequestURI, + }).Debug("Authenticate request") if a.Auth.CheckAuthentication(&w, r, domain.GetID(r)) { return diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 4f4427ff9..2dbda0138 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -59,7 +59,7 @@ func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { if session != nil { // Cookie just for this domain session.Options = &sessions.Options{ - Path: "/", + Path: "/", } } @@ -75,7 +75,7 @@ func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.S // Save cookie again errsave := session.Save(r, w) if errsave != nil { - log.WithError(errsave).Error("Failed to save the session") + logRequest(r).WithError(errsave).Error("Failed to save the session") httperrors.Serve500(w) return nil, errsave } @@ -104,7 +104,7 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain return false } - log.Debug("Authentication callback") + logRequest(r).Debug("Authentication callback") if a.handleProxyingAuth(session, w, r, dm, lock) { return true @@ -113,7 +113,7 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain // If callback is not successful errorParam := r.URL.Query().Get("error") if errorParam != "" { - log.WithField("error", errorParam).Debug("OAuth endpoint returned error") + logRequest(r).WithField("error", errorParam).Debug("OAuth endpoint returned error") httperrors.Serve401(w) return true @@ -131,7 +131,7 @@ func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.Res if !validateState(r, session) { // State is NOT ok - log.Debug("Authentication state did not match expected") + logRequest(r).Debug("Authentication state did not match expected") httperrors.Serve401(w) return @@ -142,7 +142,7 @@ func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.Res // Fetching token not OK if err != nil { - log.WithError(err).Debug("Fetching access token failed") + logRequest(r).WithError(err).Debug("Fetching access token failed") httperrors.Serve503(w) return @@ -152,13 +152,13 @@ func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.Res session.Values["access_token"] = token.AccessToken err = session.Save(r, w) if err != nil { - log.WithError(err).Error("Failed to save the session") + logRequest(r).WithError(err).Error("Failed to save the session") httperrors.Serve500(w) return } // Redirect back to requested URI - log.Debug("Authentication was successful, redirecting user back to requested page") + logRequest(r).Debug("Authentication was successful, redirecting user back to requested page") http.Redirect(w, r, session.Values["uri"].(string), 302) } @@ -166,8 +166,9 @@ func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.Res func (a *Auth) domainAllowed(domain string, dm domain.Map, lock *sync.RWMutex) bool { lock.RLock() defer lock.RUnlock() + domain = strings.ToLower(domain) _, present := dm[domain] - return strings.HasSuffix(strings.ToLower(domain), a.pagesDomain) || present + return domain == a.pagesDomain || strings.HasSuffix("."+domain, a.pagesDomain) || present } func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request, dm domain.Map, lock *sync.RWMutex) bool { @@ -178,7 +179,7 @@ func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWrit proxyurl, err := url.Parse(domain) if err != nil { - log.WithField("domain", domain).Error("Failed to parse domain query parameter") + logRequest(r).WithField("domain", domain).Error("Failed to parse domain query parameter") httperrors.Serve500(w) return true } @@ -188,18 +189,18 @@ func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWrit } if !a.domainAllowed(host, dm, lock) { - log.WithField("domain", host).Debug("Domain is not configured") + logRequest(r).WithField("domain", host).Debug("Domain is not configured") httperrors.Serve401(w) return true } - log.WithField("domain", domain).Debug("User is authenticating via domain") + logRequest(r).WithField("domain", domain).Debug("User is authenticating via domain") session.Values["proxy_auth_domain"] = domain err = session.Save(r, w) if err != nil { - log.WithError(err).Error("Failed to save the session") + logRequest(r).WithError(err).Error("Failed to save the session") httperrors.Serve500(w) return true } @@ -213,7 +214,7 @@ func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWrit // If auth request callback should be proxied to custom domain if shouldProxyCallbackToCustomDomain(r, session) { // Auth request is from custom domain, proxy callback there - log.Debug("Redirecting auth callback to custom domain") + logRequest(r).Debug("Redirecting auth callback to custom domain") // Store access token proxyDomain := session.Values["proxy_auth_domain"].(string) @@ -222,7 +223,7 @@ func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWrit delete(session.Values, "proxy_auth_domain") err := session.Save(r, w) if err != nil { - log.WithError(err).Error("Failed to save the session") + logRequest(r).WithError(err).Error("Failed to save the session") httperrors.Serve500(w) return true } @@ -315,7 +316,7 @@ func (a *Auth) fetchAccessToken(code string) (tokenResponse, error) { func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter, r *http.Request) bool { // If no access token redirect to OAuth login page if session.Values["access_token"] == nil { - log.Debug("No access token exists, redirecting user to OAuth2 login") + logRequest(r).Debug("No access token exists, redirecting user to OAuth2 login") // Generate state hash and store requested address state := base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(16)) @@ -327,7 +328,7 @@ func (a *Auth) checkTokenExists(session *sessions.Session, w http.ResponseWriter err := session.Save(r, w) if err != nil { - log.WithError(err).Error("Failed to save the session") + logRequest(r).WithError(err).Error("Failed to save the session") httperrors.Serve500(w) return true } @@ -346,13 +347,13 @@ func (a *Auth) getProxyAddress(r *http.Request, state string) string { } func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Request) { - log.Debug("Destroying session") + logRequest(r).Debug("Destroying session") // Invalidate access token and redirect back for refreshing and re-authenticating delete(session.Values, "access_token") err := session.Save(r, w) if err != nil { - log.WithError(err).Error("Failed to save the session") + logRequest(r).WithError(err).Error("Failed to save the session") httperrors.Serve500(w) return } @@ -390,7 +391,7 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. req, err := http.NewRequest("GET", url, nil) if err != nil { - log.WithError(err).Debug("Failed to authenticate request") + logRequest(r).WithError(err).Debug("Failed to authenticate request") httperrors.Serve500(w) return true @@ -400,7 +401,7 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. resp, err := a.apiClient.Do(req) if checkResponseForInvalidToken(resp, err) { - log.Debug("Access token was invalid, destroying session") + logRequest(r).Debug("Access token was invalid, destroying session") destroySession(session, w, r) return true @@ -409,7 +410,7 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. if err != nil || resp.StatusCode != 200 { // We return 404 if for some reason token is not valid to avoid (not) existence leak if err != nil { - log.WithError(err).Debug("Failed to retrieve info with token") + logRequest(r).WithError(err).Debug("Failed to retrieve info with token") } httperrors.Serve404(w) @@ -423,7 +424,7 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { if a == nil { - log.Debug("Authentication is not configured") + logRequest(r).Debug("Authentication is not configured") httperrors.Serve500(w) return true } @@ -450,7 +451,7 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje resp, err := a.apiClient.Do(req) if checkResponseForInvalidToken(resp, err) { - log.Debug("Access token was invalid, destroying session") + logRequest(r).Debug("Access token was invalid, destroying session") destroySession(session, w, r) return true @@ -458,7 +459,7 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje if err != nil || resp.StatusCode != 200 { if err != nil { - log.WithError(err).Debug("Failed to retrieve info with token") + logRequest(r).WithError(err).Debug("Failed to retrieve info with token") } // We return 404 if user has no access to avoid user knowing if the pages really existed or not @@ -489,6 +490,13 @@ func checkResponseForInvalidToken(resp *http.Response, err error) bool { return false } +func logRequest(r *http.Request) *log.Entry { + return log.WithFields(log.Fields{ + "host": r.Host, + "path": r.RequestURI, + }) +} + // New when authentication supported this will be used to create authentication handler func New(pagesDomain string, storeSecret string, clientID string, clientSecret string, redirectURI string, gitLabServer string) *Auth { diff --git a/internal/domain/domain.go b/internal/domain/domain.go index a402d9e0b..c9bda506b 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -372,7 +372,6 @@ func (d *D) serveFileFromGroup(w http.ResponseWriter, r *http.Request) bool { } func (d *D) serveNotFoundFromGroup(w http.ResponseWriter, r *http.Request) { - // The Path always contains "/" at the beginning project, projectName, _ := d.getProjectWithSubpath(r) if project == nil { httperrors.Serve404(w) @@ -447,9 +446,9 @@ func (d *D) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { if d.config != nil { d.serveNotFoundFromConfig(w, r) + } else { + d.serveNotFoundFromGroup(w, r) } - - d.serveNotFoundFromGroup(w, r) } func endsWithSlash(path string) bool { -- GitLab From f919cbee022c7d71bfbe83e7188843fcab5deca6 Mon Sep 17 00:00:00 2001 From: Tuomo Ala-Vannesluoma Date: Thu, 4 Oct 2018 19:36:52 +0300 Subject: [PATCH 39/39] Set session cookie HttpOnly to true --- internal/auth/auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 2dbda0138..c9f10961a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -59,7 +59,8 @@ func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { if session != nil { // Cookie just for this domain session.Options = &sessions.Options{ - Path: "/", + Path: "/", + HttpOnly: true, } } -- GitLab