From 119e0756e589eb3a2ddda0b47cc3a3ee77b19a9b Mon Sep 17 00:00:00 2001 From: stonezdj Date: Tue, 14 Jan 2025 14:05:35 +0800 Subject: [PATCH] Add user login event to audit log Add common event handler Register login event Update previous audit log event redirect to auditlogext table Signed-off-by: stonezdj --- .../event/handler/auditlog/auditlog.go | 18 +- src/controller/event/handler/init.go | 1 + .../event/metadata/commonevent/model.go | 4 +- src/controller/event/model/event.go | 36 +++- src/controller/event/topic.go | 187 ++++++++++-------- src/core/main.go | 1 + src/lib/config/userconfig.go | 3 + src/pkg/auditext/dao/dao_test.go | 12 +- src/pkg/auditext/event/login/login.go | 86 ++++++++ src/pkg/auditext/event/login/login_test.go | 109 ++++++++++ src/pkg/auditext/event/login/logout.go | 61 ++++++ src/pkg/auditext/event/login/logout_test.go | 100 ++++++++++ src/pkg/auditext/model/model.go | 2 +- src/server/v2.0/handler/auditlog.go | 2 +- tests/apitests/python/library/project.py | 2 +- tests/apitests/python/library/projectV2.py | 2 +- 16 files changed, 524 insertions(+), 102 deletions(-) create mode 100644 src/pkg/auditext/event/login/login.go create mode 100644 src/pkg/auditext/event/login/login_test.go create mode 100644 src/pkg/auditext/event/login/logout.go create mode 100644 src/pkg/auditext/event/login/logout_test.go diff --git a/src/controller/event/handler/auditlog/auditlog.go b/src/controller/event/handler/auditlog/auditlog.go index ef7cdbdda56..7451840cb35 100644 --- a/src/controller/event/handler/auditlog/auditlog.go +++ b/src/controller/event/handler/auditlog/auditlog.go @@ -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) } diff --git a/src/controller/event/handler/init.go b/src/controller/event/handler/init.go index 841847a5ccf..ecfc527cc35 100644 --- a/src/controller/event/handler/init.go +++ b/src/controller/event/handler/init.go @@ -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{}) diff --git a/src/controller/event/metadata/commonevent/model.go b/src/controller/event/metadata/commonevent/model.go index 8a92544fdaf..ac12bb3c03d 100644 --- a/src/controller/event/metadata/commonevent/model.go +++ b/src/controller/event/metadata/commonevent/model.go @@ -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 diff --git a/src/controller/event/model/event.go b/src/controller/event/model/event.go index 2e7021bc36d..023bc8205e3 100644 --- a/src/controller/event/model/event.go +++ b/src/controller/event/model/event.go @@ -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 +} diff --git a/src/controller/event/topic.go b/src/controller/event/topic.go index f8f64b42060..9fd88b01b6a 100644 --- a/src/controller/event/topic.go +++ b/src/controller/event/topic.go @@ -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 } diff --git a/src/core/main.go b/src/core/main.go index f0bc9656452..cec2c4f05c0 100644 --- a/src/core/main.go +++ b/src/core/main.go @@ -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" diff --git a/src/lib/config/userconfig.go b/src/lib/config/userconfig.go index 1937b3b7e95..b9fa3e45de6 100644 --- a/src/lib/config/userconfig.go +++ b/src/lib/config/userconfig.go @@ -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 { diff --git a/src/pkg/auditext/dao/dao_test.go b/src/pkg/auditext/dao/dao_test.go index 6571f352136..86a3d361912 100644 --- a/src/pkg/auditext/dao/dao_test.go +++ b/src/pkg/auditext/dao/dao_test.go @@ -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) diff --git a/src/pkg/auditext/event/login/login.go b/src/pkg/auditext/event/login/login.go new file mode 100644 index 00000000000..bea0009e4e7 --- /dev/null +++ b/src/pkg/auditext/event/login/login.go @@ -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())), "" +} diff --git a/src/pkg/auditext/event/login/login_test.go b/src/pkg/auditext/event/login/login_test.go new file mode 100644 index 00000000000..41e7cd5c5d2 --- /dev/null +++ b/src/pkg/auditext/event/login/login_test.go @@ -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) + } + }) + } +} diff --git a/src/pkg/auditext/event/login/logout.go b/src/pkg/auditext/event/login/logout.go new file mode 100644 index 00000000000..1764dc6c4db --- /dev/null +++ b/src/pkg/auditext/event/login/logout.go @@ -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())), "" +} diff --git a/src/pkg/auditext/event/login/logout_test.go b/src/pkg/auditext/event/login/logout_test.go new file mode 100644 index 00000000000..0e84b5ad47e --- /dev/null +++ b/src/pkg/auditext/event/login/logout_test.go @@ -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) + } + }) + } +} diff --git a/src/pkg/auditext/model/model.go b/src/pkg/auditext/model/model.go index e3e1077f5c9..12a3f04f318 100644 --- a/src/pkg/auditext/model/model.go +++ b/src/pkg/auditext/model/model.go @@ -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"` diff --git a/src/server/v2.0/handler/auditlog.go b/src/server/v2.0/handler/auditlog.go index e0e2d6baa27..3e149040c16 100644 --- a/src/server/v2.0/handler/auditlog.go +++ b/src/server/v2.0/handler/auditlog.go @@ -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), }) } diff --git a/tests/apitests/python/library/project.py b/tests/apitests/python/library/project.py index a5342663a1c..b4a7669364a 100644 --- a/tests/apitests/python/library/project.py +++ b/tests/apitests/python/library/project.py @@ -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 diff --git a/tests/apitests/python/library/projectV2.py b/tests/apitests/python/library/projectV2.py index 446a08ae3c2..e3d9ab35b3d 100644 --- a/tests/apitests/python/library/projectV2.py +++ b/tests/apitests/python/library/projectV2.py @@ -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