Files
mcias/internal/policy/engine_test.go
Kyle Isom 22158824bd Checkpoint: password reset, rule expiry, migrations
- Self-service and admin password-change endpoints
  (PUT /v1/auth/password, PUT /v1/accounts/{id}/password)
- Policy rule time-scoped expiry (not_before / expires_at)
  with migration 000006 and engine filtering
- golang-migrate integration; embedded SQL migrations
- PolicyRecord fieldalignment lint fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 14:38:38 -07:00

510 lines
14 KiB
Go

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