diff --git a/src/ui/proxy/interceptor_test.go b/src/ui/proxy/interceptor_test.go index f7406652d..a659d29fd 100644 --- a/src/ui/proxy/interceptor_test.go +++ b/src/ui/proxy/interceptor_test.go @@ -94,6 +94,22 @@ func TestMatchPullManifest(t *testing.T) { assert.Equal("sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", tag7) } +func TestMatchListRepos(t *testing.T) { + assert := assert.New(t) + req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/_catalog", nil) + res1 := MatchListRepos(req1) + assert.False(res1, "%s %v is not a request to list repos", req1.Method, req1.URL) + + req2, _ := http.NewRequest("GET", "http://127.0.0.1:5000/v2/_catalog", nil) + res2 := MatchListRepos(req2) + assert.True(res2, "%s %v is a request to list repos", req2.Method, req2.URL) + + req3, _ := http.NewRequest("GET", "https://192.168.0.5:443/v1/_catalog", nil) + res3 := MatchListRepos(req3) + assert.False(res3, "%s %v is not a request to pull manifest", req3.Method, req3.URL) + +} + func TestEnvPolicyChecker(t *testing.T) { assert := assert.New(t) if err := os.Setenv("PROJECT_CONTENT_TRUST", "1"); err != nil { @@ -191,3 +207,9 @@ func TestMarshalError(t *testing.T) { js := marshalError("Not Found", 404) assert.Equal("{\"code\":404,\"message\":\"Not Found\",\"details\":\"Not Found\"}", js) } + +func TestIsDigest(t *testing.T) { + assert := assert.New(t) + assert.False(isDigest("latest")) + assert.True(isDigest("sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7")) +} diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go index 3b0020244..e38bc4081 100644 --- a/src/ui/proxy/interceptors.go +++ b/src/ui/proxy/interceptors.go @@ -8,9 +8,9 @@ import ( "github.com/vmware/harbor/src/common/utils/clair" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/notary" - // "github.com/vmware/harbor/src/ui/api" "github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/projectmanager" + uiutils "github.com/vmware/harbor/src/ui/utils" "context" "fmt" @@ -18,6 +18,7 @@ import ( "net/http/httptest" "os" "regexp" + "strconv" "strings" ) @@ -25,6 +26,7 @@ type contextKey string const ( manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})` + catalogURLPattern = `/v2/_catalog` imageInfoCtxKey = contextKey("ImageInfo") //TODO: temp solution, remove after vmware/harbor#2242 is resolved. tokenUsername = "harbor-ui" @@ -54,6 +56,19 @@ func MatchPullManifest(req *http.Request) (bool, string, string) { return false, "", "" } +// MatchListRepos checks if the request looks like a request to list repositories. +func MatchListRepos(req *http.Request) bool { + if req.Method != http.MethodGet { + return false + } + re := regexp.MustCompile(catalogURLPattern) + s := re.FindStringSubmatch(req.URL.Path) + if len(s) == 1 { + return true + } + return false +} + // policyChecker checks the policy of a project by project name, to determine if it's needed to check the image's status under this project. type policyChecker interface { // contentTrustEnabled returns whether a project has enabled content trust. @@ -100,7 +115,6 @@ func newPMSPolicyChecker(pm projectmanager.ProjectManager) policyChecker { } } -// TODO: Get project manager with PM factory. func getPolicyChecker() policyChecker { if config.WithAdmiral() { return newPMSPolicyChecker(config.GlobalProjectMgr) @@ -110,7 +124,7 @@ func getPolicyChecker() policyChecker { type imageInfo struct { repository string - tag string + reference string projectName string digest string } @@ -119,31 +133,37 @@ type urlHandler struct { next http.Handler } -//TODO: wrap a ResponseWriter to get the status code? - func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { log.Debugf("in url handler, path: %s", req.URL.Path) req.URL.Path = strings.TrimPrefix(req.URL.Path, RegistryProxyPrefix) - flag, repository, tag := MatchPullManifest(req) + flag, repository, reference := MatchPullManifest(req) if flag { components := strings.SplitN(repository, "/", 2) if len(components) < 2 { http.Error(rw, marshalError(fmt.Sprintf("Bad repository name: %s", repository), http.StatusInternalServerError), http.StatusBadRequest) return } - rec = httptest.NewRecorder() - uh.next.ServeHTTP(rec, req) - if rec.Result().StatusCode != http.StatusOK { - copyResp(rec, rw) + + client, err := uiutils.NewRepositoryClientForUI(tokenUsername, repository) + if err != nil { + log.Errorf("Error creating repository Client: %v", err) + http.Error(rw, marshalError(fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalServerError), http.StatusInternalServerError) return } - digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest")) + digest, _, err := client.ManifestExist(reference) + if err != nil { + log.Errorf("Failed to get digest for reference: %s, error: %v", reference, err) + http.Error(rw, marshalError(fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalServerError), http.StatusInternalServerError) + return + } + img := imageInfo{ repository: repository, - tag: tag, + reference: reference, projectName: components[0], digest: digest, } + log.Debugf("image info of the request: %#v", img) ctx := context.WithValue(req.Context(), imageInfoCtxKey, img) req = req.WithContext(ctx) @@ -151,6 +171,58 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { uh.next.ServeHTTP(rw, req) } +type listReposHandler struct { + next http.Handler +} + +func (lrh listReposHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + listReposFlag := MatchListRepos(req) + if listReposFlag { + rec = httptest.NewRecorder() + lrh.next.ServeHTTP(rec, req) + if rec.Result().StatusCode != http.StatusOK { + copyResp(rec, rw) + return + } + var ctlg struct { + Repositories []string `json:"repositories"` + } + decoder := json.NewDecoder(rec.Body) + if err := decoder.Decode(&ctlg); err != nil { + log.Errorf("Decode repositories error: %v", err) + copyResp(rec, rw) + return + } + var entries []string + for repo := range ctlg.Repositories { + log.Debugf("the repo in the reponse %s", ctlg.Repositories[repo]) + exist := dao.RepositoryExists(ctlg.Repositories[repo]) + if exist { + entries = append(entries, ctlg.Repositories[repo]) + } + } + type Repos struct { + Repositories []string `json:"repositories"` + } + resp := &Repos{Repositories: entries} + respJSON, err := json.Marshal(resp) + if err != nil { + log.Errorf("Encode repositories error: %v", err) + copyResp(rec, rw) + return + } + + for k, v := range rec.Header() { + rw.Header()[k] = v + } + clen := len(respJSON) + rw.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(clen)) + rw.Write(respJSON) + return + } + lrh.next.ServeHTTP(rw, req) +} + type contentTrustHandler struct { next http.Handler } @@ -162,6 +234,10 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque return } img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo) + if img.digest == "" { + cth.next.ServeHTTP(rw, req) + return + } if !getPolicyChecker().contentTrustEnabled(img.projectName) { cth.next.ServeHTTP(rw, req) return @@ -190,6 +266,10 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) return } img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo) + if img.digest == "" { + vh.next.ServeHTTP(rw, req) + return + } projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName) if !projectVulnerableEnabled { vh.next.ServeHTTP(rw, req) @@ -197,7 +277,7 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) } overview, err := dao.GetImgScanOverview(img.digest) if err != nil { - log.Errorf("failed to get ImgScanOverview with repo: %s, tag: %s, digest: %s. Error: %v", img.repository, img.tag, img.digest, err) + log.Errorf("failed to get ImgScanOverview with repo: %s, reference: %s, digest: %s. Error: %v", img.repository, img.reference, img.digest, err) http.Error(rw, marshalError("Failed to get ImgScanOverview.", http.StatusPreconditionFailed), http.StatusPreconditionFailed) return } @@ -216,39 +296,42 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) vh.next.ServeHTTP(rw, req) } -type funnelHandler struct { - next http.Handler -} - -func (fu funnelHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - imgRaw := req.Context().Value(imageInfoCtxKey) - if imgRaw != nil { - log.Debugf("Return the original response as no the interceptor takes action.") - copyResp(rec, rw) - return - } - fu.next.ServeHTTP(rw, req) -} - func matchNotaryDigest(img imageInfo) (bool, error) { targets, err := notary.GetInternalTargets(NotaryEndpoint, tokenUsername, img.repository) if err != nil { return false, err } for _, t := range targets { - if t.Tag == img.tag { - log.Debugf("found tag: %s in notary, try to match digest.", img.tag) + if isDigest(img.reference) { d, err := notary.DigestFromTarget(t) if err != nil { return false, err } - return img.digest == d, nil + if img.digest == d { + return true, nil + } + } else { + if t.Tag == img.reference { + log.Debugf("found reference: %s in notary, try to match digest.", img.reference) + d, err := notary.DigestFromTarget(t) + if err != nil { + return false, err + } + if img.digest == d { + return true, nil + } + } } } log.Debugf("image: %#v, not found in notary", img) return false, nil } +//A sha256 is a string with 64 characters. +func isDigest(ref string) bool { + return strings.HasPrefix(ref, "sha256:") && len(ref) == 71 +} + func copyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) { for k, v := range rec.Header() { rw.Header()[k] = v diff --git a/src/ui/proxy/proxy.go b/src/ui/proxy/proxy.go index be8d5b728..9fed58932 100644 --- a/src/ui/proxy/proxy.go +++ b/src/ui/proxy/proxy.go @@ -41,8 +41,7 @@ func Init(urls ...string) error { return err } Proxy = httputil.NewSingleHostReverseProxy(targetURL) - //TODO: add vulnerable interceptor. - handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: vulnerableHandler{next: funnelHandler{next: Proxy}}}}} + handlers = handlerChain{head: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}} return nil }