Files
egutierrez 65377afde4 feat: security/ YAML files + shell/security/ loader (issue 0024b)
Crea los tres archivos YAML de configuración de seguridad centralizada en
security/ (user-groups.yaml, agent-groups.yaml, permissions.yaml) y el
loader impuro shell/security/loader.go que los lee y construye un
security.SecurityPolicy puro.

- security/user-groups.yaml: grupos de usuarios (admins, everyone)
- security/agent-groups.yaml: grupos de agentes (assistants, all)
- security/permissions.yaml: políticas de permisos por grupo de agentes
- shell/security/loader.go: Load(dir) → SecurityPolicy; usa structs YAML
  intermedios para mantener pkg/security/ libre de gopkg.in/yaml.v3
- shell/security/loader_test.go: 6 tests cubren los casos del issue
  (dir inexistente, vacío, 3 YAMLs, solo uno, malformado, wildcards)

El código se mergea con feature flag centralized-security-groups = false
(loader creado, todavía no wired al launcher — eso es 0024c).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:32:05 +00:00

149 lines
3.8 KiB
Go

// Package security provides the impure loader for security policy YAML files.
// It reads security/ directory files and returns a pure security.SecurityPolicy.
package security
import (
"errors"
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
"github.com/enmanuel/agents/pkg/security"
)
// --- YAML intermediate types (private, only for parsing) ---
type yamlUserGroups struct {
Groups map[string]struct {
Members []string `yaml:"members"`
} `yaml:"groups"`
}
type yamlAgentGroups struct {
Groups map[string]struct {
Agents []string `yaml:"agents"`
} `yaml:"groups"`
}
type yamlPermissions struct {
Policies []struct {
AgentGroup string `yaml:"agent_group"`
Permissions []struct {
UserGroup string `yaml:"user_group"`
Actions []string `yaml:"actions"`
} `yaml:"permissions"`
} `yaml:"policies"`
}
// Load reads the security YAML files from dir and returns a SecurityPolicy.
// If dir does not exist or is empty, returns an empty policy without error.
// If an individual file is missing, that section is left empty.
// If a YAML file is malformed, returns an error naming the file.
func Load(dir string) (security.SecurityPolicy, error) {
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
return security.SecurityPolicy{}, nil
}
userGroups, err := loadUserGroups(filepath.Join(dir, "user-groups.yaml"))
if err != nil {
return security.SecurityPolicy{}, err
}
agentGroups, err := loadAgentGroups(filepath.Join(dir, "agent-groups.yaml"))
if err != nil {
return security.SecurityPolicy{}, err
}
policies, err := loadPermissions(filepath.Join(dir, "permissions.yaml"))
if err != nil {
return security.SecurityPolicy{}, err
}
return security.SecurityPolicy{
UserGroups: userGroups,
AgentGroups: agentGroups,
Policies: policies,
}, nil
}
func loadUserGroups(path string) ([]security.UserGroup, error) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("security: reading %s: %w", path, err)
}
var raw yamlUserGroups
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("security: parsing %s: %w", path, err)
}
groups := make([]security.UserGroup, 0, len(raw.Groups))
for name, g := range raw.Groups {
groups = append(groups, security.UserGroup{
Name: name,
Members: g.Members,
})
}
return groups, nil
}
func loadAgentGroups(path string) ([]security.AgentGroup, error) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("security: reading %s: %w", path, err)
}
var raw yamlAgentGroups
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("security: parsing %s: %w", path, err)
}
groups := make([]security.AgentGroup, 0, len(raw.Groups))
for name, g := range raw.Groups {
groups = append(groups, security.AgentGroup{
Name: name,
Agents: g.Agents,
})
}
return groups, nil
}
func loadPermissions(path string) ([]security.AgentPolicy, error) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("security: reading %s: %w", path, err)
}
var raw yamlPermissions
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("security: parsing %s: %w", path, err)
}
policies := make([]security.AgentPolicy, 0, len(raw.Policies))
for _, p := range raw.Policies {
perms := make([]security.Permission, 0, len(p.Permissions))
for _, perm := range p.Permissions {
perms = append(perms, security.Permission{
UserGroup: perm.UserGroup,
Actions: perm.Actions,
})
}
policies = append(policies, security.AgentPolicy{
AgentGroup: p.AgentGroup,
Permissions: perms,
})
}
return policies, nil
}