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) }