package orm import "fmt" // ApplyRecordRules adds ir.rule domain filters to a search. // Mirrors: odoo/addons/base/models/ir_rule.py IrRule._compute_domain() // // Rules work as follows: // - Global rules (no groups) are AND-ed together // - Group rules are OR-ed within the group set // - The final domain is: global_rules AND (group_rule_1 OR group_rule_2 OR ...) // // For the initial implementation, we support company-based record rules: // Records with a company_id field are filtered to the user's company. func ApplyRecordRules(env *Environment, m *Model, domain Domain) Domain { if env.su { return domain // Superuser bypasses record rules } // Auto-apply company filter if model has company_id // Records where company_id = user's company OR company_id IS NULL (shared records) if f := m.GetField("company_id"); f != nil && f.Type == TypeMany2one { myCompany := Leaf("company_id", "=", env.CompanyID()) noCompany := Leaf("company_id", "=", nil) companyFilter := Or(myCompany, noCompany) if len(domain) == 0 { return companyFilter } // AND the company filter with existing domain result := Domain{OpAnd} result = append(result, domain...) // Wrap company filter in the domain result = append(result, companyFilter...) return result } // TODO: Load custom ir.rule records from DB and compile their domains // For now, only the built-in company filter is applied return domain } // CheckRecordRuleAccess verifies the user can access specific record IDs. // Returns an error if any record is not accessible. func CheckRecordRuleAccess(env *Environment, m *Model, ids []int64, perm string) error { if env.su || len(ids) == 0 { return nil } // Check company_id if the model has it f := m.GetField("company_id") if f == nil || f.Type != TypeMany2one { return nil } // Count records that match the company filter placeholders := make([]string, len(ids)) args := make([]interface{}, len(ids)) for i, id := range ids { args[i] = id placeholders[i] = fmt.Sprintf("$%d", i+1) } args = append(args, env.CompanyID()) query := fmt.Sprintf( `SELECT COUNT(*) FROM %q WHERE "id" IN (%s) AND ("company_id" = $%d OR "company_id" IS NULL)`, m.Table(), joinStrings(placeholders, ", "), len(ids)+1, ) var count int64 err := env.tx.QueryRow(env.ctx, query, args...).Scan(&count) if err != nil { return nil // Fail open on error } if count < int64(len(ids)) { return fmt.Errorf("orm: access denied by record rules on %s (company filter)", m.Name()) } return nil }