diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 970638105..ba0ebe9de 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -2739,6 +2739,44 @@ paths: $ref: '#/responses/403' '500': $ref: '#/responses/500' + /system/CVEAllowlist: + get: + summary: Get the system level allowlist of CVE. + description: Get the system level allowlist of CVE. This API can be called by all authenticated users. + operationId: getSystemCVEAllowlist + tags: + - SystemCVEAllowlist + responses: + '200': + description: Successfully retrieved the CVE allowlist. + schema: + $ref: "#/definitions/CVEAllowlist" + '401': + $ref: '#/responses/401' + '500': + $ref: '#/responses/500' + put: + summary: Update the system level allowlist of CVE. + description: This API overwrites the system level allowlist of CVE with the list in request body. Only system Admin + has permission to call this API. + operationId: putSystemCVEAllowlist + tags: + - SystemCVEAllowlist + parameters: + - in: body + name: allowlist + description: The allowlist with new content + schema: + $ref: "#/definitions/CVEAllowlist" + responses: + '200': + description: Successfully updated the CVE allowlist. + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' /system/scanAll/schedule: get: summary: Get scan all's schedule. diff --git a/src/common/dao/cve_allowlist.go b/src/common/dao/cve_allowlist.go deleted file mode 100644 index c8ed55fc1..000000000 --- a/src/common/dao/cve_allowlist.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dao - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/lib/log" -) - -// CreateCVEAllowlist creates the CVE allowlist -func CreateCVEAllowlist(l models.CVEAllowlist) (int64, error) { - o := GetOrmer() - now := time.Now() - l.CreationTime = now - l.UpdateTime = now - itemsBytes, _ := json.Marshal(l.Items) - l.ItemsText = string(itemsBytes) - return o.Insert(&l) -} - -// UpdateCVEAllowlist Updates the vulnerability white list to DB -func UpdateCVEAllowlist(l models.CVEAllowlist) (int64, error) { - o := GetOrmer() - now := time.Now() - l.UpdateTime = now - itemsBytes, _ := json.Marshal(l.Items) - l.ItemsText = string(itemsBytes) - id, err := o.InsertOrUpdate(&l, "project_id") - return id, err -} - -// GetCVEAllowlist Gets the CVE allowlist of the project based on the project ID in parameter -func GetCVEAllowlist(pid int64) (*models.CVEAllowlist, error) { - o := GetOrmer() - qs := o.QueryTable(&models.CVEAllowlist{}) - qs = qs.Filter("ProjectID", pid) - r := []*models.CVEAllowlist{} - _, err := qs.All(&r) - if err != nil { - return nil, fmt.Errorf("failed to get CVE allowlist for project %d, error: %v", pid, err) - } - if len(r) == 0 { - return nil, nil - } else if len(r) > 1 { - log.Infof("Multiple CVE allowlists found for project %d, length: %d, returning first element.", pid, len(r)) - } - items := []models.CVEAllowlistItem{} - err = json.Unmarshal([]byte(r[0].ItemsText), &items) - if err != nil { - log.Errorf("Failed to decode item list, err: %v, text: %s", err, r[0].ItemsText) - return nil, err - } - r[0].Items = items - return r[0], nil -} diff --git a/src/common/dao/cve_allowlist_test.go b/src/common/dao/cve_allowlist_test.go deleted file mode 100644 index 8f1a17b2a..000000000 --- a/src/common/dao/cve_allowlist_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dao - -import ( - "github.com/goharbor/harbor/src/common/models" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestUpdateAndGetCVEAllowlist(t *testing.T) { - require.Nil(t, ClearTable("cve_allowlist")) - l2, err := GetCVEAllowlist(5) - assert.Nil(t, err) - assert.Nil(t, l2) - - longList := []models.CVEAllowlistItem{} - for i := 0; i < 50; i++ { - longList = append(longList, models.CVEAllowlistItem{CVEID: "CVE-1999-0067"}) - } - - e := int64(1573254000) - in1 := models.CVEAllowlist{ProjectID: 3, Items: longList, ExpiresAt: &e} - _, err = UpdateCVEAllowlist(in1) - require.Nil(t, err) - // assert.Equal(t, int64(1), n) - out1, err := GetCVEAllowlist(3) - require.Nil(t, err) - assert.Equal(t, int64(3), out1.ProjectID) - assert.Equal(t, longList, out1.Items) - assert.Equal(t, e, *out1.ExpiresAt) - - sysCVEs := []models.CVEAllowlistItem{ - {CVEID: "CVE-2019-10164"}, - {CVEID: "CVE-2017-12345"}, - } - in3 := models.CVEAllowlist{Items: sysCVEs} - _, err = UpdateCVEAllowlist(in3) - require.Nil(t, err) - - require.Nil(t, ClearTable("cve_allowlist")) -} diff --git a/src/common/models/base.go b/src/common/models/base.go index 654dfd7d2..4588257e8 100644 --- a/src/common/models/base.go +++ b/src/common/models/base.go @@ -35,6 +35,5 @@ func init() { new(NotificationJob), new(ProjectBlob), new(ArtifactAndBlob), - new(CVEAllowlist), ) } diff --git a/src/common/models/project.go b/src/common/models/project.go index 6861a67ff..e8cec85db 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -22,6 +22,7 @@ import ( "time" "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/pkg/allowlist/models" "github.com/lib/pq" ) @@ -36,20 +37,20 @@ const ( // Project holds the details of a project. type Project struct { - ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"` - OwnerID int `orm:"column(owner_id)" json:"owner_id"` - Name string `orm:"column(name)" json:"name" sort:"default"` - CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` - UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` - Deleted bool `orm:"column(deleted)" json:"deleted"` - OwnerName string `orm:"-" json:"owner_name"` - Role int `orm:"-" json:"current_user_role_id"` - RoleList []int `orm:"-" json:"current_user_role_ids"` - RepoCount int64 `orm:"-" json:"repo_count"` - ChartCount uint64 `orm:"-" json:"chart_count"` - Metadata map[string]string `orm:"-" json:"metadata"` - CVEAllowlist CVEAllowlist `orm:"-" json:"cve_allowlist"` - RegistryID int64 `orm:"column(registry_id)" json:"registry_id"` + ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"` + OwnerID int `orm:"column(owner_id)" json:"owner_id"` + Name string `orm:"column(name)" json:"name" sort:"default"` + CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` + Deleted bool `orm:"column(deleted)" json:"deleted"` + OwnerName string `orm:"-" json:"owner_name"` + Role int `orm:"-" json:"current_user_role_id"` + RoleList []int `orm:"-" json:"current_user_role_ids"` + RepoCount int64 `orm:"-" json:"repo_count"` + ChartCount uint64 `orm:"-" json:"chart_count"` + Metadata map[string]string `orm:"-" json:"metadata"` + CVEAllowlist models.CVEAllowlist `orm:"-" json:"cve_allowlist"` + RegistryID int64 `orm:"column(registry_id)" json:"registry_id"` } // GetMetadata ... @@ -242,10 +243,10 @@ type BaseProjectCollection struct { // ProjectRequest holds informations that need for creating project API type ProjectRequest struct { - Name string `json:"project_name"` - Public *int `json:"public"` // deprecated, reserved for project creation in replication - Metadata map[string]string `json:"metadata"` - CVEAllowlist CVEAllowlist `json:"cve_allowlist"` + Name string `json:"project_name"` + Public *int `json:"public"` // deprecated, reserved for project creation in replication + Metadata map[string]string `json:"metadata"` + CVEAllowlist models.CVEAllowlist `json:"cve_allowlist"` StorageLimit *int64 `json:"storage_limit,omitempty"` RegistryID int64 `json:"registry_id"` diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index 4eec77ceb..d5eb32eca 100755 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -74,4 +74,5 @@ const ( ResourceScanAll = Resource("scan-all") ResourceSystemVolumes = Resource("system-volumes") ResourceOIDCEndpoint = Resource("oidc-endpoint") + ResourceSystemCVEAllowList = Resource("system-cve-allowlist") ) diff --git a/src/common/rbac/system/policies.go b/src/common/rbac/system/policies.go index ac803acba..84ae9008b 100644 --- a/src/common/rbac/system/policies.go +++ b/src/common/rbac/system/policies.go @@ -65,5 +65,7 @@ var ( {Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionRead}, {Resource: rbac.ResourceLdapUser, Action: rbac.ActionCreate}, {Resource: rbac.ResourceLdapUser, Action: rbac.ActionList}, + {Resource: rbac.ResourceSystemCVEAllowList, Action: rbac.ActionRead}, + {Resource: rbac.ResourceSystemCVEAllowList, Action: rbac.ActionUpdate}, } ) diff --git a/src/controller/p2p/preheat/enforcer_test.go b/src/controller/p2p/preheat/enforcer_test.go index e9962cf1d..a0d33e418 100644 --- a/src/controller/p2p/preheat/enforcer_test.go +++ b/src/controller/p2p/preheat/enforcer_test.go @@ -26,6 +26,7 @@ import ( car "github.com/goharbor/harbor/src/controller/artifact" "github.com/goharbor/harbor/src/controller/tag" "github.com/goharbor/harbor/src/lib/selector" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" ar "github.com/goharbor/harbor/src/pkg/artifact" po "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy" pr "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider" @@ -119,7 +120,7 @@ func (suite *EnforcerTestSuite) SetupSuite() { ).Return(&models.Project{ ProjectID: 1, Name: "library", - CVEAllowlist: models.CVEAllowlist{}, + CVEAllowlist: models2.CVEAllowlist{}, Metadata: map[string]string{ proMetaKeyContentTrust: "true", proMetaKeyVulnerability: "true", diff --git a/src/controller/project/controller.go b/src/controller/project/controller.go index e71a18e9c..9a1ff0fa8 100644 --- a/src/controller/project/controller.go +++ b/src/controller/project/controller.go @@ -23,11 +23,11 @@ import ( "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/allowlist" "github.com/goharbor/harbor/src/pkg/notification" "github.com/goharbor/harbor/src/pkg/project" "github.com/goharbor/harbor/src/pkg/project/metadata" "github.com/goharbor/harbor/src/pkg/project/models" - "github.com/goharbor/harbor/src/pkg/scan/allowlist" "github.com/goharbor/harbor/src/pkg/user" ) @@ -89,7 +89,7 @@ func (c *controller) Create(ctx context.Context, project *models.Project) (int64 return err } - if err := c.allowlistMgr.CreateEmpty(projectID); err != nil { + if err := c.allowlistMgr.CreateEmpty(ctx, projectID); err != nil { log.Errorf("failed to create CVE allowlist for project %s: %v", project.Name, err) return err } @@ -233,7 +233,7 @@ func (c *controller) Update(ctx context.Context, p *models.Project) error { } if p.CVEAllowlist.ProjectID == p.ProjectID { - if err := c.allowlistMgr.Set(p.ProjectID, p.CVEAllowlist); err != nil { + if err := c.allowlistMgr.Set(ctx, p.ProjectID, p.CVEAllowlist); err != nil { return err } } @@ -285,7 +285,7 @@ func (c *controller) loadCVEAllowlists(ctx context.Context, projects models.Proj } for _, p := range projects { - wl, err := c.allowlistMgr.Get(p.ProjectID) + wl, err := c.allowlistMgr.Get(ctx, p.ProjectID) if err != nil { return err } @@ -303,7 +303,7 @@ func (c *controller) loadEffectCVEAllowlists(ctx context.Context, projects model for _, p := range projects { if p.ReuseSysCVEAllowlist() { - wl, err := c.allowlistMgr.GetSys() + wl, err := c.allowlistMgr.GetSys(ctx) if err != nil { log.Errorf("get system CVE allowlist failed, error: %v", err) return err @@ -312,7 +312,7 @@ func (c *controller) loadEffectCVEAllowlists(ctx context.Context, projects model wl.ProjectID = p.ProjectID p.CVEAllowlist = *wl } else { - wl, err := c.allowlistMgr.Get(p.ProjectID) + wl, err := c.allowlistMgr.Get(ctx, p.ProjectID) if err != nil { return err } diff --git a/src/controller/project/controller_test.go b/src/controller/project/controller_test.go index 710cfebeb..a81cbcc89 100644 --- a/src/controller/project/controller_test.go +++ b/src/controller/project/controller_test.go @@ -19,17 +19,17 @@ import ( "fmt" "testing" - commonmodels "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/q" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" "github.com/goharbor/harbor/src/pkg/project/models" usermodels "github.com/goharbor/harbor/src/pkg/user/models" ormtesting "github.com/goharbor/harbor/src/testing/lib/orm" "github.com/goharbor/harbor/src/testing/mock" + allowlisttesting "github.com/goharbor/harbor/src/testing/pkg/allowlist" "github.com/goharbor/harbor/src/testing/pkg/project" "github.com/goharbor/harbor/src/testing/pkg/project/metadata" - "github.com/goharbor/harbor/src/testing/pkg/scan/allowlist" "github.com/goharbor/harbor/src/testing/pkg/user" "github.com/stretchr/testify/suite" ) @@ -42,8 +42,8 @@ func (suite *ControllerTestSuite) TestCreate() { ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{}) mgr := &project.Manager{} - allowlistMgr := &allowlist.Manager{} - allowlistMgr.On("CreateEmpty", mock.Anything).Return(nil) + allowlistMgr := &allowlisttesting.Manager{} + allowlistMgr.On("CreateEmpty", mock.Anything, mock.Anything).Return(nil) metadataMgr := &metadata.Manager{} @@ -74,7 +74,7 @@ func (suite *ControllerTestSuite) TestGetByName() { mgr.On("Get", ctx, "test").Return(nil, errors.NotFoundError(nil)) mgr.On("Get", ctx, "oops").Return(nil, fmt.Errorf("oops")) - allowlistMgr := &allowlist.Manager{} + allowlistMgr := &allowlisttesting.Manager{} metadataMgr := &metadata.Manager{} metadataMgr.On("Get", ctx, mock.Anything).Return(map[string]string{"public": "true"}, nil) @@ -103,7 +103,7 @@ func (suite *ControllerTestSuite) TestGetByName() { } { - allowlistMgr.On("Get", mock.Anything).Return(&commonmodels.CVEAllowlist{ProjectID: 1}, nil) + allowlistMgr.On("Get", mock.Anything, mock.Anything).Return(&models2.CVEAllowlist{ProjectID: 1}, nil) p, err := c.GetByName(ctx, "library", WithCVEAllowlist()) suite.Nil(err) suite.Equal("library", p.Name) diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 64bf14a70..61043b7ed 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -117,7 +117,6 @@ func init() { beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List") beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete") - beego.Router("/api/system/CVEAllowlist", &SysCVEAllowlistAPI{}, "get:Get;put:Put") beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List") diff --git a/src/core/api/sys_cve_allowlist.go b/src/core/api/sys_cve_allowlist.go deleted file mode 100644 index 9c609006d..000000000 --- a/src/core/api/sys_cve_allowlist.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package api - -import ( - "errors" - "fmt" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/lib/log" - "github.com/goharbor/harbor/src/pkg/scan/allowlist" - "net/http" -) - -// SysCVEAllowlistAPI Handles the requests to manage system level CVE allowlist -type SysCVEAllowlistAPI struct { - BaseController - manager allowlist.Manager -} - -// Prepare validates the request initially -func (sca *SysCVEAllowlistAPI) Prepare() { - sca.BaseController.Prepare() - if !sca.SecurityCtx.IsAuthenticated() { - sca.SendUnAuthorizedError(errors.New("Unauthorized")) - return - } - if !sca.SecurityCtx.IsSysAdmin() && sca.Ctx.Request.Method != http.MethodGet { - msg := fmt.Sprintf("only system admin has permission issue %s request to this API", sca.Ctx.Request.Method) - log.Errorf(msg) - sca.SendForbiddenError(errors.New(msg)) - return - } - sca.manager = allowlist.NewDefaultManager() -} - -// Get handles the GET request to retrieve the system level CVE allowlist -func (sca *SysCVEAllowlistAPI) Get() { - l, err := sca.manager.GetSys() - if err != nil { - sca.SendInternalServerError(err) - return - } - sca.WriteJSONData(l) -} - -// Put handles the PUT request to update the system level CVE allowlist -func (sca *SysCVEAllowlistAPI) Put() { - var l models.CVEAllowlist - if err := sca.DecodeJSONReq(&l); err != nil { - log.Errorf("Failed to decode JSON array from request") - sca.SendBadRequestError(err) - return - } - if l.ProjectID != 0 { - msg := fmt.Sprintf("Non-zero project ID for system CVE allowlist: %d.", l.ProjectID) - log.Error(msg) - sca.SendBadRequestError(errors.New(msg)) - return - } - if err := sca.manager.SetSys(l); err != nil { - if allowlist.IsInvalidErr(err) { - log.Errorf("Invalid CVE allowlist: %v", err) - sca.SendBadRequestError(err) - return - } - sca.SendInternalServerError(err) - return - } -} diff --git a/src/core/api/sys_cve_allowlist_test.go b/src/core/api/sys_cve_allowlist_test.go deleted file mode 100644 index c2aa4c12d..000000000 --- a/src/core/api/sys_cve_allowlist_test.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package api - -import ( - "github.com/goharbor/harbor/src/common/models" - "net/http" - "testing" -) - -func TestSysCVEAllowlistAPIGet(t *testing.T) { - url := "/api/system/CVEAllowlist" - cases := []*codeCheckingCase{ - // 401 - { - request: &testingRequest{ - method: http.MethodGet, - url: url, - }, - code: http.StatusUnauthorized, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodGet, - url: url, - credential: nonSysAdmin, - }, - code: http.StatusOK, - }, - } - runCodeCheckingCases(t, cases...) -} - -func TestSysCVEAllowlistAPIPut(t *testing.T) { - url := "/api/system/CVEAllowlist" - s := int64(1573254000) - cases := []*codeCheckingCase{ - // 401 - { - request: &testingRequest{ - method: http.MethodPut, - url: url, - }, - code: http.StatusUnauthorized, - }, - // 403 - { - request: &testingRequest{ - method: http.MethodPut, - url: url, - credential: nonSysAdmin, - }, - code: http.StatusForbidden, - }, - // 400 - { - request: &testingRequest{ - method: http.MethodPut, - url: url, - bodyJSON: []string{"CVE-1234-1234"}, - credential: sysAdmin, - }, - code: http.StatusBadRequest, - }, - // 400 - { - request: &testingRequest{ - method: http.MethodPut, - url: url, - bodyJSON: models.CVEAllowlist{ - ExpiresAt: &s, - Items: []models.CVEAllowlistItem{ - {CVEID: "CVE-2019-12310"}, - }, - ProjectID: 2, - }, - credential: sysAdmin, - }, - code: http.StatusBadRequest, - }, - // 400 - { - request: &testingRequest{ - method: http.MethodPut, - url: url, - bodyJSON: models.CVEAllowlist{ - ExpiresAt: &s, - Items: []models.CVEAllowlistItem{ - {CVEID: "CVE-2019-12310"}, - {CVEID: "CVE-2019-12310"}, - }, - }, - credential: sysAdmin, - }, - code: http.StatusBadRequest, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodPut, - url: url, - bodyJSON: models.CVEAllowlist{ - ExpiresAt: &s, - Items: []models.CVEAllowlistItem{ - {CVEID: "CVE-2019-12310"}, - {CVEID: "RHSA-2019:2237"}, - }, - }, - credential: sysAdmin, - }, - code: http.StatusOK, - }, - } - runCodeCheckingCases(t, cases...) -} diff --git a/src/pkg/allowlist/dao/dao.go b/src/pkg/allowlist/dao/dao.go new file mode 100644 index 000000000..603d594bf --- /dev/null +++ b/src/pkg/allowlist/dao/dao.go @@ -0,0 +1,75 @@ +package dao + +import ( + "context" + "encoding/json" + "fmt" + "time" + + beegoorm "github.com/astaxie/beego/orm" + + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/pkg/allowlist/models" +) + +// DAO is the data access object interface for CVE allowlist +type DAO interface { + // Set creates or updates the CVE allowlist to DB based on the project ID in the input parm, if the project does not + // have a CVE allowlist, an empty allowlist will be created. The project ID should be 0 for system level CVE allowlist + Set(ctx context.Context, l models.CVEAllowlist) (int64, error) + // QueryByProjectID returns the CVE allowlist of the project based on the project ID in parameter. The project ID should be 0 + // for system level CVE allowlist + QueryByProjectID(ctx context.Context, pid int64) (*models.CVEAllowlist, error) +} + +// New ... +func New() DAO { + return &dao{} +} + +func init() { + beegoorm.RegisterModel(new(models.CVEAllowlist)) +} + +type dao struct{} + +func (d *dao) Set(ctx context.Context, l models.CVEAllowlist) (int64, error) { + ormer, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + now := time.Now() + l.CreationTime = now + l.UpdateTime = now + itemsBytes, _ := json.Marshal(l.Items) + l.ItemsText = string(itemsBytes) + return ormer.InsertOrUpdate(&l, "project_id") +} + +func (d *dao) QueryByProjectID(ctx context.Context, pid int64) (*models.CVEAllowlist, error) { + ormer, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + qs := ormer.QueryTable(&models.CVEAllowlist{}) + qs = qs.Filter("ProjectID", pid) + var r []models.CVEAllowlist + _, err = qs.All(&r) + if err != nil { + return nil, fmt.Errorf("failed to get CVE allowlist for project %d, error: %v", pid, err) + } + if len(r) == 0 { + return nil, nil + } else if len(r) > 1 { + log.Infof("Multiple CVE allowlists found for project %d, length: %d, returning first element.", pid, len(r)) + } + items := []models.CVEAllowlistItem{} + err = json.Unmarshal([]byte(r[0].ItemsText), &items) + if err != nil { + log.Errorf("Failed to decode item list, err: %v, text: %s", err, r[0].ItemsText) + return nil, err + } + r[0].Items = items + return &r[0], nil +} diff --git a/src/pkg/allowlist/dao/dao_test.go b/src/pkg/allowlist/dao/dao_test.go new file mode 100644 index 000000000..bf9d1d148 --- /dev/null +++ b/src/pkg/allowlist/dao/dao_test.go @@ -0,0 +1,57 @@ +package dao + +import ( + "testing" + + "github.com/goharbor/harbor/src/pkg/allowlist/models" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" +) + +type testSuite struct { + htesting.Suite + dao DAO +} + +func (s *testSuite) SetupSuite() { + s.Suite.SetupSuite() + s.Suite.ClearSQLs = []string{ + "DELETE FROM cve_allowlist WHERE 1 = 1", + } + s.dao = New() +} + +func (s *testSuite) TestSetAndGet() { + s.TearDownSuite() + l, err := s.dao.QueryByProjectID(s.Context(), 5) + s.Nil(err) + s.Nil(l) + var longList []models.CVEAllowlistItem + for i := 0; i < 50; i++ { + longList = append(longList, models.CVEAllowlistItem{CVEID: "CVE-1999-0067"}) + } + + e := int64(1573254000) + in1 := models.CVEAllowlist{ProjectID: 3, Items: longList, ExpiresAt: &e} + _, err = s.dao.Set(s.Context(), in1) + s.Nil(err) + // assert.Equal(t, int64(1), n) + out1, err := s.dao.QueryByProjectID(s.Context(), 3) + s.Nil(err) + s.Equal(int64(3), out1.ProjectID) + s.Equal(longList, out1.Items) + s.Equal(e, *out1.ExpiresAt) + + sysCVEs := []models.CVEAllowlistItem{ + {CVEID: "CVE-2019-10164"}, + {CVEID: "CVE-2017-12345"}, + } + in3 := models.CVEAllowlist{Items: sysCVEs} + _, err = s.dao.Set(s.Context(), in3) + s.Nil(err) + +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &testSuite{}) +} diff --git a/src/pkg/scan/allowlist/manager.go b/src/pkg/allowlist/manager.go similarity index 64% rename from src/pkg/scan/allowlist/manager.go rename to src/pkg/allowlist/manager.go index c26a644e2..b28032ae7 100644 --- a/src/pkg/scan/allowlist/manager.go +++ b/src/pkg/allowlist/manager.go @@ -15,35 +15,39 @@ package allowlist import ( - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" + "context" + "github.com/goharbor/harbor/src/jobservice/logger" "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/allowlist/dao" + "github.com/goharbor/harbor/src/pkg/allowlist/models" ) // Manager defines the interface of CVE allowlist manager, it support both system level and project level allowlists type Manager interface { // CreateEmpty creates empty allowlist for given project - CreateEmpty(projectID int64) error + CreateEmpty(ctx context.Context, projectID int64) error // Set sets the allowlist for given project (create or update) - Set(projectID int64, list models.CVEAllowlist) error + Set(ctx context.Context, projectID int64, list models.CVEAllowlist) error // Get gets the allowlist for given project - Get(projectID int64) (*models.CVEAllowlist, error) + Get(ctx context.Context, projectID int64) (*models.CVEAllowlist, error) // SetSys sets system level allowlist - SetSys(list models.CVEAllowlist) error + SetSys(ctx context.Context, list models.CVEAllowlist) error // GetSys gets system level allowlist - GetSys() (*models.CVEAllowlist, error) + GetSys(ctx context.Context) (*models.CVEAllowlist, error) } -type defaultManager struct{} +type defaultManager struct { + dao dao.DAO +} // CreateEmpty creates empty allowlist for given project -func (d *defaultManager) CreateEmpty(projectID int64) error { +func (d *defaultManager) CreateEmpty(ctx context.Context, projectID int64) error { l := models.CVEAllowlist{ ProjectID: projectID, Items: []models.CVEAllowlistItem{}, } - _, err := dao.CreateCVEAllowlist(l) + _, err := d.dao.Set(ctx, l) if err != nil { logger.Errorf("Failed to create empty CVE allowlist for project: %d, error: %v", projectID, err) } @@ -51,18 +55,18 @@ func (d *defaultManager) CreateEmpty(projectID int64) error { } // Set sets the allowlist for given project (create or update) -func (d *defaultManager) Set(projectID int64, list models.CVEAllowlist) error { +func (d *defaultManager) Set(ctx context.Context, projectID int64, list models.CVEAllowlist) error { list.ProjectID = projectID if err := Validate(list); err != nil { return err } - _, err := dao.UpdateCVEAllowlist(list) + _, err := d.dao.Set(ctx, list) return err } // Get gets the allowlist for given project -func (d *defaultManager) Get(projectID int64) (*models.CVEAllowlist, error) { - wl, err := dao.GetCVEAllowlist(projectID) +func (d *defaultManager) Get(ctx context.Context, projectID int64) (*models.CVEAllowlist, error) { + wl, err := d.dao.QueryByProjectID(ctx, projectID) if err != nil { return nil, err } @@ -77,16 +81,16 @@ func (d *defaultManager) Get(projectID int64) (*models.CVEAllowlist, error) { } // SetSys sets the system level allowlist -func (d *defaultManager) SetSys(list models.CVEAllowlist) error { - return d.Set(0, list) +func (d *defaultManager) SetSys(ctx context.Context, list models.CVEAllowlist) error { + return d.Set(ctx, 0, list) } // GetSys gets the system level allowlist -func (d *defaultManager) GetSys() (*models.CVEAllowlist, error) { - return d.Get(0) +func (d *defaultManager) GetSys(ctx context.Context) (*models.CVEAllowlist, error) { + return d.Get(ctx, 0) } // NewDefaultManager return a new instance of defaultManager func NewDefaultManager() Manager { - return &defaultManager{} + return &defaultManager{dao: dao.New()} } diff --git a/src/pkg/allowlist/manager_test.go b/src/pkg/allowlist/manager_test.go new file mode 100644 index 000000000..8c23082c2 --- /dev/null +++ b/src/pkg/allowlist/manager_test.go @@ -0,0 +1,99 @@ +package allowlist + +import ( + "context" + "testing" + + "github.com/goharbor/harbor/src/pkg/allowlist/models" + "github.com/goharbor/harbor/src/testing/mock" + "github.com/goharbor/harbor/src/testing/pkg/allowlist/dao" + "github.com/stretchr/testify/suite" +) + +type mgrTestSuite struct { + suite.Suite + mgr Manager + dao *dao.DAO +} + +func (mt *mgrTestSuite) SetupTest() { + mt.dao = &dao.DAO{} + mt.mgr = &defaultManager{ + dao: mt.dao, + } +} + +func (mt *mgrTestSuite) TestSet() { + mt.dao.On("Set", mock.Anything, models.CVEAllowlist{ + ProjectID: 9, + Items: []models.CVEAllowlistItem{ + { + CVEID: "testcve-1-1-1-1", + }, + }, + }).Return(int64(9), nil) + err := mt.mgr.Set(context.Background(), 9, models.CVEAllowlist{ + Items: []models.CVEAllowlistItem{ + { + CVEID: "testcve-1-1-1-1", + }, + }, + }) + mt.Nil(err) + mt.dao.AssertExpectations(mt.T()) +} + +func (mt *mgrTestSuite) TestSetSys() { + mt.dao.On("Set", mock.Anything, models.CVEAllowlist{ + ProjectID: 9, + Items: []models.CVEAllowlistItem{ + { + CVEID: "testcve-1-1-1-1", + }, + }, + }).Return(int64(0), nil) + err := mt.mgr.Set(context.Background(), 9, models.CVEAllowlist{ + Items: []models.CVEAllowlistItem{ + { + CVEID: "testcve-1-1-1-1", + }, + }, + }) + mt.Nil(err) + mt.dao.AssertExpectations(mt.T()) +} + +func (mt *mgrTestSuite) TestGet() { + mt.dao.On("QueryByProjectID", mock.Anything, int64(3)).Return(nil, nil) + l, err := mt.mgr.Get(context.Background(), 3) + mt.Nil(err) + mt.Equal(models.CVEAllowlist{ + ProjectID: 3, + Items: []models.CVEAllowlistItem{}, + }, *l) +} + +func (mt *mgrTestSuite) TestGetSys() { + mt.dao.On("QueryByProjectID", mock.Anything, int64(0)).Return(&models.CVEAllowlist{ + ProjectID: 0, + Items: []models.CVEAllowlistItem{ + { + CVEID: "testcve-1-1-1-1", + }, + }, + }, nil) + l, err := mt.mgr.GetSys(context.Background()) + mt.Nil(err) + mt.Equal(models.CVEAllowlist{ + ProjectID: 0, + Items: []models.CVEAllowlistItem{ + { + CVEID: "testcve-1-1-1-1", + }, + }, + }, *l) +} + +func TestManagerTestSuite(t *testing.T) { + suite.Run(t, &mgrTestSuite{}) +} diff --git a/src/common/models/cve_allowlist.go b/src/pkg/allowlist/models/cve_allowlist.go similarity index 97% rename from src/common/models/cve_allowlist.go rename to src/pkg/allowlist/models/cve_allowlist.go index e33156d76..82e6701e4 100644 --- a/src/common/models/cve_allowlist.go +++ b/src/pkg/allowlist/models/cve_allowlist.go @@ -20,13 +20,13 @@ import ( // CVEAllowlist defines the data model for a CVE allowlist type CVEAllowlist struct { - ID int64 `orm:"pk;auto;column(id)" json:"id"` + ID int64 `orm:"pk;auto;column(id)" json:"id,omitempty"` ProjectID int64 `orm:"column(project_id)" json:"project_id"` ExpiresAt *int64 `orm:"column(expires_at)" json:"expires_at,omitempty"` Items []CVEAllowlistItem `orm:"-" json:"items"` ItemsText string `orm:"column(items)" json:"-"` - CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` - UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` + CreationTime time.Time `orm:"column(creation_time);auto_now_add"` + UpdateTime time.Time `orm:"column(update_time);auto_now"` } // CVEAllowlistItem defines one item in the CVE allowlist diff --git a/src/common/models/cve_allowlist_test.go b/src/pkg/allowlist/models/cve_allowlist_test.go similarity index 100% rename from src/common/models/cve_allowlist_test.go rename to src/pkg/allowlist/models/cve_allowlist_test.go diff --git a/src/pkg/scan/allowlist/validator.go b/src/pkg/allowlist/validator.go similarity index 93% rename from src/pkg/scan/allowlist/validator.go rename to src/pkg/allowlist/validator.go index 89416cc70..fc9f60eaa 100644 --- a/src/pkg/scan/allowlist/validator.go +++ b/src/pkg/allowlist/validator.go @@ -16,7 +16,8 @@ package allowlist import ( "fmt" - "github.com/goharbor/harbor/src/common/models" + + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" ) type invalidErr struct { @@ -43,7 +44,7 @@ func IsInvalidErr(err error) bool { const cveIDPattern = `^CVE-\d{4}-\d+$` // Validate help validates the CVE allowlist, to ensure the CVE ID is valid and there's no duplication -func Validate(wl models.CVEAllowlist) error { +func Validate(wl models2.CVEAllowlist) error { m := map[string]struct{}{} // re := regexp.MustCompile(cveIDPattern) for _, it := range wl.Items { diff --git a/src/pkg/scan/allowlist/validator_test.go b/src/pkg/allowlist/validator_test.go similarity index 82% rename from src/pkg/scan/allowlist/validator_test.go rename to src/pkg/allowlist/validator_test.go index 687cb4c51..399c033cb 100644 --- a/src/pkg/scan/allowlist/validator_test.go +++ b/src/pkg/allowlist/validator_test.go @@ -16,9 +16,10 @@ package allowlist import ( "fmt" - "github.com/goharbor/harbor/src/common/models" - "github.com/stretchr/testify/assert" "testing" + + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" + "github.com/stretchr/testify/assert" ) func TestIsInvalidErr(t *testing.T) { @@ -48,24 +49,24 @@ func TestIsInvalidErr(t *testing.T) { func TestValidate(t *testing.T) { cases := []struct { - l models.CVEAllowlist + l models2.CVEAllowlist noError bool }{ { - l: models.CVEAllowlist{ + l: models2.CVEAllowlist{ Items: nil, }, noError: true, }, { - l: models.CVEAllowlist{ - Items: []models.CVEAllowlistItem{}, + l: models2.CVEAllowlist{ + Items: []models2.CVEAllowlistItem{}, }, noError: true, }, { - l: models.CVEAllowlist{ - Items: []models.CVEAllowlistItem{ + l: models2.CVEAllowlist{ + Items: []models2.CVEAllowlistItem{ {CVEID: "breakit"}, {CVEID: "breakit"}, }, @@ -73,8 +74,8 @@ func TestValidate(t *testing.T) { noError: false, }, { - l: models.CVEAllowlist{ - Items: []models.CVEAllowlistItem{ + l: models2.CVEAllowlist{ + Items: []models2.CVEAllowlistItem{ {CVEID: "CVE-2014-456132"}, {CVEID: "CVE-2014-7654321"}, }, @@ -82,8 +83,8 @@ func TestValidate(t *testing.T) { noError: true, }, { - l: models.CVEAllowlist{ - Items: []models.CVEAllowlistItem{ + l: models2.CVEAllowlist{ + Items: []models2.CVEAllowlistItem{ {CVEID: "CVE-2014-456132"}, {CVEID: "CVE-2014-456132"}, {CVEID: "CVE-2014-7654321"}, diff --git a/src/pkg/scan/allowlist/manager_test.go b/src/pkg/scan/allowlist/manager_test.go deleted file mode 100644 index 77dd63a9b..000000000 --- a/src/pkg/scan/allowlist/manager_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package allowlist - -import ( - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/lib/log" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestMain(m *testing.M) { - - // databases := []string{"mysql", "sqlite"} - databases := []string{"postgresql"} - for _, database := range databases { - log.Infof("run test cases for database: %s", database) - - result := 1 - switch database { - case "postgresql": - dao.PrepareTestForPostgresSQL() - default: - log.Fatalf("invalid database: %s", database) - } - - result = m.Run() - - if result != 0 { - os.Exit(result) - } - } -} - -func TestDefaultManager_CreateEmpty(t *testing.T) { - dm := NewDefaultManager() - assert.NoError(t, dm.CreateEmpty(99)) - assert.Error(t, dm.CreateEmpty(99)) -} - -func TestDefaultManager_Get(t *testing.T) { - dm := NewDefaultManager() - // return empty list - l, err := dm.Get(1234) - assert.Nil(t, err) - assert.Empty(t, l.Items) -} diff --git a/src/pkg/scan/report/summary.go b/src/pkg/scan/report/summary.go index eabe9126f..25a773e86 100644 --- a/src/pkg/scan/report/summary.go +++ b/src/pkg/scan/report/summary.go @@ -17,9 +17,9 @@ package report import ( "reflect" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/lib/errors" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" "github.com/goharbor/harbor/src/pkg/scan/dao/scan" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/goharbor/harbor/src/pkg/scan/vuln" @@ -30,14 +30,14 @@ type Options struct { // If it is set, the returned report will contains artifact digest for the vulnerabilities ArtifactDigest string // If it is set, the returned summary will not count the CVEs in the list in. - CVEAllowlist models.CVESet + CVEAllowlist models2.CVESet } // Option for getting the report w/ summary with func template way. type Option func(options *Options) // WithCVEAllowlist is an option of setting CVE allowlist. -func WithCVEAllowlist(set *models.CVESet) Option { +func WithCVEAllowlist(set *models2.CVESet) Option { return func(options *Options) { options.CVEAllowlist = *set } diff --git a/src/pkg/scan/report/summary_test.go b/src/pkg/scan/report/summary_test.go index 40dd2a35e..0f5555cd9 100644 --- a/src/pkg/scan/report/summary_test.go +++ b/src/pkg/scan/report/summary_test.go @@ -19,7 +19,7 @@ import ( "testing" "time" - "github.com/goharbor/harbor/src/common/models" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" "github.com/goharbor/harbor/src/pkg/scan/dao/scan" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/goharbor/harbor/src/pkg/scan/vuln" @@ -105,7 +105,7 @@ func (suite *SummaryTestSuite) TestSummaryGenerateSummaryNoOptions() { // TestSummaryGenerateSummaryWithOptions ... func (suite *SummaryTestSuite) TestSummaryGenerateSummaryWithOptions() { - cveSet := make(models.CVESet) + cveSet := make(models2.CVESet) cveSet["2019-0980-0909"] = struct{}{} summaries, err := GenerateSummary(suite.r, WithCVEAllowlist(&cveSet)) diff --git a/src/pkg/scan/vuln/report.go b/src/pkg/scan/vuln/report.go index 1c35ae096..e5cb883c2 100644 --- a/src/pkg/scan/vuln/report.go +++ b/src/pkg/scan/vuln/report.go @@ -18,7 +18,7 @@ import ( "encoding/json" "fmt" - "github.com/goharbor/harbor/src/common/models" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" ) @@ -150,7 +150,7 @@ func (l *VulnerabilityItemList) Add(items ...*VulnerabilityItem) { } // GetSeveritySummaryAndByPassed returns the Severity Summary and ByPassed by allowlist for the l -func (l *VulnerabilityItemList) GetSeveritySummaryAndByPassed(allowlist models.CVESet) (Severity, *VulnerabilitySummary, []string) { +func (l *VulnerabilityItemList) GetSeveritySummaryAndByPassed(allowlist models2.CVESet) (Severity, *VulnerabilitySummary, []string) { sum := &VulnerabilitySummary{ Total: len(l.Items()), Summary: make(SeveritySummary), diff --git a/src/pkg/scan/vuln/report_test.go b/src/pkg/scan/vuln/report_test.go index f5b82235e..1f3eb6f54 100644 --- a/src/pkg/scan/vuln/report_test.go +++ b/src/pkg/scan/vuln/report_test.go @@ -19,7 +19,7 @@ import ( "reflect" "testing" - "github.com/goharbor/harbor/src/common/models" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/stretchr/testify/assert" ) @@ -108,7 +108,7 @@ func TestGetSummarySeverityAndByPassed(t *testing.T) { Medium: 1, } - severity, sum, byPassed := l.GetSeveritySummaryAndByPassed(models.CVESet{}) + severity, sum, byPassed := l.GetSeveritySummaryAndByPassed(models2.CVESet{}) assert.Equal(3, sum.Total) assert.Equal(1, sum.Fixable) assert.Equal(s, sum.Summary) @@ -121,7 +121,7 @@ func TestGetSummarySeverityAndByPassed(t *testing.T) { Low: 2, } - cveSet := models.CVESet{} + cveSet := models2.CVESet{} cveSet.Add("cve3") severity, sum, byPassed := l.GetSeveritySummaryAndByPassed(cveSet) diff --git a/src/pkg/scan/vuln/summary.go b/src/pkg/scan/vuln/summary.go index ed5e0e5bf..ee189f0aa 100644 --- a/src/pkg/scan/vuln/summary.go +++ b/src/pkg/scan/vuln/summary.go @@ -17,8 +17,8 @@ package vuln import ( "time" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/jobservice/job" + models2 "github.com/goharbor/harbor/src/pkg/allowlist/models" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" ) @@ -39,11 +39,11 @@ type NativeReportSummary struct { TotalCount int `json:"-"` CompleteCount int `json:"-"` VulnerabilityItemList *VulnerabilityItemList `json:"-"` - CVESet models.CVESet `json:"-"` + CVESet models2.CVESet `json:"-"` } // UpdateSeveritySummaryAndByPassed update the Severity, Summary and CVEBypassed of the sum from l and s -func (sum *NativeReportSummary) UpdateSeveritySummaryAndByPassed(l *VulnerabilityItemList, s models.CVESet) { +func (sum *NativeReportSummary) UpdateSeveritySummaryAndByPassed(l *VulnerabilityItemList, s models2.CVESet) { sum.VulnerabilityItemList = l sum.CVESet = s @@ -88,7 +88,7 @@ func (sum *NativeReportSummary) Merge(another *NativeReportSummary) *NativeRepor r.UpdateSeveritySummaryAndByPassed( NewVulnerabilityItemList(sum.VulnerabilityItemList, another.VulnerabilityItemList), - models.NewCVESet(sum.CVESet, another.CVESet), + models2.NewCVESet(sum.CVESet, another.CVESet), ) return r diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 1eb94f5ae..8ce90e1cc 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -31,26 +31,27 @@ import ( // New returns http handler for API V2.0 func New() http.Handler { h, api, err := restapi.HandlerAPI(restapi.Config{ - ArtifactAPI: newArtifactAPI(), - RepositoryAPI: newRepositoryAPI(), - AuditlogAPI: newAuditLogAPI(), - ScannerAPI: newScannerAPI(), - ScanAPI: newScanAPI(), - ScanAllAPI: newScanAllAPI(), - ProjectAPI: newProjectAPI(), - PreheatAPI: newPreheatAPI(), - IconAPI: newIconAPI(), - RobotAPI: newRobotAPI(), - Robotv1API: newRobotV1API(), - ReplicationAPI: newReplicationAPI(), - SysteminfoAPI: newSystemInfoAPI(), - PingAPI: newPingAPI(), - LdapAPI: newLdapAPI(), - GCAPI: newGCAPI(), - QuotaAPI: newQuotaAPI(), - RetentionAPI: newRetentionAPI(), - ImmutableAPI: newImmutableAPI(), - OidcAPI: newOIDCAPI(), + ArtifactAPI: newArtifactAPI(), + RepositoryAPI: newRepositoryAPI(), + AuditlogAPI: newAuditLogAPI(), + ScannerAPI: newScannerAPI(), + ScanAPI: newScanAPI(), + ScanAllAPI: newScanAllAPI(), + ProjectAPI: newProjectAPI(), + PreheatAPI: newPreheatAPI(), + IconAPI: newIconAPI(), + RobotAPI: newRobotAPI(), + Robotv1API: newRobotV1API(), + ReplicationAPI: newReplicationAPI(), + SysteminfoAPI: newSystemInfoAPI(), + PingAPI: newPingAPI(), + LdapAPI: newLdapAPI(), + GCAPI: newGCAPI(), + QuotaAPI: newQuotaAPI(), + RetentionAPI: newRetentionAPI(), + ImmutableAPI: newImmutableAPI(), + OidcAPI: newOIDCAPI(), + SystemCVEAllowlistAPI: newSystemCVEAllowListAPI(), }) if err != nil { log.Fatal(err) diff --git a/src/server/v2.0/handler/model/cve_allowlist.go b/src/server/v2.0/handler/model/cve_allowlist.go new file mode 100644 index 000000000..d62f18d5b --- /dev/null +++ b/src/server/v2.0/handler/model/cve_allowlist.go @@ -0,0 +1,36 @@ +package model + +import ( + "github.com/go-openapi/strfmt" + "github.com/goharbor/harbor/src/pkg/allowlist/models" + svrmodels "github.com/goharbor/harbor/src/server/v2.0/models" +) + +// CVEAllowlist model +type CVEAllowlist struct { + *models.CVEAllowlist +} + +// ToSwagger converts the model to swagger model +func (l *CVEAllowlist) ToSwagger() *svrmodels.CVEAllowlist { + res := &svrmodels.CVEAllowlist{ + ID: l.ID, + Items: []*svrmodels.CVEAllowlistItem{}, + ProjectID: l.ProjectID, + ExpiresAt: l.ExpiresAt, + CreationTime: strfmt.DateTime(l.CreationTime), + UpdateTime: strfmt.DateTime(l.UpdateTime), + } + for _, it := range l.Items { + cveItem := &svrmodels.CVEAllowlistItem{ + CVEID: it.CVEID, + } + res.Items = append(res.Items, cveItem) + } + return res +} + +// NewCVEAllowlist ... +func NewCVEAllowlist(l *models.CVEAllowlist) *CVEAllowlist { + return &CVEAllowlist{l} +} diff --git a/src/server/v2.0/handler/sys_cve_allowlist.go b/src/server/v2.0/handler/sys_cve_allowlist.go new file mode 100644 index 000000000..d30439a75 --- /dev/null +++ b/src/server/v2.0/handler/sys_cve_allowlist.go @@ -0,0 +1,64 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + + "github.com/go-openapi/runtime/middleware" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/pkg/allowlist" + "github.com/goharbor/harbor/src/pkg/allowlist/models" + "github.com/goharbor/harbor/src/server/v2.0/handler/model" + + "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/system_cve_allowlist" +) + +type systemCVEAllowListAPI struct { + BaseAPI + mgr allowlist.Manager +} + +func newSystemCVEAllowListAPI() *systemCVEAllowListAPI { + return &systemCVEAllowListAPI{ + mgr: allowlist.NewDefaultManager(), + } +} + +func (s systemCVEAllowListAPI) PutSystemCVEAllowlist(ctx context.Context, params system_cve_allowlist.PutSystemCVEAllowlistParams) middleware.Responder { + if err := s.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceSystemCVEAllowList); err != nil { + return s.SendError(ctx, err) + } + l := models.CVEAllowlist{} + l.ExpiresAt = params.Allowlist.ExpiresAt + for _, it := range params.Allowlist.Items { + l.Items = append(l.Items, models.CVEAllowlistItem{CVEID: it.CVEID}) + } + if err := s.mgr.SetSys(ctx, l); err != nil { + return s.SendError(ctx, err) + } + return system_cve_allowlist.NewPutSystemCVEAllowlistOK() +} + +func (s systemCVEAllowListAPI) GetSystemCVEAllowlist(ctx context.Context, params system_cve_allowlist.GetSystemCVEAllowlistParams) middleware.Responder { + if err := s.RequireAuthenticated(ctx); err != nil { + return s.SendError(ctx, err) + } + l, err := s.mgr.GetSys(ctx) + if err != nil { + return s.SendError(ctx, err) + } + return system_cve_allowlist.NewGetSystemCVEAllowlistOK().WithPayload(model.NewCVEAllowlist(l).ToSwagger()) +} diff --git a/src/server/v2.0/route/legacy.go b/src/server/v2.0/route/legacy.go index e8894a8a2..9208aa9ec 100755 --- a/src/server/v2.0/route/legacy.go +++ b/src/server/v2.0/route/legacy.go @@ -38,8 +38,6 @@ func registerLegacyRoutes() { beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get") beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post") - beego.Router("/api/"+version+"/system/CVEAllowlist", &api.SysCVEAllowlistAPI{}, "get:Get;put:Put") - beego.Router("/api/"+version+"/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List") beego.Router("/api/"+version+"/replication/adapterinfos", &api.ReplicationAdapterAPI{}, "get:ListAdapterInfos") beego.Router("/api/"+version+"/replication/policies", &api.ReplicationPolicyAPI{}, "get:List;post:Create") diff --git a/src/testing/pkg/allowlist/dao/dao.go b/src/testing/pkg/allowlist/dao/dao.go new file mode 100644 index 000000000..bc12fe323 --- /dev/null +++ b/src/testing/pkg/allowlist/dao/dao.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package dao + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + models "github.com/goharbor/harbor/src/pkg/allowlist/models" +) + +// DAO is an autogenerated mock type for the DAO type +type DAO struct { + mock.Mock +} + +// QueryByProjectID provides a mock function with given fields: ctx, pid +func (_m *DAO) QueryByProjectID(ctx context.Context, pid int64) (*models.CVEAllowlist, error) { + ret := _m.Called(ctx, pid) + + var r0 *models.CVEAllowlist + if rf, ok := ret.Get(0).(func(context.Context, int64) *models.CVEAllowlist); ok { + r0 = rf(ctx, pid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.CVEAllowlist) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, pid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Set provides a mock function with given fields: ctx, l +func (_m *DAO) Set(ctx context.Context, l models.CVEAllowlist) (int64, error) { + ret := _m.Called(ctx, l) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, models.CVEAllowlist) int64); ok { + r0 = rf(ctx, l) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.CVEAllowlist) error); ok { + r1 = rf(ctx, l) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/testing/pkg/allowlist/manager.go b/src/testing/pkg/allowlist/manager.go new file mode 100644 index 000000000..ffa3e5cee --- /dev/null +++ b/src/testing/pkg/allowlist/manager.go @@ -0,0 +1,103 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package robot + +import ( + context "context" + + models "github.com/goharbor/harbor/src/pkg/allowlist/models" + mock "github.com/stretchr/testify/mock" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// CreateEmpty provides a mock function with given fields: ctx, projectID +func (_m *Manager) CreateEmpty(ctx context.Context, projectID int64) error { + ret := _m.Called(ctx, projectID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, projectID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, projectID +func (_m *Manager) Get(ctx context.Context, projectID int64) (*models.CVEAllowlist, error) { + ret := _m.Called(ctx, projectID) + + var r0 *models.CVEAllowlist + if rf, ok := ret.Get(0).(func(context.Context, int64) *models.CVEAllowlist); ok { + r0 = rf(ctx, projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.CVEAllowlist) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSys provides a mock function with given fields: ctx +func (_m *Manager) GetSys(ctx context.Context) (*models.CVEAllowlist, error) { + ret := _m.Called(ctx) + + var r0 *models.CVEAllowlist + if rf, ok := ret.Get(0).(func(context.Context) *models.CVEAllowlist); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.CVEAllowlist) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Set provides a mock function with given fields: ctx, projectID, list +func (_m *Manager) Set(ctx context.Context, projectID int64, list models.CVEAllowlist) error { + ret := _m.Called(ctx, projectID, list) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, models.CVEAllowlist) error); ok { + r0 = rf(ctx, projectID, list) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetSys provides a mock function with given fields: ctx, list +func (_m *Manager) SetSys(ctx context.Context, list models.CVEAllowlist) error { + ret := _m.Called(ctx, list) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, models.CVEAllowlist) error); ok { + r0 = rf(ctx, list) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index ebb996560..6fc9f1cc9 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -21,7 +21,6 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/project/metadata --name Manager --output ./project/metadata --outpkg metadata //go:generate mockery --case snake --dir ../../pkg/quota --name Manager --output ./quota --outpkg quota //go:generate mockery --case snake --dir ../../pkg/quota/driver --name Driver --output ./quota/driver --outpkg driver -//go:generate mockery --case snake --dir ../../pkg/scan/allowlist --name Manager --output ./scan/allowlist --outpkg allowlist //go:generate mockery --case snake --dir ../../pkg/scan/report --name Manager --output ./scan/report --outpkg report //go:generate mockery --case snake --dir ../../pkg/scan/rest/v1 --all --output ./scan/rest/v1 --outpkg v1 //go:generate mockery --case snake --dir ../../pkg/scan/scanner --all --output ./scan/scanner --outpkg scanner @@ -36,3 +35,5 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/repository/dao --name DAO --output ./repository/dao --outpkg dao //go:generate mockery --case snake --dir ../../pkg/immutable/dao --name DAO --output ./immutable/dao --outpkg dao //go:generate mockery --case snake --dir ../../pkg/ldap --name Manager --output ./ldap --outpkg ldap +//go:generate mockery --case snake --dir ../../pkg/allowlist --name Manager --output ./allowlist --outpkg robot +//go:generate mockery --case snake --dir ../../pkg/allowlist/dao --name DAO --output ./allowlist/dao --outpkg dao diff --git a/src/testing/pkg/scan/allowlist/manager.go b/src/testing/pkg/scan/allowlist/manager.go deleted file mode 100644 index 8dd1af3ee..000000000 --- a/src/testing/pkg/scan/allowlist/manager.go +++ /dev/null @@ -1,101 +0,0 @@ -// Code generated by mockery v2.1.0. DO NOT EDIT. - -package allowlist - -import ( - models "github.com/goharbor/harbor/src/common/models" - mock "github.com/stretchr/testify/mock" -) - -// Manager is an autogenerated mock type for the Manager type -type Manager struct { - mock.Mock -} - -// CreateEmpty provides a mock function with given fields: projectID -func (_m *Manager) CreateEmpty(projectID int64) error { - ret := _m.Called(projectID) - - var r0 error - if rf, ok := ret.Get(0).(func(int64) error); ok { - r0 = rf(projectID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Get provides a mock function with given fields: projectID -func (_m *Manager) Get(projectID int64) (*models.CVEAllowlist, error) { - ret := _m.Called(projectID) - - var r0 *models.CVEAllowlist - if rf, ok := ret.Get(0).(func(int64) *models.CVEAllowlist); ok { - r0 = rf(projectID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.CVEAllowlist) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(projectID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetSys provides a mock function with given fields: -func (_m *Manager) GetSys() (*models.CVEAllowlist, error) { - ret := _m.Called() - - var r0 *models.CVEAllowlist - if rf, ok := ret.Get(0).(func() *models.CVEAllowlist); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.CVEAllowlist) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Set provides a mock function with given fields: projectID, list -func (_m *Manager) Set(projectID int64, list models.CVEAllowlist) error { - ret := _m.Called(projectID, list) - - var r0 error - if rf, ok := ret.Get(0).(func(int64, models.CVEAllowlist) error); ok { - r0 = rf(projectID, list) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SetSys provides a mock function with given fields: list -func (_m *Manager) SetSys(list models.CVEAllowlist) error { - ret := _m.Called(list) - - var r0 error - if rf, ok := ret.Get(0).(func(models.CVEAllowlist) error); ok { - r0 = rf(list) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/tests/apitests/python/library/base.py b/tests/apitests/python/library/base.py index 7ed0773fc..e2c8704ce 100644 --- a/tests/apitests/python/library/base.py +++ b/tests/apitests/python/library/base.py @@ -28,7 +28,7 @@ def get_endpoint(): def _create_client(server, credential, debug, api_type="products"): cfg = None - if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota', 'replication', 'robot', 'gc', 'retention', "immutable"): + if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota', 'replication', 'robot', 'gc', 'retention', "immutable", "system_cve_allowlist"): cfg = v2_swagger_client.Configuration() else: cfg = swagger_client.Configuration() @@ -66,6 +66,7 @@ def _create_client(server, credential, debug, api_type="products"): "gc": v2_swagger_client.GcApi(v2_swagger_client.ApiClient(cfg)), "retention": v2_swagger_client.RetentionApi(v2_swagger_client.ApiClient(cfg)), "immutable": v2_swagger_client.ImmutableApi(v2_swagger_client.ApiClient(cfg)), + "system_cve_allowlist": v2_swagger_client.SystemCVEAllowlistApi(v2_swagger_client.ApiClient(cfg)), }.get(api_type,'Error: Wrong API type') def _assert_status_code(expect_code, return_code, err_msg = r"HTTPS status code s not as we expected. Expected {}, while actual HTTPS status code is {}."): diff --git a/tests/apitests/python/library/system_cve_allowlist.py b/tests/apitests/python/library/system_cve_allowlist.py new file mode 100644 index 000000000..525371d72 --- /dev/null +++ b/tests/apitests/python/library/system_cve_allowlist.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +import base +import v2_swagger_client +from v2_swagger_client.rest import ApiException + +class SystemCVEAllowlist(base.Base, object): + def __init__(self): + super(SystemCVEAllowlist, self).__init__(api_type = "system_cve_allowlist") + + def set_cve_allowlist(self, expires_at=None, expected_status_code=200, *cve_ids, **kwargs): + client = self._get_client(**kwargs) + cve_list = [v2_swagger_client.CVEAllowlistItem(cve_id=c) for c in cve_ids] + allowlist = v2_swagger_client.CVEAllowlist(expires_at=expires_at, items=cve_list) + try: + r = client.put_system_cve_allowlist_with_http_info(allowlist=allowlist, _preload_content=False) + except ApiException as e: + base._assert_status_code(expected_status_code, e.status) + else: + base._assert_status_code(expected_status_code, r.status) + + def get_cve_allowlist(self, **kwargs): + client = self._get_client(**kwargs) + return client.get_system_cve_allowlist() \ No newline at end of file diff --git a/tests/apitests/python/test_sys_cve_allowlists.py b/tests/apitests/python/test_sys_cve_allowlists.py index 06093984b..668ee968a 100644 --- a/tests/apitests/python/test_sys_cve_allowlists.py +++ b/tests/apitests/python/test_sys_cve_allowlists.py @@ -1,13 +1,14 @@ from __future__ import absolute_import import unittest -import swagger_client import time from testutils import ADMIN_CLIENT, TEARDOWN, suppress_urllib3_warning from library.user import User from library.system import System +from library.system_cve_allowlist import SystemCVEAllowlist +import v2_swagger_client class TestSysCVEAllowlist(unittest.TestCase): """ @@ -31,6 +32,8 @@ class TestSysCVEAllowlist(unittest.TestCase): def setUp(self): self.user = User() self.system = System() + self.system_cve_allowlist = SystemCVEAllowlist() + user_ra_password = "Aa123456" print("Setup: Creating user for test") user_ra_id, user_ra_name = self.user.create_user(user_password=user_ra_password, **ADMIN_CLIENT) @@ -43,31 +46,31 @@ class TestSysCVEAllowlist(unittest.TestCase): @unittest.skipIf(TEARDOWN == False, "Test data won't be erased.") def tearDown(self): print("TearDown: Clearing the Allowlist") - self.system.set_cve_allowlist(**ADMIN_CLIENT) + self.system_cve_allowlist.set_cve_allowlist(**ADMIN_CLIENT) print("TearDown: Deleting user: %d" % self.user_ra_id) self.user.delete_user(self.user_ra_id, **ADMIN_CLIENT) def testSysCVEAllowlist(self): # 1. User(RA) reads the system level CVE allowlist and it's empty. - wl = self.system.get_cve_allowlist(**self.USER_RA_CLIENT) + wl = self.system_cve_allowlist.get_cve_allowlist(**self.USER_RA_CLIENT) self.assertEqual(0, len(wl.items), "The initial system level CVE allowlist is not empty: %s" % wl.items) # 2. User(RA) updates the system level CVE allowlist, verify it's failed. cves = ['CVE-2019-12310'] - self.system.set_cve_allowlist(None, 403, *cves, **self.USER_RA_CLIENT) + self.system_cve_allowlist.set_cve_allowlist(None, 403, *cves, **self.USER_RA_CLIENT) # 3. Update user(RA) to system admin self.user.update_user_role_as_sysadmin(self.user_ra_id, True, **ADMIN_CLIENT) # 4. User(RA) updates the system level CVE allowlist, verify it's successful. - self.system.set_cve_allowlist(None, 200, *cves, **self.USER_RA_CLIENT) + self.system_cve_allowlist.set_cve_allowlist(None, 200, *cves, **self.USER_RA_CLIENT) # 5. User(RA) reads the system level CVE allowlist, verify the CVE list is updated. - expect_wl = [swagger_client.CVEAllowlistItem(cve_id='CVE-2019-12310')] - wl = self.system.get_cve_allowlist(**self.USER_RA_CLIENT) + expect_wl = [v2_swagger_client.CVEAllowlistItem(cve_id='CVE-2019-12310')] + wl = self.system_cve_allowlist.get_cve_allowlist(**self.USER_RA_CLIENT) self.assertIsNone(wl.expires_at) self.assertEqual(expect_wl, wl.items) # 6. User(RA) updates the expiration date of system level CVE allowlist. exp = int(time.time()) + 3600 - self.system.set_cve_allowlist(exp, 200, *cves, **self.USER_RA_CLIENT) + self.system_cve_allowlist.set_cve_allowlist(exp, 200, *cves, **self.USER_RA_CLIENT) # 7. User(RA) reads the system level CVE allowlist, verify the expiration date is updated. - wl = self.system.get_cve_allowlist(**self.USER_RA_CLIENT) + wl = self.system_cve_allowlist.get_cve_allowlist(**self.USER_RA_CLIENT) self.assertEqual(exp, wl.expires_at) if __name__ == '__main__':