diff --git a/src/server/middleware/regtoken/regtoken.go b/src/server/middleware/regtoken/regtoken.go new file mode 100644 index 000000000..f4388a9b7 --- /dev/null +++ b/src/server/middleware/regtoken/regtoken.go @@ -0,0 +1,66 @@ +package regtoken + +import ( + "errors" + "github.com/docker/distribution/registry/auth" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/utils/log" + pkg_token "github.com/goharbor/harbor/src/pkg/token" + "github.com/goharbor/harbor/src/pkg/token/claims/registry" + "github.com/goharbor/harbor/src/server/middleware" + reg_err "github.com/goharbor/harbor/src/server/registry/error" + "net/http" + "strings" +) + +// Middleware parses the docker pull bearer token and check whether it's a scanner pull. +func Middleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + err := parseToken(req) + if err != nil { + reg_err.Handle(rw, req, err) + return + } + next.ServeHTTP(rw, req) + }) + } +} + +func parseToken(req *http.Request) error { + mf, ok := middleware.ManifestInfoFromContext(req.Context()) + if !ok { + return errors.New("cannot get the manifest information from request context") + } + + parts := strings.Split(req.Header.Get("Authorization"), " ") + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + return nil + } + + rawToken := parts[1] + opt := pkg_token.DefaultTokenOptions() + regTK, err := pkg_token.Parse(opt, rawToken, ®istry.Claim{}) + if err != nil { + log.Errorf("failed to decode reg token: %v, the error is skipped and round the request to native registry.", err) + return nil + } + + accessItems := []auth.Access{} + accessItems = append(accessItems, auth.Access{ + Resource: auth.Resource{ + Type: rbac.ResourceRepository.String(), + Name: mf.Repository, + }, + Action: rbac.ActionScannerPull.String(), + }) + + accessSet := regTK.Claims.(*registry.Claim).GetAccess() + for _, access := range accessItems { + if accessSet.Contains(access) { + *req = *(req.WithContext(middleware.NewScannerPullContext(req.Context(), true))) + } + } + + return nil +} diff --git a/src/server/middleware/regtoken/regtoken_test.go b/src/server/middleware/regtoken/regtoken_test.go new file mode 100644 index 000000000..5662e797c --- /dev/null +++ b/src/server/middleware/regtoken/regtoken_test.go @@ -0,0 +1,64 @@ +package regtoken + +import ( + "fmt" + "github.com/goharbor/harbor/src/core/middlewares/util" + "github.com/goharbor/harbor/src/server/middleware" + "github.com/stretchr/testify/suite" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +type HandlerSuite struct { + suite.Suite +} + +func doPullManifestRequest(projectName, name, tag string, next ...http.HandlerFunc) int { + repository := fmt.Sprintf("%s/%s", projectName, name) + + url := fmt.Sprintf("/v2/%s/manifests/%s", repository, tag) + req, _ := http.NewRequest("GET", url, nil) + + token := "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkNWUTc6REM3NTpHVEROOkxTTUs6VUFJTjpIUUVWOlZVSDQ6Q0lRRDpRV01COlM0Qzc6U0c0STpGRUhYIn0.eyJpc3MiOiJoYXJib3ItdG9rZW4taXNzdWVyIiwic3ViIjoicm9ib3QkZGVtbzExIiwiYXVkIjoiaGFyYm9yLXJlZ2lzdHJ5IiwiZXhwIjoxNTcxNzYzOTI2LCJuYmYiOjE1NzE3NjM4NjYsImlhdCI6MTU3MTc2Mzg2NiwianRpIjoiTnRaZWx4Z01KTUU1MXlEMCIsImFjY2VzcyI6W3sidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoibGlicmFyeS9oZWxsby13b3JsZCIsImFjdGlvbnMiOlsicHVzaCIsIioiLCJwdWxsIiwic2Nhbm5lcnB1bGwiXX1dfQ.GlWuvtoxmChnpvbWaG5901Z9-g63DrzyNUREWlDbR5gnNeuOKjLNyE4QpogAQKx2yYtcGxbqNL3VfJkExJ_gMS0Qw8e10utGOawwqD4oqf_J06eKq4HzpZJengZfcjMA4g2RoeOlqdVdwimB_PdX9vkBO1od0wX0Cc2v0p2w5TkibcThKRoeLeVs2oRewkKLuVHNSM8wwRIlAvpWJuNnvRCFlHRkLcZM_KpGXqT7H-PZETTisWCi1pMxeYEwIsDFLlTKdV8LaiDeDmH-RaLOsuyAySYEW9Ynk5K3P_dUl2c_SYQXloPyi0MvXxSn6EWE4eHF2oQDM_SvIzR9sOVB8TtjMjKKMQ4yr_mqgMcfEpnInJATExBR56wmxNdLESncHl8rUYCe2jCjQFuR9NGQA1tGdjI4NoBN-OVD0dBs9rm_mkb2tgD-3gEhyzAw6hg0uzDsF7bj5Aq8scoi42UurhX2bZM89s4-TWBp4DWuBG0HDiwpOiBvB3RMm6MpQxsqrl0hQm_WH18L6QCknAW2e3d_6DJWJ0eBzISrhDr7LkqJKl1J8pv4zqoh_EUVeLyzTmjEULm-VbnpVF4wW5yTLF3S6F7Ox4vwWtVfi1XQNVOcJDB3VPUsRgiTTuCW-ZGcBLw-OdIcwaJ3T_QZkEjUw1f6i1JcGa0Mpgl83aLiSdQ 0xc0003c77c0 map[alg:RS256 kid:CVQ7:DC75:GTDN:LSMK:UAIN:HQEV:VUH4:CIQD:QWMB:S4C7:SG4I:FEHX typ:JWT] 0xc000496000 GlWuvtoxmChnpvbWaG5901Z9-g63DrzyNUREWlDbR5gnNeuOKjLNyE4QpogAQKx2yYtcGxbqNL3VfJkExJ_gMS0Qw8e10utGOawwqD4oqf_J06eKq4HzpZJengZfcjMA4g2RoeOlqdVdwimB_PdX9vkBO1od0wX0Cc2v0p2w5TkibcThKRoeLeVs2oRewkKLuVHNSM8wwRIlAvpWJuNnvRCFlHRkLcZM_KpGXqT7H-PZETTisWCi1pMxeYEwIsDFLlTKdV8LaiDeDmH-RaLOsuyAySYEW9Ynk5K3P_dUl2c_SYQXloPyi0MvXxSn6EWE4eHF2oQDM_SvIzR9sOVB8TtjMjKKMQ4yr_mqgMcfEpnInJATExBR56wmxNdLESncHl8rUYCe2jCjQFuR9NGQA1tGdjI4NoBN-OVD0dBs9rm_mkb2tgD-3gEhyzAw6hg0uzDsF7bj5Aq8scoi42UurhX2bZM89s4-TWBp4DWuBG0HDiwpOiBvB3RMm6MpQxsqrl0hQm_WH18L6QCknAW2e3d_6DJWJ0eBzISrhDr7LkqJKl1J8pv4zqoh_EUVeLyzTmjEULm-VbnpVF4wW5yTLF3S6F7Ox4vwWtVfi1XQNVOcJDB3VPUsRgiTTuCW-ZGcBLw-OdIcwaJ3T_QZkEjUw1f6i1JcGa0Mpgl83aLiSdQ" + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + rr := httptest.NewRecorder() + + mfInfo := &middleware.ManifestInfo{ + ProjectID: 1, + Repository: name, + Tag: tag, + Digest: "", + } + + var n http.HandlerFunc + if len(next) > 0 { + n = next[0] + } else { + n = func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusNotFound) + } + } + *req = *(req.WithContext(middleware.NewManifestInfoContext(req.Context(), mfInfo))) + + h := Middleware()(http.HandlerFunc(n)) + h.ServeHTTP(util.NewCustomResponseWriter(rr), req) + + return rr.Code +} + +func (suite *HandlerSuite) TestPullManifest() { + code1 := doPullManifestRequest("library", "photon", "release-1.10") + suite.Equal(http.StatusNotFound, code1) +} + +func TestMain(m *testing.M) { + if result := m.Run(); result != 0 { + os.Exit(result) + } +} + +func TestRunHandlerSuite(t *testing.T) { + suite.Run(t, new(HandlerSuite)) +} diff --git a/src/server/middleware/util.go b/src/server/middleware/util.go index 286d1dfd2..4ff140c0e 100644 --- a/src/server/middleware/util.go +++ b/src/server/middleware/util.go @@ -9,6 +9,8 @@ type contextKey string const ( // manifestInfoKey the context key for manifest info manifestInfoKey = contextKey("ManifestInfo") + // ScannerPullCtxKey the context key for robot account to bypass the pull policy check. + ScannerPullCtxKey = contextKey("ScannerPullCheck") ) // ManifestInfo ... @@ -29,3 +31,8 @@ func ManifestInfoFromContext(ctx context.Context) (*ManifestInfo, bool) { info, ok := ctx.Value(manifestInfoKey).(*ManifestInfo) return info, ok } + +// NewScannerPullContext returns context with policy check info +func NewScannerPullContext(ctx context.Context, scannerPull bool) context.Context { + return context.WithValue(ctx, ScannerPullCtxKey, scannerPull) +} diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go index 4d43fb009..31267f3dc 100644 --- a/src/server/registry/handler.go +++ b/src/server/registry/handler.go @@ -19,6 +19,8 @@ import ( pkg_repo "github.com/goharbor/harbor/src/pkg/repository" pkg_tag "github.com/goharbor/harbor/src/pkg/tag" "github.com/goharbor/harbor/src/server/middleware" + "github.com/goharbor/harbor/src/server/middleware/manifestinfo" + "github.com/goharbor/harbor/src/server/middleware/regtoken" "github.com/goharbor/harbor/src/server/registry/blob" "net/http" "net/http/httputil" @@ -49,7 +51,7 @@ 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).Handler(manifest.NewHandler(project.Mgr, proxy)) + manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), manifestinfo.Middleware(), regtoken.Middleware())) 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()))