package rbac import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "context" "fmt" "log/slog" "sync/atomic" ) type snapshot struct { rolePerms map[string]map[string]struct{} // roleName -> set of permissionKeys } type Service struct { store ports.RBACStore cache atomic.Value // holds *snapshot logger *slog.Logger } func NewService(store ports.RBACStore, logger *slog.Logger) *Service { s := &Service{ store: store, logger: logger, } // Initialize with empty snapshot s.cache.Store(&snapshot{rolePerms: make(map[string]map[string]struct{})}) return s } // HasPermission checks if a role has a specific permission key. // SUPER_ADMIN always returns true. func (s *Service) HasPermission(roleName, permKey string) bool { if roleName == string(domain.RoleSuperAdmin) { return true } snap := s.cache.Load().(*snapshot) perms, ok := snap.rolePerms[roleName] if !ok { return false } _, has := perms[permKey] return has } // Reload rebuilds the in-memory cache from the database. func (s *Service) Reload(ctx context.Context) error { rolePerms, err := s.store.GetAllRolesWithPermissions(ctx) if err != nil { return fmt.Errorf("rbac reload: %w", err) } s.cache.Store(&snapshot{rolePerms: rolePerms}) s.logger.Info("RBAC cache reloaded", "roles", len(rolePerms)) return nil } // SeedPermissions upserts all permission definitions into the database. func (s *Service) SeedPermissions(ctx context.Context) error { for _, p := range AllPermissions { if _, err := s.store.UpsertPermission(ctx, p); err != nil { return fmt.Errorf("seed permission %s: %w", p.Key, err) } } s.logger.Info("RBAC permissions seeded", "count", len(AllPermissions)) return nil } // SeedDefaultRolePermissions assigns the default permission sets to each system role. // It only assigns permissions if the role currently has zero permissions (first-time setup). func (s *Service) SeedDefaultRolePermissions(ctx context.Context) error { for roleName, permKeys := range DefaultRolePermissions { role, err := s.store.GetRoleByName(ctx, roleName) if err != nil { s.logger.Warn("skipping role permission seed: role not found", "role", roleName) continue } // Insert missing permissions without wiping existing role permissions. var permIDs []int64 for _, key := range permKeys { perm, err := s.store.GetPermissionByKey(ctx, key) if err != nil { s.logger.Warn("permission key not found during seed", "key", key, "role", roleName) continue } permIDs = append(permIDs, perm.ID) } if len(permIDs) == 0 { continue } if err := s.store.AddRolePermissions(ctx, role.ID, permIDs); err != nil { return fmt.Errorf("seed role permissions for %s: %w", roleName, err) } s.logger.Info("ensured default permissions for role", "role", roleName, "count", len(permIDs)) } return nil } // --- Role CRUD (pass-through to store + reload cache) --- func (s *Service) CreateRole(ctx context.Context, name, description string) (domain.RoleRecord, error) { role, err := s.store.CreateRole(ctx, name, description, false) if err != nil { return domain.RoleRecord{}, err } _ = s.Reload(ctx) return role, nil } func (s *Service) GetRoleByID(ctx context.Context, id int64) (domain.RoleRecord, error) { return s.store.GetRoleByID(ctx, id) } func (s *Service) ListRoles(ctx context.Context, filter domain.RoleListFilter) ([]domain.RoleRecord, int64, error) { return s.store.ListRoles(ctx, filter) } func (s *Service) UpdateRole(ctx context.Context, id int64, name, description string) error { if err := s.store.UpdateRole(ctx, id, name, description); err != nil { return err } _ = s.Reload(ctx) return nil } func (s *Service) DeleteRole(ctx context.Context, id int64) error { if err := s.store.DeleteRole(ctx, id); err != nil { return err } _ = s.Reload(ctx) return nil } // --- Permission queries --- func (s *Service) ListPermissions(ctx context.Context) ([]domain.Permission, error) { return s.store.ListPermissions(ctx) } func (s *Service) ListPermissionGroups(ctx context.Context) ([]string, error) { return s.store.ListPermissionGroups(ctx) } // --- Role-Permission management --- func (s *Service) SetRolePermissions(ctx context.Context, roleID int64, permissionIDs []int64) error { if err := s.store.SetRolePermissions(ctx, roleID, permissionIDs); err != nil { return err } _ = s.Reload(ctx) return nil } func (s *Service) GetRolePermissions(ctx context.Context, roleID int64) ([]domain.Permission, error) { return s.store.GetRolePermissions(ctx, roleID) }