diff --git a/src/internal/error/errors.go b/src/internal/error/errors.go index 9bab36bf1..4b4d36131 100644 --- a/src/internal/error/errors.go +++ b/src/internal/error/errors.go @@ -89,6 +89,9 @@ const ( PreconditionCode = "PRECONDITION" // GeneralCode ... GeneralCode = "UNKNOWN" + + // DENIED it's used by middleware(readonly, vul and content trust) and returned to docker client to index the request is denied. + DENIED = "DENIED" ) // New ... diff --git a/src/server/middleware/readonly.go b/src/server/middleware/readonly.go new file mode 100644 index 000000000..b9ecd4c2e --- /dev/null +++ b/src/server/middleware/readonly.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + internal_errors "github.com/goharbor/harbor/src/internal/error" + "net/http" +) + +type readonlyHandler struct { + next http.Handler +} + +// ReadOnly middleware reject request when harbor set to readonly +func ReadOnly() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if config.ReadOnly() { + log.Warningf("The request is prohibited in readonly mode, url is: %s", req.URL.Path) + pkgE := internal_errors.New(nil).WithCode(internal_errors.DENIED).WithMessage("The system is in read only mode. Any modification is prohibited.") + http.Error(rw, internal_errors.NewErrs(pkgE).Error(), http.StatusForbidden) + return + } + next.ServeHTTP(rw, req) + }) + } +} diff --git a/src/server/middleware/readonly_test.go b/src/server/middleware/readonly_test.go new file mode 100644 index 000000000..96369ce5e --- /dev/null +++ b/src/server/middleware/readonly_test.go @@ -0,0 +1,50 @@ +package middleware + +import ( + "github.com/goharbor/harbor/src/common" + config2 "github.com/goharbor/harbor/src/common/config" + "github.com/goharbor/harbor/src/core/config" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + conf := map[string]interface{}{ + common.ReadOnly: "true", + } + kp := &config2.PresetKeyProvider{Key: "naa4JtarA1Zsc3uY"} + config.InitWithSettings(conf, kp) + result := m.Run() + if result != 0 { + os.Exit(result) + } +} + +func TestReadOnly(t *testing.T) { + assert := assert.New(t) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }) + + // delete + req := httptest.NewRequest(http.MethodDelete, "/readonly1", nil) + rec := httptest.NewRecorder() + ReadOnly()(next).ServeHTTP(rec, req) + assert.Equal(rec.Code, http.StatusForbidden) + + update := map[string]interface{}{ + common.ReadOnly: "false", + } + config.GetCfgManager().UpdateConfig(update) + + req2 := httptest.NewRequest(http.MethodDelete, "/readonly2", nil) + rec2 := httptest.NewRecorder() + ReadOnly()(next).ServeHTTP(rec2, req2) + assert.Equal(rec2.Code, http.StatusOK) + +} diff --git a/src/server/registry/blob/blob.go b/src/server/registry/blob/blob.go new file mode 100644 index 000000000..f963c4621 --- /dev/null +++ b/src/server/registry/blob/blob.go @@ -0,0 +1,22 @@ +package blob + +import ( + "net/http" + "net/http/httputil" +) + +// NewHandler returns the handler to handler catalog request +func NewHandler(proxy *httputil.ReverseProxy) http.Handler { + return &handler{ + proxy: proxy, + } +} + +type handler struct { + proxy *httputil.ReverseProxy +} + +// ServeHTTP ... +func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + h.proxy.ServeHTTP(w, req) +} diff --git a/src/server/registry/blob/blob_test.go b/src/server/registry/blob/blob_test.go new file mode 100644 index 000000000..f3a8cab65 --- /dev/null +++ b/src/server/registry/blob/blob_test.go @@ -0,0 +1 @@ +package blob diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go index 3b7fc300f..8229f9fe5 100644 --- a/src/server/registry/handler.go +++ b/src/server/registry/handler.go @@ -16,6 +16,8 @@ package registry import ( "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/server/middleware" + "github.com/goharbor/harbor/src/server/registry/blob" "net/http" "net/http/httputil" "net/url" @@ -45,14 +47,20 @@ func New(url *url.URL) http.Handler { // handle manifest // TODO maybe we should split it into several sub routers based on the method manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter() - manifestRouter.NewRoute().Methods(http.MethodGet, http.MethodHead, http.MethodPut, http.MethodDelete). - Handler(manifest.NewHandler(project.Mgr, proxy)) + manifestRouter.NewRoute().Methods(http.MethodGet).Handler(manifest.NewHandler(project.Mgr, proxy)) + manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(project.Mgr, proxy)) + manifestRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), middleware.ReadOnly())) + manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), middleware.ReadOnly())) // handle blob // as we need to apply middleware to the blob requests, so create a sub router to handle the blob APIs blobRouter := rootRouter.PathPrefix("/v2/{name:.*}/blobs/").Subrouter() - blobRouter.NewRoute().Methods(http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete). - Handler(proxy) + blobRouter.NewRoute().Methods(http.MethodGet).Handler(blob.NewHandler(proxy)) + blobRouter.NewRoute().Methods(http.MethodHead).Handler(blob.NewHandler(proxy)) + blobRouter.NewRoute().Methods(http.MethodPost).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) + blobRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) + blobRouter.NewRoute().Methods(http.MethodPatch).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) + blobRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) // all other APIs are proxy to the backend docker registry rootRouter.PathPrefix("/").Handler(proxy)