package policy import ( "testing" "time" ) // adminInput is a convenience helper for building admin PolicyInputs. func adminInput(action Action, resType ResourceType) PolicyInput { return PolicyInput{ Subject: "admin-uuid", AccountType: "human", Roles: []string{"admin"}, Action: action, Resource: Resource{Type: resType}, } } func TestEvaluate_DefaultDeny(t *testing.T) { // No operator rules, non-admin subject: should hit default-deny for an // action that is not covered by built-in self-service defaults. input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionListAccounts, Resource: Resource{Type: ResourceAccount}, } effect, rule := Evaluate(input, nil) if effect != Deny { t.Errorf("expected Deny, got %s", effect) } if rule != nil { t.Errorf("expected nil rule on default-deny, got %+v", rule) } } func TestEvaluate_AdminWildcard(t *testing.T) { actions := []Action{ ActionListAccounts, ActionCreateAccount, ActionReadPGCreds, ActionWritePGCreds, ActionReadAudit, ActionManageRules, } for _, a := range actions { t.Run(string(a), func(t *testing.T) { effect, rule := Evaluate(adminInput(a, ResourceAccount), nil) if effect != Allow { t.Errorf("admin should be allowed %s, got Deny", a) } if rule == nil || rule.ID != -1 { t.Errorf("expected admin wildcard rule (-1), got %v", rule) } }) } } func TestEvaluate_SelfServiceLogout(t *testing.T) { input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionLogout, Resource: Resource{Type: ResourceToken}, } effect, _ := Evaluate(input, nil) if effect != Allow { t.Error("expected any authenticated user to be allowed to logout") } } func TestEvaluate_SelfServiceRenew(t *testing.T) { input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionRenewToken, Resource: Resource{Type: ResourceToken}, } effect, _ := Evaluate(input, nil) if effect != Allow { t.Error("expected any authenticated user to be allowed to renew token") } } func TestEvaluate_SystemOwnPGCreds(t *testing.T) { input := PolicyInput{ Subject: "svc-uuid", AccountType: "system", Roles: []string{}, Action: ActionReadPGCreds, Resource: Resource{ Type: ResourcePGCreds, OwnerUUID: "svc-uuid", // owner matches subject }, } effect, rule := Evaluate(input, nil) if effect != Allow { t.Errorf("system account should be allowed to read own pgcreds, got Deny") } if rule == nil || rule.ID != -4 { t.Errorf("expected built-in rule -4, got %v", rule) } } func TestEvaluate_SystemOtherPGCreds_Denied(t *testing.T) { // System account trying to read another system account's pgcreds. input := PolicyInput{ Subject: "svc-uuid", AccountType: "system", Roles: []string{}, Action: ActionReadPGCreds, Resource: Resource{ Type: ResourcePGCreds, OwnerUUID: "other-svc-uuid", // different owner }, } effect, _ := Evaluate(input, nil) if effect != Allow { // This is the expected behavior: default-deny. return } t.Error("system account must not read another account's pgcreds without an explicit rule") } func TestEvaluate_DenyWins(t *testing.T) { // Operator adds a Deny rule for a specific subject; a broader Allow rule // also matches. Deny must win regardless of order. operatorRules := []Rule{ { ID: 1, Description: "broad allow", Priority: 100, Actions: []Action{ActionReadPGCreds}, ResourceType: ResourcePGCreds, Effect: Allow, }, { ID: 2, Description: "specific deny", Priority: 50, // higher precedence than the allow SubjectUUID: "bad-actor-uuid", ResourceType: ResourcePGCreds, Effect: Deny, }, } input := PolicyInput{ Subject: "bad-actor-uuid", AccountType: "human", Roles: []string{}, Action: ActionReadPGCreds, Resource: Resource{Type: ResourcePGCreds}, } effect, rule := Evaluate(input, operatorRules) if effect != Deny { t.Errorf("deny rule should win over allow rule, got Allow") } if rule == nil || rule.ID != 2 { t.Errorf("expected deny rule ID 2, got %v", rule) } } func TestEvaluate_ServiceNameGating(t *testing.T) { operatorRules := []Rule{ { ID: 3, Description: "alice may read payments-api pgcreds", Priority: 50, Roles: []string{"svc:payments-api"}, Actions: []Action{ActionReadPGCreds}, ResourceType: ResourcePGCreds, ServiceNames: []string{"payments-api"}, Effect: Allow, }, } alice := PolicyInput{ Subject: "alice-uuid", AccountType: "human", Roles: []string{"svc:payments-api"}, Action: ActionReadPGCreds, Resource: Resource{ Type: ResourcePGCreds, ServiceName: "payments-api", }, } effect, _ := Evaluate(alice, operatorRules) if effect != Allow { t.Error("alice should be allowed to read payments-api pgcreds") } // Same principal, wrong service — should be denied. alice.Resource.ServiceName = "user-service" effect, _ = Evaluate(alice, operatorRules) if effect != Deny { t.Error("alice should be denied access to user-service pgcreds") } } func TestEvaluate_MachineTagGating(t *testing.T) { operatorRules := []Rule{ { ID: 4, Description: "deploy-agent: staging only", Priority: 50, SubjectUUID: "deploy-agent-uuid", Actions: []Action{ActionReadPGCreds}, ResourceType: ResourcePGCreds, RequiredTags: []string{"env:staging"}, Effect: Allow, }, { ID: 5, Description: "deploy-agent: deny production (belt-and-suspenders)", Priority: 10, // evaluated before the allow SubjectUUID: "deploy-agent-uuid", ResourceType: ResourcePGCreds, RequiredTags: []string{"env:production"}, Effect: Deny, }, } staging := PolicyInput{ Subject: "deploy-agent-uuid", AccountType: "system", Roles: []string{}, Action: ActionReadPGCreds, Resource: Resource{ Type: ResourcePGCreds, Tags: []string{"env:staging", "svc:payments-api"}, }, } effect, _ := Evaluate(staging, operatorRules) if effect != Allow { t.Error("deploy-agent should be allowed to read staging pgcreds") } production := staging production.Resource.Tags = []string{"env:production", "svc:payments-api"} effect, rule := Evaluate(production, operatorRules) if effect != Deny { t.Error("deploy-agent should be denied access to production pgcreds") } if rule == nil || rule.ID != 5 { t.Errorf("expected deny rule ID 5 for production, got %v", rule) } } func TestEvaluate_OwnerMatchesSubject(t *testing.T) { // Operator rule: a user may read account details for accounts they own. operatorRules := []Rule{ { ID: 6, Description: "principals may read their own account", Priority: 50, Actions: []Action{ActionReadAccount}, ResourceType: ResourceAccount, OwnerMatchesSubject: true, Effect: Allow, }, } // Reading own account — should be allowed. own := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionReadAccount, Resource: Resource{ Type: ResourceAccount, OwnerUUID: "user-uuid", }, } effect, _ := Evaluate(own, operatorRules) if effect != Allow { t.Error("user should be allowed to read their own account") } // Reading another user's account — should be denied. other := own other.Resource.OwnerUUID = "other-uuid" effect, _ = Evaluate(other, operatorRules) if effect != Deny { t.Error("user must not read another user's account without an explicit rule") } } func TestEvaluate_PriorityOrder(t *testing.T) { // Two Allow rules at different priorities: the lower-priority number wins. operatorRules := []Rule{ {ID: 10, Description: "low priority allow", Priority: 200, Actions: []Action{ActionReadAudit}, Effect: Allow}, {ID: 11, Description: "high priority allow", Priority: 10, Actions: []Action{ActionReadAudit}, Effect: Allow}, } input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionReadAudit, Resource: Resource{Type: ResourceAuditLog}, } _, rule := Evaluate(input, operatorRules) if rule == nil || rule.ID != 11 { t.Errorf("expected higher-priority rule (ID 11) to match first, got %v", rule) } } func TestEvaluate_MultipleRequiredTags(t *testing.T) { // RequiredTags requires ALL tags to be present. operatorRules := []Rule{ { ID: 20, Description: "allow if both env:staging and svc:payments-api tags present", Priority: 50, Actions: []Action{ActionReadPGCreds}, ResourceType: ResourcePGCreds, RequiredTags: []string{"env:staging", "svc:payments-api"}, Effect: Allow, }, } // Both tags present — allowed. input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionReadPGCreds, Resource: Resource{ Type: ResourcePGCreds, Tags: []string{"env:staging", "svc:payments-api", "extra:tag"}, }, } effect, _ := Evaluate(input, operatorRules) if effect != Allow { t.Error("both required tags present: should be allowed") } // Only one tag present — denied (default-deny). input.Resource.Tags = []string{"env:staging"} effect, _ = Evaluate(input, operatorRules) if effect != Deny { t.Error("only one required tag present: should be denied") } // No tags — denied. input.Resource.Tags = nil effect, _ = Evaluate(input, operatorRules) if effect != Deny { t.Error("no tags: should be denied") } } func TestEvaluate_AccountTypeGating(t *testing.T) { // Rule only applies to system accounts. operatorRules := []Rule{ { ID: 30, Description: "system accounts may list accounts", Priority: 50, AccountTypes: []string{"system"}, Actions: []Action{ActionListAccounts}, Effect: Allow, }, } sysInput := PolicyInput{ Subject: "svc-uuid", AccountType: "system", Roles: []string{}, Action: ActionListAccounts, Resource: Resource{Type: ResourceAccount}, } effect, _ := Evaluate(sysInput, operatorRules) if effect != Allow { t.Error("system account should be allowed by account-type rule") } humanInput := sysInput humanInput.AccountType = "human" effect, _ = Evaluate(humanInput, operatorRules) if effect != Deny { t.Error("human account should not match system-only rule") } } // ---- Engine.SetRules time-filtering tests ---- func TestSetRules_SkipsExpiredRule(t *testing.T) { engine := NewEngine() past := time.Now().Add(-1 * time.Hour) err := engine.SetRules([]PolicyRecord{ { ID: 1, Description: "expired", Priority: 100, RuleJSON: `{"effect":"allow","actions":["accounts:list"]}`, Enabled: true, ExpiresAt: &past, }, }) if err != nil { t.Fatalf("SetRules: %v", err) } // The expired rule should not be in the cache; evaluation should deny. input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionListAccounts, Resource: Resource{Type: ResourceAccount}, } effect, _ := engine.Evaluate(input) if effect != Deny { t.Error("expired rule should not match; expected Deny") } } func TestSetRules_SkipsNotYetActiveRule(t *testing.T) { engine := NewEngine() future := time.Now().Add(1 * time.Hour) err := engine.SetRules([]PolicyRecord{ { ID: 2, Description: "not yet active", Priority: 100, RuleJSON: `{"effect":"allow","actions":["accounts:list"]}`, Enabled: true, NotBefore: &future, }, }) if err != nil { t.Fatalf("SetRules: %v", err) } input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionListAccounts, Resource: Resource{Type: ResourceAccount}, } effect, _ := engine.Evaluate(input) if effect != Deny { t.Error("future not_before rule should not match; expected Deny") } } func TestSetRules_IncludesActiveWindowRule(t *testing.T) { engine := NewEngine() past := time.Now().Add(-1 * time.Hour) future := time.Now().Add(1 * time.Hour) err := engine.SetRules([]PolicyRecord{ { ID: 3, Description: "currently active", Priority: 100, RuleJSON: `{"effect":"allow","actions":["accounts:list"]}`, Enabled: true, NotBefore: &past, ExpiresAt: &future, }, }) if err != nil { t.Fatalf("SetRules: %v", err) } input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionListAccounts, Resource: Resource{Type: ResourceAccount}, } effect, _ := engine.Evaluate(input) if effect != Allow { t.Error("rule within its active window should match; expected Allow") } } func TestSetRules_NilTimesAlwaysActive(t *testing.T) { engine := NewEngine() err := engine.SetRules([]PolicyRecord{ { ID: 4, Description: "no time constraints", Priority: 100, RuleJSON: `{"effect":"allow","actions":["accounts:list"]}`, Enabled: true, // NotBefore and ExpiresAt are both nil. }, }) if err != nil { t.Fatalf("SetRules: %v", err) } input := PolicyInput{ Subject: "user-uuid", AccountType: "human", Roles: []string{}, Action: ActionListAccounts, Resource: Resource{Type: ResourceAccount}, } effect, _ := engine.Evaluate(input) if effect != Allow { t.Error("nil time fields mean always active; expected Allow") } }