Skip to content

Commit

Permalink
Add user login event to audit log (#21415)
Browse files Browse the repository at this point in the history
Add common event handler
  Register login event
  Update previous audit log event redirect to auditlogext table

Signed-off-by: stonezdj <[email protected]>
stonezdj authored Jan 23, 2025

Verified

This commit was signed with the committer’s verified signature.
sandhose Quentin Gliech
1 parent 39b2898 commit f808f33
Showing 16 changed files with 524 additions and 102 deletions.
18 changes: 9 additions & 9 deletions src/controller/event/handler/auditlog/auditlog.go
Original file line number Diff line number Diff line change
@@ -16,12 +16,14 @@ package auditlog

import (
"context"
"fmt"

"github.com/goharbor/harbor/src/controller/event"
evtModel "github.com/goharbor/harbor/src/controller/event/model"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/audit"
am "github.com/goharbor/harbor/src/pkg/audit/model"
"github.com/goharbor/harbor/src/pkg/auditext"
am "github.com/goharbor/harbor/src/pkg/auditext/model"
)

// Handler - audit log handler
@@ -30,7 +32,7 @@ type Handler struct {

// AuditResolver - interface to resolve to AuditLog
type AuditResolver interface {
ResolveToAuditLog() (*am.AuditLog, error)
ResolveToAuditLog() (*am.AuditLogExt, error)
}

// Name ...
@@ -40,13 +42,12 @@ func (h *Handler) Name() string {

// Handle ...
func (h *Handler) Handle(ctx context.Context, value interface{}) error {
var auditLog *am.AuditLog
var addAuditLog bool
switch v := value.(type) {
case *event.PushArtifactEvent, *event.DeleteArtifactEvent,
*event.DeleteRepositoryEvent, *event.CreateProjectEvent, *event.DeleteProjectEvent,
*event.DeleteTagEvent, *event.CreateTagEvent,
*event.CreateRobotEvent, *event.DeleteRobotEvent:
*event.CreateRobotEvent, *event.DeleteRobotEvent, *evtModel.CommonEvent:
addAuditLog = true
case *event.PullArtifactEvent:
addAuditLog = !config.PullAuditLogDisable(ctx)
@@ -56,14 +57,13 @@ func (h *Handler) Handle(ctx context.Context, value interface{}) error {

if addAuditLog {
resolver := value.(AuditResolver)
al, err := resolver.ResolveToAuditLog()
auditLog, err := resolver.ResolveToAuditLog()
if err != nil {
log.Errorf("failed to handler event %v", err)
return err
}
auditLog = al
if auditLog != nil {
_, err := audit.Mgr.Create(ctx, auditLog)
if auditLog != nil && config.AuditLogEventEnabled(ctx, fmt.Sprintf("%v_%v", auditLog.Operation, auditLog.ResourceType)) {
_, err := auditext.Mgr.Create(ctx, auditLog)
if err != nil {
log.Debugf("add audit log err: %v", err)
}
1 change: 1 addition & 0 deletions src/controller/event/handler/init.go
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ func init() {
_ = notifier.Subscribe(event.TopicDeleteTag, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicCreateRobot, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicDeleteRobot, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicCommonEvent, &auditlog.Handler{})

// internal
_ = notifier.Subscribe(event.TopicPullArtifact, &internal.ArtifactEventHandler{})
4 changes: 3 additions & 1 deletion src/controller/event/metadata/commonevent/model.go
Original file line number Diff line number Diff line change
@@ -62,8 +62,10 @@ type Metadata struct {
IPAddress string
// ResponseLocation response location
ResponseLocation string
// ResourceName
// ResourceName resource name
ResourceName string
// Payload request payload
Payload string
}

// Resolve parse the audit information from CommonEventMetadata
36 changes: 35 additions & 1 deletion src/controller/event/model/event.go
Original file line number Diff line number Diff line change
@@ -14,7 +14,12 @@

package model

import "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
import (
"time"

"github.com/goharbor/harbor/src/pkg/auditext/model"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
)

// Replication describes replication infos
type Replication struct {
@@ -80,3 +85,32 @@ type Scan struct {
// ScanType the scan type
ScanType string `json:"scan_type,omitempty"`
}

// CommonEvent ...
type CommonEvent struct {
Operator string
ProjectID int64
OcurrAt time.Time
Operation string
Payload string
SourceIP string
ResourceType string
ResourceName string
OperationDescription string
IsSuccessful bool
}

// ResolveToAuditLog ...
func (c *CommonEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: c.ProjectID,
OpTime: c.OcurrAt,
Operation: c.Operation,
Username: c.Operator,
ResourceType: c.ResourceType,
Resource: c.ResourceName,
OperationDescription: c.OperationDescription,
IsSuccessful: c.IsSuccessful,
}
return auditLog, nil
}
187 changes: 106 additions & 81 deletions src/controller/event/topic.go
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/lib/selector"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/audit/model"
"github.com/goharbor/harbor/src/pkg/auditext/model"
proModels "github.com/goharbor/harbor/src/pkg/project/models"
robotModel "github.com/goharbor/harbor/src/pkg/robot/model"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
@@ -43,13 +43,19 @@ const (
TopicScanningStopped = "SCANNING_STOPPED"
TopicScanningCompleted = "SCANNING_COMPLETED"
// QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85%
TopicQuotaWarning = "QUOTA_WARNING"
TopicQuotaExceed = "QUOTA_EXCEED"
TopicReplication = "REPLICATION"
TopicArtifactLabeled = "ARTIFACT_LABELED"
TopicTagRetention = "TAG_RETENTION"
TopicCreateRobot = "CREATE_ROBOT"
TopicDeleteRobot = "DELETE_ROBOT"
TopicQuotaWarning = "QUOTA_WARNING"
TopicQuotaExceed = "QUOTA_EXCEED"
TopicReplication = "REPLICATION"
TopicArtifactLabeled = "ARTIFACT_LABELED"
TopicTagRetention = "TAG_RETENTION"
TopicCreateRobot = "CREATE_ROBOT"
TopicDeleteRobot = "DELETE_ROBOT"
TopicCommonEvent = "COMMON_API"
ResourceTypeProject = "project"
ResourceTypeArtifact = "artifact"
ResourceTypeRepository = "repository"
ResourceTypeRobot = "robot"
ResourceTypeTag = "tag"
)

// CreateProjectEvent is the creating project event
@@ -62,14 +68,16 @@ type CreateProjectEvent struct {
}

// ResolveToAuditLog ...
func (c *CreateProjectEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: c.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: "project",
Resource: c.Project}
func (c *CreateProjectEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: c.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: ResourceTypeProject,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("create project: %s", c.Project),
Resource: c.Project}
return auditLog, nil
}

@@ -88,14 +96,16 @@ type DeleteProjectEvent struct {
}

// ResolveToAuditLog ...
func (d *DeleteProjectEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: d.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: "project",
Resource: d.Project}
func (d *DeleteProjectEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: d.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: ResourceTypeProject,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("delete project: %s", d.Project),
Resource: d.Project}
return auditLog, nil
}

@@ -114,14 +124,16 @@ type DeleteRepositoryEvent struct {
}

// ResolveToAuditLog ...
func (d *DeleteRepositoryEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: d.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: "repository",
Resource: d.Repository,
func (d *DeleteRepositoryEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: d.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: ResourceTypeRepository,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("delete repository: %s", d.Repository),
Resource: d.Repository,
}
return auditLog, nil
}
@@ -154,13 +166,15 @@ type PushArtifactEvent struct {
}

// ResolveToAuditLog ...
func (p *PushArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: p.Artifact.ProjectID,
OpTime: p.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: p.Operator,
ResourceType: "artifact"}
func (p *PushArtifactEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: p.Artifact.ProjectID,
OpTime: p.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: p.Operator,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("push artifact: %s@%s", p.Artifact.RepositoryName, p.Artifact.Digest),
ResourceType: ResourceTypeArtifact}

if len(p.Tags) == 0 {
auditLog.Resource = fmt.Sprintf("%s@%s",
@@ -183,13 +197,15 @@ type PullArtifactEvent struct {
}

// ResolveToAuditLog ...
func (p *PullArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: p.Artifact.ProjectID,
OpTime: p.OccurAt,
Operation: rbac.ActionPull.String(),
Username: p.Operator,
ResourceType: "artifact"}
func (p *PullArtifactEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: p.Artifact.ProjectID,
OpTime: p.OccurAt,
Operation: rbac.ActionPull.String(),
Username: p.Operator,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("pull artifact: %s@%s", p.Artifact.RepositoryName, p.Artifact.Digest),
ResourceType: ResourceTypeArtifact}

if len(p.Tags) == 0 {
auditLog.Resource = fmt.Sprintf("%s@%s",
@@ -219,14 +235,16 @@ type DeleteArtifactEvent struct {
}

// ResolveToAuditLog ...
func (d *DeleteArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: d.Artifact.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: "artifact",
Resource: fmt.Sprintf("%s@%s", d.Artifact.RepositoryName, d.Artifact.Digest)}
func (d *DeleteArtifactEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: d.Artifact.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: ResourceTypeArtifact,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("delete artifact: %s@%s", d.Artifact.RepositoryName, d.Artifact.Digest),
Resource: fmt.Sprintf("%s@%s", d.Artifact.RepositoryName, d.Artifact.Digest)}
return auditLog, nil
}

@@ -246,14 +264,16 @@ type CreateTagEvent struct {
}

// ResolveToAuditLog ...
func (c *CreateTagEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: c.AttachedArtifact.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: "tag",
Resource: fmt.Sprintf("%s:%s", c.Repository, c.Tag)}
func (c *CreateTagEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: c.AttachedArtifact.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: ResourceTypeTag,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("create tag: %s:%s", c.Repository, c.Tag),
Resource: fmt.Sprintf("%s:%s", c.Repository, c.Tag)}
return auditLog, nil
}

@@ -275,13 +295,14 @@ type DeleteTagEvent struct {
}

// ResolveToAuditLog ...
func (d *DeleteTagEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
func (d *DeleteTagEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: d.AttachedArtifact.ProjectID,
OpTime: d.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: d.Operator,
ResourceType: "tag",
ResourceType: ResourceTypeTag,
IsSuccessful: true,
Resource: fmt.Sprintf("%s:%s", d.Repository, d.Tag)}
return auditLog, nil
}
@@ -385,14 +406,16 @@ type CreateRobotEvent struct {
}

// ResolveToAuditLog ...
func (c *CreateRobotEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: c.Robot.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: "robot",
Resource: c.Robot.Name}
func (c *CreateRobotEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: c.Robot.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: ResourceTypeRobot,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("create robot: %s", c.Robot.Name),
Resource: c.Robot.Name}
return auditLog, nil
}

@@ -410,14 +433,16 @@ type DeleteRobotEvent struct {
}

// ResolveToAuditLog ...
func (c *DeleteRobotEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: c.Robot.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: c.Operator,
ResourceType: "robot",
Resource: c.Robot.Name}
func (c *DeleteRobotEvent) ResolveToAuditLog() (*model.AuditLogExt, error) {
auditLog := &model.AuditLogExt{
ProjectID: c.Robot.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: c.Operator,
ResourceType: ResourceTypeRobot,
IsSuccessful: true,
OperationDescription: fmt.Sprintf("delete robot: %s", c.Robot.Name),
Resource: c.Robot.Name}
return auditLog, nil
}

1 change: 1 addition & 0 deletions src/core/main.go
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ import (
_ "github.com/goharbor/harbor/src/pkg/accessory/model/sbom"
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
"github.com/goharbor/harbor/src/pkg/audit"
_ "github.com/goharbor/harbor/src/pkg/auditext/event/login"
dbCfg "github.com/goharbor/harbor/src/pkg/config/db"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/notification"
3 changes: 3 additions & 0 deletions src/lib/config/userconfig.go
Original file line number Diff line number Diff line change
@@ -264,6 +264,9 @@ func BannerMessage(ctx context.Context) string {

// AuditLogEventEnabled returns the audit log enabled setting for a specific event_type, such as delete_user, create_user
func AuditLogEventEnabled(ctx context.Context, eventType string) bool {
if DefaultMgr() == nil || DefaultMgr().Get(ctx, common.AuditLogEventsDisabled) == nil {
return true
}
disableListStr := DefaultMgr().Get(ctx, common.AuditLogEventsDisabled).GetString()
disableList := strings.Split(disableListStr, ",")
for _, t := range disableList {
12 changes: 6 additions & 6 deletions src/pkg/auditext/dao/dao_test.go
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ func (d *daoTestSuite) SetupSuite() {
Resource: "user01",
Username: "admin",
OperationDescription: "Create user",
OperationResult: true,
IsSuccessful: true,
OpTime: time.Now().AddDate(0, 0, -8),
})
d.Require().Nil(err)
@@ -148,11 +148,11 @@ func (d *daoTestSuite) TestListPIDs() {

func (d *daoTestSuite) TestCreate() {
audit := &model.AuditLogExt{
Operation: "create",
ResourceType: "user",
Resource: "user02",
OperationResult: true,
Username: "admin",
Operation: "create",
ResourceType: "user",
Resource: "user02",
IsSuccessful: true,
Username: "admin",
}
_, err := d.dao.Create(d.ctx, audit)
d.Require().Nil(err)
86 changes: 86 additions & 0 deletions src/pkg/auditext/event/login/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 login

import (
"context"
"fmt"
"net/http"
"regexp"
"time"

"github.com/goharbor/harbor/src/common/rbac"
ctlEvent "github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
"github.com/goharbor/harbor/src/controller/event/model"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/pkg/notifier/event"
)

func init() {
var login = &loginResolver{}
var logout = &logoutResolver{}
commonevent.RegisterResolver(`/c/login$`, login)
commonevent.RegisterResolver(`/c/log_out$`, logout)
}

const (
opLogout = "logout"
opLogin = "login"
logoutSuffix = "log_out"
payloadPattern = `principal=(.*?)&password`
)

type loginResolver struct {
}

func (l *loginResolver) Resolve(ce *commonevent.Metadata, event *event.Event) error {
e := &model.CommonEvent{
Operator: ce.Username,
ResourceType: rbac.ResourceUser.String(),
ResourceName: ce.Username,
OcurrAt: time.Now(),
Operation: opLogin,
OperationDescription: opLogin,
IsSuccessful: true,
}

// Extract the username from payload
re := regexp.MustCompile(payloadPattern)
if len(ce.RequestPayload) > 0 {
match := re.FindStringSubmatch(ce.RequestPayload)
if len(match) > 1 {
e.ResourceName = match[1]
e.Operator = match[1]
}
}
if ce.ResponseCode != http.StatusOK {
e.IsSuccessful = false
}
event.Topic = ctlEvent.TopicCommonEvent
event.Data = e
return nil
}

func (l *loginResolver) PreCheck(ctx context.Context, _ string, method string) (bool, string) {
operation := ""
if method == http.MethodPost {
operation = opLogin
}
if len(operation) == 0 {
return false, ""
}
return config.AuditLogEventEnabled(ctx, fmt.Sprintf("%v_%v", operation, rbac.ResourceUser.String())), ""
}
109 changes: 109 additions & 0 deletions src/pkg/auditext/event/login/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 login

import (
"context"
"testing"

"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
"github.com/goharbor/harbor/src/controller/event/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
)

func Test_login_Resolve(t *testing.T) {
type args struct {
ce *commonevent.Metadata
event *event.Event
}
tests := []struct {
name string
l *loginResolver
args args
wantErr bool
wantUsername string
wantOperation string
wantOperationDescription string
wantIsSuccessful bool
}{

{"test normal", &loginResolver{}, args{
ce: &commonevent.Metadata{
Username: "test",
RequestURL: "/c/login",
RequestMethod: "POST",
Payload: "principal=test&password=123456",
ResponseCode: 200,
}, event: &event.Event{}}, false, "test", "login", "login", true},
{"test fail", &loginResolver{}, args{
ce: &commonevent.Metadata{
Username: "test",
RequestURL: "/c/login",
RequestMethod: "POST",
Payload: "principal=test&password=123456",
ResponseCode: 401,
}, event: &event.Event{}}, false, "test", "login", "login", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &loginResolver{}
if err := l.Resolve(tt.args.ce, tt.args.event); (err != nil) != tt.wantErr {
t.Errorf("resolver.Resolve() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.args.event.Data.(*model.CommonEvent).Operator != tt.wantUsername {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).Operator, tt.wantUsername)
}
if tt.args.event.Data.(*model.CommonEvent).Operation != tt.wantOperation {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).Operation, tt.wantOperation)
}
if tt.args.event.Data.(*model.CommonEvent).OperationDescription != tt.wantOperationDescription {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).OperationDescription, tt.wantOperationDescription)
}
if tt.args.event.Data.(*model.CommonEvent).IsSuccessful != tt.wantIsSuccessful {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).IsSuccessful, tt.wantIsSuccessful)
}
})
}
}

func Test_login_PreCheck(t *testing.T) {
type args struct {
ctx context.Context
url string
method string
}
tests := []struct {
name string
e *loginResolver
args args
wantMatched bool
wantResourceName string
}{
{"test normal", &loginResolver{}, args{context.Background(), "/c/login", "POST"}, true, ""},
{"test fail method", &loginResolver{}, args{context.Background(), "/c/login", "PUT"}, false, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &loginResolver{}
got, got1 := e.PreCheck(tt.args.ctx, tt.args.url, tt.args.method)
if got != tt.wantMatched {
t.Errorf("resolver.PreCheck() got = %v, want %v", got, tt.wantMatched)
}
if got1 != tt.wantResourceName {
t.Errorf("resolver.PreCheck() got1 = %v, want %v", got1, tt.wantResourceName)
}
})
}
}
61 changes: 61 additions & 0 deletions src/pkg/auditext/event/login/logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 login

import (
"context"
"fmt"
"net/http"
"time"

"github.com/goharbor/harbor/src/common/rbac"
ctlEvent "github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
"github.com/goharbor/harbor/src/controller/event/model"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/pkg/notifier/event"
)

type logoutResolver struct {
}

func (l *logoutResolver) Resolve(ce *commonevent.Metadata, event *event.Event) error {
e := &model.CommonEvent{
Operator: ce.Username,
ResourceType: rbac.ResourceUser.String(),
ResourceName: ce.Username,
OcurrAt: time.Now(),
Operation: opLogout,
OperationDescription: opLogout,
IsSuccessful: true,
}
if ce.ResponseCode != http.StatusOK {
e.IsSuccessful = false
}
event.Topic = ctlEvent.TopicCommonEvent
event.Data = e
return nil
}

func (l *logoutResolver) PreCheck(ctx context.Context, _ string, method string) (bool, string) {
operation := ""
if method == http.MethodGet {
operation = opLogout
}
if len(operation) == 0 {
return false, ""
}
return config.AuditLogEventEnabled(ctx, fmt.Sprintf("%v_%v", operation, rbac.ResourceUser.String())), ""
}
100 changes: 100 additions & 0 deletions src/pkg/auditext/event/login/logout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 login

import (
"context"
"testing"

"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
"github.com/goharbor/harbor/src/controller/event/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
)

func Test_logout_Resolve(t *testing.T) {
type args struct {
ce *commonevent.Metadata
event *event.Event
}
tests := []struct {
name string
l *logoutResolver
args args
wantErr bool
wantUsername string
wantOperation string
wantOperationDescription string
wantIsSuccessful bool
}{
{"test logout", &logoutResolver{}, args{
ce: &commonevent.Metadata{
Username: "test",
RequestURL: "/c/log_out",
RequestMethod: "GET",
Payload: "",
ResponseCode: 200,
}, event: &event.Event{}}, false, "test", "logout", "logout", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &logoutResolver{}
if err := l.Resolve(tt.args.ce, tt.args.event); (err != nil) != tt.wantErr {
t.Errorf("resolver.Resolve() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.args.event.Data.(*model.CommonEvent).Operator != tt.wantUsername {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).Operator, tt.wantUsername)
}
if tt.args.event.Data.(*model.CommonEvent).Operation != tt.wantOperation {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).Operation, tt.wantOperation)
}
if tt.args.event.Data.(*model.CommonEvent).OperationDescription != tt.wantOperationDescription {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).OperationDescription, tt.wantOperationDescription)
}
if tt.args.event.Data.(*model.CommonEvent).IsSuccessful != tt.wantIsSuccessful {
t.Errorf("resolver.Resolve() got = %v, want %v", tt.args.event.Data.(*model.CommonEvent).IsSuccessful, tt.wantIsSuccessful)
}
})
}
}

func Test_logout_PreCheck(t *testing.T) {
type args struct {
ctx context.Context
url string
method string
}
tests := []struct {
name string
e *logoutResolver
args args
wantMatched bool
wantResourceName string
}{
{"test normal", &logoutResolver{}, args{context.Background(), "/c/log_out", "GET"}, true, ""},
{"test fail wrong url", &logoutResolver{}, args{context.Background(), "/c/logout", "DELETE"}, false, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &logoutResolver{}
got, got1 := e.PreCheck(tt.args.ctx, tt.args.url, tt.args.method)
if got != tt.wantMatched {
t.Errorf("resolver.PreCheck() got = %v, want %v", got, tt.wantMatched)
}
if got1 != tt.wantResourceName {
t.Errorf("resolver.PreCheck() got1 = %v, want %v", got1, tt.wantResourceName)
}
})
}
}
2 changes: 1 addition & 1 deletion src/pkg/auditext/model/model.go
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ type AuditLogExt struct {
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
Operation string `orm:"column(operation)" json:"operation"`
OperationDescription string `orm:"column(op_desc)" json:"operation_description"`
OperationResult bool `orm:"column(op_result)" json:"operation_result"`
IsSuccessful bool `orm:"column(op_result)" json:"is_successful"`
ResourceType string `orm:"column(resource_type)" json:"resource_type"`
Resource string `orm:"column(resource)" json:"resource"`
Username string `orm:"column(username)" json:"username"`
2 changes: 1 addition & 1 deletion src/server/v2.0/handler/auditlog.go
Original file line number Diff line number Diff line change
@@ -191,7 +191,7 @@ func convertToModelAuditLogExt(logs []*model.AuditLogExt) []*models.AuditLogExt
Username: log.Username,
Operation: log.Operation,
OperationDescription: log.OperationDescription,
OperationResult: log.OperationResult,
OperationResult: log.IsSuccessful,
OpTime: strfmt.DateTime(log.OpTime),
})
}
2 changes: 1 addition & 1 deletion tests/apitests/python/library/project.py
Original file line number Diff line number Diff line change
@@ -111,7 +111,7 @@ def delete_project(self, project_id, expect_status_code = 200, **kwargs):
base._assert_status_code(expect_status_code, status_code)

def get_project_log(self, project_name, expect_status_code = 200, **kwargs):
body, status_code, _ = self._get_client(**kwargs).get_logs_with_http_info(project_name)
body, status_code, _ = self._get_client(**kwargs).get_log_exts_with_http_info(project_name)
base._assert_status_code(expect_status_code, status_code)
return body

2 changes: 1 addition & 1 deletion tests/apitests/python/library/projectV2.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ def __init__(self):
super(ProjectV2,self).__init__(api_type = "projectv2")

def get_project_log(self, project_name, expect_status_code = 200, **kwargs):
body, status_code, _ = self._get_client(**kwargs).get_logs_with_http_info(project_name)
body, status_code, _ = self._get_client(**kwargs).get_log_exts_with_http_info(project_name)
base._assert_status_code(expect_status_code, status_code)
return body

0 comments on commit f808f33

Please sign in to comment.