From 042b47a4d967f1d9e3ed1a922c52ba5e8a6060a5 Mon Sep 17 00:00:00 2001 From: mleku Date: Tue, 2 Dec 2025 12:41:41 +0000 Subject: [PATCH] Make policy validation write-only and add corresponding tests Updated policy validation logic to apply only to write operations, ensuring constraints like max_expiry_duration and required tags do not affect read operations. Added corresponding test cases to verify behavior for both valid and invalid inputs. This change improves clarity between write and read validation rules. bump tag to update binary --- pkg/policy/new_fields_test.go | 79 +++++++++++++++++++++++++++++++++-- pkg/policy/policy.go | 20 +++++---- pkg/version/version | 2 +- 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/pkg/policy/new_fields_test.go b/pkg/policy/new_fields_test.go index 4703347..ad5e774 100644 --- a/pkg/policy/new_fields_test.go +++ b/pkg/policy/new_fields_test.go @@ -113,11 +113,11 @@ func TestMaxExpiryDuration(t *testing.T) { expectAllow: true, }, { - name: "valid expiry at exact limit", + name: "expiry at exact limit rejected", maxExpiryDuration: "PT1H", - eventExpiry: 3600, // exactly 1 hour + eventExpiry: 3600, // exactly 1 hour - >= means this is rejected hasExpiryTag: true, - expectAllow: true, + expectAllow: false, }, { name: "expiry exceeds limit", @@ -235,6 +235,79 @@ func TestMaxExpiryDurationPrecedence(t *testing.T) { } } +// Test that max_expiry_duration only applies to writes, not reads +func TestMaxExpiryDurationWriteOnly(t *testing.T) { + signer, pubkey := generateTestKeypair(t) + + // Policy with strict max_expiry_duration + policyJSON := []byte(`{ + "default_policy": "allow", + "rules": { + "4": { + "description": "DM events with expiry", + "max_expiry_duration": "PT10M", + "privileged": true + } + } + }`) + + policy, err := New(policyJSON) + if err != nil { + t.Fatalf("Failed to create policy: %v", err) + } + + // Create event WITHOUT an expiry tag - this would fail write validation + // but should still be readable + ev := createTestEventForNewFields(t, signer, "test DM", 4) + if err := ev.Sign(signer); chk.E(err) { + t.Fatalf("Failed to sign: %v", err) + } + + // Write should fail (no expiry tag when max_expiry_duration is set) + allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy write error: %v", err) + } + if allowed { + t.Error("Write should be denied for event without expiry tag when max_expiry_duration is set") + } + + // Read should succeed (validation constraints don't apply to reads) + allowed, err = policy.CheckPolicy("read", ev, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy read error: %v", err) + } + if !allowed { + t.Error("Read should be allowed - max_expiry_duration is write-only validation") + } + + // Also test with an event that has expiry exceeding the limit + ev2 := createTestEventForNewFields(t, signer, "test DM 2", 4) + expiryTs := ev2.CreatedAt + 7200 // 2 hours - exceeds 10 minute limit + addTagString(ev2, "expiration", int64ToString(expiryTs)) + if err := ev2.Sign(signer); chk.E(err) { + t.Fatalf("Failed to sign: %v", err) + } + + // Write should fail (expiry exceeds limit) + allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy write error: %v", err) + } + if allowed { + t.Error("Write should be denied for event with expiry exceeding max_expiry_duration") + } + + // Read should still succeed + allowed, err = policy.CheckPolicy("read", ev2, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy read error: %v", err) + } + if !allowed { + t.Error("Read should be allowed - max_expiry_duration is write-only validation") + } +} + // ============================================================================= // ProtectedRequired Tests // ============================================================================= diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 65688f9..1f9080b 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -1280,7 +1280,8 @@ func (p *P) checkRulePolicy( } // Check required tags - if len(rule.MustHaveTags) > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && len(rule.MustHaveTags) > 0 { for _, requiredTag := range rule.MustHaveTags { if ev.Tags.GetFirst([]byte(requiredTag)) == nil { return false, nil @@ -1289,7 +1290,8 @@ func (p *P) checkRulePolicy( } // Check expiry time (uses maxExpirySeconds which is parsed from MaxExpiryDuration or MaxExpiry) - if rule.maxExpirySeconds != nil && *rule.maxExpirySeconds > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.maxExpirySeconds != nil && *rule.maxExpirySeconds > 0 { expiryTag := ev.Tags.GetFirst([]byte("expiration")) if expiryTag == nil { return false, nil // Must have expiry if max_expiry is set @@ -1302,7 +1304,7 @@ func (p *P) checkRulePolicy( return false, nil // Invalid expiry format } maxAllowedExpiry := ev.CreatedAt + *rule.maxExpirySeconds - if expiryTs > maxAllowedExpiry { + if expiryTs >= maxAllowedExpiry { log.D.F("expiration %d exceeds max allowed %d (created_at %d + max_expiry %d)", expiryTs, maxAllowedExpiry, ev.CreatedAt, *rule.maxExpirySeconds) return false, nil // Expiry too far in the future @@ -1310,7 +1312,8 @@ func (p *P) checkRulePolicy( } // Check ProtectedRequired (NIP-70: events must have "-" tag) - if rule.ProtectedRequired { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.ProtectedRequired { protectedTag := ev.Tags.GetFirst([]byte("-")) if protectedTag == nil { log.D.F("protected_required: event missing '-' tag (NIP-70)") @@ -1319,7 +1322,8 @@ func (p *P) checkRulePolicy( } // Check IdentifierRegex (validates "d" tag values) - if rule.identifierRegexCache != nil { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.identifierRegexCache != nil { dTags := ev.Tags.GetAll([]byte("d")) if len(dTags) == 0 { log.D.F("identifier_regex: event missing 'd' tag") @@ -1336,7 +1340,8 @@ func (p *P) checkRulePolicy( } // Check MaxAgeOfEvent (maximum age of event in seconds) - if rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 { currentTime := time.Now().Unix() maxAllowedTime := currentTime - *rule.MaxAgeOfEvent if ev.CreatedAt < maxAllowedTime { @@ -1345,7 +1350,8 @@ func (p *P) checkRulePolicy( } // Check MaxAgeEventInFuture (maximum time event can be in the future in seconds) - if rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 { currentTime := time.Now().Unix() maxFutureTime := currentTime + *rule.MaxAgeEventInFuture if ev.CreatedAt > maxFutureTime { diff --git a/pkg/version/version b/pkg/version/version index 477e302..c580c49 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.31.6 +v0.31.7