Enhance policy system with global rules and age validation
- 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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.16.1
|
||||
v0.16.2
|
||||
Reference in New Issue
Block a user