package policy import ( "encoding/json" "fmt" "sync" "time" ) // Engine wraps the stateless Evaluate function with an in-memory cache of // operator rules loaded from the database. Built-in default rules are always // merged in at evaluation time; they do not appear in the cache. // // The Engine is safe for concurrent use. Call Reload() after any change to the // policy_rules table to refresh the cached rule set without restarting. type Engine struct { rules []Rule mu sync.RWMutex } // NewEngine creates an Engine with an initially empty operator rule set. // Call Reload (or load rules directly) before use in production. func NewEngine() *Engine { return &Engine{} } // SetRules atomically replaces the cached operator rule set. // records is a slice of PolicyRuleRecord values (from the database layer). // Only enabled records are converted to Rule values. // // Security: rule_json is decoded into a RuleBody struct before being merged // into a Rule. This prevents the database from injecting values into the ID or // Description fields that are stored as dedicated columns. func (e *Engine) SetRules(records []PolicyRecord) error { now := time.Now() rules := make([]Rule, 0, len(records)) for _, rec := range records { if !rec.Enabled { continue } // Skip rules outside their validity window. if rec.NotBefore != nil && now.Before(*rec.NotBefore) { continue } if rec.ExpiresAt != nil && now.After(*rec.ExpiresAt) { continue } var body RuleBody if err := json.Unmarshal([]byte(rec.RuleJSON), &body); err != nil { return fmt.Errorf("policy: decode rule %d %q: %w", rec.ID, rec.Description, err) } rules = append(rules, Rule{ ID: rec.ID, Description: rec.Description, Priority: rec.Priority, Roles: body.Roles, AccountTypes: body.AccountTypes, SubjectUUID: body.SubjectUUID, Actions: body.Actions, ResourceType: body.ResourceType, OwnerMatchesSubject: body.OwnerMatchesSubject, ServiceNames: body.ServiceNames, RequiredTags: body.RequiredTags, Effect: body.Effect, }) } e.mu.Lock() e.rules = rules e.mu.Unlock() return nil } // Evaluate runs the policy engine against the given input using the cached // operator rules plus compiled-in defaults. func (e *Engine) Evaluate(input PolicyInput) (Effect, *Rule) { e.mu.RLock() rules := e.rules e.mu.RUnlock() return Evaluate(input, rules) } // PolicyRecord is the minimal interface the Engine needs from the DB layer. // Using a local struct avoids importing the db or model packages from policy, // which would create a dependency cycle. type PolicyRecord struct { NotBefore *time.Time ExpiresAt *time.Time Description string RuleJSON string ID int64 Priority int Enabled bool }