diff options
author | Alessio Caiazza <acaiazza@gitlab.com> | 2023-11-28 16:35:27 +0300 |
---|---|---|
committer | Alessio Caiazza <acaiazza@gitlab.com> | 2023-11-28 16:35:27 +0300 |
commit | 121f1e96d05346988e88349597ee4992dfdd9b15 (patch) | |
tree | be015457e45fb14e7cf78fcad31b646800f0446a | |
parent | 93d2dc70ccac3920439712fba3afde55f05777fa (diff) | |
parent | 7e9d46766d1926f93bb4a0d6f00d03d122316ad6 (diff) |
Merge branch 'naman/17584-pages-namespace-in-path' into 'master'
Add path in session cookie when namespace is provided in path
See merge request https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/929
Merged-by: Alessio Caiazza <acaiazza@gitlab.com>
Approved-by: Alessio Caiazza <acaiazza@gitlab.com>
Co-authored-by: ngala <ngala@gitlab.com>
-rw-r--r-- | app.go | 1 | ||||
-rw-r--r-- | internal/auth/auth.go | 39 | ||||
-rw-r--r-- | internal/auth/auth_test.go | 51 | ||||
-rw-r--r-- | internal/auth/session.go | 19 | ||||
-rw-r--r-- | internal/config/config.go | 4 | ||||
-rw-r--r-- | internal/config/flags.go | 2 |
6 files changed, 108 insertions, 8 deletions
@@ -422,6 +422,7 @@ func (a *theApp) setAuth(config *cfg.Config) error { AuthScope: config.Authentication.Scope, AuthTimeout: config.Authentication.Timeout, CookieSessionTimeout: config.Authentication.CookieSessionTimeout, + AllowNamespaceInPath: config.General.NamespaceInPath, }) if err != nil { return fmt.Errorf("could not initialize auth package: %w", err) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index dcc81eee..fe092ae9 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -67,6 +67,7 @@ type Auth struct { store sessions.Store now func() time.Time // allows to stub time.Now() easily in tests cookieSessionTimeout time.Duration + allowNamespaceInPath bool } type tokenResponse struct { @@ -140,7 +141,8 @@ func (a *Auth) checkAuthenticationResponse(session *hostSession, w http.Response return } - decryptedCode, err := a.DecryptCode(r.URL.Query().Get("code"), getRequestDomain(r)) + decryptedCode, err := a.DecryptCode(r.URL.Query().Get("code"), + getRequestDomain(r, session.getNamespaceInPathFromSession())) if err != nil { logRequest(r).WithError(err).Error("failed to decrypt secure code") errortracking.CaptureErrWithReqAndStackTrace(err, r) @@ -317,11 +319,16 @@ func getRequestAddress(r *http.Request) string { return "http://" + r.Host + r.RequestURI } -func getRequestDomain(r *http.Request) string { +func getRequestDomain(r *http.Request, namespace string) string { + requestDomain := r.Host + if len(namespace) > 0 && strings.HasPrefix(r.Host, namespace) { + requestDomain = strings.TrimPrefix(r.Host, namespace+".") + "/" + namespace + } + if request.IsHTTPS(r) { - return "https://" + r.Host + return "https://" + requestDomain } - return "http://" + r.Host + return "http://" + requestDomain } func shouldProxyAuthToGitlab(r *http.Request) bool { @@ -440,15 +447,18 @@ func (a *Auth) checkTokenExists(session *hostSession, w http.ResponseWriter, r * // 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, session.Values["state"].(string)), http.StatusFound) + http.Redirect(w, + r, + a.getProxyAddress(r, session.Values["state"].(string), session.getNamespaceInPathFromSession()), + http.StatusFound) return true } return false } -func (a *Auth) getProxyAddress(r *http.Request, state string) string { - return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, getRequestDomain(r), state) +func (a *Auth) getProxyAddress(r *http.Request, state string, namespace string) string { + return fmt.Sprintf(authorizeProxyTemplate, a.redirectURI, getRequestDomain(r, namespace), state) } func destroySession(session *hostSession, w http.ResponseWriter, r *http.Request) { @@ -602,6 +612,19 @@ func (a *Auth) CheckResponseForInvalidToken(w http.ResponseWriter, r *http.Reque return false } +func (a *Auth) getNamespaceInPath(r *http.Request) string { + if !a.allowNamespaceInPath { + return "" + } + + namespaceInPath := r.Header.Get("X-Gitlab-Namespace-In-Path") + if namespaceInPath == "" || !strings.HasPrefix(r.Host, namespaceInPath+"."+a.pagesDomain) { + return "" + } + + return namespaceInPath +} + func checkResponseForInvalidToken(resp *http.Response, session *hostSession, w http.ResponseWriter, r *http.Request) bool { if resp.StatusCode == http.StatusUnauthorized { errResp := errorResponse{} @@ -664,6 +687,7 @@ type Options struct { AuthScope string AuthTimeout time.Duration CookieSessionTimeout time.Duration + AllowNamespaceInPath bool } // New when authentication supported this will be used to create authentication handler @@ -692,5 +716,6 @@ func New(options *Options) (*Auth, error) { jwtExpiry: time.Minute, now: time.Now, cookieSessionTimeout: options.CookieSessionTimeout, + allowNamespaceInPath: options.AllowNamespaceInPath, }, nil } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 40c6db15..8582a973 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -33,6 +33,7 @@ func createTestAuth(t *testing.T, internalServer string, publicServer string) *A AuthScope: "scope", AuthTimeout: 5 * time.Second, CookieSessionTimeout: 10 * time.Minute, + AllowNamespaceInPath: false, }) require.NoError(t, err) @@ -63,6 +64,9 @@ func setSessionValues(t *testing.T, r *http.Request, auth *Auth, values map[inte t.Helper() tmpRequest, err := http.NewRequest("GET", "http://"+r.Host, nil) + if len(r.Header.Get("X-Gitlab-Namespace-In-Path")) > 0 { + tmpRequest.Header.Set("X-Gitlab-Namespace-In-Path", r.Header.Get("X-Gitlab-Namespace-In-Path")) + } require.NoError(t, err) result := httptest.NewRecorder() @@ -629,3 +633,50 @@ func TestDomainAllowed(t *testing.T) { }) } } + +func testNamespaceInPath(t *testing.T, allowNamespaceInPath bool, namespace string, expectedPath string, msg string) { + auth := createTestAuth(t, "", "") + auth.allowNamespaceInPath = allowNamespaceInPath + + result := httptest.NewRecorder() + + r, err := http.NewRequest("Get", "https://namespace.pages.gitlab-example.com", nil) + r.Header.Set("X-Gitlab-Namespace-In-Path", namespace) + require.NoError(t, err) + + // pre-set an state + setSessionValues(t, r, auth, map[interface{}]interface{}{ + "state": "given_state", + }) + + contentServed := auth.CheckAuthentication(result, r, &domainMock{projectID: 1000}) + require.True(t, contentServed) + + // check if the state was re-used instead of re-created + session, _ := auth.getSessionFromStore(r) + require.Equal(t, expectedPath, session.Options.Path, msg) +} + +func TestValidSessionPathWhenNamespaceInPathIsDisabled(t *testing.T) { + testNamespaceInPath(t, + false, + "namespace", + "/", + "did not set path as '/' in session options") +} + +func TestValidSessionPathWhenNamespaceInPathIsEnabled(t *testing.T) { + testNamespaceInPath(t, + true, + "namespace", + "/namespace", + "did not set namespace path in session options") +} + +func TestForgedNamespaceWhenNamespaceInPathIsEnabled(t *testing.T) { + testNamespaceInPath(t, + true, + "namespace-forged", + "/", + "did not set path as '/' in session options") +} diff --git a/internal/auth/session.go b/internal/auth/session.go index 5bfc8e03..c9b4ee8d 100644 --- a/internal/auth/session.go +++ b/internal/auth/session.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gorilla/sessions" + "gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/gitlab-pages/internal/errortracking" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" @@ -22,17 +23,33 @@ func (s *hostSession) Save(r *http.Request, w http.ResponseWriter) error { return s.Session.Save(r, w) } +func (s *hostSession) getNamespaceInPathFromSession() string { + namespaceInPath := "" + if len(s.Options.Path) > 1 && s.Options.Path[0] == '/' { + namespaceInPath = s.Options.Path[1:] + } + return namespaceInPath +} + func (a *Auth) getSessionFromStore(r *http.Request) (*hostSession, error) { session, err := a.store.Get(r, "gitlab-pages") if session != nil { + namespaceInPath := a.getNamespaceInPath(r) + // Cookie just for this domain - session.Options.Path = "/" + session.Options.Path = "/" + namespaceInPath session.Options.HttpOnly = true session.Options.Secure = request.IsHTTPS(r) session.Options.MaxAge = int(a.cookieSessionTimeout.Seconds()) if session.Values[sessionHostKey] == nil || session.Values[sessionHostKey] != r.Host { + logRequest(r).WithFields(log.Fields{ + "Session host": session.Values[sessionHostKey], + "Request host": r.Host, + "Namespace in path": namespaceInPath, + }).Info("Resetting session values") + session.Values = make(map[interface{}]interface{}) } } diff --git a/internal/config/config.go b/internal/config/config.go index d2016550..ba227016 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,6 +61,8 @@ type General struct { ShowVersion bool CustomHeaders http.Header + + NamespaceInPath bool } // RateLimit config struct @@ -317,6 +319,7 @@ func loadConfig() (*Config, error) { InsecureCiphers: *insecureCiphers, PropagateCorrelationID: *propagateCorrelationID, ShowVersion: *showVersion, + NamespaceInPath: *namespaceInPath, }, RateLimit: RateLimit{ SourceIPLimitPerSecond: *rateLimitSourceIP, @@ -518,6 +521,7 @@ func logFields(config *Config) map[string]any { "sentry-dsn": config.Sentry.DSN, "sentry-environment": config.Sentry.Environment, "version": config.General.ShowVersion, + "namespace-in-path": *namespaceInPath, } } diff --git a/internal/config/flags.go b/internal/config/flags.go index bce3b07e..069173c3 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -107,6 +107,8 @@ var ( header = MultiStringFlag{separator: ";;"} + namespaceInPath = flag.Bool("namespace-in-path", false, "Enable Namespace in path") + // flags that won't be logged to the output on Pages boot nonLoggableFlags = map[string]bool{ "auth-client-id": true, |