diff --git a/src/controller/event/handler/auditlog/auditlog.go b/src/controller/event/handler/auditlog/auditlog.go index 6c6b5c3e9..ef7cdbdda 100644 --- a/src/controller/event/handler/auditlog/auditlog.go +++ b/src/controller/event/handler/auditlog/auditlog.go @@ -45,7 +45,8 @@ func (h *Handler) Handle(ctx context.Context, value interface{}) error { switch v := value.(type) { case *event.PushArtifactEvent, *event.DeleteArtifactEvent, *event.DeleteRepositoryEvent, *event.CreateProjectEvent, *event.DeleteProjectEvent, - *event.DeleteTagEvent, *event.CreateTagEvent: + *event.DeleteTagEvent, *event.CreateTagEvent, + *event.CreateRobotEvent, *event.DeleteRobotEvent: addAuditLog = true case *event.PullArtifactEvent: addAuditLog = !config.PullAuditLogDisable(ctx) diff --git a/src/controller/event/handler/init.go b/src/controller/event/handler/init.go index aa28103da..841847a5c 100644 --- a/src/controller/event/handler/init.go +++ b/src/controller/event/handler/init.go @@ -65,6 +65,8 @@ func init() { _ = notifier.Subscribe(event.TopicDeleteRepository, &auditlog.Handler{}) _ = notifier.Subscribe(event.TopicCreateTag, &auditlog.Handler{}) _ = notifier.Subscribe(event.TopicDeleteTag, &auditlog.Handler{}) + _ = notifier.Subscribe(event.TopicCreateRobot, &auditlog.Handler{}) + _ = notifier.Subscribe(event.TopicDeleteRobot, &auditlog.Handler{}) // internal _ = notifier.Subscribe(event.TopicPullArtifact, &internal.ArtifactEventHandler{}) diff --git a/src/controller/event/metadata/robot.go b/src/controller/event/metadata/robot.go new file mode 100644 index 000000000..a4b325b34 --- /dev/null +++ b/src/controller/event/metadata/robot.go @@ -0,0 +1,73 @@ +// 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 metadata + +import ( + "context" + "fmt" + "time" + + "github.com/goharbor/harbor/src/common/security" + event2 "github.com/goharbor/harbor/src/controller/event" + "github.com/goharbor/harbor/src/lib/config" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/goharbor/harbor/src/pkg/robot/model" +) + +// CreateRobotEventMetadata is the metadata from which the create robot event can be resolved +type CreateRobotEventMetadata struct { + Ctx context.Context + Robot *model.Robot +} + +// Resolve to the event from the metadata +func (c *CreateRobotEventMetadata) Resolve(event *event.Event) error { + data := &event2.CreateRobotEvent{ + EventType: event2.TopicCreateRobot, + Robot: c.Robot, + OccurAt: time.Now(), + } + cx, exist := security.FromContext(c.Ctx) + if exist { + data.Operator = cx.GetUsername() + } + data.Robot.Name = fmt.Sprintf("%s%s", config.RobotPrefix(c.Ctx), data.Robot.Name) + event.Topic = event2.TopicCreateRobot + event.Data = data + return nil +} + +// DeleteRobotEventMetadata is the metadata from which the delete robot event can be resolved +type DeleteRobotEventMetadata struct { + Ctx context.Context + Robot *model.Robot +} + +// Resolve to the event from the metadata +func (d *DeleteRobotEventMetadata) Resolve(event *event.Event) error { + data := &event2.DeleteRobotEvent{ + EventType: event2.TopicDeleteRobot, + Robot: d.Robot, + OccurAt: time.Now(), + } + cx, exist := security.FromContext(d.Ctx) + if exist { + data.Operator = cx.GetUsername() + } + data.Robot.Name = fmt.Sprintf("%s%s", config.RobotPrefix(d.Ctx), data.Robot.Name) + event.Topic = event2.TopicDeleteRobot + event.Data = data + return nil +} diff --git a/src/controller/event/metadata/robot_test.go b/src/controller/event/metadata/robot_test.go new file mode 100644 index 000000000..b7a2be3d7 --- /dev/null +++ b/src/controller/event/metadata/robot_test.go @@ -0,0 +1,83 @@ +// 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 metadata + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/common" + event2 "github.com/goharbor/harbor/src/controller/event" + "github.com/goharbor/harbor/src/lib/config" + _ "github.com/goharbor/harbor/src/pkg/config/inmemory" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/goharbor/harbor/src/pkg/robot/model" +) + +type robotEventTestSuite struct { + suite.Suite +} + +func (t *tagEventTestSuite) TestResolveOfCreateRobotEventMetadata() { + cfg := map[string]interface{}{ + common.RobotPrefix: "robot$", + } + config.InitWithSettings(cfg) + + e := &event.Event{} + metadata := &CreateRobotEventMetadata{ + Ctx: context.Background(), + Robot: &model.Robot{ + ID: 1, + Name: "test", + }, + } + err := metadata.Resolve(e) + t.Require().Nil(err) + t.Equal(event2.TopicCreateRobot, e.Topic) + t.Require().NotNil(e.Data) + data, ok := e.Data.(*event2.CreateRobotEvent) + t.Require().True(ok) + t.Equal(int64(1), data.Robot.ID) + t.Equal("robot$test", data.Robot.Name) +} + +func (t *tagEventTestSuite) TestResolveOfDeleteRobotEventMetadata() { + cfg := map[string]interface{}{ + common.RobotPrefix: "robot$", + } + config.InitWithSettings(cfg) + + e := &event.Event{} + metadata := &DeleteRobotEventMetadata{ + Ctx: context.Background(), + Robot: &model.Robot{ + ID: 1, + }, + } + err := metadata.Resolve(e) + t.Require().Nil(err) + t.Equal(event2.TopicDeleteRobot, e.Topic) + t.Require().NotNil(e.Data) + data, ok := e.Data.(*event2.DeleteRobotEvent) + t.Require().True(ok) + t.Equal(int64(1), data.Robot.ID) +} + +func TestRobotEventTestSuite(t *testing.T) { + suite.Run(t, &robotEventTestSuite{}) +} diff --git a/src/controller/event/topic.go b/src/controller/event/topic.go index 08e133e1a..60dd8107a 100644 --- a/src/controller/event/topic.go +++ b/src/controller/event/topic.go @@ -23,6 +23,7 @@ import ( "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/audit/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" ) @@ -47,6 +48,8 @@ const ( TopicReplication = "REPLICATION" TopicArtifactLabeled = "ARTIFACT_LABELED" TopicTagRetention = "TAG_RETENTION" + TopicCreateRobot = "CREATE_ROBOT" + TopicDeleteRobot = "DELETE_ROBOT" ) // CreateProjectEvent is the creating project event @@ -369,3 +372,53 @@ func (r *RetentionEvent) String() string { return fmt.Sprintf("TaskID-%d Status-%s Deleted-%s OccurAt-%s", r.TaskID, r.Status, candidates, r.OccurAt.Format("2006-01-02 15:04:05")) } + +// CreateRobotEvent is the creating robot event +type CreateRobotEvent struct { + EventType string + Robot *robotModel.Robot + Operator string + OccurAt time.Time +} + +// 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} + return auditLog, nil +} + +func (c *CreateRobotEvent) String() string { + return fmt.Sprintf("Name-%s Operator-%s OccurAt-%s", + c.Robot.Name, c.Operator, c.OccurAt.Format("2006-01-02 15:04:05")) +} + +// DeleteRobotEvent is the deleting robot event +type DeleteRobotEvent struct { + EventType string + Robot *robotModel.Robot + Operator string + OccurAt time.Time +} + +// 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} + return auditLog, nil +} + +func (c *DeleteRobotEvent) String() string { + return fmt.Sprintf("Name-%s Operator-%s OccurAt-%s", + c.Robot.Name, c.Operator, c.OccurAt.Format("2006-01-02 15:04:05")) +} diff --git a/src/controller/robot/controller.go b/src/controller/robot/controller.go index 28eef53e4..f0073810f 100644 --- a/src/controller/robot/controller.go +++ b/src/controller/robot/controller.go @@ -23,12 +23,14 @@ import ( rbac_project "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/controller/event/metadata" "github.com/goharbor/harbor/src/lib/config" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/retry" "github.com/goharbor/harbor/src/pkg" + "github.com/goharbor/harbor/src/pkg/notification" "github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/project" "github.com/goharbor/harbor/src/pkg/rbac" @@ -121,7 +123,7 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error if r.Level == LEVELPROJECT { name = fmt.Sprintf("%s+%s", r.ProjectName, r.Name) } - robotID, err := d.robotMgr.Create(ctx, &model.Robot{ + rCreate := &model.Robot{ Name: name, Description: r.Description, ProjectID: r.ProjectID, @@ -130,7 +132,8 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error Duration: r.Duration, Salt: salt, Visible: r.Visible, - }) + } + robotID, err := d.robotMgr.Create(ctx, rCreate) if err != nil { return 0, "", err } @@ -138,17 +141,31 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error if err := d.createPermission(ctx, r); err != nil { return 0, "", err } + // fire event + notification.AddEvent(ctx, &metadata.CreateRobotEventMetadata{ + Ctx: ctx, + Robot: rCreate, + }) return robotID, pwd, nil } // Delete ... func (d *controller) Delete(ctx context.Context, id int64) error { + rDelete, err := d.robotMgr.Get(ctx, id) + if err != nil { + return err + } if err := d.robotMgr.Delete(ctx, id); err != nil { return err } if err := d.rbacMgr.DeletePermissionsByRole(ctx, ROBOTTYPE, id); err != nil { return err } + // fire event + notification.AddEvent(ctx, &metadata.DeleteRobotEventMetadata{ + Ctx: ctx, + Robot: rDelete, + }) return nil } diff --git a/src/controller/robot/controller_test.go b/src/controller/robot/controller_test.go index 2a97d0592..4bb1b4cfc 100644 --- a/src/controller/robot/controller_test.go +++ b/src/controller/robot/controller_test.go @@ -145,6 +145,12 @@ func (suite *ControllerTestSuite) TestDelete() { c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr} ctx := context.TODO() + robotMgr.On("Get", mock.Anything, mock.Anything).Return(&model.Robot{ + Name: "library+test", + Description: "test get method", + ProjectID: 1, + Secret: utils.RandStringBytes(10), + }, nil) robotMgr.On("Delete", mock.Anything, mock.Anything).Return(nil) rbacMgr.On("DeletePermissionsByRole", mock.Anything, mock.Anything, mock.Anything).Return(nil)