diff --git a/src/ui/proxy/interceptor_test.go b/src/ui/proxy/interceptor_test.go index 1544ef4fde..a3157beda0 100644 --- a/src/ui/proxy/interceptor_test.go +++ b/src/ui/proxy/interceptor_test.go @@ -3,6 +3,7 @@ package proxy import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmware/harbor/src/adminserver/client" "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/models" notarytest "github.com/vmware/harbor/src/common/utils/notary/test" @@ -19,6 +20,7 @@ import ( var endpoint = "10.117.4.142" var notaryServer *httptest.Server var adminServer *httptest.Server +var adminserverClient client.Client var admiralEndpoint = "http://127.0.0.1:8282" var token = "" @@ -43,6 +45,7 @@ func TestMain(m *testing.M) { if err := config.Init(); err != nil { panic(err) } + adminserverClient = client.NewClient(adminServer.URL, nil) result := m.Run() if result != 0 { os.Exit(result) @@ -95,18 +98,46 @@ func TestEnvPolicyChecker(t *testing.T) { if err := os.Setenv("PROJECT_CONTENT_TRUST", "1"); err != nil { t.Fatalf("Failed to set env variable: %v", err) } + if err2 := os.Setenv("PROJECT_VULNERABLE", "1"); err2 != nil { + t.Fatalf("Failed to set env variable: %v", err2) + } + if err3 := os.Setenv("PROJECT_SEVERITY", "negligible"); err3 != nil { + t.Fatalf("Failed to set env variable: %v", err3) + } contentTrustFlag := getPolicyChecker().contentTrustEnabled("whatever") - vulFlag := getPolicyChecker().vulnerableEnabled("whatever") + vulFlag, sev := getPolicyChecker().vulnerablePolicy("whatever") assert.True(contentTrustFlag) - assert.False(vulFlag) + assert.True(vulFlag) + assert.Equal(sev, models.SevNone) } func TestPMSPolicyChecker(t *testing.T) { + + var defaultConfigAdmiral = map[string]interface{}{ + common.ExtEndpoint: "https://" + endpoint, + common.WithNotary: true, + common.CfgExpiration: 5, + common.AdmiralEndpoint: admiralEndpoint, + } + adminServer, err := utilstest.NewAdminserver(defaultConfigAdmiral) + if err != nil { + panic(err) + } + defer adminServer.Close() + if err := os.Setenv("ADMIN_SERVER_URL", adminServer.URL); err != nil { + panic(err) + } + if err := config.Init(); err != nil { + panic(err) + } + pm := pms.NewProjectManager(admiralEndpoint, token) - name := "project_for_test_get_true" + name := "project_for_test_get_sev_low" id, err := pm.Create(&models.Project{ - Name: name, - EnableContentTrust: true, + Name: name, + EnableContentTrust: true, + PreventVulnerableImagesFromRunning: false, + PreventVulnerableImagesFromRunningSeverity: "low", }) require.Nil(t, err) defer func(id int64) { @@ -117,29 +148,27 @@ func TestPMSPolicyChecker(t *testing.T) { project, err := pm.Get(id) assert.Nil(t, err) assert.Equal(t, id, project.ProjectID) - server, err2 := utilstest.NewAdminserver(nil) - if err2 != nil { - t.Fatalf("failed to create a mock admin server: %v", err2) - } - defer server.Close() - contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_true") + + contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low") assert.True(t, contentTrustFlag) + projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low") + assert.False(t, projectVulnerableEnabled) + assert.Equal(t, projectVulnerableSeverity, models.SevLow) } func TestMatchNotaryDigest(t *testing.T) { assert := assert.New(t) //The data from common/utils/notary/helper_test.go - img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo"} - img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo"} - res1, err := matchNotaryDigest(img1, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7") + img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo", "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"} + img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo", "sha256:12345678"} + + res1, err := matchNotaryDigest(img1) assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1) assert.True(res1) - res2, err := matchNotaryDigest(img1, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a8") - assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img1) + + res2, err := matchNotaryDigest(img2) + assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img2) assert.False(res2) - res3, err := matchNotaryDigest(img2, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7") - assert.Nil(err, "Unexpected error: %v, image: %#v", err, img2) - assert.False(res3) } func TestCopyResp(t *testing.T) { diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go index 536be11f58..a31b52e46b 100644 --- a/src/ui/proxy/interceptors.go +++ b/src/ui/proxy/interceptors.go @@ -1,9 +1,12 @@ package proxy import ( - // "github.com/vmware/harbor/src/ui/api" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" + "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" "github.com/vmware/harbor/src/ui/projectmanager/pms" @@ -26,6 +29,9 @@ const ( tokenUsername = "admin" ) +// Record the docker deamon raw response. +var rec *httptest.ResponseRecorder + // NotaryEndpoint , exported for testing. var NotaryEndpoint = config.InternalNotaryEndpoint() @@ -51,8 +57,8 @@ func MatchPullManifest(req *http.Request) (bool, string, string) { type policyChecker interface { // contentTrustEnabled returns whether a project has enabled content trust. contentTrustEnabled(name string) bool - // vulnerableEnabled returns whether a project has enabled content trust. - vulnerableEnabled(name string) bool + // vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity. + vulnerablePolicy(name string) (bool, models.Severity) } //For testing @@ -61,9 +67,8 @@ type envPolicyChecker struct{} func (ec envPolicyChecker) contentTrustEnabled(name string) bool { return os.Getenv("PROJECT_CONTENT_TRUST") == "1" } -func (ec envPolicyChecker) vulnerableEnabled(name string) bool { - // TODO: May need get more information in vulnerable policies. - return os.Getenv("PROJECT_VULNERABBLE") == "1" +func (ec envPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) { + return os.Getenv("PROJECT_VULNERABLE") == "1", clair.ParseClairSev(os.Getenv("PROJECT_SEVERITY")) } type pmsPolicyChecker struct { @@ -78,8 +83,13 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool { } return project.EnableContentTrust } -func (pc pmsPolicyChecker) vulnerableEnabled(name string) bool { - return true +func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) { + project, err := pc.pm.Get(name) + if err != nil { + log.Errorf("Unexpected error when getting the project, error: %v", err) + return true, models.SevUnknown + } + return project.PreventVulnerableImagesFromRunning, clair.ParseClairSev(project.PreventVulnerableImagesFromRunningSeverity) } // newPMSPolicyChecker returns an instance of an pmsPolicyChecker @@ -101,7 +111,7 @@ type imageInfo struct { repository string tag string projectName string - // digest string + digest string } type urlHandler struct { @@ -120,38 +130,22 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { http.Error(rw, fmt.Sprintf("Bad repository name: %s", repository), http.StatusBadRequest) return } - /* - //Need to get digest of the image. - endpoint, err := config.RegistryURL() - if err != nil { - log.Errorf("Error getting Registry URL: %v", err) - http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError) - return - } - rc, err := api.NewRepositoryClient(endpoint, false, username, repository, "repository", repository, "pull") - if err != nil { - log.Errorf("Error creating repository Client: %v", err) - http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError) - return - } - digest, exist, err := rc.ManifestExist(tag) - if err != nil { - log.Errorf("Failed to get digest for tag: %s, error: %v", tag, err) - http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError) - return - } - */ - + rec = httptest.NewRecorder() + uh.next.ServeHTTP(rec, req) + if rec.Result().StatusCode != http.StatusOK { + copyResp(rec, rw) + return + } + digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest")) img := imageInfo{ repository: repository, tag: tag, 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) - } uh.next.ServeHTTP(rw, req) } @@ -171,31 +165,70 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque cth.next.ServeHTTP(rw, req) return } - //May need to update status code, let's use recorder - rec := httptest.NewRecorder() - cth.next.ServeHTTP(rec, req) - if rec.Result().StatusCode != http.StatusOK { - copyResp(rec, rw) - return - } - log.Debugf("showing digest") - digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest")) - log.Debugf("digest: %s", digest) - match, err := matchNotaryDigest(img, digest) + match, err := matchNotaryDigest(img) if err != nil { http.Error(rw, "Failed in communication with Notary please check the log", http.StatusInternalServerError) return } - if match { - log.Debugf("Passing the response to outter responseWriter") - copyResp(rec, rw) - } else { + if !match { log.Debugf("digest mismatch, failing the response.") http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed) + return } + cth.next.ServeHTTP(rw, req) } -func matchNotaryDigest(img imageInfo, digest string) (bool, error) { +type vulnerableHandler struct { + next http.Handler +} + +func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + imgRaw := req.Context().Value(imageInfoCtxKey) + if imgRaw == nil || !config.WithClair() { + vh.next.ServeHTTP(rw, req) + return + } + img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo) + projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName) + if !projectVulnerableEnabled { + vh.next.ServeHTTP(rw, req) + return + } + 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) + http.Error(rw, "Failed to get ImgScanOverview.", http.StatusPreconditionFailed) + return + } + if overview == nil { + log.Debugf("cannot get the image scan overview info, failing the response.") + http.Error(rw, "Cannot get the image scan overview info.", http.StatusPreconditionFailed) + return + } + imageSev := overview.Sev + if imageSev > int(projectVulnerableSeverity) { + log.Debugf("the image severity is higher then project setting, failing the response.") + http.Error(rw, "The image scan result doesn't pass the project setting.", http.StatusPreconditionFailed) + return + } + 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 @@ -207,7 +240,7 @@ func matchNotaryDigest(img imageInfo, digest string) (bool, error) { if err != nil { return false, err } - return digest == d, nil + return img.digest == d, nil } } log.Debugf("image: %#v, not found in notary", img) diff --git a/src/ui/proxy/proxy.go b/src/ui/proxy/proxy.go index b580d7aff1..be8d5b728f 100644 --- a/src/ui/proxy/proxy.go +++ b/src/ui/proxy/proxy.go @@ -42,7 +42,7 @@ func Init(urls ...string) error { } Proxy = httputil.NewSingleHostReverseProxy(targetURL) //TODO: add vulnerable interceptor. - handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}} + handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: vulnerableHandler{next: funnelHandler{next: Proxy}}}}} return nil }