diff --git a/src/.mockery.yaml b/src/.mockery.yaml index 7c5e85697..b5c945a72 100644 --- a/src/.mockery.yaml +++ b/src/.mockery.yaml @@ -462,6 +462,16 @@ packages: DAO: config: dir: testing/pkg/audit/dao + github.com/goharbor/harbor/src/pkg/auditext: + interfaces: + Manager: + config: + dir: testing/pkg/auditext + github.com/goharbor/harbor/src/pkg/auditext/dao: + interfaces: + DAO: + config: + dir: testing/pkg/auditext/dao github.com/goharbor/harbor/src/pkg/systemartifact: interfaces: Manager: diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go index 2a3a37289..61e816bee 100644 --- a/src/common/utils/utils.go +++ b/src/common/utils/utils.go @@ -337,3 +337,13 @@ func MostMatchSorter(a, b string, matchWord string) bool { func IsLocalPath(path string) bool { return len(path) == 0 || (strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")) } + +// StringInSlice check if the string is in the slice +func StringInSlice(str string, slice []string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +} diff --git a/src/jobservice/job/impl/gdpr/audit_logs_data_masking.go b/src/jobservice/job/impl/gdpr/audit_logs_data_masking.go index 2d6832e60..ad8ba29db 100644 --- a/src/jobservice/job/impl/gdpr/audit_logs_data_masking.go +++ b/src/jobservice/job/impl/gdpr/audit_logs_data_masking.go @@ -20,6 +20,7 @@ import ( "github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/pkg/audit" + "github.com/goharbor/harbor/src/pkg/auditext" "github.com/goharbor/harbor/src/pkg/user" ) @@ -27,6 +28,7 @@ const UserNameParam = "username" type AuditLogsDataMasking struct { manager audit.Manager + extManager auditext.Manager userManager user.Manager } @@ -58,6 +60,9 @@ func (a *AuditLogsDataMasking) init() { if a.userManager == nil { a.userManager = user.New() } + if a.extManager == nil { + a.extManager = auditext.Mgr + } } func (a AuditLogsDataMasking) Run(ctx job.Context, params job.Parameters) error { @@ -69,7 +74,11 @@ func (a AuditLogsDataMasking) Run(ctx job.Context, params job.Parameters) error return err } logger.Infof("Masking log entries for a user: %s", username) - return a.manager.UpdateUsername(ctx.SystemContext(), username, a.userManager.GenerateCheckSum(username)) + err = a.manager.UpdateUsername(ctx.SystemContext(), username, a.userManager.GenerateCheckSum(username)) + if err != nil { + return err + } + return a.extManager.UpdateUsername(ctx.SystemContext(), username, a.userManager.GenerateCheckSum(username)) } func (a AuditLogsDataMasking) parseParams(params job.Parameters) (string, error) { diff --git a/src/jobservice/job/impl/gdpr/audit_logs_data_masking_test.go b/src/jobservice/job/impl/gdpr/audit_logs_data_masking_test.go index d928a88cf..b062dfc8e 100644 --- a/src/jobservice/job/impl/gdpr/audit_logs_data_masking_test.go +++ b/src/jobservice/job/impl/gdpr/audit_logs_data_masking_test.go @@ -23,6 +23,7 @@ import ( "github.com/goharbor/harbor/src/jobservice/job" mockjobservice "github.com/goharbor/harbor/src/testing/jobservice" "github.com/goharbor/harbor/src/testing/pkg/audit" + "github.com/goharbor/harbor/src/testing/pkg/auditext" "github.com/goharbor/harbor/src/testing/pkg/user" ) @@ -35,12 +36,14 @@ func TestAuditLogsCleanupJobValidateParams(t *testing.T) { const validUsername = "user" var ( manager = &audit.Manager{} + extManger = &auditext.Manager{} userManager = &user.Manager{} ) rep := &AuditLogsDataMasking{ manager: manager, userManager: userManager, + extManager: extManger, } err := rep.Validate(nil) // parameters are required @@ -61,6 +64,7 @@ func TestAuditLogsCleanupJobValidateParams(t *testing.T) { ctx.On("GetLogger").Return(logger) userManager.On("GenerateCheckSum", validUsername).Return("hash") manager.On("UpdateUsername", context.TODO(), validUsername, "hash").Return(nil) + extManger.On("UpdateUsername", context.TODO(), validUsername, "hash").Return(nil) err = rep.Run(ctx, validParams) assert.Nil(t, err) diff --git a/src/pkg/auditext/dao/dao.go b/src/pkg/auditext/dao/dao.go new file mode 100644 index 000000000..dfee58e0e --- /dev/null +++ b/src/pkg/auditext/dao/dao.go @@ -0,0 +1,205 @@ +// 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 dao + +import ( + "context" + "strings" + + beegorm "github.com/beego/beego/v2/client/orm" + + "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/auditext/model" +) + +// DAO is the data access object for audit log +type DAO interface { + // Create the audit log ext + Create(ctx context.Context, access *model.AuditLogExt) (id int64, err error) + // Count returns the total count of audit log ext according to the query + Count(ctx context.Context, query *q.Query) (total int64, err error) + // List audit log ext according to the query + List(ctx context.Context, query *q.Query) (access []*model.AuditLogExt, err error) + // Get the audit log ext specified by ID + Get(ctx context.Context, id int64) (access *model.AuditLogExt, err error) + // Delete the audit log ext specified by ID + Delete(ctx context.Context, id int64) (err error) + // Purge the audit log ext + Purge(ctx context.Context, retentionHour int, includeOperations []string, dryRun bool) (int64, error) + // UpdateUsername replaces username in matched records + UpdateUsername(ctx context.Context, username string, usernameReplace string) error +} + +// New returns an instance of the default DAO +func New() DAO { + return &dao{} +} + +type dao struct{} + +func (d *dao) UpdateUsername(ctx context.Context, username string, usernameReplace string) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + _, err = o.Raw("UPDATE audit_log_ext SET username = ? WHERE username = ?", usernameReplace, username).Exec() + return err +} + +// Count ... +func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) { + qs, err := orm.QuerySetterForCount(ctx, &model.AuditLogExt{}, query) + if err != nil { + return 0, err + } + return qs.Count() +} + +// List ... +func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.AuditLogExt, error) { + audit := []*model.AuditLogExt{} + qs, err := orm.QuerySetter(ctx, &model.AuditLogExt{}, query) + if err != nil { + return nil, err + } + if _, err = qs.All(&audit); err != nil { + return nil, err + } + return audit, nil +} + +// Get ... +func (d *dao) Get(ctx context.Context, id int64) (*model.AuditLogExt, error) { + audit := &model.AuditLogExt{ + ID: id, + } + ormer, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + if err := ormer.Read(audit); err != nil { + if e := orm.AsNotFoundError(err, "audit %d not found", id); e != nil { + err = e + } + return nil, err + } + return audit, nil +} + +// Create ... +func (d *dao) Create(ctx context.Context, audit *model.AuditLogExt) (int64, error) { + ormer, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + // the max length of username in database is 255, replace the last + // three charaters with "..." if the length is greater than 256 + if len(audit.Username) > 255 { + audit.Username = audit.Username[:252] + "..." + } + id, err := ormer.Insert(audit) + if err != nil { + return 0, err + } + return id, err +} + +// Delete ... +func (d *dao) Delete(ctx context.Context, id int64) error { + ormer, err := orm.FromContext(ctx) + if err != nil { + return err + } + n, err := ormer.Delete(&model.AuditLogExt{ + ID: id, + }) + if err != nil { + return err + } + if n == 0 { + return errors.NotFoundError(nil).WithMessagef("access %d not found", id) + } + return nil +} + +// Purge delete expired audit log ext +func (*dao) Purge(ctx context.Context, retentionHour int, includeEventTypes []string, dryRun bool) (int64, error) { + ormer, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + if dryRun { + return dryRunPurge(ormer, retentionHour, includeEventTypes) + } + filterEvents := permitEventTypes(includeEventTypes) + if len(filterEvents) == 0 { + log.Infof("no operation selected, skip to purge audit log") + return 0, nil + } + sql := "DELETE FROM audit_log_ext WHERE op_time < NOW() - ? * interval '1 hour' AND lower(operation || '_' || resource_type) IN ('" + strings.Join(filterEvents, "','") + "')" + log.Debugf("purge audit logs raw sql: %v", sql) + + r, err := ormer.Raw(sql, retentionHour).Exec() + if err != nil { + log.Errorf("failed to purge audit log, error %v", err) + return 0, err + } + delRows, rErr := r.RowsAffected() + if rErr != nil { + log.Errorf("failed to purge audit log, error %v", rErr) + return 0, rErr + } + log.Infof("purged %d audit logs in the database", delRows) + + return delRows, err +} + +func dryRunPurge(ormer beegorm.QueryExecutor, retentionHour int, includeEventTypes []string) (int64, error) { + filterEvents := permitEventTypes(includeEventTypes) + if len(filterEvents) == 0 { + log.Infof("[DRYRUN]no operation selected, skip to purge audit log") + return 0, nil + } + sql := "SELECT count(1) cnt FROM audit_log_ext WHERE op_time < NOW() - ? * interval '1 hour' AND lower(operation || '_' || resource_type) IN ('" + strings.Join(filterEvents, "','") + "')" + log.Debugf("purge audit log count raw sql: %v", sql) + + var cnt int64 + err := ormer.Raw(sql, retentionHour).QueryRow(&cnt) + if err != nil { + log.Errorf("failed to dry run purge audit log, error %v", err) + return 0, err + } + log.Infof("[DRYRUN]purged %d audit logs in the database", cnt) + return cnt, nil +} + +// permitEventTypes filter not allowed event type, if no event types specified, purge no operation, use this function to avoid SQL injection +func permitEventTypes(includeEventTypes []string) []string { + if includeEventTypes == nil { + return nil + } + var filterEvents []string + for _, e := range includeEventTypes { + event := strings.ToLower(e) + if utils.StringInSlice(event, model.EventTypes) { + filterEvents = append(filterEvents, e) + } + } + return filterEvents +} diff --git a/src/pkg/auditext/dao/dao_test.go b/src/pkg/auditext/dao/dao_test.go new file mode 100644 index 000000000..6571f3521 --- /dev/null +++ b/src/pkg/auditext/dao/dao_test.go @@ -0,0 +1,188 @@ +// 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 dao + +import ( + "context" + "testing" + "time" + + beegoorm "github.com/beego/beego/v2/client/orm" + "github.com/stretchr/testify/suite" + + common_dao "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/auditext/model" +) + +type daoTestSuite struct { + suite.Suite + dao DAO + auditID int64 + ctx context.Context +} + +func (d *daoTestSuite) SetupSuite() { + d.dao = New() + common_dao.PrepareTestForPostgresSQL() + d.ctx = orm.NewContext(nil, beegoorm.NewOrm()) + artifactID, err := d.dao.Create(d.ctx, &model.AuditLogExt{ + Operation: "Create", + ResourceType: "user", + Resource: "user01", + Username: "admin", + OperationDescription: "Create user", + OperationResult: true, + OpTime: time.Now().AddDate(0, 0, -8), + }) + d.Require().Nil(err) + d.auditID = artifactID +} + +func (d *daoTestSuite) TearDownSuite() { + ormer, err := orm.FromContext(d.ctx) + d.Require().Nil(err) + _, err = ormer.Raw("delete from audit_log_ext").Exec() + d.Require().Nil(err) + +} + +func (d *daoTestSuite) TestList() { + // nil query + audits, err := d.dao.List(d.ctx, nil) + d.Require().Nil(err) + + // query by repository ID and name + audits, err = d.dao.List(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "Resource": "user01", + }, + }) + d.Require().Nil(err) + d.Require().Equal(1, len(audits)) + d.Equal("admin", audits[0].Username) +} + +func (d *daoTestSuite) TestGet() { + // get the non-exist tag + _, err := d.dao.Get(d.ctx, 10000) + d.Require().NotNil(err) + d.True(errors.IsErr(err, errors.NotFoundCode)) + + audit, err := d.dao.Get(d.ctx, d.auditID) + d.Require().Nil(err) + d.Require().NotNil(audit) + d.Equal(d.auditID, audit.ID) +} + +func (d *daoTestSuite) TestCount() { + total, err := d.dao.Count(d.ctx, nil) + d.Require().Nil(err) + d.True(total > 0) + total, err = d.dao.Count(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "Resource": "user01", + }, + }) + d.Require().Nil(err) + d.Equal(int64(1), total) +} + +func (d *daoTestSuite) TestListPIDs() { + // get the non-exist tag + id1, err := d.dao.Create(d.ctx, &model.AuditLogExt{ + Operation: "create", + ResourceType: "artifact", + Resource: "library/hello-world", + Username: "admin", + ProjectID: 11, + }) + d.Require().Nil(err) + id2, err := d.dao.Create(d.ctx, &model.AuditLogExt{ + Operation: "create", + ResourceType: "artifact", + Resource: "library/hello-world", + Username: "admin", + ProjectID: 12, + }) + d.Require().Nil(err) + id3, err := d.dao.Create(d.ctx, &model.AuditLogExt{ + Operation: "delete", + ResourceType: "artifact", + Resource: "library/hello-world", + Username: "admin", + ProjectID: 13, + }) + d.Require().Nil(err) + + // query by repository ID and name + ol := &q.OrList{} + for _, item := range []int64{11, 12, 13} { + ol.Values = append(ol.Values, item) + } + audits, err := d.dao.List(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "ProjectID": ol, + }, + }) + d.Require().Nil(err) + d.Require().Equal(3, len(audits)) + d.dao.Delete(d.ctx, id1) + d.dao.Delete(d.ctx, id2) + d.dao.Delete(d.ctx, id3) +} + +func (d *daoTestSuite) TestCreate() { + audit := &model.AuditLogExt{ + Operation: "create", + ResourceType: "user", + Resource: "user02", + OperationResult: true, + Username: "admin", + } + _, err := d.dao.Create(d.ctx, audit) + d.Require().Nil(err) +} + +func (d *daoTestSuite) TestPurge() { + // try to purge the audit log ext with the time range of 30 days, false + result, err := d.dao.Purge(d.ctx, 24*30, []string{"create_user"}, true) + d.Require().Nil(err) + d.Require().Equal(int64(0), result) + // try to purge the audit log ext with the time range of 7 days, true + result1, err := d.dao.Purge(d.ctx, 24*7, []string{"create_user"}, true) + d.Require().Nil(err) + d.Require().Equal(int64(1), result1) + +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &daoTestSuite{}) +} + +func TestPermitEventTypes(t *testing.T) { + // test permit event types + eventTypes := permitEventTypes([]string{"create_user", "delete_user", "delete_anything"}) + if len(eventTypes) != 2 { + t.Errorf("permitEventTypes failed") + } + eventTypes2 := permitEventTypes([]string{}) + if len(eventTypes2) != 0 { + t.Errorf("permitEventTypes failed") + } + +} diff --git a/src/pkg/auditext/manager.go b/src/pkg/auditext/manager.go new file mode 100644 index 000000000..7f2dc3fdb --- /dev/null +++ b/src/pkg/auditext/manager.go @@ -0,0 +1,99 @@ +// 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 auditext + +import ( + "context" + + "github.com/goharbor/harbor/src/lib/config" + "github.com/goharbor/harbor/src/lib/q" + auditV1 "github.com/goharbor/harbor/src/pkg/audit" + "github.com/goharbor/harbor/src/pkg/auditext/dao" + "github.com/goharbor/harbor/src/pkg/auditext/model" +) + +// Mgr is the global audit log manager instance +var Mgr = New() + +// Manager is used for audit log management +type Manager interface { + // Count returns the total count of audit logs according to the query + Count(ctx context.Context, query *q.Query) (total int64, err error) + // List audit logs according to the query + List(ctx context.Context, query *q.Query) (audits []*model.AuditLogExt, err error) + // Get the audit log specified by ID + Get(ctx context.Context, id int64) (audit *model.AuditLogExt, err error) + // Create the audit log + Create(ctx context.Context, audit *model.AuditLogExt) (id int64, err error) + // Delete the audit log specified by ID + Delete(ctx context.Context, id int64) (err error) + // Purge delete the audit log with retention hours + Purge(ctx context.Context, retentionHour int, includeOperations []string, dryRun bool) (int64, error) + // UpdateUsername Replace all log records username with its hash + UpdateUsername(ctx context.Context, username string, replaceWith string) error +} + +// New returns a default implementation of Manager +func New() Manager { + return &manager{ + dao: dao.New(), + } +} + +type manager struct { + dao dao.DAO +} + +func (m *manager) UpdateUsername(ctx context.Context, username string, replaceWith string) error { + return m.dao.UpdateUsername(ctx, username, replaceWith) +} + +// Count ... +func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) { + return m.dao.Count(ctx, query) +} + +// List ... +func (m *manager) List(ctx context.Context, query *q.Query) ([]*model.AuditLogExt, error) { + return m.dao.List(ctx, query) +} + +// Get ... +func (m *manager) Get(ctx context.Context, id int64) (*model.AuditLogExt, error) { + return m.dao.Get(ctx, id) +} + +// Create ... +func (m *manager) Create(ctx context.Context, audit *model.AuditLogExt) (int64, error) { + if len(config.AuditLogForwardEndpoint(ctx)) > 0 { + auditV1.LogMgr.DefaultLogger(ctx).WithField("operator", audit.Username). + WithField("time", audit.OpTime).WithField("resourceType", audit.ResourceType). + Infof("action:%s, resource:%s", audit.Operation, audit.Resource) + } + if config.SkipAuditLogDatabase(ctx) { + return 0, nil + } + return m.dao.Create(ctx, audit) +} + +// Purge ... +func (m *manager) Purge(ctx context.Context, retentionHour int, includeOperations []string, dryRun bool) (int64, error) { + return m.dao.Purge(ctx, retentionHour, includeOperations, dryRun) +} + +// Delete ... +func (m *manager) Delete(ctx context.Context, id int64) error { + return m.dao.Delete(ctx, id) +} diff --git a/src/pkg/auditext/manager_test.go b/src/pkg/auditext/manager_test.go new file mode 100644 index 000000000..e3438a71e --- /dev/null +++ b/src/pkg/auditext/manager_test.go @@ -0,0 +1,104 @@ +// 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 auditext + +import ( + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/pkg/auditext/model" + _ "github.com/goharbor/harbor/src/pkg/config/db" + mockDAO "github.com/goharbor/harbor/src/testing/pkg/auditext/dao" +) + +type managerTestSuite struct { + suite.Suite + mgr *manager + dao *mockDAO.DAO +} + +func (m *managerTestSuite) SetupTest() { + m.dao = &mockDAO.DAO{} + m.mgr = &manager{ + dao: m.dao, + } +} + +func (m *managerTestSuite) TestCount() { + m.dao.On("Count", mock.Anything, mock.Anything).Return(int64(1), nil) + total, err := m.mgr.Count(nil, nil) + m.Require().Nil(err) + m.Equal(int64(1), total) +} + +func (m *managerTestSuite) TestList() { + audit := &model.AuditLogExt{ + ProjectID: 1, + Resource: "library/hello-world", + ResourceType: "artifact", + } + m.dao.On("List", mock.Anything, mock.Anything).Return([]*model.AuditLogExt{audit}, nil) + auditLogs, err := m.mgr.List(nil, nil) + m.Require().Nil(err) + m.Equal(1, len(auditLogs)) + m.Equal(audit.Resource, auditLogs[0].Resource) +} + +func (m *managerTestSuite) TestGet() { + audit := &model.AuditLogExt{ + ProjectID: 1, + Resource: "library/hello-world", + ResourceType: "artifact", + } + m.dao.On("Get", mock.Anything, mock.Anything).Return(audit, nil) + au, err := m.mgr.Get(nil, 1) + m.Require().Nil(err) + m.dao.AssertExpectations(m.T()) + m.Require().NotNil(au) + m.Equal(audit.Resource, au.Resource) +} + +func (m *managerTestSuite) TestCreate() { + m.dao.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil) + id, err := m.mgr.Create(nil, &model.AuditLogExt{ + ProjectID: 1, + Resource: "library/hello-world", + ResourceType: "artifact", + }) + m.Require().Nil(err) + m.dao.AssertExpectations(m.T()) + m.Equal(int64(1), id) +} + +func (m *managerTestSuite) TestDelete() { + m.dao.On("Delete", mock.Anything, mock.Anything).Return(nil) + err := m.mgr.Delete(nil, 1) + m.Require().Nil(err) + m.dao.AssertExpectations(m.T()) +} + +func (m *managerTestSuite) TestPurge() { + m.dao.On("Purge", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) + total, err := m.mgr.Purge(nil, 1, nil, false) + m.Require().Nil(err) + m.Equal(int64(1), total) + m.dao.AssertExpectations(m.T()) +} + +func TestManager(t *testing.T) { + suite.Run(t, &managerTestSuite{}) +} diff --git a/src/pkg/auditext/model/model.go b/src/pkg/auditext/model/model.go new file mode 100644 index 000000000..e3e1077f5 --- /dev/null +++ b/src/pkg/auditext/model/model.go @@ -0,0 +1,62 @@ +// 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 model + +import ( + "time" + + beego_orm "github.com/beego/beego/v2/client/orm" +) + +func init() { + beego_orm.RegisterModel(&AuditLogExt{}) +} + +// AuditLogExt is the model for audit log ext +type AuditLogExt struct { + ID int64 `orm:"pk;auto;column(id)" json:"id"` + 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"` + ResourceType string `orm:"column(resource_type)" json:"resource_type"` + Resource string `orm:"column(resource)" json:"resource"` + Username string `orm:"column(username)" json:"username"` + OpTime time.Time `orm:"column(op_time)" json:"op_time" sort:"default:desc"` + Payload string `orm:"-" json:"payload"` +} + +// TableName for audit log +func (a *AuditLogExt) TableName() string { + return "audit_log_ext" +} + +// EventTypes defines the types of audit log event +var EventTypes = []string{ + "create_artifact", + "delete_artifact", + "pull_artifact", + "create_project", + "delete_project", + "delete_repository", + "login_user", + "logout_user", + "create_user", + "delete_user", + "update_user", + "create_robot", + "delete_robot", + "update_configure", +} diff --git a/src/testing/pkg/auditext/dao/dao.go b/src/testing/pkg/auditext/dao/dao.go new file mode 100644 index 000000000..44da05c35 --- /dev/null +++ b/src/testing/pkg/auditext/dao/dao.go @@ -0,0 +1,212 @@ +// Code generated by mockery v2.46.2. DO NOT EDIT. + +package dao + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + model "github.com/goharbor/harbor/src/pkg/auditext/model" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// DAO is an autogenerated mock type for the DAO type +type DAO struct { + mock.Mock +} + +// Count provides a mock function with given fields: ctx, query +func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for Count") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Create provides a mock function with given fields: ctx, access +func (_m *DAO) Create(ctx context.Context, access *model.AuditLogExt) (int64, error) { + ret := _m.Called(ctx, access) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *model.AuditLogExt) (int64, error)); ok { + return rf(ctx, access) + } + if rf, ok := ret.Get(0).(func(context.Context, *model.AuditLogExt) int64); ok { + r0 = rf(ctx, access) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *model.AuditLogExt) error); ok { + r1 = rf(ctx, access) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *DAO) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, id +func (_m *DAO) Get(ctx context.Context, id int64) (*model.AuditLogExt, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *model.AuditLogExt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*model.AuditLogExt, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *model.AuditLogExt); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AuditLogExt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, query +func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*model.AuditLogExt, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []*model.AuditLogExt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*model.AuditLogExt, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.AuditLogExt); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.AuditLogExt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Purge provides a mock function with given fields: ctx, retentionHour, includeOperations, dryRun +func (_m *DAO) Purge(ctx context.Context, retentionHour int, includeOperations []string, dryRun bool) (int64, error) { + ret := _m.Called(ctx, retentionHour, includeOperations, dryRun) + + if len(ret) == 0 { + panic("no return value specified for Purge") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int, []string, bool) (int64, error)); ok { + return rf(ctx, retentionHour, includeOperations, dryRun) + } + if rf, ok := ret.Get(0).(func(context.Context, int, []string, bool) int64); ok { + r0 = rf(ctx, retentionHour, includeOperations, dryRun) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int, []string, bool) error); ok { + r1 = rf(ctx, retentionHour, includeOperations, dryRun) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateUsername provides a mock function with given fields: ctx, username, usernameReplace +func (_m *DAO) UpdateUsername(ctx context.Context, username string, usernameReplace string) error { + ret := _m.Called(ctx, username, usernameReplace) + + if len(ret) == 0 { + panic("no return value specified for UpdateUsername") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, username, usernameReplace) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewDAO creates a new instance of DAO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDAO(t interface { + mock.TestingT + Cleanup(func()) +}) *DAO { + mock := &DAO{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/src/testing/pkg/auditext/manager.go b/src/testing/pkg/auditext/manager.go new file mode 100644 index 000000000..0600a0ac7 --- /dev/null +++ b/src/testing/pkg/auditext/manager.go @@ -0,0 +1,211 @@ +// Code generated by mockery v2.46.2. DO NOT EDIT. + +package auditext + +import ( + context "context" + + model "github.com/goharbor/harbor/src/pkg/auditext/model" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// Count provides a mock function with given fields: ctx, query +func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for Count") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Create provides a mock function with given fields: ctx, audit +func (_m *Manager) Create(ctx context.Context, audit *model.AuditLogExt) (int64, error) { + ret := _m.Called(ctx, audit) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *model.AuditLogExt) (int64, error)); ok { + return rf(ctx, audit) + } + if rf, ok := ret.Get(0).(func(context.Context, *model.AuditLogExt) int64); ok { + r0 = rf(ctx, audit) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *model.AuditLogExt) error); ok { + r1 = rf(ctx, audit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *Manager) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, id +func (_m *Manager) Get(ctx context.Context, id int64) (*model.AuditLogExt, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *model.AuditLogExt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*model.AuditLogExt, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *model.AuditLogExt); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AuditLogExt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, query +func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*model.AuditLogExt, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []*model.AuditLogExt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*model.AuditLogExt, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.AuditLogExt); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.AuditLogExt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Purge provides a mock function with given fields: ctx, retentionHour, includeOperations, dryRun +func (_m *Manager) Purge(ctx context.Context, retentionHour int, includeOperations []string, dryRun bool) (int64, error) { + ret := _m.Called(ctx, retentionHour, includeOperations, dryRun) + + if len(ret) == 0 { + panic("no return value specified for Purge") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int, []string, bool) (int64, error)); ok { + return rf(ctx, retentionHour, includeOperations, dryRun) + } + if rf, ok := ret.Get(0).(func(context.Context, int, []string, bool) int64); ok { + r0 = rf(ctx, retentionHour, includeOperations, dryRun) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int, []string, bool) error); ok { + r1 = rf(ctx, retentionHour, includeOperations, dryRun) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateUsername provides a mock function with given fields: ctx, username, replaceWith +func (_m *Manager) UpdateUsername(ctx context.Context, username string, replaceWith string) error { + ret := _m.Called(ctx, username, replaceWith) + + if len(ret) == 0 { + panic("no return value specified for UpdateUsername") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, username, replaceWith) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}