Merge remote-tracking branch 'upstream/master' into 170608_project

This commit is contained in:
Wenkai Yin 2017-06-09 16:41:13 +08:00
commit 20cf8de0f7
42 changed files with 790 additions and 218 deletions

View File

@ -232,8 +232,8 @@ paths:
description: Project ID does not exist. description: Project ID does not exist.
500: 500:
description: Unexpected internal errors. description: Unexpected internal errors.
/projects/{project_id}/logs/filter: /projects/{project_id}/logs:
post: get:
summary: Get access logs accompany with a relevant project. summary: Get access logs accompany with a relevant project.
description: | description: |
This endpoint let user search access logs filtered by operations and date time ranges. This endpoint let user search access logs filtered by operations and date time ranges.
@ -244,11 +244,36 @@ paths:
format: int64 format: int64
required: true required: true
description: Relevant project ID description: Relevant project ID
- name: access_log - name: username
in: body in: query
schema: type: string
$ref: '#/definitions/AccessLogFilter' required: false
description: Search results of access logs. 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 - name: page
in: query in: query
type: integer type: integer
@ -876,6 +901,36 @@ paths:
description: | description: |
This endpoint let user see the recent operation logs of the projects which he is member of This endpoint let user see the recent operation logs of the projects which he is member of
parameters: 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 - name: page
in: query in: query
type: integer type: integer

View File

@ -67,13 +67,19 @@ func logQueryConditions(query *models.LogQueryParam) orm.QuerySeter {
qs = qs.Filter("username__contains", query.Username) qs = qs.Filter("username__contains", query.Username)
} }
if len(query.Repository) != 0 { 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 { 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 { operations := []string{}
qs = qs.Filter("operation__in", query.Operations) 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 { if query.BeginTime != nil {
qs = qs.Filter("op_time__gte", query.BeginTime) qs = qs.Filter("op_time__gte", query.BeginTime)

View File

@ -19,19 +19,15 @@ import (
) )
// AccessLog holds information about logs which are used to record the actions that user take to the resourses. // 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 { type AccessLog struct {
LogID int `orm:"pk;auto;column(log_id)" json:"log_id"` LogID int `orm:"pk;auto;column(log_id)" json:"log_id"`
Username string `orm:"column(username)" json:"username"` Username string `orm:"column(username)" json:"username"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"` ProjectID int64 `orm:"column(project_id)" json:"project_id"`
RepoName string `orm:"column(repo_name)" json:"repo_name"` RepoName string `orm:"column(repo_name)" json:"repo_name"`
RepoTag string `orm:"column(repo_tag)" json:"repo_tag"` RepoTag string `orm:"column(repo_tag)" json:"repo_tag"`
GUID string `orm:"column(GUID)" json:"guid"` GUID string `orm:"column(GUID)" json:"guid"`
Operation string `orm:"column(operation)" json:"operation"` Operation string `orm:"column(operation)" json:"operation"`
OpTime time.Time `orm:"column(op_time)" json:"op_time"` 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"`
} }
// LogQueryParam is used to set query conditions when listing // LogQueryParam is used to set query conditions when listing

View File

@ -0,0 +1,59 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 models
//ClairLayer ...
type ClairLayer struct {
Name string `json:"Name,omitempty"`
NamespaceNames []string `json:"NamespaceNames,omitempty"`
Path string `json:"Path,omitempty"`
Headers map[string]string `json:"Headers,omitempty"`
ParentName string `json:"ParentName,omitempty"`
Format string `json:"Format,omitempty"`
Features []ClairFeature `json:"Features,omitempty"`
}
//ClairFeature ...
type ClairFeature struct {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
VersionFormat string `json:"VersionFormat,omitempty"`
Version string `json:"Version,omitempty"`
Vulnerabilities []ClairVulnerability `json:"Vulnerabilities,omitempty"`
AddedBy string `json:"AddedBy,omitempty"`
}
//ClairVulnerability ...
type ClairVulnerability struct {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
Description string `json:"Description,omitempty"`
Link string `json:"Link,omitempty"`
Severity string `json:"Severity,omitempty"`
Metadata map[string]interface{} `json:"Metadata,omitempty"`
FixedBy string `json:"FixedBy,omitempty"`
FixedIn []ClairFeature `json:"FixedIn,omitempty"`
}
//ClairError ...
type ClairError struct {
Message string `json:"Message,omitempty"`
}
//ClairLayerEnvelope ...
type ClairLayerEnvelope struct {
Layer *ClairLayer `json:"Layer,omitempty"`
Error *ClairError `json:"Error,omitempty"`
}

View File

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "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) 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
}

View File

@ -17,8 +17,12 @@ package utils
import ( import (
"encoding/base64" "encoding/base64"
"net/http/httptest" "net/http/httptest"
"strconv"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert"
) )
func TestParseEndpoint(t *testing.T) { 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) 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())
}

View File

@ -22,6 +22,7 @@ import (
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry/auth" "github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/jobservice/config" "github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/jobservice/utils" "github.com/vmware/harbor/src/jobservice/utils"
) )
@ -83,5 +84,8 @@ func (isj *ImageScanJob) Post() {
isj.RenderError(http.StatusInternalServerError, "Failed to insert scan job data.") isj.RenderError(http.StatusInternalServerError, "Failed to insert scan job data.")
return return
} }
log.Debugf("job id: %d", jid) log.Debugf("Scan job id: %d", jid)
sj := job.NewScanJob(jid)
log.Debugf("Sent job to scheduler, job: %v", sj)
job.Schedule(sj)
} }

View File

@ -124,7 +124,7 @@ func TestScanJob(t *testing.T) {
assert.Nil(err) assert.Nil(err)
j, err := dao.GetScanJob(scanJobID) j, err := dao.GetScanJob(scanJobID)
assert.Equal(models.JobRetrying, j.Status) assert.Equal(models.JobRetrying, j.Status)
assert.Equal("sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f", sj.parm.digest) assert.Equal("sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f", sj.parm.Digest)
sj2 := NewScanJob(99999) sj2 := NewScanJob(99999)
err = sj2.Init() err = sj2.Init()
assert.NotNil(err) assert.NotNil(err)

View File

@ -176,9 +176,9 @@ type ScanJob struct {
//ScanJobParm wraps the parms of a image scan job. //ScanJobParm wraps the parms of a image scan job.
type ScanJobParm struct { type ScanJobParm struct {
repository string Repository string
tag string Tag string
digest string Digest string
} }
//ID returns the id of the scan //ID returns the id of the scan
@ -216,9 +216,9 @@ func (sj *ScanJob) Init() error {
return fmt.Errorf("The job doesn't exist in DB, job id: %d", sj.id) return fmt.Errorf("The job doesn't exist in DB, job id: %d", sj.id)
} }
sj.parm = &ScanJobParm{ sj.parm = &ScanJobParm{
repository: job.Repository, Repository: job.Repository,
tag: job.Tag, Tag: job.Tag,
digest: job.Digest, Digest: job.Digest,
} }
return nil return nil
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config" "github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/replication" "github.com/vmware/harbor/src/jobservice/replication"
"github.com/vmware/harbor/src/jobservice/scan"
) )
// SM is the state machine to handle job, it handles one job at a time. // SM is the state machine to handle job, it handles one job at a time.
@ -231,7 +232,12 @@ func (sm *SM) initTransitions() error {
return fmt.Errorf("unsupported operation: %s", jobParm.Operation) return fmt.Errorf("unsupported operation: %s", jobParm.Operation)
} }
case ScanType: case ScanType:
log.Debugf("TODO for scan job, job: %v", sm.CurrentJob) scanJob, ok := sm.CurrentJob.(*ScanJob)
if !ok {
//Shouldn't be here.
return fmt.Errorf("The job: %v is not a type of ScanJob", sm.CurrentJob)
}
addImgScanTransition(sm, scanJob.parm)
return nil return nil
default: default:
return fmt.Errorf("Unsupported job type: %v", sm.CurrentJob.Type()) return fmt.Errorf("Unsupported job type: %v", sm.CurrentJob.Type())
@ -247,6 +253,20 @@ func addTestTransition(sm *SM) error {
} }
*/ */
func addImgScanTransition(sm *SM, parm *ScanJobParm) {
ctx := &scan.JobContext{
Repository: parm.Repository,
Tag: parm.Tag,
Digest: parm.Digest,
Logger: sm.Logger,
}
sm.AddTransition(models.JobRunning, scan.StateInitialize, &scan.Initializer{Context: ctx})
sm.AddTransition(scan.StateInitialize, scan.StateScanLayer, &scan.LayerScanHandler{Context: ctx})
sm.AddTransition(scan.StateScanLayer, scan.StateSummarize, &scan.SummarizeHandler{Context: ctx})
sm.AddTransition(scan.StateSummarize, models.JobFinished, &StatusUpdater{sm.CurrentJob, models.JobFinished})
}
func addImgTransferTransition(sm *SM, parm *RepJobParm) { func addImgTransferTransition(sm *SM, parm *RepJobParm) {
base := replication.InitBaseHandler(parm.Repository, parm.LocalRegURL, config.JobserviceSecret(), base := replication.InitBaseHandler(parm.Repository, parm.LocalRegURL, config.JobserviceSecret(),
parm.TargetURL, parm.TargetUsername, parm.TargetPassword, parm.TargetURL, parm.TargetUsername, parm.TargetPassword,

View File

@ -0,0 +1,44 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 scan
import (
"github.com/vmware/harbor/src/common/utils/log"
)
const (
// StateInitialize in this state the handler will initialize the job context.
StateInitialize = "initialize"
// StateScanLayer in this state the handler will POST layer of clair to scan layer by layer of the image.
StateScanLayer = "scanlayer"
// StateSummarize in this state, the layers are scanned by clair it will call clair API to update vulnerability overview in Harbor DB. After this state, the job is finished.
StateSummarize = "summarize"
)
//JobContext is for sharing data across handlers in a execution of a scan job.
type JobContext struct {
Repository string
Tag string
Digest string
//the digests of layers
layers []string
//each layer name has to be unique, so it should be ${img-digest}-${layer-digest}
layerNames []string
//the index of current layer
current int
//token for accessing the registry
token string
Logger *log.Logger
}

View File

@ -0,0 +1,70 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 scan
import (
"github.com/vmware/harbor/src/common/models"
)
// Initializer will handle the initialise state pull the manifest, prepare token.
type Initializer struct {
Context *JobContext
}
// Enter ...
func (iz *Initializer) Enter() (string, error) {
logger := iz.Context.Logger
logger.Infof("Entered scan initializer")
return StateScanLayer, nil
}
// Exit ...
func (iz *Initializer) Exit() error {
return nil
}
//LayerScanHandler will call clair API to trigger scanning.
type LayerScanHandler struct {
Context *JobContext
}
// Enter ...
func (ls *LayerScanHandler) Enter() (string, error) {
logger := ls.Context.Logger
logger.Infof("Entered scan layer handler")
return StateSummarize, nil
}
// Exit ...
func (ls *LayerScanHandler) Exit() error {
return nil
}
// SummarizeHandler will summarize the vulnerability and feature information of Clair, and store into Harbor's DB.
type SummarizeHandler struct {
Context *JobContext
}
// Enter ...
func (sh *SummarizeHandler) Enter() (string, error) {
logger := sh.Context.Logger
logger.Infof("Entered summarize handler")
return models.JobFinished, nil
}
// Exit ...
func (sh *SummarizeHandler) Exit() error {
return nil
}

View File

@ -96,7 +96,7 @@ func init() {
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword") beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic") 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/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put")
beego.Router("/api/repositories", &RepositoryAPI{}) beego.Router("/api/repositories", &RepositoryAPI{})
beego.Router("/api/statistics", &StatisticAPI{}) beego.Router("/api/statistics", &StatisticAPI{})
@ -373,27 +373,12 @@ func (a testapi) ToggleProjectPublicity(prjUsr usrInfo, projectID string, ispubl
} }
//Get access logs accompany with a relevant project. //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) ProjectLogs(prjUsr usrInfo, projectID string, query *apilib.LogQuery) (int, []byte, error) {
//func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLog) (int, apilib.AccessLog, error) { _sling := sling.New().Get(a.basePath).
_sling := sling.New().Post(a.basePath) Path("/api/projects/" + projectID + "/logs").
QueryStruct(query)
path := "/api/projects/" + projectID + "/logs/filter" return request(_sling, jsonAcceptHeader, prjUsr)
_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
} }
//-------------------------Member Test---------------------------------------// //-------------------------Member Test---------------------------------------//

View File

@ -19,6 +19,7 @@ import (
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
) )
//LogAPI handles request api/logs //LogAPI handles request api/logs
@ -43,12 +44,36 @@ func (l *LogAPI) Prepare() {
func (l *LogAPI) Get() { func (l *LogAPI) Get() {
page, size := l.GetPaginationParams() page, size := l.GetPaginationParams()
query := &models.LogQueryParam{ query := &models.LogQueryParam{
Username: l.GetString("username"),
Repository: l.GetString("repository"),
Tag: l.GetString("tag"),
Operations: l.GetStrings("operation"),
Pagination: &models.Pagination{ Pagination: &models.Pagination{
Page: page, Page: page,
Size: size, 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 { if !l.isSysAdmin {
projects, err := l.ProjectMgr.GetByMember(l.username) projects, err := l.ProjectMgr.GetByMember(l.username)
if err != nil { if err != nil {

View File

@ -18,11 +18,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"strings"
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "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/common/utils/log"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
@ -391,8 +391,8 @@ func (p *ProjectAPI) ToggleProjectPublic() {
} }
} }
// FilterAccessLog handles GET to /api/projects/{}/logs // Logs ...
func (p *ProjectAPI) FilterAccessLog() { func (p *ProjectAPI) Logs() {
if !p.SecurityCtx.IsAuthenticated() { if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized() p.HandleUnauthorized()
return return
@ -403,51 +403,54 @@ func (p *ProjectAPI) FilterAccessLog() {
return return
} }
var query models.AccessLog page, size := p.GetPaginationParams()
p.DecodeJSONReq(&query) query := &models.LogQueryParam{
queryParm := &models.LogQueryParam{
ProjectIDs: []int64{p.project.ProjectID}, ProjectIDs: []int64{p.project.ProjectID},
Username: query.Username, Username: p.GetString("username"),
Repository: query.RepoName, Repository: p.GetString("repository"),
Tag: query.RepoTag, Tag: p.GetString("tag"),
Operations: p.GetStrings("operation"),
Pagination: &models.Pagination{
Page: page,
Size: size,
},
} }
if len(query.Keywords) > 0 { timestamp := p.GetString("begin_timestamp")
queryParm.Operations = strings.Split(query.Keywords, "/") 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 { timestamp = p.GetString("end_timestamp")
beginTime := time.Unix(query.BeginTimestamp, 0) if len(timestamp) > 0 {
queryParm.BeginTime = &beginTime 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 { total, err := dao.GetTotalOfAccessLogs(query)
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)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.HandleInternalServerError(fmt.Sprintf(
"failed to get total of access log: %v", err)) "failed to get total of access log: %v", err))
return return
} }
logs, err := dao.GetAccessLogs(queryParm) logs, err := dao.GetAccessLogs(query)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.HandleInternalServerError(fmt.Sprintf(
"failed to get access log: %v", err)) "failed to get access log: %v", err))
return return
} }
p.SetPaginationHeader(total, page, pageSize) p.SetPaginationHeader(total, page, size)
p.Data["json"] = logs p.Data["json"] = logs
p.ServeJSON() p.ServeJSON()
} }

View File

@ -334,19 +334,19 @@ func TestProjectLogsFilter(t *testing.T) {
apiTest := newHarborAPI() apiTest := newHarborAPI()
endTimestamp := time.Now().Unix() query := &apilib.LogQuery{
startTimestamp := endTimestamp - 3600
accessLog := &apilib.AccessLogFilter{
Username: "admin", Username: "admin",
Keywords: "", Repository: "",
BeginTimestamp: startTimestamp, Tag: "",
EndTimestamp: endTimestamp, Operation: []string{""},
BeginTimestamp: 0,
EndTimestamp: time.Now().Unix(),
} }
//-------------------case1: Response Code=200------------------------------// //-------------------case1: Response Code=200------------------------------//
fmt.Println("case 1: respose code:200") fmt.Println("case 1: respose code:200")
projectID := "1" projectID := "1"
httpStatusCode, _, err := apiTest.ProjectLogsFilter(*admin, projectID, *accessLog) httpStatusCode, _, err := apiTest.ProjectLogs(*admin, projectID, query)
if err != nil { if err != nil {
t.Error("Error while search access logs") t.Error("Error while search access logs")
t.Log(err) t.Log(err)
@ -356,7 +356,7 @@ func TestProjectLogsFilter(t *testing.T) {
//-------------------case2: Response Code=401:User need to log in first.------------------------------// //-------------------case2: Response Code=401:User need to log in first.------------------------------//
fmt.Println("case 2: respose code:401:User need to log in first.") fmt.Println("case 2: respose code:401:User need to log in first.")
projectID = "1" projectID = "1"
httpStatusCode, _, err = apiTest.ProjectLogsFilter(*unknownUsr, projectID, *accessLog) httpStatusCode, _, err = apiTest.ProjectLogs(*unknownUsr, projectID, query)
if err != nil { if err != nil {
t.Error("Error while search access logs") t.Error("Error while search access logs")
t.Log(err) t.Log(err)
@ -366,7 +366,7 @@ func TestProjectLogsFilter(t *testing.T) {
//-------------------case3: Response Code=404:Project does not exist.-------------------------// //-------------------case3: Response Code=404:Project does not exist.-------------------------//
fmt.Println("case 3: respose code:404:Illegal format of provided ID value.") fmt.Println("case 3: respose code:404:Illegal format of provided ID value.")
projectID = "11111" projectID = "11111"
httpStatusCode, _, err = apiTest.ProjectLogsFilter(*admin, projectID, *accessLog) httpStatusCode, _, err = apiTest.ProjectLogs(*admin, projectID, query)
if err != nil { if err != nil {
t.Error("Error while search access logs") t.Error("Error while search access logs")
t.Log(err) t.Log(err)

View File

@ -60,6 +60,7 @@ func (ec envPolicyChecker) contentTrustEnabled(name string) bool {
return os.Getenv("PROJECT_CONTENT_TRUST") == "1" return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
} }
func (ec envPolicyChecker) vulnerableEnabled(name string) bool { func (ec envPolicyChecker) vulnerableEnabled(name string) bool {
// TODO: May need get more information in vulnerable policies.
return os.Getenv("PROJECT_VULNERABBLE") == "1" return os.Getenv("PROJECT_VULNERABBLE") == "1"
} }
@ -161,8 +162,8 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
log.Debugf("Passing the response to outter responseWriter") log.Debugf("Passing the response to outter responseWriter")
copyResp(rec, rw) copyResp(rec, rw)
} else { } else {
log.Debugf("digest miamatch, failing the response.") log.Debugf("digest mismatch, failing the response.")
http.Error(rw, "Failure in content trust handler", http.StatusPreconditionFailed) http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed)
} }
} }

View File

@ -41,6 +41,7 @@ func Init(urls ...string) error {
return err return err
} }
Proxy = httputil.NewSingleHostReverseProxy(targetURL) Proxy = httputil.NewSingleHostReverseProxy(targetURL)
//TODO: add vulnerable interceptor.
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}} handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}}
return nil return nil
} }

View File

@ -66,7 +66,7 @@ func initRouters() {
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head") 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]+)", &api.ProjectAPI{})
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") 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/statistics", &api.StatisticAPI{})
beego.Router("/api/users/?:id", &api.UserAPI{}) beego.Router("/api/users/?:id", &api.UserAPI{})
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword") beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")

View File

@ -1,73 +1,74 @@
{ {
"name": "harbor-ui", "name": "harbor-ui",
"version": "0.1.0", "version": "0.1.0",
"description": "Harbor shared UI components based on Clarity and Angular4", "description": "Harbor shared UI components based on Clarity and Angular4",
"scripts": { "scripts": {
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json", "start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
"lint": "tslint \"src/**/*.ts\"", "lint": "tslint \"src/**/*.ts\"",
"test": "ng test --single-run", "test": "ng test --single-run",
"test:once": "karma start karma.conf.js --single-run", "test:once": "karma start karma.conf.js --single-run",
"pree2e": "webdriver-manager update", "pree2e": "webdriver-manager update",
"e2e": "protractor", "e2e": "protractor",
"cleanup": "rimraf dist", "cleanup": "rimraf dist",
"copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist", "copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist",
"transpile": "ngc -p tsconfig.json", "transpile": "ngc -p tsconfig.json",
"package": "rollup -c", "package": "rollup -c",
"minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js", "minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js",
"build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy" "build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^4.1.0", "@angular/animations": "^4.1.0",
"@angular/common": "^4.1.0", "@angular/common": "^4.1.0",
"@angular/compiler": "^4.1.0", "@angular/compiler": "^4.1.0",
"@angular/core": "^4.1.0", "@angular/core": "^4.1.0",
"@angular/forms": "^4.1.0", "@angular/forms": "^4.1.0",
"@angular/http": "^4.1.0", "@angular/http": "^4.1.0",
"@angular/platform-browser": "^4.1.0", "@angular/platform-browser": "^4.1.0",
"@angular/platform-browser-dynamic": "^4.1.0", "@angular/platform-browser-dynamic": "^4.1.0",
"@angular/router": "^4.1.0", "@angular/router": "^4.1.0",
"@webcomponents/custom-elements": "1.0.0-alpha.3", "@ngx-translate/core": "^6.0.0",
"web-animations-js": "^2.2.1", "@ngx-translate/http-loader": "0.0.3",
"clarity-angular": "^0.9.7", "@webcomponents/custom-elements": "1.0.0-alpha.3",
"clarity-icons": "^0.9.7", "clarity-angular": "^0.9.7",
"clarity-ui": "^0.9.7", "clarity-icons": "^0.9.7",
"core-js": "^2.4.1", "clarity-ui": "^0.9.7",
"rxjs": "^5.0.1", "core-js": "^2.4.1",
"ts-helpers": "^1.1.1", "intl": "^1.2.5",
"zone.js": "^0.8.4", "mutationobserver-shim": "^0.3.2",
"mutationobserver-shim": "^0.3.2", "ngx-clipboard": "^8.0.2",
"@ngx-translate/core": "^6.0.0", "ngx-cookie": "^1.0.0",
"@ngx-translate/http-loader": "0.0.3", "rxjs": "^5.0.1",
"ngx-cookie": "^1.0.0", "ts-helpers": "^1.1.1",
"intl": "^1.2.5" "web-animations-js": "^2.2.1",
}, "zone.js": "^0.8.4"
"devDependencies": { },
"@angular/cli": "^1.0.0", "devDependencies": {
"@angular/compiler-cli": "^4.0.1", "@angular/cli": "^1.0.0",
"@types/core-js": "^0.9.41", "@angular/compiler-cli": "^4.0.1",
"@types/jasmine": "~2.2.30", "@types/core-js": "^0.9.41",
"@types/node": "^6.0.42", "@types/jasmine": "~2.2.30",
"bootstrap": "4.0.0-alpha.5", "@types/node": "^6.0.42",
"codelyzer": "~2.0.0-beta.4", "bootstrap": "4.0.0-alpha.5",
"enhanced-resolve": "^3.0.0", "codelyzer": "~2.0.0-beta.4",
"jasmine-core": "2.4.1", "enhanced-resolve": "^3.0.0",
"jasmine-spec-reporter": "2.5.0", "jasmine-core": "2.4.1",
"karma": "1.2.0", "jasmine-spec-reporter": "2.5.0",
"karma-cli": "^1.0.1", "karma": "1.2.0",
"karma-jasmine": "^1.0.2", "karma-cli": "^1.0.1",
"karma-mocha-reporter": "^2.2.1", "karma-jasmine": "^1.0.2",
"karma-phantomjs-launcher": "^1.0.0", "karma-mocha-reporter": "^2.2.1",
"karma-remap-istanbul": "^0.2.1", "karma-phantomjs-launcher": "^1.0.0",
"protractor": "^4.0.9", "karma-remap-istanbul": "^0.2.1",
"rollup": "^0.41.6", "protractor": "^4.0.9",
"ts-node": "1.2.1", "rollup": "^0.41.6",
"tslint": "^4.1.1", "ts-node": "1.2.1",
"typescript": "~2.2.0", "tslint": "^4.1.1",
"typings": "^1.4.0", "typescript": "~2.2.0",
"uglify-js": "^2.8.22", "typings": "^1.4.0",
"webdriver-manager": "10.2.5", "uglify-js": "^2.8.22",
"rimraf": "^2.6.1", "webdriver-manager": "10.2.5",
"copyfiles": "^1.2.0" "rimraf": "^2.6.1",
} "copyfiles": "^1.2.0"
} }
}

View File

@ -21,6 +21,7 @@ import { CONFIRMATION_DIALOG_DIRECTIVES } from './confirmation-dialog/index';
import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index'; import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index';
import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index'; import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index';
import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index'; import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index';
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index';
import { import {
SystemInfoService, SystemInfoService,
@ -142,7 +143,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
LIST_REPLICATION_RULE_DIRECTIVES, LIST_REPLICATION_RULE_DIRECTIVES,
CREATE_EDIT_RULE_DIRECTIVES, CREATE_EDIT_RULE_DIRECTIVES,
DATETIME_PICKER_DIRECTIVES, DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES
], ],
exports: [ exports: [
LOG_DIRECTIVES, LOG_DIRECTIVES,
@ -160,6 +162,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
CREATE_EDIT_RULE_DIRECTIVES, CREATE_EDIT_RULE_DIRECTIVES,
DATETIME_PICKER_DIRECTIVES, DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES, VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES,
TranslateModule TranslateModule
], ],
providers: [] providers: []

View File

@ -12,4 +12,5 @@ export * from './tag/index';
export * from './list-replication-rule/index'; export * from './list-replication-rule/index';
export * from './replication/index'; export * from './replication/index';
export * from './vulnerability-scanning/index'; export * from './vulnerability-scanning/index';
export * from './i18n/index'; export * from './i18n/index';
export * from './push-image/index';

View File

@ -0,0 +1,48 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { COPY_INPUT_HTML } from './copy-input.html';
import { PUSH_IMAGE_STYLE } from './push-image.css';
export const enum CopyStatus {
NORMAL, SUCCESS, ERROR
}
@Component({
selector: 'hbr-copy-input',
styles: [PUSH_IMAGE_STYLE],
template: COPY_INPUT_HTML,
providers: []
})
export class CopyInputComponent {
@Input() inputSize: number = 40;
@Input() headerTitle: string = "Copy Input";
@Input() defaultValue: string = "N/A";
state: CopyStatus = CopyStatus.NORMAL;
@Output() onCopySuccess: EventEmitter<any> = new EventEmitter<any>();
@Output() onCopyError: EventEmitter<any> = new EventEmitter<any>();
onSuccess($event: any): void {
this.state = CopyStatus.SUCCESS;
this.onCopySuccess.emit($event);
}
onError(error: any): void {
this.state = CopyStatus.ERROR;
this.onCopyError.emit(error);
}
reset(): void {
this.state = CopyStatus.NORMAL;
}
public get isCopied(): boolean {
return this.state === CopyStatus.SUCCESS;
}
public get hasCopyError(): boolean {
return this.state === CopyStatus.ERROR;
}
}

View File

@ -0,0 +1,15 @@
export const COPY_INPUT_HTML: string = `
<div>
<div class="command-title">
{{headerTitle}}
</div>
<div>
<span>
<input type="text" class="command-input" size="{{inputSize}}" [(ngModel)]="defaultValue" #inputTarget readonly/>
</span>
<span>
<clr-icon shape="copy" [class.is-success]="isCopied" [class.is-error]="hasCopyError" class="info-tips-icon" size="24" [ngxClipboard]="inputTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)"></clr-icon>
</span>
</div>
</div>
`;

View File

@ -0,0 +1,11 @@
import { Type } from "@angular/core";
import { PushImageButtonComponent } from './push-image.component';
import { CopyInputComponent } from './copy-input.component';
export * from "./push-image.component";
export * from './copy-input.component';
export const PUSH_IMAGE_BUTTON_DIRECTIVES: Type<any>[] = [
CopyInputComponent,
PushImageButtonComponent
];

View File

@ -0,0 +1,64 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { PushImageButtonComponent } from './push-image.component';
import { CopyInputComponent } from './copy-input.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { SharedModule } from '../shared/shared.module';
import { click } from '../utils';
describe('PushImageButtonComponent (inline template)', () => {
let component: PushImageButtonComponent;
let fixture: ComponentFixture<PushImageButtonComponent>;
let serviceConfig: IServiceConfig;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [InlineAlertComponent, CopyInputComponent, PushImageButtonComponent],
providers: [
{ provide: SERVICE_CONFIG, useValue: {} }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(PushImageButtonComponent);
component = fixture.componentInstance;
component.projectName = "testing";
component.registryUrl = "https://testing.harbor.com"
serviceConfig = TestBed.get(SERVICE_CONFIG);
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should open the drop-down panel', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('button');
expect(el).not.toBeNull();
el.click();
fixture.detectChanges();
let copyInputs: HTMLInputElement[] = fixture.nativeElement.querySelectorAll('.command-input');
expect(copyInputs.length).toEqual(2);
expect(copyInputs[0].value.trim()).toEqual(`docker tag SOURCE_IMAGE[:TAG] ${component.registryUrl}/${component.projectName}/IMAGE[:TAG]`);
expect(copyInputs[1].value.trim()).toEqual(`docker push ${component.registryUrl}/${component.projectName}/IMAGE[:TAG]`);
});
}));
});

View File

@ -0,0 +1,51 @@
import { Component, Input, ViewChild } from '@angular/core';
import { CopyInputComponent } from './copy-input.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { PUSH_IMAGE_STYLE } from './push-image.css';
import { PUSH_IMAGE_HTML } from './push-image.html';
@Component({
selector: 'hbr-push-image-button',
template: PUSH_IMAGE_HTML,
styles: [PUSH_IMAGE_STYLE],
providers: []
})
export class PushImageButtonComponent {
@Input() registryUrl: string = "unknown";
@Input() projectName: string = "unknown";
@ViewChild("tagCopy") tagCopyInput: CopyInputComponent;
@ViewChild("pushCopy") pushCopyInput: CopyInputComponent;
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
public get tagCommand(): string {
return `docker tag SOURCE_IMAGE[:TAG] ${this.registryUrl}/${this.projectName}/IMAGE[:TAG]`;
}
public get pushCommand(): string {
return `docker push ${this.registryUrl}/${this.projectName}/IMAGE[:TAG]`;
}
onclick(): void {
if (this.tagCopyInput) {
this.tagCopyInput.reset();
}
if (this.pushCopyInput) {
this.pushCopyInput.reset();
}
if(this.copyAlert){
this.copyAlert.close();
}
}
onCpError($event: any): void {
if(this.copyAlert){
this.copyAlert.showInlineError("PUSH_IMAGE.COPY_ERROR");
}
}
}

View File

@ -0,0 +1,42 @@
export const PUSH_IMAGE_STYLE: string = `
.commands-container {
min-width: 360px;
max-width: 720px;
padding-left: 24px;
}
.h5-override {
display: inline-block !important;
margin-top: 0px !important;
margin-bottom: 12px;
}
.commands-section {
margin-top: 12px;
margin-bottom: 24px;
}
.info-tips-icon {
color: grey;
}
.info-tips-icon:hover {
color: #007CBB;
cursor: pointer;
}
.command-title {
font-size: 14px;
padding-left: 6px;
}
.command-input {
font-size: 14px;
font-weight: 500;
}
:host>>>.dropdown-menu {
min-width: 360px;
max-width: 720px;
}
`;

View File

@ -0,0 +1,34 @@
export const PUSH_IMAGE_HTML: string = `
<div>
<clr-dropdown [clrMenuPosition]="'bottom-right'">
<button class="btn btn-link" clrDropdownToggle (click)="onclick()">
{{ 'PUSH_IMAGE.TITLE' | translate | uppercase}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu" style="min-width:500px;">
<div class="commands-container">
<section>
<span><h5 class="h5-override">{{ 'PUSH_IMAGE.TITLE' | translate }}</h5></span>
<span>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{ 'PUSH_IMAGE.TOOLTIP' | translate }}</span>
</a>
</span>
</section>
<section>
<inline-alert #copyAlert></inline-alert>
</section>
<section>
<article class="commands-section">
<hbr-copy-input #tagCopy (onCopyError)="onCpError($event)" inputSize="50" headerTitle="{{ 'PUSH_IMAGE.TAG_COMMAND' | translate }}" defaultValue="{{tagCommand}}"></hbr-copy-input>
</article>
<article class="commands-section">
<hbr-copy-input #pushCopy (onCopyError)="onCpError($event)" inputSize="50" headerTitle="{{ 'PUSH_IMAGE.PUSH_COMMAND' | translate }}" defaultValue="{{pushCommand}}"></hbr-copy-input>
</article>
</section>
</div>
</div>
</clr-dropdown>
</div>
`;

View File

@ -3,7 +3,7 @@ export const REPLICATION_TEMPLATE: string = `
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left"> <div class="flex-xs-middle option-left">
<button *ngIf="withReplicationJob" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button> <button *ngIf="projectId" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule> <create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
</div> </div>
<div class="flex-xs-middle option-right"> <div class="flex-xs-middle option-right">

View File

@ -114,9 +114,6 @@ export class ReplicationComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
if(!this.projectId) {
this.errorHandler.warning('Project ID is unset.');
}
this.currentRuleStatus = this.ruleStatus[0]; this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0]; this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0; this.currentJobSearchOption = 0;

View File

@ -1,7 +1,6 @@
export const REPOSITORY_STACKVIEW_STYLES: string = ` export const REPOSITORY_STACKVIEW_STYLES: string = `
.option-right { .option-right {
padding-right: 16px; padding-right: 16px;
margin-top: 32px;
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@ -1,5 +1,4 @@
export const REPOSITORY_STYLE = `.option-right { export const REPOSITORY_STYLE = `.option-right {
padding-right: 16px; padding-right: 16px;
margin-top: 32px;
margin-bottom: 12px; margin-bottom: 12px;
}`; }`;

View File

@ -9,6 +9,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslatorJsonLoader } from '../i18n/local-json.loader'; import { TranslatorJsonLoader } from '../i18n/local-json.loader';
import { IServiceConfig, SERVICE_CONFIG } from '../service.config'; import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
import { CookieService, CookieModule } from 'ngx-cookie'; import { CookieService, CookieModule } from 'ngx-cookie';
import { ClipboardModule } from 'ngx-clipboard';
/*export function HttpLoaderFactory(http: Http) { /*export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json'); return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
@ -40,6 +41,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
CommonModule, CommonModule,
HttpModule, HttpModule,
FormsModule, FormsModule,
ClipboardModule,
CookieModule.forRoot(), CookieModule.forRoot(),
ClarityModule.forRoot(), ClarityModule.forRoot(),
TranslateModule.forRoot({ TranslateModule.forRoot({
@ -59,6 +61,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
HttpModule, HttpModule,
FormsModule, FormsModule,
CookieModule, CookieModule,
ClipboardModule,
ClarityModule, ClarityModule,
TranslateModule TranslateModule
], ],

View File

@ -1,4 +1,4 @@
<div class="global-message-alert" [hidden]="hideOuter"> <div [class.global-message-alert]="!isAppLevel">
<clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()"> <clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
<div class="alert-item"> <div class="alert-item">
<span class="alert-text"> <span class="alert-text">

View File

@ -33,8 +33,7 @@ export class MessageComponent implements OnInit, OnDestroy {
globalMessageOpened: boolean; globalMessageOpened: boolean;
messageText: string = ""; messageText: string = "";
timer: any = null; timer: any = null;
hideOuter: boolean = true;
appLevelMsgSub: Subscription; appLevelMsgSub: Subscription;
msgSub: Subscription; msgSub: Subscription;
clearSub: Subscription; clearSub: Subscription;
@ -79,8 +78,6 @@ export class MessageComponent implements OnInit, OnDestroy {
let hackDom: any = queryDoms[0]; let hackDom: any = queryDoms[0];
hackDom.className += ' alert-global alert-global-align'; hackDom.className += ' alert-global alert-global-align';
} }
this.hideOuter = false;
}, 0); }, 0);
} }
@ -138,6 +135,5 @@ export class MessageComponent implements OnInit, OnDestroy {
clearTimeout(this.timer); clearTimeout(this.timer);
} }
this.globalMessageOpened = false; this.globalMessageOpened = false;
this.hideOuter = true;
} }
} }

View File

@ -151,7 +151,7 @@ export class AuditLogComponent implements OnInit {
for(var i in this.filterOptions) { for(var i in this.filterOptions) {
let filterOption = this.filterOptions[i]; let filterOption = this.filterOptions[i];
if(filterOption.checked) { if(filterOption.checked) {
operationFilter.push(this.filterOptions[i].key); operationFilter.push('operation=' + this.filterOptions[i].key);
}else{ }else{
selectAll = false; selectAll = false;
} }
@ -159,7 +159,7 @@ export class AuditLogComponent implements OnInit {
if(selectAll) { if(selectAll) {
operationFilter = []; operationFilter = [];
} }
this.queryParam.keywords = operationFilter.join('/'); this.queryParam.keywords = operationFilter.join('&');
this.retrieve(); this.retrieve();
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http'; import { Http, Headers, RequestOptions, URLSearchParams } from '@angular/http';
import { AuditLog } from './audit-log'; import { AuditLog } from './audit-log';
@ -35,15 +35,24 @@ export class AuditLogService {
constructor(private http: Http) {} constructor(private http: Http) {}
listAuditLogs(queryParam: AuditLog): Observable<any> { listAuditLogs(queryParam: AuditLog): Observable<any> {
let params: URLSearchParams = new URLSearchParams(queryParam.keywords);
if(queryParam.begin_timestamp) {
params.set('begin_timestamp', <string>queryParam.begin_timestamp);
}
if(queryParam.end_timestamp) {
params.set('end_timestamp', <string>queryParam.end_timestamp);
}
if(queryParam.username) {
params.set('username', queryParam.username);
}
if(queryParam.page) {
params.set('page', <string>queryParam.page);
}
if(queryParam.page_size) {
params.set('page_size', <string>queryParam.page_size);
}
return this.http return this.http
.post(`/api/projects/${queryParam.project_id}/logs/filter?page=${queryParam.page}&page_size=${queryParam.page_size}`, { .get(`/api/projects/${queryParam.project_id}/logs`, {params: params})
begin_timestamp: queryParam.begin_timestamp,
end_timestamp: queryParam.end_timestamp,
keywords: queryParam.keywords,
operation: queryParam.operation,
project_id: queryParam.project_id,
username: queryParam.username
})
.map(response => response) .map(response => response)
.catch(error => Observable.throw(error)); .catch(error => Observable.throw(error));
} }

View File

@ -30,18 +30,18 @@
} }
*/ */
export class AuditLog { export class AuditLog {
log_id: number; log_id: number | string;
project_id: number; project_id: number | string;
username: string; username: string;
repo_name: string; repo_name: string;
repo_tag: string; repo_tag: string;
operation: string; operation: string;
op_time: Date; op_time: Date;
begin_timestamp: number = 0; begin_timestamp: number | string;
end_timestamp: number = 0; end_timestamp: number | string;
keywords: string; keywords: string;
page: number; page: number | string;
page_size: number; page_size: number | string;
fromTime: string; fromTime: string;
toTime: string; toTime: string;
} }

View File

@ -12,7 +12,7 @@
</span> </span>
</div> </div>
<div> <div>
<clr-datagrid> <clr-datagrid [clrDgLoading]="inProgress">
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column> <clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column> <clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column> <clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
@ -27,7 +27,10 @@
<clr-dg-cell>{{user.email}}</clr-dg-cell> <clr-dg-cell>{{user.email}}</clr-dg-cell>
<clr-dg-cell>{{user.creation_time | date: 'short'}}</clr-dg-cell> <clr-dg-cell>{{user.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer>{{users.length}} {{'USER.ITEMS' | translate}}</clr-dg-footer> <clr-dg-footer>{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} users
<clr-dg-pagination #pagination [clrDgPageSize]="15" [clrDgTotalItems]="users.length"> {{'USER.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid> </clr-datagrid>
</div> </div>
<new-user-modal (addNew)="addUserToList($event)"></new-user-modal> <new-user-modal (addNew)="addUserToList($event)"></new-user-modal>

View File

@ -38,10 +38,10 @@ import { AppConfigService } from '../app-config.service';
export class UserComponent implements OnInit, OnDestroy { export class UserComponent implements OnInit, OnDestroy {
users: User[] = []; users: User[] = [];
originalUsers: Promise<User[]>; originalUsers: Promise<User[]>;
onGoing: boolean = false; private onGoing: boolean = true;
adminMenuText: string = ""; private adminMenuText: string = "";
adminColumn: string = ""; private adminColumn: string = "";
deletionSubscription: Subscription; private deletionSubscription: Subscription;
currentTerm: string; currentTerm: string;

View File

@ -1,10 +1,10 @@
/* /*
* Harbor API * Harbor API
* *
* These APIs provide services for manipulating Harbor project. * These APIs provide services for manipulating Harbor project.
* *
* OpenAPI spec version: 0.3.0 * OpenAPI spec version: 0.3.0
* *
* Generated by: https://github.com/swagger-api/swagger-codegen.git * Generated by: https://github.com/swagger-api/swagger-codegen.git
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -22,17 +22,13 @@
package apilib package apilib
type AccessLogFilter struct { type LogQuery struct {
Username string `json:"username"`
// Relevant user's name that accessed this project. Repository string `json:"repository"`
Username string `json:"username,omitempty"` Tag string `json:"tag"`
Operation []string `json:"operation"`
// Operation name specified when project created. BeginTimestamp int64 `json:"begin_timestamp"`
Keywords string `json:"keywords,omitempty"` EndTimestamp int64 `json:"end_timestamp"`
Page int64 `json:"page"`
// Begin timestamp for querying access logs. PageSize int64 `json:"page_size"`
BeginTimestamp int64 `json:"begin_timestamp,omitempty"`
// End timestamp for querying accessl logs.
EndTimestamp int64 `json:"end_timestamp,omitempty"`
} }