Files
breakpilot-lehrer/edu-search-service/internal/policy/loader.go
Benjamin Boenisch 414e0f5ec0
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
feat: edu-search-service migriert, voice-service/geo-service entfernt
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor)
- opensearch + edu-search-service in docker-compose.yml hinzugefuegt
- voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core)
- geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt)
- CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt
  (Go lint, test mit go mod download, build, SBOM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 18:36:38 +01:00

256 lines
6.3 KiB
Go

package policy
import (
"context"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// Loader handles loading policy configuration from YAML files.
type Loader struct {
store *Store
}
// NewLoader creates a new Loader instance.
func NewLoader(store *Store) *Loader {
return &Loader{store: store}
}
// LoadFromFile loads policy configuration from a YAML file.
func (l *Loader) LoadFromFile(ctx context.Context, path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read YAML file: %w", err)
}
config, err := ParseYAML(data)
if err != nil {
return fmt.Errorf("failed to parse YAML: %w", err)
}
return l.store.LoadFromYAML(ctx, config)
}
// ParseYAML parses YAML configuration data.
func ParseYAML(data []byte) (*BundeslaenderConfig, error) {
// First, parse as a generic map to handle the inline Bundeslaender
var rawConfig map[string]interface{}
if err := yaml.Unmarshal(data, &rawConfig); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
config := &BundeslaenderConfig{
Bundeslaender: make(map[string]PolicyConfig),
}
// Parse federal
if federal, ok := rawConfig["federal"]; ok {
if federalMap, ok := federal.(map[string]interface{}); ok {
config.Federal = parsePolicyConfig(federalMap)
}
}
// Parse default_operations
if ops, ok := rawConfig["default_operations"]; ok {
if opsMap, ok := ops.(map[string]interface{}); ok {
config.DefaultOperations = parseOperationsConfig(opsMap)
}
}
// Parse pii_rules
if rules, ok := rawConfig["pii_rules"]; ok {
if rulesSlice, ok := rules.([]interface{}); ok {
for _, rule := range rulesSlice {
if ruleMap, ok := rule.(map[string]interface{}); ok {
config.PIIRules = append(config.PIIRules, parsePIIRuleConfig(ruleMap))
}
}
}
}
// Parse Bundeslaender (2-letter codes)
bundeslaender := []string{"BW", "BY", "BE", "BB", "HB", "HH", "HE", "MV", "NI", "NW", "RP", "SL", "SN", "ST", "SH", "TH"}
for _, bl := range bundeslaender {
if blConfig, ok := rawConfig[bl]; ok {
if blMap, ok := blConfig.(map[string]interface{}); ok {
config.Bundeslaender[bl] = parsePolicyConfig(blMap)
}
}
}
return config, nil
}
func parsePolicyConfig(m map[string]interface{}) PolicyConfig {
pc := PolicyConfig{}
if name, ok := m["name"].(string); ok {
pc.Name = name
}
if sources, ok := m["sources"].([]interface{}); ok {
for _, src := range sources {
if srcMap, ok := src.(map[string]interface{}); ok {
pc.Sources = append(pc.Sources, parseSourceConfig(srcMap))
}
}
}
return pc
}
func parseSourceConfig(m map[string]interface{}) SourceConfig {
sc := SourceConfig{
TrustBoost: 0.5, // Default
}
if domain, ok := m["domain"].(string); ok {
sc.Domain = domain
}
if name, ok := m["name"].(string); ok {
sc.Name = name
}
if license, ok := m["license"].(string); ok {
sc.License = license
}
if legalBasis, ok := m["legal_basis"].(string); ok {
sc.LegalBasis = legalBasis
}
if citation, ok := m["citation_template"].(string); ok {
sc.CitationTemplate = citation
}
if trustBoost, ok := m["trust_boost"].(float64); ok {
sc.TrustBoost = trustBoost
}
return sc
}
func parseOperationsConfig(m map[string]interface{}) OperationsConfig {
oc := OperationsConfig{}
if lookup, ok := m["lookup"].(map[string]interface{}); ok {
oc.Lookup = parseOperationConfig(lookup)
}
if rag, ok := m["rag"].(map[string]interface{}); ok {
oc.RAG = parseOperationConfig(rag)
}
if training, ok := m["training"].(map[string]interface{}); ok {
oc.Training = parseOperationConfig(training)
}
if export, ok := m["export"].(map[string]interface{}); ok {
oc.Export = parseOperationConfig(export)
}
return oc
}
func parseOperationConfig(m map[string]interface{}) OperationConfig {
oc := OperationConfig{}
if allowed, ok := m["allowed"].(bool); ok {
oc.Allowed = allowed
}
if requiresCitation, ok := m["requires_citation"].(bool); ok {
oc.RequiresCitation = requiresCitation
}
return oc
}
func parsePIIRuleConfig(m map[string]interface{}) PIIRuleConfig {
rc := PIIRuleConfig{
Severity: "block", // Default
}
if name, ok := m["name"].(string); ok {
rc.Name = name
}
if ruleType, ok := m["type"].(string); ok {
rc.Type = ruleType
}
if pattern, ok := m["pattern"].(string); ok {
rc.Pattern = pattern
}
if severity, ok := m["severity"].(string); ok {
rc.Severity = severity
}
return rc
}
// LoadDefaults loads a minimal set of default data (for testing or when no YAML exists).
func (l *Loader) LoadDefaults(ctx context.Context) error {
// Create federal policy with KMK
federalPolicy, err := l.store.CreatePolicy(ctx, &CreateSourcePolicyRequest{
Name: "KMK & Bundesebene",
})
if err != nil {
return fmt.Errorf("failed to create federal policy: %w", err)
}
trustBoost := 0.95
legalBasis := "Amtliche Werke (§5 UrhG)"
citation := "Quelle: KMK, {title}, {date}"
_, err = l.store.CreateSource(ctx, &CreateAllowedSourceRequest{
PolicyID: federalPolicy.ID,
Domain: "kmk.org",
Name: "Kultusministerkonferenz",
License: LicenseParagraph5,
LegalBasis: &legalBasis,
CitationTemplate: &citation,
TrustBoost: &trustBoost,
})
if err != nil {
return fmt.Errorf("failed to create KMK source: %w", err)
}
// Create default PII rules
defaultRules := DefaultPIIRules()
for _, rule := range defaultRules {
_, err := l.store.CreatePIIRule(ctx, &CreatePIIRuleRequest{
Name: rule.Name,
RuleType: PIIRuleType(rule.Type),
Pattern: rule.Pattern,
Severity: PIISeverity(rule.Severity),
})
if err != nil {
return fmt.Errorf("failed to create PII rule %s: %w", rule.Name, err)
}
}
return nil
}
// HasData checks if the policy tables already have data.
func (l *Loader) HasData(ctx context.Context) (bool, error) {
policies, _, err := l.store.ListPolicies(ctx, &PolicyListFilter{Limit: 1})
if err != nil {
return false, err
}
return len(policies) > 0, nil
}
// LoadIfEmpty loads data from YAML only if tables are empty.
func (l *Loader) LoadIfEmpty(ctx context.Context, path string) error {
hasData, err := l.HasData(ctx)
if err != nil {
return err
}
if hasData {
return nil // Already has data, skip loading
}
// Check if file exists
if _, err := os.Stat(path); os.IsNotExist(err) {
// File doesn't exist, load defaults
return l.LoadDefaults(ctx)
}
return l.LoadFromFile(ctx, path)
}