package policy import ( "path" "sort" ) // Action represents a registry operation subject to policy evaluation. type Action string const ( ActionVersionCheck Action = "registry:version_check" ActionPull Action = "registry:pull" ActionPush Action = "registry:push" ActionDelete Action = "registry:delete" ActionCatalog Action = "registry:catalog" ActionPolicyManage Action = "policy:manage" ) // Effect is the outcome of a policy decision. type Effect string const ( Allow Effect = "allow" Deny Effect = "deny" ) // PolicyInput describes the request being evaluated against policy rules. type PolicyInput struct { Subject string // MCIAS account UUID AccountType string // "human" or "system" Roles []string // roles from MCIAS JWT Action Action Repository string // target repository name; empty for global operations } // Rule is a policy rule that grants or denies access. type Rule struct { ID int64 Priority int Description string Effect Effect // Principal conditions (all populated fields are ANDed). Roles []string // principal must hold at least one AccountTypes []string // "human", "system", or both SubjectUUID string // exact principal UUID // Action condition. Actions []Action // Resource condition. Repositories []string // glob patterns via path.Match; empty = wildcard } // Evaluate applies rules to an input and returns the resulting effect and // the decisive rule (nil when the default deny applies). It implements // deny-wins, default-deny semantics per ARCHITECTURE.md ยง4. func Evaluate(input PolicyInput, rules []Rule) (Effect, *Rule) { // Sort by priority ascending (stable). sorted := make([]Rule, len(rules)) copy(sorted, rules) sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Priority < sorted[j].Priority }) // Collect all matching rules. var matched []*Rule for i := range sorted { if ruleMatches(&sorted[i], &input) { matched = append(matched, &sorted[i]) } } // Deny-wins: if any matching rule denies, return deny. for _, r := range matched { if r.Effect == Deny { return Deny, r } } // First allow wins. for _, r := range matched { if r.Effect == Allow { return Allow, r } } // Default deny. return Deny, nil } // ruleMatches checks whether a single rule matches the given input. // All populated fields are ANDed; empty fields are wildcards. func ruleMatches(rule *Rule, input *PolicyInput) bool { if !matchRoles(rule.Roles, input.Roles) { return false } if !matchAccountTypes(rule.AccountTypes, input.AccountType) { return false } if rule.SubjectUUID != "" && rule.SubjectUUID != input.Subject { return false } if !matchActions(rule.Actions, input.Action) { return false } return matchRepositories(rule.Repositories, input.Repository) } // matchRoles returns true if the rule's Roles field is empty (wildcard) // or if the principal holds at least one of the listed roles. func matchRoles(ruleRoles, inputRoles []string) bool { if len(ruleRoles) == 0 { return true } for _, rr := range ruleRoles { for _, ir := range inputRoles { if rr == ir { return true } } } return false } // matchAccountTypes returns true if the rule's AccountTypes field is empty // (wildcard) or if the principal's account type is in the list. func matchAccountTypes(ruleTypes []string, inputType string) bool { if len(ruleTypes) == 0 { return true } for _, rt := range ruleTypes { if rt == inputType { return true } } return false } // matchActions returns true if the rule's Actions field is empty (wildcard) // or if the request action is in the list. func matchActions(ruleActions []Action, inputAction Action) bool { if len(ruleActions) == 0 { return true } for _, ra := range ruleActions { if ra == inputAction { return true } } return false } // matchRepositories returns true if the rule's Repositories field is empty // (wildcard) or if the request repository matches at least one glob pattern. // When input.Repository is empty (global operations), only rules with an // empty Repositories field match. func matchRepositories(patterns []string, repo string) bool { if len(patterns) == 0 { return true } if repo == "" { return false } for _, p := range patterns { if matched, err := path.Match(p, repo); err == nil && matched { return true } } return false }