diff options
author | Tuomo Ala-Vannesluoma <tuomoav@gmail.com> | 2018-08-07 20:16:26 +0300 |
---|---|---|
committer | Tuomo Ala-Vannesluoma <tuomoav@gmail.com> | 2018-08-07 20:31:02 +0300 |
commit | 90690a9d77b673df5845f05d626ff8f6e75529c7 (patch) | |
tree | eff442c8af947a379badc80018a79efe0df1b0cc | |
parent | 2666c24dacb27efd22ad78044d4f321beed63772 (diff) |
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
-rw-r--r-- | acceptance_test.go | 85 | ||||
-rw-r--r-- | internal/auth/auth.go | 143 | ||||
-rw-r--r-- | internal/domain/domain.go | 12 | ||||
-rw-r--r-- | internal/domain/domain_config.go | 10 | ||||
-rw-r--r-- | internal/domain/map_test.go | 1 | ||||
-rw-r--r-- | shared/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 483f4295..8a842290 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 dedb9341..ea185ea2 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 e32ba097..77429372 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 672f939c..2ab2ce6c 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 f20f98bd..45658e95 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 292ba673..e7d754a0 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 +} |