Enhance policy system with global rules and age validation
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled

- Updated policy configuration to include global rules applicable to all events, allowing for site-wide security policies.
- Introduced age validation features to prevent replay and clock skew attacks, with configurable maximum age limits for events.
- Enhanced example policy and README documentation to reflect new global rules and age validation capabilities.
- Added comprehensive tests for global rule checks and age validation scenarios.
- Bumped version to v0.16.2.
This commit is contained in:
2025-10-16 12:41:32 +01:00
parent 585ce11f71
commit db941a18ea
8 changed files with 677 additions and 119 deletions

View File

@@ -60,6 +60,10 @@ type Rule struct {
Privileged bool `json:"privileged,omitempty"`
// RateLimit is the amount of data can be written to the relay per second by the authenticated pubkey. If 0, there is no rate limit. This is applied via the use of an EWMA of the event publication history on the authenticated connection
RateLimit *int64 `json:"rate_limit,omitempty"`
// MaxAgeOfEvent is the offset in seconds that is the oldest timestamp allowed for an event's created_at time. If 0, there is no maximum age. Events must have a created_at time if this is set, and it must be no more than this value in the past compared to the current time.
MaxAgeOfEvent *int64 `json:"max_age_of_event,omitempty"`
// MaxAgeEventInFuture is the offset in seconds that is the newest timestamp allowed for an event's created_at time ahead of the current time.
MaxAgeEventInFuture *int64 `json:"max_age_event_in_future,omitempty"`
}
// PolicyEvent represents an event with additional context for policy scripts
@@ -131,8 +135,10 @@ type P struct {
Kind Kinds `json:"kind"`
// Rules is a map of rules for criteria that must be met for the event to be allowed to be written to the relay.
Rules map[int]Rule `json:"rules"`
// Global is a rule set that applies to all events.
Global Rule `json:"global"`
// Manager handles policy script execution
Manager *PolicyManager
Manager *PolicyManager `json:"-"`
}
// New creates a new policy from JSON configuration
@@ -215,7 +221,12 @@ func (p *P) CheckPolicy(access string, ev *event.E, loggedInPubkey []byte, ipAdd
return false, fmt.Errorf("event cannot be nil")
}
// First check kinds white/blacklist
// First check global rule filter (applies to all events)
if !p.checkGlobalRulePolicy(access, ev, loggedInPubkey) {
return false, nil
}
// Then check kinds white/blacklist
if !p.checkKindsPolicy(ev.Kind) {
return false, nil
}
@@ -223,7 +234,7 @@ func (p *P) CheckPolicy(access string, ev *event.E, loggedInPubkey []byte, ipAdd
// Get rule for this kind
rule, hasRule := p.Rules[int(ev.Kind)]
if !hasRule {
// No specific rule for this kind, allow if kinds policy passed
// No specific rule for this kind, allow if global and kinds policy passed
return true, nil
}
@@ -260,6 +271,17 @@ func (p *P) checkKindsPolicy(kind uint16) bool {
return true
}
// checkGlobalRulePolicy checks if the event passes the global rule filter
func (p *P) checkGlobalRulePolicy(access string, ev *event.E, loggedInPubkey []byte) bool {
// Apply global rule filtering
allowed, err := p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
if err != nil {
log.E.F("global rule policy check failed: %v", err)
return false
}
return allowed
}
// checkRulePolicy applies rule-based filtering (pubkey lists, size limits, etc.)
func (p *P) checkRulePolicy(access string, ev *event.E, rule Rule, loggedInPubkey []byte) (allowed bool, err error) {
pubkeyHex := hex.Enc(ev.Pubkey)
@@ -340,6 +362,24 @@ func (p *P) checkRulePolicy(access string, ev *event.E, rule Rule, loggedInPubke
// TODO: Parse and validate expiry time
}
// Check MaxAgeOfEvent (maximum age of event in seconds)
if rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 {
currentTime := time.Now().Unix()
maxAllowedTime := currentTime - *rule.MaxAgeOfEvent
if ev.CreatedAt < maxAllowedTime {
return false, nil // Event is too old
}
}
// Check MaxAgeEventInFuture (maximum time event can be in the future in seconds)
if rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 {
currentTime := time.Now().Unix()
maxFutureTime := currentTime + *rule.MaxAgeEventInFuture
if ev.CreatedAt > maxFutureTime {
return false, nil // Event is too far in the future
}
}
// Check privileged events
if rule.Privileged {
if len(loggedInPubkey) == 0 {

View File

@@ -814,27 +814,201 @@ func TestEdgeCasesManagerDoubleStart(t *testing.T) {
}
}
func TestEdgeCasesManagerDoubleStop(t *testing.T) {
// Test double stop without actually starting (simpler test)
ctx := context.Background()
manager := &PolicyManager{
ctx: ctx,
configDir: "/tmp",
scriptPath: "/tmp/policy.sh",
enabled: true,
disabled: false,
responseChan: make(chan PolicyResponse, 100),
func TestCheckGlobalRulePolicy(t *testing.T) {
tests := []struct {
name string
globalRule Rule
event *event.E
loggedInPubkey []byte
expected bool
}{
{
name: "global rule with write allow - event allowed",
globalRule: Rule{
WriteAllow: []string{"746573742d7075626b6579"},
},
event: createTestEvent("test-id", "test-pubkey", "test content", 1),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: true,
},
{
name: "global rule with write deny - event denied",
globalRule: Rule{
WriteDeny: []string{"746573742d7075626b6579"},
},
event: createTestEvent("test-id", "test-pubkey", "test content", 1),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: false,
},
{
name: "global rule with size limit - event too large",
globalRule: Rule{
SizeLimit: func() *int64 { v := int64(10); return &v }(),
},
event: createTestEvent("test-id", "test-pubkey", "this is a very long content that exceeds the size limit", 1),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: false,
},
{
name: "global rule with max age of event - event too old",
globalRule: Rule{
MaxAgeOfEvent: func() *int64 { v := int64(3600); return &v }(), // 1 hour
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() - 7200 // 2 hours ago
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: false,
},
{
name: "global rule with max age event in future - event too far in future",
globalRule: Rule{
MaxAgeEventInFuture: func() *int64 { v := int64(3600); return &v }(), // 1 hour
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() + 7200 // 2 hours in future
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: false,
},
}
// Try to stop when not running - should fail
err := manager.StopPolicy()
if err == nil {
t.Error("Expected error when stopping policy manager that's not running")
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policy := &P{
Global: tt.globalRule,
}
// Try to stop again - should still fail
err = manager.StopPolicy()
if err == nil {
t.Error("Expected error when stopping policy manager twice")
result := policy.checkGlobalRulePolicy("write", tt.event, tt.loggedInPubkey)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestCheckPolicyWithGlobalRule(t *testing.T) {
// Test that global rule is applied first
policy := &P{
Global: Rule{
WriteDeny: []string{"746573742d7075626b6579"}, // Deny test-pubkey globally
},
Kind: Kinds{
Whitelist: []int{1}, // Allow kind 1
},
Rules: map[int]Rule{
1: {
WriteAllow: []string{"746573742d7075626b6579"}, // Allow test-pubkey for kind 1
},
},
}
event := createTestEvent("test-id", "test-pubkey", "test content", 1)
loggedInPubkey := []byte("test-logged-in-pubkey")
// Global rule should deny this event even though kind-specific rule would allow it
allowed, err := policy.CheckPolicy("write", event, loggedInPubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy failed: %v", err)
}
if allowed {
t.Error("Expected event to be denied by global rule, but it was allowed")
}
}
func TestMaxAgeChecks(t *testing.T) {
tests := []struct {
name string
rule Rule
event *event.E
loggedInPubkey []byte
expected bool
}{
{
name: "max age of event - event within allowed age",
rule: Rule{
MaxAgeOfEvent: func() *int64 { v := int64(3600); return &v }(), // 1 hour
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() - 1800 // 30 minutes ago
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: true,
},
{
name: "max age of event - event too old",
rule: Rule{
MaxAgeOfEvent: func() *int64 { v := int64(3600); return &v }(), // 1 hour
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() - 7200 // 2 hours ago
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: false,
},
{
name: "max age event in future - event within allowed future time",
rule: Rule{
MaxAgeEventInFuture: func() *int64 { v := int64(3600); return &v }(), // 1 hour
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() + 1800 // 30 minutes in future
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: true,
},
{
name: "max age event in future - event too far in future",
rule: Rule{
MaxAgeEventInFuture: func() *int64 { v := int64(3600); return &v }(), // 1 hour
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() + 7200 // 2 hours in future
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: false,
},
{
name: "both age checks - event within both limits",
rule: Rule{
MaxAgeOfEvent: func() *int64 { v := int64(3600); return &v }(), // 1 hour
MaxAgeEventInFuture: func() *int64 { v := int64(1800); return &v }(), // 30 minutes
},
event: func() *event.E {
ev := createTestEvent("test-id", "test-pubkey", "test content", 1)
ev.CreatedAt = time.Now().Unix() + 900 // 15 minutes in future
return ev
}(),
loggedInPubkey: []byte("test-logged-in-pubkey"),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policy := &P{}
allowed, err := policy.checkRulePolicy("write", tt.event, tt.rule, tt.loggedInPubkey)
if err != nil {
t.Fatalf("checkRulePolicy failed: %v", err)
}
if allowed != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, allowed)
}
})
}
}