diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c2a57e99a..65575a4e9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -217,8 +217,8 @@ paths: description: Project ID does not exist. 500: description: Unexpected internal errors. - /projects/{project_id}/logs/filter: - post: + /projects/{project_id}/logs: + get: summary: Get access logs accompany with a relevant project. description: | This endpoint let user search access logs filtered by operations and date time ranges. @@ -229,11 +229,36 @@ paths: format: int64 required: true description: Relevant project ID - - name: access_log - in: body - schema: - $ref: '#/definitions/AccessLogFilter' - description: Search results of access logs. + - name: username + in: query + type: string + required: false + description: Username of the operator. + - name: repository + in: query + type: string + required: false + description: The name of repository + - name: tag + in: query + type: string + required: false + description: The name of tag + - name: operation + in: query + type: string + required: false + description: The operation + - name: begin_timestamp + in: query + type: string + required: false + description: The begin timestamp + - name: end_timestamp + in: query + type: string + required: false + description: The end timestamp - name: page in: query type: integer @@ -861,6 +886,36 @@ paths: description: | This endpoint let user see the recent operation logs of the projects which he is member of parameters: + - name: username + in: query + type: string + required: false + description: Username of the operator. + - name: repository + in: query + type: string + required: false + description: The name of repository + - name: tag + in: query + type: string + required: false + description: The name of tag + - name: operation + in: query + type: string + required: false + description: The operation + - name: begin_timestamp + in: query + type: string + required: false + description: The begin timestamp + - name: end_timestamp + in: query + type: string + required: false + description: The end timestamp - name: page in: query type: integer diff --git a/src/common/dao/accesslog.go b/src/common/dao/accesslog.go index 654744648..b99cd7bbf 100644 --- a/src/common/dao/accesslog.go +++ b/src/common/dao/accesslog.go @@ -67,13 +67,19 @@ func logQueryConditions(query *models.LogQueryParam) orm.QuerySeter { qs = qs.Filter("username__contains", query.Username) } if len(query.Repository) != 0 { - qs = qs.Filter("repo_name", query.Repository) + qs = qs.Filter("repo_name__contains", query.Repository) } if len(query.Tag) != 0 { - qs = qs.Filter("repo_tag", query.Tag) + qs = qs.Filter("repo_tag__contains", query.Tag) } - if len(query.Operations) > 0 { - qs = qs.Filter("operation__in", query.Operations) + operations := []string{} + for _, operation := range query.Operations { + if len(operation) > 0 { + operations = append(operations, operation) + } + } + if len(operations) > 0 { + qs = qs.Filter("operation__in", operations) } if query.BeginTime != nil { qs = qs.Filter("op_time__gte", query.BeginTime) diff --git a/src/common/models/accesslog.go b/src/common/models/accesslog.go index 4a52d073a..6ddad661c 100644 --- a/src/common/models/accesslog.go +++ b/src/common/models/accesslog.go @@ -19,19 +19,15 @@ import ( ) // AccessLog holds information about logs which are used to record the actions that user take to the resourses. -// TODO remove useless attrs type AccessLog struct { - LogID int `orm:"pk;auto;column(log_id)" json:"log_id"` - Username string `orm:"column(username)" json:"username"` - ProjectID int64 `orm:"column(project_id)" json:"project_id"` - RepoName string `orm:"column(repo_name)" json:"repo_name"` - RepoTag string `orm:"column(repo_tag)" json:"repo_tag"` - GUID string `orm:"column(GUID)" json:"guid"` - Operation string `orm:"column(operation)" json:"operation"` - OpTime time.Time `orm:"column(op_time)" json:"op_time"` - Keywords string `orm:"-" json:"keywords"` - BeginTimestamp int64 `orm:"-" json:"begin_timestamp"` - EndTimestamp int64 `orm:"-" json:"end_timestamp"` + LogID int `orm:"pk;auto;column(log_id)" json:"log_id"` + Username string `orm:"column(username)" json:"username"` + ProjectID int64 `orm:"column(project_id)" json:"project_id"` + RepoName string `orm:"column(repo_name)" json:"repo_name"` + RepoTag string `orm:"column(repo_tag)" json:"repo_tag"` + GUID string `orm:"column(GUID)" json:"guid"` + Operation string `orm:"column(operation)" json:"operation"` + OpTime time.Time `orm:"column(op_time)" json:"op_time"` } // LogQueryParam is used to set query conditions when listing diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go index 1ebdd5cfd..c8cdbc750 100644 --- a/src/common/utils/utils.go +++ b/src/common/utils/utils.go @@ -19,6 +19,7 @@ import ( "fmt" "net" "net/url" + "strconv" "strings" "time" @@ -114,3 +115,13 @@ func TestTCPConn(addr string, timeout, interval int) error { return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout) } } + +// ParseTimeStamp parse timestamp to time +func ParseTimeStamp(timestamp string) (*time.Time, error) { + i, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return nil, err + } + t := time.Unix(i, 0) + return &t, nil +} diff --git a/src/common/utils/utils_test.go b/src/common/utils/utils_test.go index 241ff72e3..05f041cfc 100644 --- a/src/common/utils/utils_test.go +++ b/src/common/utils/utils_test.go @@ -17,8 +17,12 @@ package utils import ( "encoding/base64" "net/http/httptest" + "strconv" "strings" "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestParseEndpoint(t *testing.T) { @@ -191,3 +195,19 @@ func TestTestTCPConn(t *testing.T) { t.Fatalf("failed to test tcp connection of %s: %v", addr, err) } } + +func TestParseTimeStamp(t *testing.T) { + // invalid input + _, err := ParseTimeStamp("") + assert.NotNil(t, err) + + // invalid input + _, err = ParseTimeStamp("invalid") + assert.NotNil(t, err) + + // valid + now := time.Now().Unix() + result, err := ParseTimeStamp(strconv.FormatInt(now, 10)) + assert.Nil(t, err) + assert.Equal(t, now, result.Unix()) +} diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index 135225287..7dd50f37c 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -96,7 +96,7 @@ func init() { beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword") beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic") - beego.Router("/api/projects/:id([0-9]+)/logs/filter", &ProjectAPI{}, "post:FilterAccessLog") + beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs") beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put") beego.Router("/api/repositories", &RepositoryAPI{}) beego.Router("/api/statistics", &StatisticAPI{}) @@ -379,27 +379,12 @@ func (a testapi) ToggleProjectPublicity(prjUsr usrInfo, projectID string, ispubl } //Get access logs accompany with a relevant project. -func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLogFilter) (int, []byte, error) { - //func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLog) (int, apilib.AccessLog, error) { - _sling := sling.New().Post(a.basePath) +func (a testapi) ProjectLogs(prjUsr usrInfo, projectID string, query *apilib.LogQuery) (int, []byte, error) { + _sling := sling.New().Get(a.basePath). + Path("/api/projects/" + projectID + "/logs"). + QueryStruct(query) - path := "/api/projects/" + projectID + "/logs/filter" - - _sling = _sling.Path(path) - - // body params - _sling = _sling.BodyJSON(accessLog) - - //var successPayload []apilib.AccessLog - - httpStatusCode, body, err := request(_sling, jsonAcceptHeader, prjUsr) - /* - if err == nil && httpStatusCode == 200 { - err = json.Unmarshal(body, &successPayload) - } - */ - return httpStatusCode, body, err - // return httpStatusCode, successPayload, err + return request(_sling, jsonAcceptHeader, prjUsr) } //-------------------------Member Test---------------------------------------// diff --git a/src/ui/api/log.go b/src/ui/api/log.go index ec92bfbdf..d9fdb6640 100644 --- a/src/ui/api/log.go +++ b/src/ui/api/log.go @@ -19,6 +19,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils" ) //LogAPI handles request api/logs @@ -43,12 +44,36 @@ func (l *LogAPI) Prepare() { func (l *LogAPI) Get() { page, size := l.GetPaginationParams() query := &models.LogQueryParam{ + Username: l.GetString("username"), + Repository: l.GetString("repository"), + Tag: l.GetString("tag"), + Operations: l.GetStrings("operation"), Pagination: &models.Pagination{ Page: page, Size: size, }, } + timestamp := l.GetString("begin_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + l.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp)) + return + } + query.BeginTime = t + } + + timestamp = l.GetString("end_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + l.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp)) + return + } + query.EndTime = t + } + if !l.isSysAdmin { projects, err := l.ProjectMgr.GetByMember(l.username) if err != nil { diff --git a/src/ui/api/project.go b/src/ui/api/project.go index 36596c2cb..c2a48959c 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -18,11 +18,11 @@ import ( "fmt" "net/http" "regexp" - "strings" "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/config" @@ -368,8 +368,8 @@ func (p *ProjectAPI) ToggleProjectPublic() { } } -// FilterAccessLog handles GET to /api/projects/{}/logs -func (p *ProjectAPI) FilterAccessLog() { +// Logs ... +func (p *ProjectAPI) Logs() { if !p.SecurityCtx.IsAuthenticated() { p.HandleUnauthorized() return @@ -380,51 +380,54 @@ func (p *ProjectAPI) FilterAccessLog() { return } - var query models.AccessLog - p.DecodeJSONReq(&query) - - queryParm := &models.LogQueryParam{ + page, size := p.GetPaginationParams() + query := &models.LogQueryParam{ ProjectIDs: []int64{p.project.ProjectID}, - Username: query.Username, - Repository: query.RepoName, - Tag: query.RepoTag, + Username: p.GetString("username"), + Repository: p.GetString("repository"), + Tag: p.GetString("tag"), + Operations: p.GetStrings("operation"), + Pagination: &models.Pagination{ + Page: page, + Size: size, + }, } - if len(query.Keywords) > 0 { - queryParm.Operations = strings.Split(query.Keywords, "/") + timestamp := p.GetString("begin_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + p.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp)) + return + } + query.BeginTime = t } - if query.BeginTimestamp > 0 { - beginTime := time.Unix(query.BeginTimestamp, 0) - queryParm.BeginTime = &beginTime + timestamp = p.GetString("end_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + p.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp)) + return + } + query.EndTime = t } - if query.EndTimestamp > 0 { - endTime := time.Unix(query.EndTimestamp, 0) - queryParm.EndTime = &endTime - } - - page, pageSize := p.GetPaginationParams() - queryParm.Pagination = &models.Pagination{ - Page: page, - Size: pageSize, - } - - total, err := dao.GetTotalOfAccessLogs(queryParm) + total, err := dao.GetTotalOfAccessLogs(query) if err != nil { p.HandleInternalServerError(fmt.Sprintf( "failed to get total of access log: %v", err)) return } - logs, err := dao.GetAccessLogs(queryParm) + logs, err := dao.GetAccessLogs(query) if err != nil { p.HandleInternalServerError(fmt.Sprintf( "failed to get access log: %v", err)) return } - p.SetPaginationHeader(total, page, pageSize) + p.SetPaginationHeader(total, page, size) p.Data["json"] = logs p.ServeJSON() } diff --git a/src/ui/api/project_test.go b/src/ui/api/project_test.go index 3f725af6a..f819b53e4 100644 --- a/src/ui/api/project_test.go +++ b/src/ui/api/project_test.go @@ -320,19 +320,19 @@ func TestProjectLogsFilter(t *testing.T) { apiTest := newHarborAPI() - endTimestamp := time.Now().Unix() - startTimestamp := endTimestamp - 3600 - accessLog := &apilib.AccessLogFilter{ + query := &apilib.LogQuery{ Username: "admin", - Keywords: "", - BeginTimestamp: startTimestamp, - EndTimestamp: endTimestamp, + Repository: "", + Tag: "", + Operation: []string{""}, + BeginTimestamp: 0, + EndTimestamp: time.Now().Unix(), } //-------------------case1: Response Code=200------------------------------// fmt.Println("case 1: respose code:200") projectID := "1" - httpStatusCode, _, err := apiTest.ProjectLogsFilter(*admin, projectID, *accessLog) + httpStatusCode, _, err := apiTest.ProjectLogs(*admin, projectID, query) if err != nil { t.Error("Error while search access logs") t.Log(err) @@ -342,7 +342,7 @@ func TestProjectLogsFilter(t *testing.T) { //-------------------case2: Response Code=401:User need to log in first.------------------------------// fmt.Println("case 2: respose code:401:User need to log in first.") projectID = "1" - httpStatusCode, _, err = apiTest.ProjectLogsFilter(*unknownUsr, projectID, *accessLog) + httpStatusCode, _, err = apiTest.ProjectLogs(*unknownUsr, projectID, query) if err != nil { t.Error("Error while search access logs") t.Log(err) @@ -352,7 +352,7 @@ func TestProjectLogsFilter(t *testing.T) { //-------------------case3: Response Code=404:Project does not exist.-------------------------// fmt.Println("case 3: respose code:404:Illegal format of provided ID value.") projectID = "11111" - httpStatusCode, _, err = apiTest.ProjectLogsFilter(*admin, projectID, *accessLog) + httpStatusCode, _, err = apiTest.ProjectLogs(*admin, projectID, query) if err != nil { t.Error("Error while search access logs") t.Log(err) diff --git a/src/ui/router.go b/src/ui/router.go index 69f0f9912..a2ae3b01e 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -66,7 +66,7 @@ func initRouters() { beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head") beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{}) beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") - beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog") + beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/users/?:id", &api.UserAPI{}) beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword") diff --git a/tests/apitests/apilib/access_log_filter.go b/tests/apitests/apilib/access_log_filter.go index 9c6b87296..0b6b63d68 100644 --- a/tests/apitests/apilib/access_log_filter.go +++ b/tests/apitests/apilib/access_log_filter.go @@ -1,10 +1,10 @@ -/* +/* * Harbor API * * These APIs provide services for manipulating Harbor project. * * OpenAPI spec version: 0.3.0 - * + * * Generated by: https://github.com/swagger-api/swagger-codegen.git * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,17 +22,13 @@ package apilib -type AccessLogFilter struct { - - // Relevant user's name that accessed this project. - Username string `json:"username,omitempty"` - - // Operation name specified when project created. - Keywords string `json:"keywords,omitempty"` - - // Begin timestamp for querying access logs. - BeginTimestamp int64 `json:"begin_timestamp,omitempty"` - - // End timestamp for querying accessl logs. - EndTimestamp int64 `json:"end_timestamp,omitempty"` +type LogQuery struct { + Username string `json:"username"` + Repository string `json:"repository"` + Tag string `json:"tag"` + Operation []string `json:"operation"` + BeginTimestamp int64 `json:"begin_timestamp"` + EndTimestamp int64 `json:"end_timestamp"` + Page int64 `json:"page"` + PageSize int64 `json:"page_size"` }