From c0a187d46106e3b50832b84656e4053a28db31a7 Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 19 Dec 2025 14:26:05 +0200 Subject: [PATCH] implement basic acl operations and tests --- internal/acl/models.go | 30 +++++++ internal/acl/service.go | 143 ++++++++++++++++++++++++++++++ internal/acl_test/crud_test.go | 156 +++++++++++++++++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 internal/acl/models.go create mode 100644 internal/acl/service.go create mode 100644 internal/acl_test/crud_test.go diff --git a/internal/acl/models.go b/internal/acl/models.go new file mode 100644 index 0000000..b357c07 --- /dev/null +++ b/internal/acl/models.go @@ -0,0 +1,30 @@ +package acl + +type UserRole struct { + UserID uint `gorm:"primaryKey" json:"userId"` + RoleID uint `gorm:"primaryKey" json:"roleId"` + + Role Role `gorm:"constraint:OnDelete:CASCADE;foreignKey:RoleID;references:ID" json:"role"` + //User user.User `gorm:"constraint:OnDelete:CASCADE;foreignKey:UserID;references:ID"` +} + +type Resource struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Key string `gorm:"unique;not null" json:"key"` +} + +type Role struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Name string `gorm:"unique;not null" json:"name"` + + Resources []Resource `gorm:"many2many:role_resources" json:"resources"` + //Users []user.User `gorm:"many2many:user_roles"` +} + +type RoleResource struct { + RoleID uint `gorm:"primaryKey" json:"roleId"` + ResourceID uint `gorm:"primaryKey" json:"resourceId"` + + Role Role `gorm:"constraint:OnDelete:CASCADE;foreignKey:RoleID;references:ID" json:"role"` + Resource Resource `gorm:"constraint:OnDelete:CASCADE;foreignKey:ResourceID;references:ID" json:"resource"` +} diff --git a/internal/acl/service.go b/internal/acl/service.go new file mode 100644 index 0000000..d0d633f --- /dev/null +++ b/internal/acl/service.go @@ -0,0 +1,143 @@ +package acl + +import ( + "fmt" + + "gorm.io/gorm" +) + +type Service struct { + initialized bool + + db *gorm.DB +} + +func NewService(db *gorm.DB) (*Service, error) { + if db == nil { + return nil, fmt.Errorf("db is required") + } + return &Service{ + db: db, + }, nil +} + +func (s *Service) isInitialized() bool { + return s.initialized +} + +func (s *Service) Init() error { + if s.isInitialized() { + return nil + } + + // AutoMigrate models + err := s.db.AutoMigrate(&UserRole{}, &Resource{}, &Role{}, &RoleResource{}) + if err != nil { + return fmt.Errorf("failed to migrate ACL models: %w", err) + } + + s.initialized = true + return nil +} + +// Admin crud functions + +// CreateRole creates a new role with the given name +func (s *Service) CreateRole(name string) error { + if !s.isInitialized() { + return fmt.Errorf("acl service is not initialized") + } + role := Role{Name: name} + return s.db.FirstOrCreate(&role, &Role{Name: name}).Error +} + +// CreateResource creates a new resource with the given key +func (s *Service) CreateResource(key string) error { + if !s.isInitialized() { + return fmt.Errorf("acl service is not initialized") + } + res := Resource{Key: key} + return s.db.FirstOrCreate(&res, &Resource{Key: key}).Error +} + +// AssignResourceToRole assigns a resource to a role +func (s *Service) AssignResourceToRole(roleID, resourceID uint) error { + if !s.isInitialized() { + return fmt.Errorf("acl service is not initialized") + } + rr := RoleResource{ + RoleID: roleID, + ResourceID: resourceID, + } + return s.db.FirstOrCreate(&rr, RoleResource{RoleID: roleID, ResourceID: resourceID}).Error +} + +// AssignRoleToUser assigns a role to a user +func (s *Service) AssignRoleToUser(roleID, userID uint) error { + if !s.isInitialized() { + return fmt.Errorf("acl service is not initialized") + } + ur := UserRole{ + UserID: userID, + RoleID: roleID, + } + return s.db.FirstOrCreate(&ur, UserRole{UserID: userID, RoleID: roleID}).Error +} + +// RemoveResourceFromRole removes a resource from a role +func (s *Service) RemoveResourceFromRole(roleID, resourceID uint) error { + if !s.isInitialized() { + return fmt.Errorf("acl service is not initialized") + } + return s.db.Where("role_id = ? AND resource_id = ?", roleID, resourceID).Delete(&RoleResource{}).Error +} + +// RemoveRoleFromUser removes a role from a user +func (s *Service) RemoveRoleFromUser(roleID, userID uint) error { + if !s.isInitialized() { + return fmt.Errorf("acl service is not initialized") + } + return s.db.Where("role_id = ? AND user_id = ?", roleID, userID).Delete(&UserRole{}).Error +} + +// GetRoles returns all roles +func (s *Service) GetRoles() ([]Role, error) { + if !s.isInitialized() { + return nil, fmt.Errorf("acl service is not initialized") + } + var roles []Role + err := s.db.Preload("Resources").Order("id").Find(&roles).Error + return roles, err +} + +// GetPermissions returns all permissions +func (s *Service) GetPermissions() ([]Resource, error) { + if !s.isInitialized() { + return nil, fmt.Errorf("acl service is not initialized") + } + var resources []Resource + err := s.db.Order("id").Find(&resources).Error + return resources, err +} + +// GetRoleResources returns all resources for a given role +func (s *Service) GetRoleResources(roleID uint) ([]Resource, error) { + if !s.isInitialized() { + return nil, fmt.Errorf("acl service is not initialized") + } + var resources []Resource + err := s.db.Joins("JOIN role_resources rr ON rr.resource_id = resources.id"). + Where("rr.role_id = ?", roleID).Find(&resources).Error + return resources, err +} + +// GetUserRoles returns all roles for a given user +func (s *Service) GetUserRoles(userID uint) ([]Role, error) { + if !s.isInitialized() { + return nil, fmt.Errorf("acl service is not initialized") + } + var roles []Role + err := s.db.Joins("JOIN user_roles ur ON ur.role_id = roles.id"). + Where("ur.user_id = ?", userID).Find(&roles).Error + return roles, err +} diff --git a/internal/acl_test/crud_test.go b/internal/acl_test/crud_test.go new file mode 100644 index 0000000..274633c --- /dev/null +++ b/internal/acl_test/crud_test.go @@ -0,0 +1,156 @@ +package acl_test + +import ( + "os" + "path/filepath" + "testing" + + "git.oblat.lv/alex/triggerssmith/internal/acl" + "git.oblat.lv/alex/triggerssmith/internal/user" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func openTestDB(t *testing.T) *gorm.DB { + t.Helper() + + // Путь к файлу базы + dbPath := filepath.Join("testdata", "test.db") + + // Удаляем старую базу, если есть + os.Remove(dbPath) + + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + t.Fatalf("failed to open test db: %v", err) + } + + // Миграция таблицы User для связи с ACL + if err := db.AutoMigrate(&user.User{}); err != nil { + t.Fatalf("failed to migrate User: %v", err) + } + + return db +} + +func TestACLService_CRUD(t *testing.T) { + db := openTestDB(t) + + // Создаём сервис ACL + svc, err := acl.NewService(db) + if err != nil { + t.Fatalf("failed to create ACL service: %v", err) + } + + if err := svc.Init(); err != nil { + t.Fatalf("failed to init ACL service: %v", err) + } + + // Создаём роли + if err := svc.CreateRole("admin"); err != nil { + t.Fatalf("CreateRole failed: %v", err) + } + if err := svc.CreateRole("guest"); err != nil { + t.Fatalf("CreateRole failed: %v", err) + } + + roles, err := svc.GetRoles() + if err != nil { + t.Fatalf("GetRoles failed: %v", err) + } + if len(roles) != 2 { + t.Fatalf("expected 2 roles, got %d", len(roles)) + } + + // Создаём ресурсы + if err := svc.CreateResource("*"); err != nil { + t.Fatalf("CreateResource failed: %v", err) + } + if err := svc.CreateResource("html.view.*"); err != nil { + t.Fatalf("CreateResource failed: %v", err) + } + + resources, err := svc.GetPermissions() + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + if len(resources) != 2 { + t.Fatalf("expected 2 resources, got %d", len(resources)) + } + + // 1. Создаём сервис user + store, err := user.NewGormUserStore(db) + if err != nil { + t.Fatalf("failed to create user store: %v", err) + } + userSvc, err := user.NewService(store) + if err != nil { + t.Fatalf("failed to create user service: %v", err) + } + + // 2. Инициализируем + if err := userSvc.Init(); err != nil { + t.Fatalf("failed to init user service: %v", err) + } + + user := &user.User{ + Username: "testuser", + Email: "testuser@example.com", + Password: "secret", + } + + u := user + + // 3. Создаём пользователя через сервис + err = userSvc.Create(user) + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + // Привязываем роль к пользователю + adminRoleID := roles[0].ID + if err := svc.AssignRoleToUser(adminRoleID, uint(u.ID)); err != nil { + t.Fatalf("AssignRoleToUser failed: %v", err) + } + + userRoles, err := svc.GetUserRoles(uint(u.ID)) + if err != nil { + t.Fatalf("GetUserRoles failed: %v", err) + } + if len(userRoles) != 1 || userRoles[0].ID != adminRoleID { + t.Fatalf("expected user to have admin role") + } + + // Привязываем ресурсы к роли + for _, res := range resources { + if err := svc.AssignResourceToRole(adminRoleID, res.ID); err != nil { + t.Fatalf("AssignResourceToRole failed: %v", err) + } + } + + roleResources, err := svc.GetRoleResources(adminRoleID) + if err != nil { + t.Fatalf("GetRoleResources failed: %v", err) + } + if len(roleResources) != 2 { + t.Fatalf("expected role to have 2 resources") + } + + // Удаляем ресурс из роли + if err := svc.RemoveResourceFromRole(adminRoleID, resources[0].ID); err != nil { + t.Fatalf("RemoveResourceFromRole failed: %v", err) + } + roleResources, _ = svc.GetRoleResources(adminRoleID) + if len(roleResources) != 1 { + t.Fatalf("expected 1 resource after removal") + } + + // Удаляем роль у пользователя + if err := svc.RemoveRoleFromUser(adminRoleID, uint(u.ID)); err != nil { + t.Fatalf("RemoveRoleFromUser failed: %v", err) + } + userRoles, _ = svc.GetUserRoles(uint(u.ID)) + if len(userRoles) != 0 { + t.Fatalf("expected user to have 0 roles after removal") + } +}