From 848a9afda68ae9fcdb4036d50106ce19ab68bbec Mon Sep 17 00:00:00 2001 From: Tosone Date: Sat, 22 Jul 2023 12:46:33 +0800 Subject: [PATCH] :card_file_box: Add audit table --- pkg/dal/cmd/gen.go | 3 +- pkg/dal/dao/audit.go | 94 ++++ pkg/dal/dao/mocks/audit.go | 50 ++ pkg/dal/dao/mocks/audit_factory.go | 54 ++ pkg/dal/dao/namespace.go | 8 +- pkg/dal/dao/repository.go | 8 +- .../migrations/mysql/0001_initialize.up.sql | 14 + .../postgresql/0001_initialize.up.sql | 28 + .../migrations/sqlite3/0001_initialize.up.sql | 14 + pkg/dal/models/audit.go | 40 ++ pkg/dal/query/audits.gen.go | 516 ++++++++++++++++++ pkg/dal/query/gen.go | 8 + pkg/handlers/namespaces/handler.go | 7 + pkg/handlers/namespaces/namespaces_delete.go | 53 +- .../namespaces/namespaces_delete_test.go | 9 +- pkg/handlers/namespaces/namespaces_post.go | 13 + pkg/handlers/namespaces/namespaces_put.go | 39 +- .../namespaces/namespaces_put_test.go | 31 +- pkg/types/enums/enums.go | 16 + pkg/types/enums/enums_enum.go | 181 ++++++ web/package.json | 8 +- web/src/components/OrderHeader/index.tsx | 4 +- web/src/components/QuotaSimple/index.tsx | 2 +- web/src/pages/Namespace/TableItem.tsx | 8 +- web/yarn.lock | 59 +- 25 files changed, 1212 insertions(+), 55 deletions(-) create mode 100644 pkg/dal/dao/audit.go create mode 100644 pkg/dal/dao/mocks/audit.go create mode 100644 pkg/dal/dao/mocks/audit_factory.go create mode 100644 pkg/dal/models/audit.go create mode 100644 pkg/dal/query/audits.gen.go diff --git a/pkg/dal/cmd/gen.go b/pkg/dal/cmd/gen.go index 0a193ab4..c338c8f0 100644 --- a/pkg/dal/cmd/gen.go +++ b/pkg/dal/cmd/gen.go @@ -28,6 +28,8 @@ func main() { }) g.ApplyBasic( + models.User{}, + models.Audit{}, models.Namespace{}, models.Repository{}, models.Artifact{}, @@ -36,7 +38,6 @@ func main() { models.Tag{}, models.Blob{}, models.BlobUpload{}, - models.User{}, models.CasbinRule{}, ) diff --git a/pkg/dal/dao/audit.go b/pkg/dal/dao/audit.go new file mode 100644 index 00000000..b851fca1 --- /dev/null +++ b/pkg/dal/dao/audit.go @@ -0,0 +1,94 @@ +// Copyright 2023 sigma +// +// 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" + "time" + + "github.com/go-sigma/sigma/pkg/dal/models" + "github.com/go-sigma/sigma/pkg/dal/query" + "github.com/go-sigma/sigma/pkg/types/enums" +) + +//go:generate mockgen -destination=mocks/audit.go -package=mocks github.com/go-sigma/sigma/pkg/dal/dao AuditService +//go:generate mockgen -destination=mocks/audit_factory.go -package=mocks github.com/go-sigma/sigma/pkg/dal/dao AuditServiceFactory + +// AuditService is the interface that provides methods to operate on Audit model +type AuditService interface { + // Create creates a new Audit record in the database + Create(ctx context.Context, audit *models.Audit) error + // HotNamespace get top n hot namespace by user id + HotNamespace(ctx context.Context, userID int64, top int) ([]*models.Namespace, error) +} + +type auditService struct { + tx *query.Query +} + +// AuditServiceFactory is the interface that provides the audit service factory methods. +type AuditServiceFactory interface { + New(txs ...*query.Query) AuditService +} + +type auditServiceFactory struct{} + +// NewAuditServiceFactory creates a new audit service factory. +func NewAuditServiceFactory() AuditServiceFactory { + return &auditServiceFactory{} +} + +func (f *auditServiceFactory) New(txs ...*query.Query) AuditService { + tx := query.Q + if len(txs) > 0 { + tx = txs[0] + } + return &auditService{ + tx: tx, + } +} + +// Create create a new artifact if conflict do nothing. +func (s *auditService) Create(ctx context.Context, audit *models.Audit) error { + return s.tx.Audit.WithContext(ctx).Create(audit) +} + +// HotNamespace get top n hot namespace by user id +func (s *auditService) HotNamespace(ctx context.Context, userID int64, top int) ([]*models.Namespace, error) { + type result struct { + NamespaceID int64 + CreatedAt time.Time + Count int64 + } + var rs []result + err := s.tx.Audit.WithContext(ctx). + Where(s.tx.Audit.Action.Neq(enums.AuditActionDelete), s.tx.Audit.UserID.Eq(userID)). + Group(s.tx.Audit.NamespaceID). + Select(s.tx.Audit.NamespaceID, s.tx.Audit.CreatedAt.Max().As(s.tx.Audit.CreatedAt.ColumnName().String()), s.tx.Audit.ID.Count().As("count")). + Limit(top). + UnderlyingDB(). + Order("count desc, created_at desc").Find(&rs).Error + if err != nil { + return nil, err + } + if len(rs) == 0 { + return nil, nil + } + var namespaceIDs = make([]int64, 0, len(rs)) + for _, audit := range rs { + namespaceIDs = append(namespaceIDs, audit.NamespaceID) + } + return s.tx.Namespace.WithContext(ctx).Where(s.tx.Namespace.ID.In(namespaceIDs...)).Find() +} diff --git a/pkg/dal/dao/mocks/audit.go b/pkg/dal/dao/mocks/audit.go new file mode 100644 index 00000000..9b0bb731 --- /dev/null +++ b/pkg/dal/dao/mocks/audit.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/go-sigma/sigma/pkg/dal/dao (interfaces: AuditService) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + models "github.com/go-sigma/sigma/pkg/dal/models" + gomock "go.uber.org/mock/gomock" +) + +// MockAuditService is a mock of AuditService interface. +type MockAuditService struct { + ctrl *gomock.Controller + recorder *MockAuditServiceMockRecorder +} + +// MockAuditServiceMockRecorder is the mock recorder for MockAuditService. +type MockAuditServiceMockRecorder struct { + mock *MockAuditService +} + +// NewMockAuditService creates a new mock instance. +func NewMockAuditService(ctrl *gomock.Controller) *MockAuditService { + mock := &MockAuditService{ctrl: ctrl} + mock.recorder = &MockAuditServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuditService) EXPECT() *MockAuditServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockAuditService) Create(arg0 context.Context, arg1 *models.Audit) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockAuditServiceMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAuditService)(nil).Create), arg0, arg1) +} diff --git a/pkg/dal/dao/mocks/audit_factory.go b/pkg/dal/dao/mocks/audit_factory.go new file mode 100644 index 00000000..f2b1bf5c --- /dev/null +++ b/pkg/dal/dao/mocks/audit_factory.go @@ -0,0 +1,54 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/go-sigma/sigma/pkg/dal/dao (interfaces: AuditServiceFactory) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + dao "github.com/go-sigma/sigma/pkg/dal/dao" + query "github.com/go-sigma/sigma/pkg/dal/query" + gomock "go.uber.org/mock/gomock" +) + +// MockAuditServiceFactory is a mock of AuditServiceFactory interface. +type MockAuditServiceFactory struct { + ctrl *gomock.Controller + recorder *MockAuditServiceFactoryMockRecorder +} + +// MockAuditServiceFactoryMockRecorder is the mock recorder for MockAuditServiceFactory. +type MockAuditServiceFactoryMockRecorder struct { + mock *MockAuditServiceFactory +} + +// NewMockAuditServiceFactory creates a new mock instance. +func NewMockAuditServiceFactory(ctrl *gomock.Controller) *MockAuditServiceFactory { + mock := &MockAuditServiceFactory{ctrl: ctrl} + mock.recorder = &MockAuditServiceFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuditServiceFactory) EXPECT() *MockAuditServiceFactoryMockRecorder { + return m.recorder +} + +// New mocks base method. +func (m *MockAuditServiceFactory) New(arg0 ...*query.Query) dao.AuditService { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "New", varargs...) + ret0, _ := ret[0].(dao.AuditService) + return ret0 +} + +// New indicates an expected call of New. +func (mr *MockAuditServiceFactoryMockRecorder) New(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockAuditServiceFactory)(nil).New), arg0...) +} diff --git a/pkg/dal/dao/namespace.go b/pkg/dal/dao/namespace.go index 77d4a21f..0cb17194 100644 --- a/pkg/dal/dao/namespace.go +++ b/pkg/dal/dao/namespace.go @@ -120,14 +120,14 @@ func (s *namespaceService) ListNamespace(ctx context.Context, name *string, pagi if ok { switch ptr.To(sort.Method) { case enums.SortMethodDesc: - query.Order(field.Desc()) + query = query.Order(field.Desc()) case enums.SortMethodAsc: - query.Order(field) + query = query.Order(field) default: - query.Order(s.tx.Namespace.UpdatedAt.Desc()) + query = query.Order(s.tx.Namespace.UpdatedAt.Desc()) } } else { - query.Order(s.tx.Namespace.UpdatedAt.Desc()) + query = query.Order(s.tx.Namespace.UpdatedAt.Desc()) } return query.FindByPage(ptr.To(pagination.Limit)*(ptr.To(pagination.Page)-1), ptr.To(pagination.Limit)) } diff --git a/pkg/dal/dao/repository.go b/pkg/dal/dao/repository.go index f301af66..9b0eb4f6 100644 --- a/pkg/dal/dao/repository.go +++ b/pkg/dal/dao/repository.go @@ -142,14 +142,14 @@ func (s *repositoryService) ListRepository(ctx context.Context, namespaceID int6 if ok { switch ptr.To(sort.Method) { case enums.SortMethodDesc: - query.Order(field.Desc()) + query = query.Order(field.Desc()) case enums.SortMethodAsc: - query.Order(field) + query = query.Order(field) default: - query.Order(s.tx.Repository.UpdatedAt.Desc()) + query = query.Order(s.tx.Repository.UpdatedAt.Desc()) } } else { - query.Order(s.tx.Repository.UpdatedAt.Desc()) + query = query.Order(s.tx.Repository.UpdatedAt.Desc()) } return query.FindByPage(ptr.To(pagination.Limit)*(ptr.To(pagination.Page)-1), ptr.To(pagination.Limit)) } diff --git a/pkg/dal/migrations/mysql/0001_initialize.up.sql b/pkg/dal/migrations/mysql/0001_initialize.up.sql index 0a7ddb7b..44c49d30 100644 --- a/pkg/dal/migrations/mysql/0001_initialize.up.sql +++ b/pkg/dal/migrations/mysql/0001_initialize.up.sql @@ -28,6 +28,20 @@ CREATE TABLE IF NOT EXISTS `namespaces` ( CONSTRAINT `namespaces_unique_with_name` UNIQUE (`name`, `deleted_at`) ); +CREATE TABLE IF NOT EXISTS `audits` ( + `id` bigint AUTO_INCREMENT PRIMARY KEY, + `user_id` bigint NOT NULL, + `namespace_id` bigint NOT NULL, + `action` ENUM ('create', 'update', 'delete', 'pull', 'push') NOT NULL, + `resource_type` ENUM ('namespace', 'repository', 'tag') NOT NULL, + `resource` varchar(256) NOT NULL, + `created_at` timestamp NOT NULL, + `updated_at` timestamp NOT NULL, + `deleted_at` bigint NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + FOREIGN KEY (`namespace_id`) REFERENCES `namespaces` (`id`) +); + CREATE TABLE IF NOT EXISTS `repositories` ( `id` bigint AUTO_INCREMENT PRIMARY KEY, `name` varchar(64) NOT NULL, diff --git a/pkg/dal/migrations/postgresql/0001_initialize.up.sql b/pkg/dal/migrations/postgresql/0001_initialize.up.sql index 98888c18..e1c3e28b 100644 --- a/pkg/dal/migrations/postgresql/0001_initialize.up.sql +++ b/pkg/dal/migrations/postgresql/0001_initialize.up.sql @@ -38,6 +38,34 @@ CREATE TABLE IF NOT EXISTS "namespaces" ( CONSTRAINT "namespaces_unique_with_name" UNIQUE ("name", "deleted_at") ); +CREATE TYPE audit_action AS ENUM ( + 'create', + 'update', + 'delete', + 'pull', + 'push' +); + +CREATE TYPE audit_resource_type AS ENUM ( + 'namespace', + 'repository', + 'tag' +); + +CREATE TABLE IF NOT EXISTS "audits" ( + "id" bigint AUTO_INCREMENT PRIMARY KEY, + "user_id" bigint NOT NULL, + "namespace_id" bigint NOT NULL, + "action" audit_action NOT NULL, + "resource_type" audit_resource_type NOT NULL, + "resource" varchar(256) NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "deleted_at" bigint NOT NULL DEFAULT 0, + FOREIGN KEY ("user_id") REFERENCES "users" ("id"), + FOREIGN KEY ("namespace_id") REFERENCES "namespaces" ("id") +); + CREATE TABLE IF NOT EXISTS "repositories" ( "id" bigserial PRIMARY KEY, "name" varchar(64) NOT NULL, diff --git a/pkg/dal/migrations/sqlite3/0001_initialize.up.sql b/pkg/dal/migrations/sqlite3/0001_initialize.up.sql index 724a1ed9..bdad9e08 100644 --- a/pkg/dal/migrations/sqlite3/0001_initialize.up.sql +++ b/pkg/dal/migrations/sqlite3/0001_initialize.up.sql @@ -28,6 +28,20 @@ CREATE TABLE IF NOT EXISTS `namespaces` ( CONSTRAINT `namespaces_unique_with_name` UNIQUE (`name`, `deleted_at`) ); +CREATE TABLE IF NOT EXISTS `audits` ( + `id` bigint AUTO_INCREMENT PRIMARY KEY, + `user_id` bigint NOT NULL, + `namespace_id` bigint NOT NULL, + `action` text CHECK (`action` IN ('create', 'update', 'delete', 'pull', 'push')) NOT NULL, + `resource_type` text CHECK (`resource_type` IN ('namespace', 'repository', 'tag')) NOT NULL, + `resource` varchar(256) NOT NULL, + `created_at` timestamp NOT NULL, + `updated_at` timestamp NOT NULL, + `deleted_at` bigint NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + FOREIGN KEY (`namespace_id`) REFERENCES `namespaces` (`id`) +); + CREATE TABLE IF NOT EXISTS `repositories` ( `id` integer PRIMARY KEY AUTOINCREMENT, `name` varchar(64) NOT NULL, diff --git a/pkg/dal/models/audit.go b/pkg/dal/models/audit.go new file mode 100644 index 00000000..67f70198 --- /dev/null +++ b/pkg/dal/models/audit.go @@ -0,0 +1,40 @@ +// Copyright 2023 sigma +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "time" + + "gorm.io/plugin/soft_delete" + + "github.com/go-sigma/sigma/pkg/types/enums" +) + +// Audit represents a audit +type Audit struct { + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt soft_delete.DeletedAt `gorm:"softDelete:milli"` + ID int64 `gorm:"primaryKey"` + + UserID int64 + NamespaceID int64 + Action enums.AuditAction + ResourceType enums.AuditResourceType + Resource string + + Namespace Namespace + User User +} diff --git a/pkg/dal/query/audits.gen.go b/pkg/dal/query/audits.gen.go new file mode 100644 index 00000000..49ce5c7b --- /dev/null +++ b/pkg/dal/query/audits.gen.go @@ -0,0 +1,516 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package query + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "github.com/go-sigma/sigma/pkg/dal/models" +) + +func newAudit(db *gorm.DB, opts ...gen.DOOption) audit { + _audit := audit{} + + _audit.auditDo.UseDB(db, opts...) + _audit.auditDo.UseModel(&models.Audit{}) + + tableName := _audit.auditDo.TableName() + _audit.ALL = field.NewAsterisk(tableName) + _audit.CreatedAt = field.NewTime(tableName, "created_at") + _audit.UpdatedAt = field.NewTime(tableName, "updated_at") + _audit.DeletedAt = field.NewUint(tableName, "deleted_at") + _audit.ID = field.NewInt64(tableName, "id") + _audit.UserID = field.NewInt64(tableName, "user_id") + _audit.NamespaceID = field.NewInt64(tableName, "namespace_id") + _audit.Action = field.NewField(tableName, "action") + _audit.ResourceType = field.NewField(tableName, "resource_type") + _audit.Resource = field.NewString(tableName, "resource") + _audit.Namespace = auditBelongsToNamespace{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Namespace", "models.Namespace"), + } + + _audit.User = auditBelongsToUser{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("User", "models.User"), + } + + _audit.fillFieldMap() + + return _audit +} + +type audit struct { + auditDo auditDo + + ALL field.Asterisk + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Uint + ID field.Int64 + UserID field.Int64 + NamespaceID field.Int64 + Action field.Field + ResourceType field.Field + Resource field.String + Namespace auditBelongsToNamespace + + User auditBelongsToUser + + fieldMap map[string]field.Expr +} + +func (a audit) Table(newTableName string) *audit { + a.auditDo.UseTable(newTableName) + return a.updateTableName(newTableName) +} + +func (a audit) As(alias string) *audit { + a.auditDo.DO = *(a.auditDo.As(alias).(*gen.DO)) + return a.updateTableName(alias) +} + +func (a *audit) updateTableName(table string) *audit { + a.ALL = field.NewAsterisk(table) + a.CreatedAt = field.NewTime(table, "created_at") + a.UpdatedAt = field.NewTime(table, "updated_at") + a.DeletedAt = field.NewUint(table, "deleted_at") + a.ID = field.NewInt64(table, "id") + a.UserID = field.NewInt64(table, "user_id") + a.NamespaceID = field.NewInt64(table, "namespace_id") + a.Action = field.NewField(table, "action") + a.ResourceType = field.NewField(table, "resource_type") + a.Resource = field.NewString(table, "resource") + + a.fillFieldMap() + + return a +} + +func (a *audit) WithContext(ctx context.Context) *auditDo { return a.auditDo.WithContext(ctx) } + +func (a audit) TableName() string { return a.auditDo.TableName() } + +func (a audit) Alias() string { return a.auditDo.Alias() } + +func (a audit) Columns(cols ...field.Expr) gen.Columns { return a.auditDo.Columns(cols...) } + +func (a *audit) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := a.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (a *audit) fillFieldMap() { + a.fieldMap = make(map[string]field.Expr, 11) + a.fieldMap["created_at"] = a.CreatedAt + a.fieldMap["updated_at"] = a.UpdatedAt + a.fieldMap["deleted_at"] = a.DeletedAt + a.fieldMap["id"] = a.ID + a.fieldMap["user_id"] = a.UserID + a.fieldMap["namespace_id"] = a.NamespaceID + a.fieldMap["action"] = a.Action + a.fieldMap["resource_type"] = a.ResourceType + a.fieldMap["resource"] = a.Resource + +} + +func (a audit) clone(db *gorm.DB) audit { + a.auditDo.ReplaceConnPool(db.Statement.ConnPool) + return a +} + +func (a audit) replaceDB(db *gorm.DB) audit { + a.auditDo.ReplaceDB(db) + return a +} + +type auditBelongsToNamespace struct { + db *gorm.DB + + field.RelationField +} + +func (a auditBelongsToNamespace) Where(conds ...field.Expr) *auditBelongsToNamespace { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a auditBelongsToNamespace) WithContext(ctx context.Context) *auditBelongsToNamespace { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a auditBelongsToNamespace) Session(session *gorm.Session) *auditBelongsToNamespace { + a.db = a.db.Session(session) + return &a +} + +func (a auditBelongsToNamespace) Model(m *models.Audit) *auditBelongsToNamespaceTx { + return &auditBelongsToNamespaceTx{a.db.Model(m).Association(a.Name())} +} + +type auditBelongsToNamespaceTx struct{ tx *gorm.Association } + +func (a auditBelongsToNamespaceTx) Find() (result *models.Namespace, err error) { + return result, a.tx.Find(&result) +} + +func (a auditBelongsToNamespaceTx) Append(values ...*models.Namespace) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a auditBelongsToNamespaceTx) Replace(values ...*models.Namespace) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a auditBelongsToNamespaceTx) Delete(values ...*models.Namespace) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a auditBelongsToNamespaceTx) Clear() error { + return a.tx.Clear() +} + +func (a auditBelongsToNamespaceTx) Count() int64 { + return a.tx.Count() +} + +type auditBelongsToUser struct { + db *gorm.DB + + field.RelationField +} + +func (a auditBelongsToUser) Where(conds ...field.Expr) *auditBelongsToUser { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a auditBelongsToUser) WithContext(ctx context.Context) *auditBelongsToUser { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a auditBelongsToUser) Session(session *gorm.Session) *auditBelongsToUser { + a.db = a.db.Session(session) + return &a +} + +func (a auditBelongsToUser) Model(m *models.Audit) *auditBelongsToUserTx { + return &auditBelongsToUserTx{a.db.Model(m).Association(a.Name())} +} + +type auditBelongsToUserTx struct{ tx *gorm.Association } + +func (a auditBelongsToUserTx) Find() (result *models.User, err error) { + return result, a.tx.Find(&result) +} + +func (a auditBelongsToUserTx) Append(values ...*models.User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a auditBelongsToUserTx) Replace(values ...*models.User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a auditBelongsToUserTx) Delete(values ...*models.User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a auditBelongsToUserTx) Clear() error { + return a.tx.Clear() +} + +func (a auditBelongsToUserTx) Count() int64 { + return a.tx.Count() +} + +type auditDo struct{ gen.DO } + +func (a auditDo) Debug() *auditDo { + return a.withDO(a.DO.Debug()) +} + +func (a auditDo) WithContext(ctx context.Context) *auditDo { + return a.withDO(a.DO.WithContext(ctx)) +} + +func (a auditDo) ReadDB() *auditDo { + return a.Clauses(dbresolver.Read) +} + +func (a auditDo) WriteDB() *auditDo { + return a.Clauses(dbresolver.Write) +} + +func (a auditDo) Session(config *gorm.Session) *auditDo { + return a.withDO(a.DO.Session(config)) +} + +func (a auditDo) Clauses(conds ...clause.Expression) *auditDo { + return a.withDO(a.DO.Clauses(conds...)) +} + +func (a auditDo) Returning(value interface{}, columns ...string) *auditDo { + return a.withDO(a.DO.Returning(value, columns...)) +} + +func (a auditDo) Not(conds ...gen.Condition) *auditDo { + return a.withDO(a.DO.Not(conds...)) +} + +func (a auditDo) Or(conds ...gen.Condition) *auditDo { + return a.withDO(a.DO.Or(conds...)) +} + +func (a auditDo) Select(conds ...field.Expr) *auditDo { + return a.withDO(a.DO.Select(conds...)) +} + +func (a auditDo) Where(conds ...gen.Condition) *auditDo { + return a.withDO(a.DO.Where(conds...)) +} + +func (a auditDo) Order(conds ...field.Expr) *auditDo { + return a.withDO(a.DO.Order(conds...)) +} + +func (a auditDo) Distinct(cols ...field.Expr) *auditDo { + return a.withDO(a.DO.Distinct(cols...)) +} + +func (a auditDo) Omit(cols ...field.Expr) *auditDo { + return a.withDO(a.DO.Omit(cols...)) +} + +func (a auditDo) Join(table schema.Tabler, on ...field.Expr) *auditDo { + return a.withDO(a.DO.Join(table, on...)) +} + +func (a auditDo) LeftJoin(table schema.Tabler, on ...field.Expr) *auditDo { + return a.withDO(a.DO.LeftJoin(table, on...)) +} + +func (a auditDo) RightJoin(table schema.Tabler, on ...field.Expr) *auditDo { + return a.withDO(a.DO.RightJoin(table, on...)) +} + +func (a auditDo) Group(cols ...field.Expr) *auditDo { + return a.withDO(a.DO.Group(cols...)) +} + +func (a auditDo) Having(conds ...gen.Condition) *auditDo { + return a.withDO(a.DO.Having(conds...)) +} + +func (a auditDo) Limit(limit int) *auditDo { + return a.withDO(a.DO.Limit(limit)) +} + +func (a auditDo) Offset(offset int) *auditDo { + return a.withDO(a.DO.Offset(offset)) +} + +func (a auditDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *auditDo { + return a.withDO(a.DO.Scopes(funcs...)) +} + +func (a auditDo) Unscoped() *auditDo { + return a.withDO(a.DO.Unscoped()) +} + +func (a auditDo) Create(values ...*models.Audit) error { + if len(values) == 0 { + return nil + } + return a.DO.Create(values) +} + +func (a auditDo) CreateInBatches(values []*models.Audit, batchSize int) error { + return a.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (a auditDo) Save(values ...*models.Audit) error { + if len(values) == 0 { + return nil + } + return a.DO.Save(values) +} + +func (a auditDo) First() (*models.Audit, error) { + if result, err := a.DO.First(); err != nil { + return nil, err + } else { + return result.(*models.Audit), nil + } +} + +func (a auditDo) Take() (*models.Audit, error) { + if result, err := a.DO.Take(); err != nil { + return nil, err + } else { + return result.(*models.Audit), nil + } +} + +func (a auditDo) Last() (*models.Audit, error) { + if result, err := a.DO.Last(); err != nil { + return nil, err + } else { + return result.(*models.Audit), nil + } +} + +func (a auditDo) Find() ([]*models.Audit, error) { + result, err := a.DO.Find() + return result.([]*models.Audit), err +} + +func (a auditDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Audit, err error) { + buf := make([]*models.Audit, 0, batchSize) + err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (a auditDo) FindInBatches(result *[]*models.Audit, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return a.DO.FindInBatches(result, batchSize, fc) +} + +func (a auditDo) Attrs(attrs ...field.AssignExpr) *auditDo { + return a.withDO(a.DO.Attrs(attrs...)) +} + +func (a auditDo) Assign(attrs ...field.AssignExpr) *auditDo { + return a.withDO(a.DO.Assign(attrs...)) +} + +func (a auditDo) Joins(fields ...field.RelationField) *auditDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Joins(_f)) + } + return &a +} + +func (a auditDo) Preload(fields ...field.RelationField) *auditDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Preload(_f)) + } + return &a +} + +func (a auditDo) FirstOrInit() (*models.Audit, error) { + if result, err := a.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*models.Audit), nil + } +} + +func (a auditDo) FirstOrCreate() (*models.Audit, error) { + if result, err := a.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*models.Audit), nil + } +} + +func (a auditDo) FindByPage(offset int, limit int) (result []*models.Audit, count int64, err error) { + result, err = a.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = a.Offset(-1).Limit(-1).Count() + return +} + +func (a auditDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = a.Count() + if err != nil { + return + } + + err = a.Offset(offset).Limit(limit).Scan(result) + return +} + +func (a auditDo) Scan(result interface{}) (err error) { + return a.DO.Scan(result) +} + +func (a auditDo) Delete(models ...*models.Audit) (result gen.ResultInfo, err error) { + return a.DO.Delete(models) +} + +func (a *auditDo) withDO(do gen.Dao) *auditDo { + a.DO = *do.(*gen.DO) + return a +} diff --git a/pkg/dal/query/gen.go b/pkg/dal/query/gen.go index df942325..e120b59f 100644 --- a/pkg/dal/query/gen.go +++ b/pkg/dal/query/gen.go @@ -20,6 +20,7 @@ var ( Artifact *artifact ArtifactSbom *artifactSbom ArtifactVulnerability *artifactVulnerability + Audit *audit Blob *blob BlobUpload *blobUpload CasbinRule *casbinRule @@ -34,6 +35,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { Artifact = &Q.Artifact ArtifactSbom = &Q.ArtifactSbom ArtifactVulnerability = &Q.ArtifactVulnerability + Audit = &Q.Audit Blob = &Q.Blob BlobUpload = &Q.BlobUpload CasbinRule = &Q.CasbinRule @@ -49,6 +51,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { Artifact: newArtifact(db, opts...), ArtifactSbom: newArtifactSbom(db, opts...), ArtifactVulnerability: newArtifactVulnerability(db, opts...), + Audit: newAudit(db, opts...), Blob: newBlob(db, opts...), BlobUpload: newBlobUpload(db, opts...), CasbinRule: newCasbinRule(db, opts...), @@ -65,6 +68,7 @@ type Query struct { Artifact artifact ArtifactSbom artifactSbom ArtifactVulnerability artifactVulnerability + Audit audit Blob blob BlobUpload blobUpload CasbinRule casbinRule @@ -82,6 +86,7 @@ func (q *Query) clone(db *gorm.DB) *Query { Artifact: q.Artifact.clone(db), ArtifactSbom: q.ArtifactSbom.clone(db), ArtifactVulnerability: q.ArtifactVulnerability.clone(db), + Audit: q.Audit.clone(db), Blob: q.Blob.clone(db), BlobUpload: q.BlobUpload.clone(db), CasbinRule: q.CasbinRule.clone(db), @@ -106,6 +111,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { Artifact: q.Artifact.replaceDB(db), ArtifactSbom: q.ArtifactSbom.replaceDB(db), ArtifactVulnerability: q.ArtifactVulnerability.replaceDB(db), + Audit: q.Audit.replaceDB(db), Blob: q.Blob.replaceDB(db), BlobUpload: q.BlobUpload.replaceDB(db), CasbinRule: q.CasbinRule.replaceDB(db), @@ -120,6 +126,7 @@ type queryCtx struct { Artifact *artifactDo ArtifactSbom *artifactSbomDo ArtifactVulnerability *artifactVulnerabilityDo + Audit *auditDo Blob *blobDo BlobUpload *blobUploadDo CasbinRule *casbinRuleDo @@ -134,6 +141,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { Artifact: q.Artifact.WithContext(ctx), ArtifactSbom: q.ArtifactSbom.WithContext(ctx), ArtifactVulnerability: q.ArtifactVulnerability.WithContext(ctx), + Audit: q.Audit.WithContext(ctx), Blob: q.Blob.WithContext(ctx), BlobUpload: q.BlobUpload.WithContext(ctx), CasbinRule: q.CasbinRule.WithContext(ctx), diff --git a/pkg/handlers/namespaces/handler.go b/pkg/handlers/namespaces/handler.go index e4ebb665..1516e00e 100644 --- a/pkg/handlers/namespaces/handler.go +++ b/pkg/handlers/namespaces/handler.go @@ -49,6 +49,7 @@ type handlers struct { repositoryServiceFactory dao.RepositoryServiceFactory tagServiceFactory dao.TagServiceFactory artifactServiceFactory dao.ArtifactServiceFactory + auditServiceFactory dao.AuditServiceFactory } type inject struct { @@ -57,6 +58,7 @@ type inject struct { repositoryServiceFactory dao.RepositoryServiceFactory tagServiceFactory dao.TagServiceFactory artifactServiceFactory dao.ArtifactServiceFactory + auditServiceFactory dao.AuditServiceFactory } // handlerNew creates a new instance of the distribution handlers @@ -66,6 +68,7 @@ func handlerNew(injects ...inject) Handlers { repositoryServiceFactory := dao.NewRepositoryServiceFactory() tagServiceFactory := dao.NewTagServiceFactory() artifactServiceFactory := dao.NewArtifactServiceFactory() + auditServiceFactory := dao.NewAuditServiceFactory() if len(injects) > 0 { ij := injects[0] if ij.authServiceFactory != nil { @@ -83,6 +86,9 @@ func handlerNew(injects ...inject) Handlers { if ij.artifactServiceFactory != nil { artifactServiceFactory = ij.artifactServiceFactory } + if ij.auditServiceFactory != nil { + auditServiceFactory = ij.auditServiceFactory + } } return &handlers{ authServiceFactory: authServiceFactory, @@ -90,6 +96,7 @@ func handlerNew(injects ...inject) Handlers { repositoryServiceFactory: repositoryServiceFactory, tagServiceFactory: tagServiceFactory, artifactServiceFactory: artifactServiceFactory, + auditServiceFactory: auditServiceFactory, } } diff --git a/pkg/handlers/namespaces/namespaces_delete.go b/pkg/handlers/namespaces/namespaces_delete.go index 9ef13225..0a8a5d15 100644 --- a/pkg/handlers/namespaces/namespaces_delete.go +++ b/pkg/handlers/namespaces/namespaces_delete.go @@ -16,13 +16,18 @@ package namespaces import ( "errors" + "fmt" "net/http" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "gorm.io/gorm" + "github.com/go-sigma/sigma/pkg/consts" + "github.com/go-sigma/sigma/pkg/dal/models" + "github.com/go-sigma/sigma/pkg/dal/query" "github.com/go-sigma/sigma/pkg/types" + "github.com/go-sigma/sigma/pkg/types/enums" "github.com/go-sigma/sigma/pkg/utils" "github.com/go-sigma/sigma/pkg/xerrors" ) @@ -39,6 +44,17 @@ import ( func (h *handlers) DeleteNamespace(c echo.Context) error { ctx := log.Logger.WithContext(c.Request().Context()) + iuser := c.Get(consts.ContextUser) + if iuser == nil { + log.Error().Msg("Get user from header failed") + return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeUnauthorized) + } + user, ok := iuser.(*models.User) + if !ok { + log.Error().Msg("Convert user from header failed") + return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeUnauthorized) + } + var req types.DeleteNamespaceRequest err := utils.BindValidate(c, &req) if err != nil { @@ -47,14 +63,41 @@ func (h *handlers) DeleteNamespace(c echo.Context) error { } namespaceService := h.namespaceServiceFactory.New() - err = namespaceService.DeleteByID(ctx, req.ID) + namespaceObj, err := namespaceService.Get(ctx, req.ID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - log.Error().Err(err).Msg("Delete namespace from db failed") - return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeNotFound, err.Error()) + log.Error().Err(err).Int64("id", req.ID).Msg("Namespace not found") + // return xerrors.HTTPErrCodeNotFound.Detail(fmt.Sprintf("Namespace(%d) not found: %v", req.ID, err)) + return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeNotFound, fmt.Sprintf("Namespace(%d) not found", req.ID)) + } + log.Error().Err(err).Msg("Get namespace failed") + // return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get namespace(%d) failed: %v", req.ID, err)) + return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeInternalError, fmt.Sprintf("Get namespace(%d) failed", req.ID)) + } + + err = query.Q.Transaction(func(tx *query.Query) error { + namespaceService := h.namespaceServiceFactory.New(tx) + err = namespaceService.DeleteByID(ctx, req.ID) + if err != nil { + log.Error().Err(err).Msg("Delete namespace failed") + return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Namespace(%d) find failed: %v", req.ID, err)) } - log.Error().Err(err).Msg("Delete namespace from db failed") - return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeInternalError, err.Error()) + auditService := h.auditServiceFactory.New(tx) + err = auditService.Create(ctx, &models.Audit{ + UserID: user.ID, + NamespaceID: req.ID, + Action: enums.AuditActionDelete, + ResourceType: enums.AuditResourceTypeNamespace, + Resource: namespaceObj.Name, + }) + if err != nil { + log.Error().Err(err).Msg("Create audit for delete namespace failed") + return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create audit for delete namespace failed: %v", err)) + } + return nil + }) + if err != nil { + return xerrors.NewHTTPError(c, err.(xerrors.ErrCode)) } return c.NoContent(http.StatusNoContent) diff --git a/pkg/handlers/namespaces/namespaces_delete_test.go b/pkg/handlers/namespaces/namespaces_delete_test.go index 87674f96..3124a2f1 100644 --- a/pkg/handlers/namespaces/namespaces_delete_test.go +++ b/pkg/handlers/namespaces/namespaces_delete_test.go @@ -83,6 +83,7 @@ func TestDeleteNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.DeleteNamespace(c) @@ -93,6 +94,7 @@ func TestDeleteNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) err = namespaceHandler.DeleteNamespace(c) assert.NoError(t, err) assert.Equal(t, http.StatusBadRequest, c.Response().Status) @@ -101,6 +103,7 @@ func TestDeleteNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.DeleteNamespace(c) @@ -114,11 +117,14 @@ func TestDeleteNamespace(t *testing.T) { daoMockNamespaceService.EXPECT().DeleteByID(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ int64) error { return fmt.Errorf("test") }).Times(1) + daoMockNamespaceService.EXPECT().Get(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ int64) (*models.Namespace, error) { + return &models.Namespace{}, nil + }).Times(1) daoMockNamespaceServiceFactory := daomock.NewMockNamespaceServiceFactory(ctrl) daoMockNamespaceServiceFactory.EXPECT().New(gomock.Any()).DoAndReturn(func(txs ...*query.Query) dao.NamespaceService { return daoMockNamespaceService - }).Times(1) + }).Times(2) namespaceHandler = handlerNew(inject{namespaceServiceFactory: daoMockNamespaceServiceFactory}) @@ -126,6 +132,7 @@ func TestDeleteNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.DeleteNamespace(c) diff --git a/pkg/handlers/namespaces/namespaces_post.go b/pkg/handlers/namespaces/namespaces_post.go index 318ea660..4d694add 100644 --- a/pkg/handlers/namespaces/namespaces_post.go +++ b/pkg/handlers/namespaces/namespaces_post.go @@ -28,6 +28,7 @@ import ( "github.com/go-sigma/sigma/pkg/dal/models" "github.com/go-sigma/sigma/pkg/dal/query" "github.com/go-sigma/sigma/pkg/types" + "github.com/go-sigma/sigma/pkg/types/enums" "github.com/go-sigma/sigma/pkg/utils" "github.com/go-sigma/sigma/pkg/utils/ptr" "github.com/go-sigma/sigma/pkg/xerrors" @@ -100,6 +101,18 @@ func (h *handlers) PostNamespace(c echo.Context) error { log.Error().Err(err).Msg("Add role for user failed") return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Add role for user failed: %v", err)) } + auditService := h.auditServiceFactory.New(tx) + err = auditService.Create(ctx, &models.Audit{ + UserID: user.ID, + NamespaceID: namespaceObj.ID, + Action: enums.AuditActionCreate, + ResourceType: enums.AuditResourceTypeNamespace, + Resource: namespaceObj.Name, + }) + if err != nil { + log.Error().Err(err).Msg("Create audit failed") + return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create audit failed: %v", err)) + } return nil }) if err != nil { diff --git a/pkg/handlers/namespaces/namespaces_put.go b/pkg/handlers/namespaces/namespaces_put.go index 4dcb9a13..b81160fa 100644 --- a/pkg/handlers/namespaces/namespaces_put.go +++ b/pkg/handlers/namespaces/namespaces_put.go @@ -23,8 +23,11 @@ import ( "github.com/rs/zerolog/log" "gorm.io/gorm" + "github.com/go-sigma/sigma/pkg/consts" + "github.com/go-sigma/sigma/pkg/dal/models" "github.com/go-sigma/sigma/pkg/dal/query" "github.com/go-sigma/sigma/pkg/types" + "github.com/go-sigma/sigma/pkg/types/enums" "github.com/go-sigma/sigma/pkg/utils" "github.com/go-sigma/sigma/pkg/utils/ptr" "github.com/go-sigma/sigma/pkg/xerrors" @@ -42,6 +45,17 @@ import ( func (h *handlers) PutNamespace(c echo.Context) error { ctx := log.Logger.WithContext(c.Request().Context()) + iuser := c.Get(consts.ContextUser) + if iuser == nil { + log.Error().Msg("Get user from header failed") + return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeUnauthorized) + } + user, ok := iuser.(*models.User) + if !ok { + log.Error().Msg("Convert user from header failed") + return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeUnauthorized) + } + var req types.PutNamespaceRequest err := utils.BindValidate(c, &req) if err != nil { @@ -83,10 +97,29 @@ func (h *handlers) PutNamespace(c echo.Context) error { } if len(updates) > 0 { - err = namespaceService.UpdateByID(ctx, namespaceObj.ID, updates) + err = query.Q.Transaction(func(tx *query.Query) error { + namespaceService := h.namespaceServiceFactory.New(tx) + err = namespaceService.UpdateByID(ctx, namespaceObj.ID, updates) + if err != nil { + log.Error().Err(err).Msg("Update namespace failed") + return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Update namespace failed: %v", err)) + } + auditService := h.auditServiceFactory.New(tx) + err = auditService.Create(ctx, &models.Audit{ + UserID: user.ID, + NamespaceID: namespaceObj.ID, + Action: enums.AuditActionUpdate, + ResourceType: enums.AuditResourceTypeNamespace, + Resource: namespaceObj.Name, + }) + if err != nil { + log.Error().Err(err).Msg("Create audit for update namespace failed") + return xerrors.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create audit for update namespace failed: %v", err)) + } + return nil + }) if err != nil { - log.Error().Err(err).Msg("Update namespace failed") - return xerrors.NewHTTPError(c, xerrors.HTTPErrCodeInternalError, fmt.Sprintf("Update namespace failed: %v", err)) + return xerrors.NewHTTPError(c, err.(xerrors.ErrCode)) } } diff --git a/pkg/handlers/namespaces/namespaces_put_test.go b/pkg/handlers/namespaces/namespaces_put_test.go index 0d7314b3..7706e296 100644 --- a/pkg/handlers/namespaces/namespaces_put_test.go +++ b/pkg/handlers/namespaces/namespaces_put_test.go @@ -82,6 +82,7 @@ func TestPutNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.PutNamespace(c) @@ -93,6 +94,7 @@ func TestPutNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.PutNamespace(c) @@ -104,6 +106,7 @@ func TestPutNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.PutNamespace(c) @@ -114,6 +117,7 @@ func TestPutNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatInt(resultID, 10)) err = namespaceHandler.PutNamespace(c) @@ -124,6 +128,7 @@ func TestPutNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatUint(3, 10)) err = namespaceHandler.PutNamespace(c) @@ -144,7 +149,7 @@ func TestPutNamespace(t *testing.T) { daoMockNamespaceServiceFactory := daomock.NewMockNamespaceServiceFactory(ctrl) daoMockNamespaceServiceFactory.EXPECT().New(gomock.Any()).DoAndReturn(func(txs ...*query.Query) dao.NamespaceService { return daoMockNamespaceService - }).Times(1) + }).Times(2) namespaceHandler = handlerNew(inject{namespaceServiceFactory: daoMockNamespaceServiceFactory}) @@ -152,6 +157,7 @@ func TestPutNamespace(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec = httptest.NewRecorder() c = e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatUint(3, 10)) err = namespaceHandler.PutNamespace(c) @@ -163,10 +169,30 @@ func TestPutNamespaceFailed1(t *testing.T) { logger.SetLevel("debug") e := echo.New() validators.Initialize(e) + err := tests.Initialize(t) + assert.NoError(t, err) + err = tests.DB.Init() + assert.NoError(t, err) + defer func() { + conn, err := dal.DB.DB() + assert.NoError(t, err) + err = conn.Close() + assert.NoError(t, err) + err = tests.DB.DeInit() + assert.NoError(t, err) + }() ctrl := gomock.NewController(t) defer ctrl.Finish() + userServiceFactory := dao.NewUserServiceFactory() + userService := userServiceFactory.New() + + ctx := context.Background() + userObj := &models.User{Provider: enums.ProviderLocal, Username: "put-namespace", Password: ptr.Of("test"), Email: ptr.Of("test@gmail.com")} + err = userService.Create(ctx, userObj) + assert.NoError(t, err) + daoMockNamespaceService := daomock.NewMockNamespaceService(ctrl) daoMockNamespaceService.EXPECT().Get(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ int64) (*models.Namespace, error) { return nil, fmt.Errorf("test") @@ -183,9 +209,10 @@ func TestPutNamespaceFailed1(t *testing.T) { req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() c := e.NewContext(req, rec) + c.Set(consts.ContextUser, userObj) c.SetParamNames("id") c.SetParamValues(strconv.FormatUint(3, 10)) - err := namespaceHandler.PutNamespace(c) + err = namespaceHandler.PutNamespace(c) assert.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, c.Response().Status) } diff --git a/pkg/types/enums/enums.go b/pkg/types/enums/enums.go index 03b89aaa..20b8a97f 100644 --- a/pkg/types/enums/enums.go +++ b/pkg/types/enums/enums.go @@ -74,3 +74,19 @@ type SortMethod string // unknown, // ) type ArtifactType string + +// AuditAction x ENUM( +// create, +// update, +// delete, +// pull, +// push, +// ) +type AuditAction string + +// AuditResourceType x ENUM( +// namespace, +// repository, +// tag, +// ) +type AuditResourceType string diff --git a/pkg/types/enums/enums_enum.go b/pkg/types/enums/enums_enum.go index 3c00d93f..a5c9cba0 100644 --- a/pkg/types/enums/enums_enum.go +++ b/pkg/types/enums/enums_enum.go @@ -110,6 +110,187 @@ func (x ArtifactType) Value() (driver.Value, error) { return x.String(), nil } +const ( + // AuditActionCreate is a AuditAction of type create. + AuditActionCreate AuditAction = "create" + // AuditActionUpdate is a AuditAction of type update. + AuditActionUpdate AuditAction = "update" + // AuditActionDelete is a AuditAction of type delete. + AuditActionDelete AuditAction = "delete" + // AuditActionPull is a AuditAction of type pull. + AuditActionPull AuditAction = "pull" + // AuditActionPush is a AuditAction of type push. + AuditActionPush AuditAction = "push" +) + +var ErrInvalidAuditAction = errors.New("not a valid AuditAction") + +// String implements the Stringer interface. +func (x AuditAction) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AuditAction) IsValid() bool { + _, err := ParseAuditAction(string(x)) + return err == nil +} + +var _AuditActionValue = map[string]AuditAction{ + "create": AuditActionCreate, + "update": AuditActionUpdate, + "delete": AuditActionDelete, + "pull": AuditActionPull, + "push": AuditActionPush, +} + +// ParseAuditAction attempts to convert a string to a AuditAction. +func ParseAuditAction(name string) (AuditAction, error) { + if x, ok := _AuditActionValue[name]; ok { + return x, nil + } + return AuditAction(""), fmt.Errorf("%s is %w", name, ErrInvalidAuditAction) +} + +// MustParseAuditAction converts a string to a AuditAction, and panics if is not valid. +func MustParseAuditAction(name string) AuditAction { + val, err := ParseAuditAction(name) + if err != nil { + panic(err) + } + return val +} + +var errAuditActionNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AuditAction) Scan(value interface{}) (err error) { + if value == nil { + *x = AuditAction("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseAuditAction(v) + case []byte: + *x, err = ParseAuditAction(string(v)) + case AuditAction: + *x = v + case *AuditAction: + if v == nil { + return errAuditActionNilPtr + } + *x = *v + case *string: + if v == nil { + return errAuditActionNilPtr + } + *x, err = ParseAuditAction(*v) + default: + return errors.New("invalid type for AuditAction") + } + + return +} + +// Value implements the driver Valuer interface. +func (x AuditAction) Value() (driver.Value, error) { + return x.String(), nil +} + +const ( + // AuditResourceTypeNamespace is a AuditResourceType of type namespace. + AuditResourceTypeNamespace AuditResourceType = "namespace" + // AuditResourceTypeRepository is a AuditResourceType of type repository. + AuditResourceTypeRepository AuditResourceType = "repository" + // AuditResourceTypeArtifact is a AuditResourceType of type artifact. + AuditResourceTypeArtifact AuditResourceType = "artifact" + // AuditResourceTypeTag is a AuditResourceType of type tag. + AuditResourceTypeTag AuditResourceType = "tag" +) + +var ErrInvalidAuditResourceType = errors.New("not a valid AuditResourceType") + +// String implements the Stringer interface. +func (x AuditResourceType) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AuditResourceType) IsValid() bool { + _, err := ParseAuditResourceType(string(x)) + return err == nil +} + +var _AuditResourceTypeValue = map[string]AuditResourceType{ + "namespace": AuditResourceTypeNamespace, + "repository": AuditResourceTypeRepository, + "artifact": AuditResourceTypeArtifact, + "tag": AuditResourceTypeTag, +} + +// ParseAuditResourceType attempts to convert a string to a AuditResourceType. +func ParseAuditResourceType(name string) (AuditResourceType, error) { + if x, ok := _AuditResourceTypeValue[name]; ok { + return x, nil + } + return AuditResourceType(""), fmt.Errorf("%s is %w", name, ErrInvalidAuditResourceType) +} + +// MustParseAuditResourceType converts a string to a AuditResourceType, and panics if is not valid. +func MustParseAuditResourceType(name string) AuditResourceType { + val, err := ParseAuditResourceType(name) + if err != nil { + panic(err) + } + return val +} + +var errAuditResourceTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AuditResourceType) Scan(value interface{}) (err error) { + if value == nil { + *x = AuditResourceType("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseAuditResourceType(v) + case []byte: + *x, err = ParseAuditResourceType(string(v)) + case AuditResourceType: + *x = v + case *AuditResourceType: + if v == nil { + return errAuditResourceTypeNilPtr + } + *x = *v + case *string: + if v == nil { + return errAuditResourceTypeNilPtr + } + *x, err = ParseAuditResourceType(*v) + default: + return errors.New("invalid type for AuditResourceType") + } + + return +} + +// Value implements the driver Valuer interface. +func (x AuditResourceType) Value() (driver.Value, error) { + return x.String(), nil +} + const ( // DaemonVulnerability is a Daemon of type Vulnerability. DaemonVulnerability Daemon = "Vulnerability" diff --git a/web/package.json b/web/package.json index 30c125aa..3244b48a 100644 --- a/web/package.json +++ b/web/package.json @@ -26,22 +26,22 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.10.1", - "react-router-dom": "^6.14.1", + "react-router-dom": "^6.14.2", "react-toastify": "^9.1.3", "react-use": "^17.4.0" }, "devDependencies": { - "@types/node": "^20.4.2", + "@types/node": "^20.4.3", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.14", "cssnano": "^6.0.1", "json-server": "^0.17.3", - "postcss": "^8.4.26", + "postcss": "^8.4.27", "react-helmet-async": "^1.3.0", "tailwindcss": "^3.3.3", "typescript": "^5.1.6", - "vite": "^4.4.4" + "vite": "^4.4.6" } } diff --git a/web/src/components/OrderHeader/index.tsx b/web/src/components/OrderHeader/index.tsx index 12c12474..bc355e53 100644 --- a/web/src/components/OrderHeader/index.tsx +++ b/web/src/components/OrderHeader/index.tsx @@ -18,7 +18,7 @@ import { IOrder } from "../../interfaces"; export default function ({ text, orderStatus, setOrder }: { text: string, orderStatus: IOrder, setOrder: (order: IOrder) => void }) { return ( - { + { switch (orderStatus) { case IOrder.Asc: setOrder(IOrder.Desc); @@ -43,7 +43,7 @@ export default function ({ text, orderStatus, setOrder }: { text: string, orderS : orderStatus === IOrder.Desc ? ( - + diff --git a/web/src/components/QuotaSimple/index.tsx b/web/src/components/QuotaSimple/index.tsx index d817db59..f4a516a5 100644 --- a/web/src/components/QuotaSimple/index.tsx +++ b/web/src/components/QuotaSimple/index.tsx @@ -28,7 +28,7 @@ export default function ({ current, limit }: { current: number, limit: number }) <>{humanFormat(current)} ) : ( <> -
+
{humanFormat(current)} / {humanFormat(limit)} ( Settings.QuotaThreshold ? "text-red-700 dark:text-red-500" : "text-blue-700 dark:text-blue-500"}>{(current / limit * 100 > 100 ? 100 : current / limit * 100).toFixed(1)}%)
diff --git a/web/src/pages/Namespace/TableItem.tsx b/web/src/pages/Namespace/TableItem.tsx index adddf5ef..6589e927 100644 --- a/web/src/pages/Namespace/TableItem.tsx +++ b/web/src/pages/Namespace/TableItem.tsx @@ -23,6 +23,7 @@ import { Dialog, Menu, Transition } from '@headlessui/react'; import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' +import calcUnit from "../../utils/calcUnit"; import Toast from "../../components/Notification"; import { INamespace, IHTTPError } from "../../interfaces"; @@ -49,10 +50,11 @@ export default function TableItem({ localServer, index, namespace, setRefresh }: const [tagCountLimit, setTagCountLimit] = useState(namespace.tag_limit); const [tagCountLimitValid, setTagCountLimitValid] = useState(true); useEffect(() => { setTagCountLimitValid(Number.isInteger(tagCountLimit) && parseInt(tagCountLimit.toString()) >= 0) }, [tagCountLimit]) + let calcUnitObj = calcUnit(namespace.size_limit); const [realSizeLimit, setRealSizeLimit] = useState(0); - const [sizeLimit, setSizeLimit] = useState(namespace.size_limit); + const [sizeLimit, setSizeLimit] = useState(calcUnitObj.size); const [sizeLimitValid, setSizeLimitValid] = useState(true); - const [sizeLimitUnit, setSizeLimitUnit] = useState(""); + const [sizeLimitUnit, setSizeLimitUnit] = useState(calcUnitObj.unit); useEffect(() => { setSizeLimitValid(Number.isInteger(sizeLimit) && parseInt(sizeLimit.toString()) >= 0) }, [sizeLimit]) useEffect(() => { let sl = 0; @@ -441,7 +443,7 @@ export default function TableItem({ localServer, index, namespace, setRefresh }: className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:bg-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm" onClick={() => updateNamespace()} > - Create + Update