diff options
author | Vladimir Shushlin <vshushlin@gitlab.com> | 2020-07-13 15:34:36 +0300 |
---|---|---|
committer | Vladimir Shushlin <vshushlin@gitlab.com> | 2020-07-13 15:34:36 +0300 |
commit | dc7bf0c3a847320b9370decc8792aff3712dd3e2 (patch) | |
tree | e00fc0ee840f3b787a213d6f9752778ac5cc21f1 | |
parent | e1310e9432b9cdf8ac33304e4385e0564c2f023d (diff) | |
parent | 4c7d7872868361d79796e87cca2d4cf5d0e95824 (diff) |
Merge branch '183-custom-error-page-for-public-projects' into 'master'
Serve custom 404.html file for namespace domains
Closes #391 and #183
See merge request gitlab-org/gitlab-pages!263
-rw-r--r-- | acceptance_test.go | 170 | ||||
-rw-r--r-- | app.go | 45 | ||||
-rw-r--r-- | internal/auth/auth.go | 20 | ||||
-rw-r--r-- | internal/auth/auth_test.go | 44 | ||||
-rw-r--r-- | internal/domain/domain.go | 39 | ||||
-rw-r--r-- | internal/domain/domain_test.go | 85 | ||||
-rw-r--r-- | internal/source/disk/domain_test.go | 4 | ||||
-rw-r--r-- | internal/source/disk/map_test.go | 1 | ||||
-rw-r--r-- | shared/pages/group.404/domain.404/public/404.html | 2 | ||||
-rw-r--r-- | shared/pages/group.404/group.404.gitlab-example.com/public/404.html | 1 | ||||
-rw-r--r-- | shared/pages/group.404/private_project/config.json | 5 | ||||
-rw-r--r-- | shared/pages/group.404/private_project/public/404.html | 1 | ||||
-rw-r--r-- | shared/pages/group.404/private_unauthorized/config.json | 5 | ||||
-rw-r--r-- | shared/pages/group.404/private_unauthorized/public/404.html | 1 | ||||
-rw-r--r-- | shared/pages/group.auth/group.auth.gitlab-example.com/config.json | 1 | ||||
-rw-r--r-- | shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html | 1 | ||||
-rw-r--r-- | shared/pages/group.auth/private.project.1/public/404.html | 1 |
17 files changed, 381 insertions, 45 deletions
diff --git a/acceptance_test.go b/acceptance_test.go index e8f7fe1a..2b0a7800 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -197,6 +197,69 @@ func TestNestedSubgroups(t *testing.T) { }) } } + +func TestCustom404(t *testing.T) { + skipUnlessEnabled(t) + teardown := RunPagesProcess(t, *pagesBinary, listeners, "") + defer teardown() + + tests := []struct { + host string + path string + content string + }{ + { + host: "group.404.gitlab-example.com", + path: "project.404/not/existing-file", + content: "Custom 404 project page", + }, + { + host: "group.404.gitlab-example.com", + path: "project.404/", + content: "Custom 404 project page", + }, + { + host: "group.404.gitlab-example.com", + path: "not/existing-file", + content: "Custom 404 group page", + }, + { + host: "group.404.gitlab-example.com", + path: "not-existing-file", + content: "Custom 404 group page", + }, + { + host: "group.404.gitlab-example.com", + content: "Custom 404 group page", + }, + { + host: "domain.404.com", + content: "Custom domain.404 page", + }, + { + host: "group.404.gitlab-example.com", + path: "project.no.404/not/existing-file", + content: "The page you're looking for could not be found.", + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s/%s", test.host, test.path), func(t *testing.T) { + for _, spec := range listeners { + rsp, err := GetPageFromListener(t, spec, test.host, test.path) + + require.NoError(t, err) + defer rsp.Body.Close() + require.Equal(t, http.StatusNotFound, rsp.StatusCode) + + page, err := ioutil.ReadAll(rsp.Body) + require.NoError(t, err) + require.Contains(t, string(page), test.content) + } + }) + } +} + func TestCORSWhenDisabled(t *testing.T) { skipUnlessEnabled(t) teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-disable-cross-origin-requests") @@ -1143,6 +1206,113 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { require.Equal(t, http.StatusOK, authrsp.StatusCode) } +func TestCustomErrorPageWithAuth(t *testing.T) { + skipUnlessEnabled(t, "not-inplace-chroot") + testServer := makeGitLabPagesAccessStub(t) + testServer.Start() + defer testServer.Close() + + teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL) + defer teardown() + + tests := []struct { + name string + domain string + path string + expectedErrorPage string + }{ + { + name: "private_project_authorized", + domain: "group.404.gitlab-example.com", + path: "/private_project/unknown", + expectedErrorPage: "Private custom 404 error page", + }, + { + name: "public_namespace_with_private_unauthorized_project", + domain: "group.404.gitlab-example.com", + // /private_unauthorized/config.json resolves project ID to 2000 which will cause a 401 from the mock GitLab testServer + path: "/private_unauthorized/unknown", + expectedErrorPage: "Custom 404 group page", + }, + { + name: "private_namespace_authorized", + domain: "group.auth.gitlab-example.com", + path: "/unknown", + expectedErrorPage: "group.auth.gitlab-example.com namespace custom 404", + }, + { + name: "private_namespace_with_private_project_auth_failed", + domain: "group.auth.gitlab-example.com", + // project ID is 2000 + path: "/private.project.1/unknown", + expectedErrorPage: "The page you're looking for could not be found.", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rsp, err := GetRedirectPage(t, httpListener, tt.domain, tt.path) + 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") + require.Equal(t, "http://"+tt.domain, url.Query().Get("domain")) + + 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, "projects.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 + require.Equal(t, tt.domain, url.Host) + require.Equal(t, "1", url.Query().Get("code")) + require.Equal(t, state, url.Query().Get("state")) + + // Run auth callback in custom domain + authrsp, err = GetRedirectPageWithCookie(t, httpListener, tt.domain, "/auth?code=1&state="+ + state, cookie) + + require.NoError(t, err) + defer authrsp.Body.Close() + + // Will redirect to the page + groupCookie := authrsp.Header.Get("Set-Cookie") + require.Equal(t, http.StatusFound, authrsp.StatusCode) + + url, err = url.Parse(authrsp.Header.Get("Location")) + require.NoError(t, err) + + // Will redirect to custom domain error page + require.Equal(t, "http://"+tt.domain+tt.path, url.String()) + + // Fetch page in custom domain + anotherResp, err := GetRedirectPageWithCookie(t, httpListener, tt.domain, tt.path, groupCookie) + require.NoError(t, err) + + require.Equal(t, http.StatusNotFound, anotherResp.StatusCode) + + page, err := ioutil.ReadAll(anotherResp.Body) + require.NoError(t, err) + require.Contains(t, string(page), tt.expectedErrorPage) + }) + } +} + func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) { skipUnlessEnabled(t, "not-inplace-chroot") @@ -94,28 +94,18 @@ func (a *theApp) domain(host string) (*domain.Domain, error) { return a.domains.GetDomain(host) } -func (a *theApp) checkAuthenticationIfNotExists(domain *domain.Domain, w http.ResponseWriter, r *http.Request) bool { - if domain == nil || !domain.HasLookupPath(r) { - // 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 - } - } - - // Without auth, fall back to 404 - if domain == nil { - httperrors.Serve404(w) +// checkAuthAndServeNotFound performs the auth process if domain can't be found +// the main purpose of this process is to avoid leaking the project existence/not-existence +// by behaving the same if user has no access to the project or if project simply does not exists +func (a *theApp) checkAuthAndServeNotFound(domain *domain.Domain, w http.ResponseWriter, r *http.Request) bool { + // To avoid user knowing if pages exist, we will force user to login and authorize pages + if a.Auth.CheckAuthenticationWithoutProject(w, r, domain) { return true } - return false + // auth succeeded try to serve the correct 404 page + domain.ServeNotFoundAuthFailed(w, r) + return true } func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, https bool, host string, domain *domain.Domain) bool { @@ -134,8 +124,11 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht return true } - if a.checkAuthenticationIfNotExists(domain, w, r) { - return true + if !domain.HasLookupPath(r) { + // redirect to auth and serve not found + if a.checkAuthAndServeNotFound(domain, w, r) { + return true + } } if !https && domain.IsHTTPSOnly(r) { @@ -245,7 +238,7 @@ func (a *theApp) accessControlMiddleware(handler http.Handler) http.Handler { // Only for projects that have access control enabled if domain.IsAccessControlEnabled(r) { // accessControlMiddleware - if a.Auth.CheckAuthentication(w, r, domain.GetProjectID(r)) { + if a.Auth.CheckAuthentication(w, r, domain) { return } } @@ -267,16 +260,14 @@ func (a *theApp) serveFileOrNotFoundHandler() http.Handler { 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. + // namespace project is public if domain.IsNamespaceProject(r) { - if a.Auth.CheckAuthenticationWithoutProject(w, r) { + if a.Auth.CheckAuthenticationWithoutProject(w, r, domain) { return } - - httperrors.Serve404(w) - return } + // domain found and authentication succeeds domain.ServeNotFoundHTTP(w, r) } }) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 6dce1ab8..eaf3c25d 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -71,6 +71,10 @@ type errorResponse struct { Error string `json:"error"` ErrorDescription string `json:"error_description"` } +type domain interface { + GetProjectID(r *http.Request) uint64 + ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request) +} func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) { session, err := a.store.Get(r, "gitlab-pages") @@ -436,12 +440,13 @@ func (a *Auth) IsAuthSupported() bool { return a != nil } -func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool { +func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, domain domain) bool { session := a.checkSessionIsValid(w, r) if session == nil { return true } + projectID := domain.GetProjectID(r) // Access token exists, authorize request var url string if projectID > 0 { @@ -471,8 +476,8 @@ func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, proje logRequest(r).WithError(err).Error("Failed to retrieve info with token") } - // We return 404 if for some reason token is not valid to avoid (not) existence leak - httperrors.Serve404(w) + // call serve404 handler when auth fails + domain.ServeNotFoundAuthFailed(w, r) return true } @@ -480,13 +485,13 @@ func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, proje } // CheckAuthenticationWithoutProject checks if user is authenticated and has a valid token -func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request) bool { +func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request, domain domain) bool { if a == nil { // No auth supported return false } - return a.checkAuthentication(w, r, 0) + return a.checkAuthentication(w, r, domain) } // GetTokenIfExists returns the token if it exists @@ -513,7 +518,8 @@ func (a *Auth) RequireAuth(w http.ResponseWriter, r *http.Request) bool { } // 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 { +// will return contentServed = false when authFailed = true +func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, domain domain) bool { logRequest(r).Debug("Authenticate request") if a == nil { @@ -524,7 +530,7 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje return true } - return a.checkAuthentication(w, r, projectID) + return a.checkAuthentication(w, r, domain) } // CheckResponseForInvalidToken checks response for invalid token and destroys session if it was invalid diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index fc8ddb44..39a533b3 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -29,6 +29,20 @@ func defaultCookieStore() sessions.Store { return createCookieStore("something-very-secret") } +type domainMock struct { + projectID uint64 + notFoundContent string +} + +func (dm *domainMock) GetProjectID(r *http.Request) uint64 { + return dm.projectID +} + +func (dm *domainMock) ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(dm.notFoundContent)) +} + // Gorilla's sessions use request context to save session // Which makes session sharable between test code and actually manipulating session // Which leads to negative side effects: we can't test encryption, and cookie params @@ -180,8 +194,10 @@ func TestCheckAuthenticationWhenAccess(t *testing.T) { session, _ := store.Get(r, "gitlab-pages") session.Values["access_token"] = "abc" session.Save(r, result) + contentServed := auth.CheckAuthentication(result, r, &domainMock{projectID: 1000}) + require.False(t, contentServed) - require.Equal(t, false, auth.CheckAuthentication(result, r, 1000)) + // notFoundContent wasn't served so the default response from CheckAuthentication should be 200 require.Equal(t, 200, result.Code) } @@ -209,7 +225,8 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) { "http://pages.gitlab-example.com/auth", apiServer.URL) - result := httptest.NewRecorder() + w := httptest.NewRecorder() + reqURL, err := url.Parse("/auth?code=1&state=state") require.NoError(t, err) reqURL.Scheme = request.SchemeHTTPS @@ -217,10 +234,18 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) { session, _ := store.Get(r, "gitlab-pages") session.Values["access_token"] = "abc" - session.Save(r, result) + session.Save(r, w) - require.Equal(t, true, auth.CheckAuthentication(result, r, 1000)) - require.Equal(t, 404, result.Code) + contentServed := auth.CheckAuthentication(w, r, &domainMock{projectID: 1000, notFoundContent: "Generic 404"}) + require.True(t, contentServed) + res := w.Result() + defer res.Body.Close() + + require.Equal(t, 404, res.StatusCode) + + body, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, string(body), "Generic 404") } func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { @@ -257,7 +282,8 @@ func TestCheckAuthenticationWhenInvalidToken(t *testing.T) { session.Values["access_token"] = "abc" session.Save(r, result) - require.Equal(t, true, auth.CheckAuthentication(result, r, 1000)) + contentServed := auth.CheckAuthentication(result, r, &domainMock{projectID: 1000}) + require.True(t, contentServed) require.Equal(t, 302, result.Code) } @@ -295,7 +321,8 @@ func TestCheckAuthenticationWithoutProject(t *testing.T) { session.Values["access_token"] = "abc" session.Save(r, result) - require.Equal(t, false, auth.CheckAuthenticationWithoutProject(result, r)) + contentServed := auth.CheckAuthenticationWithoutProject(result, r, &domainMock{projectID: 0}) + require.False(t, contentServed) require.Equal(t, 200, result.Code) } @@ -332,7 +359,8 @@ func TestCheckAuthenticationWithoutProjectWhenInvalidToken(t *testing.T) { session.Values["access_token"] = "abc" session.Save(r, result) - require.Equal(t, true, auth.CheckAuthenticationWithoutProject(result, r)) + contentServed := auth.CheckAuthenticationWithoutProject(result, r, &domainMock{projectID: 0}) + require.True(t, contentServed) require.Equal(t, 302, result.Code) } diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 17a0e1d3..7c1639a3 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -1,6 +1,7 @@ package domain import ( + "context" "crypto/tls" "errors" "net/http" @@ -168,3 +169,41 @@ func (d *Domain) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { request.ServeNotFoundHTTP(w, r) } + +// serveNamespaceNotFound will try to find a parent namespace domain for a request +// that failed authentication so that we serve the custom namespace error page for +// public namespace domains +func (d *Domain) serveNamespaceNotFound(w http.ResponseWriter, r *http.Request) { + // clone r and override the path and try to resolve the domain name + clonedReq := r.Clone(context.Background()) + clonedReq.URL.Path = "/" + + namespaceDomain, err := d.Resolver.Resolve(clonedReq) + if err != nil || namespaceDomain.LookupPath == nil { + httperrors.Serve404(w) + return + } + + // for namespace domains that have no access control enabled + if !namespaceDomain.LookupPath.HasAccessControl { + namespaceDomain.ServeNotFoundHTTP(w, r) + return + } + + httperrors.Serve404(w) +} + +// ServeNotFoundAuthFailed handler to be called when auth failed so the correct custom +// 404 page is served. +func (d *Domain) ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request) { + if d.isUnconfigured() || !d.HasLookupPath(r) { + httperrors.Serve404(w) + return + } + if d.IsNamespaceProject(r) && !d.GetLookupPath(r).HasAccessControl { + d.ServeNotFoundHTTP(w, r) + return + } + + d.serveNamespaceNotFound(w, r) +} diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index 49d46cb3..fc5611ba 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -1,7 +1,10 @@ package domain import ( + "fmt" + "io/ioutil" "net/http" + "net/http/httptest" "os" "testing" @@ -153,3 +156,85 @@ func chdirInPath(t require.TestingT, path string) func() { chdirSet = false } } + +func TestServeNamespaceNotFound(t *testing.T) { + tests := []struct { + name string + domain string + path string + resolver *stubbedResolver + expectedResponse string + }{ + { + name: "public_namespace_domain", + domain: "group.404.gitlab-example.com", + path: "/unknown", + resolver: &stubbedResolver{ + project: &serving.LookupPath{ + Path: "../../shared/pages/group.404/group.404.gitlab-example.com/public", + IsNamespaceProject: true, + }, + subpath: "/unknown", + }, + expectedResponse: "Custom 404 group page", + }, + { + name: "private_project_under_public_namespace_domain", + domain: "group.404.gitlab-example.com", + path: "/private_project/unknown", + resolver: &stubbedResolver{ + project: &serving.LookupPath{ + Path: "../../shared/pages/group.404/group.404.gitlab-example.com/public", + IsNamespaceProject: true, + HasAccessControl: false, + }, + subpath: "/", + }, + expectedResponse: "Custom 404 group page", + }, + { + name: "private_namespace_domain", + domain: "group.404.gitlab-example.com", + path: "/unknown", + resolver: &stubbedResolver{ + project: &serving.LookupPath{ + Path: "../../shared/pages/group.404/group.404.gitlab-example.com/public", + IsNamespaceProject: true, + HasAccessControl: true, + }, + subpath: "/", + }, + expectedResponse: "The page you're looking for could not be found.", + }, + { + name: "no_parent_namespace_domain", + domain: "group.404.gitlab-example.com", + path: "/unknown", + resolver: &stubbedResolver{ + project: nil, + subpath: "/", + }, + expectedResponse: "The page you're looking for could not be found.", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &Domain{ + Name: tt.domain, + Resolver: tt.resolver, + } + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", fmt.Sprintf("http://%s%s", tt.domain, tt.path), nil) + d.serveNamespaceNotFound(w, r) + + resp := w.Result() + defer resp.Body.Close() + + require.Equal(t, http.StatusNotFound, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, string(body), tt.expectedResponse) + }) + } +} diff --git a/internal/source/disk/domain_test.go b/internal/source/disk/domain_test.go index d114ff8a..56297fd1 100644 --- a/internal/source/disk/domain_test.go +++ b/internal/source/disk/domain_test.go @@ -308,8 +308,8 @@ func TestDomain404ServeHTTP(t *testing.T) { }, } - testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page") - testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom 404 group page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom domain.404 page") + testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom domain.404 page") } func TestPredefined404ServeHTTP(t *testing.T) { diff --git a/internal/source/disk/map_test.go b/internal/source/disk/map_test.go index d904e6aa..d2162883 100644 --- a/internal/source/disk/map_test.go +++ b/internal/source/disk/map_test.go @@ -49,6 +49,7 @@ func TestReadProjects(t *testing.T) { "group.acme.test.io", "withacmechallenge.domain.com", "capitalgroup.test.io", + "group.404.gitlab-example.com", } for _, expected := range domains { diff --git a/shared/pages/group.404/domain.404/public/404.html b/shared/pages/group.404/domain.404/public/404.html index 454a3d50..ad0ed073 100644 --- a/shared/pages/group.404/domain.404/public/404.html +++ b/shared/pages/group.404/domain.404/public/404.html @@ -1 +1 @@ -Custom 404 group page +Custom domain.404 page diff --git a/shared/pages/group.404/group.404.gitlab-example.com/public/404.html b/shared/pages/group.404/group.404.gitlab-example.com/public/404.html new file mode 100644 index 00000000..454a3d50 --- /dev/null +++ b/shared/pages/group.404/group.404.gitlab-example.com/public/404.html @@ -0,0 +1 @@ +Custom 404 group page diff --git a/shared/pages/group.404/private_project/config.json b/shared/pages/group.404/private_project/config.json new file mode 100644 index 00000000..5c0ebb50 --- /dev/null +++ b/shared/pages/group.404/private_project/config.json @@ -0,0 +1,5 @@ +{ "domains": [ + { + "Domain": "group.404.gitlab-example.com" + } +], "id": 1000, "access_control": true } diff --git a/shared/pages/group.404/private_project/public/404.html b/shared/pages/group.404/private_project/public/404.html new file mode 100644 index 00000000..6993ae1a --- /dev/null +++ b/shared/pages/group.404/private_project/public/404.html @@ -0,0 +1 @@ +Private custom 404 error page diff --git a/shared/pages/group.404/private_unauthorized/config.json b/shared/pages/group.404/private_unauthorized/config.json new file mode 100644 index 00000000..79349565 --- /dev/null +++ b/shared/pages/group.404/private_unauthorized/config.json @@ -0,0 +1,5 @@ +{ "domains": [ + { + "Domain": "group.404.gitlab-example.com" + } +], "id": 2000, "access_control": true } diff --git a/shared/pages/group.404/private_unauthorized/public/404.html b/shared/pages/group.404/private_unauthorized/public/404.html new file mode 100644 index 00000000..6993ae1a --- /dev/null +++ b/shared/pages/group.404/private_unauthorized/public/404.html @@ -0,0 +1 @@ +Private custom 404 error page diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/config.json b/shared/pages/group.auth/group.auth.gitlab-example.com/config.json new file mode 100644 index 00000000..292ba673 --- /dev/null +++ b/shared/pages/group.auth/group.auth.gitlab-example.com/config.json @@ -0,0 +1 @@ +{ "domains": [], "id": 1000, "access_control": true } diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html b/shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html new file mode 100644 index 00000000..f345e8bc --- /dev/null +++ b/shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html @@ -0,0 +1 @@ +group.auth.gitlab-example.com namespace custom 404 diff --git a/shared/pages/group.auth/private.project.1/public/404.html b/shared/pages/group.auth/private.project.1/public/404.html new file mode 100644 index 00000000..3b751385 --- /dev/null +++ b/shared/pages/group.auth/private.project.1/public/404.html @@ -0,0 +1 @@ +group.auth.gitlab-example.com/private.project.1 custom 404 |