Improvements to Vulnerability Data Export functionality. (#17161)

Closes:
* https://github.com/goharbor/harbor/issues/17152
* https://github.com/goharbor/harbor/issues/17153
Better error handling in case of task executions not found in the system

Signed-off-by: prahaladdarkin <prahaladd@vmware.com>
This commit is contained in:
prahaladdarkin 2022-07-14 12:38:25 +05:30 committed by GitHub
parent 349d220372
commit 3f383e3ffd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 11 deletions

View File

@ -5662,7 +5662,6 @@ paths:
operationId: getScanDataExportExecutionList operationId: getScanDataExportExecutionList
parameters: parameters:
- $ref: '#/parameters/requestId' - $ref: '#/parameters/requestId'
- $ref: '#/parameters/userName'
responses: responses:
'200': '200':
description: Success description: Success

View File

@ -188,6 +188,8 @@ func (c *controller) isCsvArtifactPresent(ctx context.Context, execID int64, dig
repositoryName := fmt.Sprintf("scandata_export_%v", execID) repositoryName := fmt.Sprintf("scandata_export_%v", execID)
exists, err := c.sysArtifactMgr.Exists(ctx, strings.ToLower(export.Vendor), repositoryName, digest) exists, err := c.sysArtifactMgr.Exists(ctx, strings.ToLower(export.Vendor), repositoryName, digest)
if err != nil { if err != nil {
logger.Errorf("failed to check existence of csv artifact for vendor: %s repository: %s digest: %s",
strings.ToLower(export.Vendor), repositoryName, digest)
exists = false exists = false
} }
return exists return exists

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"github.com/goharbor/harbor/src/pkg" "github.com/goharbor/harbor/src/pkg"
"github.com/goharbor/harbor/src/common"
commonmodels "github.com/goharbor/harbor/src/common/models" commonmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/security/local" "github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
@ -137,6 +136,7 @@ func (dfp *DefaultFilterProcessor) getProjectQueryFilter(user *commonmodels.User
logger.Infof("User %v is sys admin. Selecting all projects for export.", user.Username) logger.Infof("User %v is sys admin. Selecting all projects for export.", user.Username)
return q.New(q.KeyWords{}) return q.New(q.KeyWords{})
} }
logger.Infof("User %v is not sys admin. Selecting projects with admin roles for export.", user.Username) logger.Infof("User %v is not sys admin. Selecting projects with admin roles for export.", user.Username)
return q.New(q.KeyWords{"member": &models.MemberQuery{UserID: user.UserID, Role: common.RoleProjectAdmin}}) return q.New(q.KeyWords{"member": &models.MemberQuery{UserID: user.UserID, GroupIDs: user.GroupIDs}})
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
commonmodels "github.com/goharbor/harbor/src/common/models" commonmodels "github.com/goharbor/harbor/src/common/models"
project3 "github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/pkg/project/models"
"github.com/goharbor/harbor/src/pkg/repository/model" "github.com/goharbor/harbor/src/pkg/repository/model"
@ -15,6 +16,7 @@ import (
"github.com/goharbor/harbor/src/testing/pkg/user" "github.com/goharbor/harbor/src/testing/pkg/user"
testifymock "github.com/stretchr/testify/mock" testifymock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"reflect"
"testing" "testing"
"time" "time"
) )
@ -69,6 +71,22 @@ func (suite *FilterProcessorTestSuite) TestProcessProjectFilter() {
suite.NoError(err) suite.NoError(err)
} }
// filtered project with group ids
{
groupIDs := []int{4, 5}
suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1, GroupIDs: groupIDs}, nil).Once()
suite.projectMgr.On("List", mock.Anything, mock.Anything).Return([]*models.Project{project1, project2}, nil).Once()
projectIds, err := suite.filterProcessor.ProcessProjectFilter(context.TODO(), "test-user", []int64{1})
suite.Equal(1, len(projectIds))
suite.Equal(int64(1), projectIds[0])
suite.NoError(err)
memberQueryMatcher := testifymock.MatchedBy(func(query *q.Query) bool {
memberQuery := query.Keywords["member"].(*project3.MemberQuery)
return len(memberQuery.GroupIDs) == 2 && reflect.DeepEqual(memberQuery.GroupIDs, groupIDs) && memberQuery.Role == 0
})
suite.projectMgr.AssertCalled(suite.T(), "List", mock.Anything, memberQueryMatcher)
}
// project listing for admin user // project listing for admin user
{ {
suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1, SysAdminFlag: true}, nil).Once() suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1, SysAdminFlag: true}, nil).Once()

View File

@ -270,9 +270,7 @@ export class OperationComponent implements OnInit, OnDestroy {
refreshExportJobs() { refreshExportJobs() {
if (this.session.getCurrentUser()) { if (this.session.getCurrentUser()) {
this.scanDataExportService this.scanDataExportService
.getScanDataExportExecutionList({ .getScanDataExportExecutionList()
userName: this.session?.getCurrentUser()?.username,
})
.subscribe(res => { .subscribe(res => {
if (res?.items) { if (res?.items) {
this.exportJobs = []; this.exportJobs = [];

View File

@ -131,8 +131,21 @@ func (se *scanDataExportAPI) DownloadScanData(ctx context.Context, params operat
return se.SendError(ctx, err) return se.SendError(ctx, err)
} }
execution, err := se.scanDataExportCtl.GetExecution(ctx, params.ExecutionID) execution, err := se.scanDataExportCtl.GetExecution(ctx, params.ExecutionID)
if err != nil { if err != nil {
se.SendError(ctx, err) if notFound := orm.AsNotFoundError(err, "execution with id: %d not found", params.ExecutionID); notFound != nil {
return middleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) {
writer.WriteHeader(http.StatusNotFound)
})
}
return se.SendError(ctx, err)
}
// check if the CSV artifact for the execution exists
if !execution.FilePresent {
return middleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) {
writer.WriteHeader(http.StatusNotFound)
})
} }
// check if the execution being downloaded is owned by the current user // check if the execution being downloaded is owned by the current user
@ -143,7 +156,7 @@ func (se *scanDataExportAPI) DownloadScanData(ctx context.Context, params operat
if secContext.GetUsername() != execution.UserName { if secContext.GetUsername() != execution.UserName {
return middleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) { return middleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) {
writer.WriteHeader(http.StatusUnauthorized) writer.WriteHeader(http.StatusForbidden)
}) })
} }
@ -173,7 +186,12 @@ func (se *scanDataExportAPI) GetScanDataExportExecutionList(ctx context.Context,
if err != nil { if err != nil {
return se.SendError(ctx, err) return se.SendError(ctx, err)
} }
executions, err := se.scanDataExportCtl.ListExecutions(ctx, params.UserName) secContext, err := se.GetSecurityContext(ctx)
if err != nil {
return se.SendError(ctx, err)
}
executions, err := se.scanDataExportCtl.ListExecutions(ctx, secContext.GetUsername())
if err != nil { if err != nil {
return se.SendError(ctx, err) return se.SendError(ctx, err)
} }

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
beegoorm "github.com/beego/beego/orm"
commonmodels "github.com/goharbor/harbor/src/common/models" commonmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/scan/export" "github.com/goharbor/harbor/src/pkg/scan/export"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
@ -299,6 +300,7 @@ func (suite *ScanExportTestSuite) TestDownloadScanData() {
EndTime: endTime, EndTime: endTime,
ExportDataDigest: "datadigest", ExportDataDigest: "datadigest",
UserName: "test-user", UserName: "test-user",
FilePresent: true,
} }
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil) mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil)
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil) mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
@ -340,6 +342,7 @@ func (suite *ScanExportTestSuite) TestDownloadScanDataUserNotOwnerofExport() {
EndTime: endTime, EndTime: endTime,
ExportDataDigest: "datadigest", ExportDataDigest: "datadigest",
UserName: "test-user", UserName: "test-user",
FilePresent: true,
} }
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil) mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil)
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil) mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
@ -353,12 +356,92 @@ func (suite *ScanExportTestSuite) TestDownloadScanDataUserNotOwnerofExport() {
mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil) mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil)
res, err := suite.DoReq(http.MethodGet, url, nil) res, err := suite.DoReq(http.MethodGet, url, nil)
suite.Equal(http.StatusUnauthorized, res.StatusCode) suite.Equal(http.StatusForbidden, res.StatusCode)
suite.Equal(nil, err)
}
func (suite *ScanExportTestSuite) TestDownloadScanDataNoCsvFilePresent() {
suite.Security.On("GetUsername").Return("test-user1")
suite.Security.On("IsAuthenticated").Return(true).Once()
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
url := "/export/cve/download/100"
endTime := time.Now()
startTime := endTime.Add(-10 * time.Minute)
execution := &export.Execution{
ID: int64(100),
UserID: int64(3),
Status: "Success",
StatusMessage: "",
Trigger: "MANUAL",
StartTime: startTime,
EndTime: endTime,
ExportDataDigest: "datadigest",
UserName: "test-user",
FilePresent: false,
}
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil)
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
// all BLOB related operations succeed
mock.OnAnything(suite.sysArtifactMgr, "Create").Return(int64(1), nil)
sampleData := "test,hello,world"
data := io.NopCloser(strings.NewReader(sampleData))
mock.OnAnything(suite.sysArtifactMgr, "Read").Return(data, nil)
mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil)
res, err := suite.DoReq(http.MethodGet, url, nil)
suite.Equal(http.StatusNotFound, res.StatusCode)
suite.Equal(nil, err)
}
func (suite *ScanExportTestSuite) TestDownloadScanDataExecutionNotPresent() {
suite.Security.On("GetUsername").Return("test-user1")
suite.Security.On("IsAuthenticated").Return(true).Once()
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
url := "/export/cve/download/100"
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(nil, beegoorm.ErrNoRows)
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
// all BLOB related operations succeed
mock.OnAnything(suite.sysArtifactMgr, "Create").Return(int64(1), nil)
sampleData := "test,hello,world"
data := io.NopCloser(strings.NewReader(sampleData))
mock.OnAnything(suite.sysArtifactMgr, "Read").Return(data, nil)
mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil)
res, err := suite.DoReq(http.MethodGet, url, nil)
suite.Equal(http.StatusNotFound, res.StatusCode)
suite.Equal(nil, err)
}
func (suite *ScanExportTestSuite) TestDownloadScanDataExecutionError() {
suite.Security.On("GetUsername").Return("test-user1")
suite.Security.On("IsAuthenticated").Return(true).Once()
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
url := "/export/cve/download/100"
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(nil, errors.New("test error"))
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
// all BLOB related operations succeed
mock.OnAnything(suite.sysArtifactMgr, "Create").Return(int64(1), nil)
sampleData := "test,hello,world"
data := io.NopCloser(strings.NewReader(sampleData))
mock.OnAnything(suite.sysArtifactMgr, "Read").Return(data, nil)
mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil)
res, err := suite.DoReq(http.MethodGet, url, nil)
suite.Equal(http.StatusInternalServerError, res.StatusCode)
suite.Equal(nil, err) suite.Equal(nil, err)
} }
func (suite *ScanExportTestSuite) TestGetScanDataExportExecutionList() { func (suite *ScanExportTestSuite) TestGetScanDataExportExecutionList() {
suite.Security.On("GetUsername").Return("test-user")
suite.Security.On("IsAuthenticated").Return(true).Once() suite.Security.On("IsAuthenticated").Return(true).Once()
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once() suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
url, err := url2.Parse("/export/cve/executions") url, err := url2.Parse("/export/cve/executions")