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

@@ -46,61 +46,61 @@ jobs:
- name: Test
run: go test -v $(go list ./... | xargs -n1 sh -c 'ls $0/*_test.go 1>/dev/null 2>&1 && echo $0' | grep .)
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
- name: Install libsecp256k1
run: ./scripts/ubuntu_install_libsecp256k1.sh
- name: Install libsecp256k1
run: ./scripts/ubuntu_install_libsecp256k1.sh
- name: Build Release Binaries
if: startsWith(github.ref, 'refs/tags/v')
run: |
# Extract version from tag (e.g., v1.2.3 -> 1.2.3)
VERSION=${GITHUB_REF#refs/tags/v}
echo "Building release binaries for version $VERSION"
- name: Build Release Binaries
if: startsWith(github.ref, 'refs/tags/v')
run: |
# Extract version from tag (e.g., v1.2.3 -> 1.2.3)
VERSION=${GITHUB_REF#refs/tags/v}
echo "Building release binaries for version $VERSION"
# Create directory for binaries
mkdir -p release-binaries
# Create directory for binaries
mkdir -p release-binaries
# Build for different platforms
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o release-binaries/orly-${VERSION}-linux-amd64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-linux-arm64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-amd64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-arm64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-windows-amd64.exe .
# Build for different platforms
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o release-binaries/orly-${VERSION}-linux-amd64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-linux-arm64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-amd64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-arm64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-windows-amd64.exe .
# Build cmd executables
for cmd in lerproxy nauth nurl vainstr walletcli; do
echo "Building $cmd"
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o release-binaries/${cmd}-${VERSION}-linux-amd64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-linux-arm64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-darwin-amd64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-darwin-arm64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-windows-amd64.exe ./cmd/${cmd}
done
# Build cmd executables
for cmd in lerproxy nauth nurl vainstr walletcli; do
echo "Building $cmd"
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o release-binaries/${cmd}-${VERSION}-linux-amd64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-linux-arm64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-darwin-amd64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-darwin-arm64 ./cmd/${cmd}
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/${cmd}-${VERSION}-windows-amd64.exe ./cmd/${cmd}
done
# Create checksums
cd release-binaries
sha256sum * > SHA256SUMS.txt
cd ..
# Create checksums
cd release-binaries
sha256sum * > SHA256SUMS.txt
cd ..
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v1
with:
files: release-binaries/*
draft: false
prerelease: false
generate_release_notes: true
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v1
with:
files: release-binaries/*
draft: false
prerelease: false
generate_release_notes: true

2
.gitignore vendored
View File

@@ -96,6 +96,8 @@ cmd/benchmark/data
!*.tsx
!bun.lock
!*.svelte
!.github/**
!.github/workflows/**
# ...even if they are in subdirectories
!*/
/blocklist.json

View File

@@ -11,7 +11,7 @@ export ORLY_POLICY_ENABLED=true
## Policy Configuration File
The policy configuration is loaded from `$HOME/.config/ORLY/policy.json`. See `example-policy.json` for a complete example.
The policy configuration is loaded from `$HOME/.config/ORLY/policy.json`. See `docs/example-policy.json` for a complete example with global rules and age validation.
### Structure
@@ -21,6 +21,17 @@ The policy configuration is loaded from `$HOME/.config/ORLY/policy.json`. See `e
"whitelist": [1, 3, 5, 7, 9735],
"blacklist": []
},
"global": {
"description": "Global rules applied to all events",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 100000,
"content_limit": 50000,
"max_age_of_event": 86400,
"max_age_event_in_future": 300
},
"rules": {
"1": {
"description": "Text notes - allow all authenticated users",
@@ -29,12 +40,33 @@ The policy configuration is loaded from `$HOME/.config/ORLY/policy.json`. See `e
"read_allow": [],
"read_deny": [],
"size_limit": 32000,
"content_limit": 10000
"content_limit": 10000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60
}
}
}
```
### Policy Evaluation Order
The policy system evaluates events in the following order:
1. **Global Rules** - Applied to all events first
2. **Kinds Filtering** - Whitelist/blacklist by event kind
3. **Kind-specific Rules** - Rules for specific event kinds
4. **Script Rules** - Custom script logic (if enabled)
### Global Rules
The `global` section defines rules that apply to **all events** regardless of their kind. These rules are evaluated **first** and take precedence over kind-specific rules.
Global rules support all the same fields as kind-specific rules, allowing you to:
- Set site-wide size limits
- Block specific pubkeys globally
- Enforce age restrictions on all events
- Apply content limits across all event types
### Kinds Filtering
- `whitelist`: If present, only these event kinds are allowed. All others are denied.
@@ -55,6 +87,50 @@ The policy configuration is loaded from `$HOME/.config/ORLY/policy.json`. See `e
- `content_limit`: Maximum content field size in bytes
- `privileged`: If true, event must be authored by authenticated user or contain authenticated user in p tags
- `rate_limit`: Rate limit in bytes per second (not yet implemented)
- `max_age_of_event`: Maximum age of event in seconds (prevents replay attacks)
- `max_age_event_in_future`: Maximum time event can be in the future in seconds (prevents clock skew attacks)
### Age Validation
The policy system includes built-in timestamp validation to prevent common attacks:
#### MaxAgeOfEvent
- **Purpose**: Prevents replay attacks by rejecting events that are too old
- **Behavior**: Events with `created_at` older than `current_time - max_age_of_event` are rejected
- **Example**: Setting `max_age_of_event: 3600` rejects events older than 1 hour
- **Use Cases**:
- Prevent replay of old events
- Ensure events are recent and relevant
- Reduce storage of stale data
#### MaxAgeEventInFuture
- **Purpose**: Prevents clock skew attacks by rejecting events too far in the future
- **Behavior**: Events with `created_at` newer than `current_time + max_age_event_in_future` are rejected
- **Example**: Setting `max_age_event_in_future: 300` rejects events more than 5 minutes in the future
- **Use Cases**:
- Prevent clock manipulation attacks
- Ensure reasonable timestamp accuracy
- Block events with impossible future timestamps
#### Age Validation Examples
```json
{
"global": {
"max_age_of_event": 86400, // Reject events older than 24 hours
"max_age_event_in_future": 300 // Reject events more than 5 minutes in future
},
"rules": {
"1": {
"max_age_of_event": 3600, // Text notes: reject older than 1 hour
"max_age_event_in_future": 60 // Text notes: reject more than 1 minute in future
},
"4": {
"max_age_of_event": 604800 // Direct messages: reject older than 7 days
}
}
}
```
## Policy Scripts
@@ -76,7 +152,7 @@ The script receives JSON events via stdin and outputs JSON responses via stdout.
### Example Script
See `example-policy.sh` for a complete example showing:
See `docs/example-policy.sh` for a complete example showing:
- IP address blocking
- Content filtering
- Authentication requirements
@@ -86,11 +162,16 @@ See `example-policy.sh` for a complete example showing:
### EVENT Processing
When policy is enabled, every EVENT envelope is checked using `CheckPolicy("write", event, loggedInPubkey, ipAddress)` before being stored.
When policy is enabled, every EVENT envelope is checked using `CheckPolicy("write", event, loggedInPubkey, ipAddress)` before being stored. The policy evaluation follows this order:
1. **Global Rules** - Applied first to all events
2. **Kinds Filtering** - Whitelist/blacklist check
3. **Kind-specific Rules** - Rules for the event's kind
4. **Script Rules** - Custom script logic (if enabled)
### REQ Processing
When policy is enabled, every event returned in REQ responses is filtered using `CheckPolicy("read", event, loggedInPubkey, ipAddress)` before being sent to the client.
When policy is enabled, every event returned in REQ responses is filtered using `CheckPolicy("read", event, loggedInPubkey, ipAddress)` before being sent to the client. The same evaluation order applies for read access.
## Error Handling
@@ -105,9 +186,31 @@ Policy decisions are logged at debug level:
- `policy rejected event <id>`
- `policy filtered out event <id> for read access`
## Best Practices
### Global Rules
- Use global rules for site-wide security policies (size limits, age restrictions)
- Keep global rules simple and broad to avoid unintended side effects
- Test global rules thoroughly as they affect all events
### Age Validation
- Set reasonable age limits based on your use case:
- **Text notes (kind 1)**: 1-24 hours max age, 1-5 minutes future tolerance
- **Direct messages (kind 4)**: 7-30 days max age, 1-5 minutes future tolerance
- **Replaceable events (kind 0, 3)**: Longer max age, shorter future tolerance
- Consider network latency when setting future tolerance
- Monitor rejected events to tune age limits appropriately
### Policy Hierarchy
- Global rules should be broader than kind-specific rules
- Use global rules for security, kind-specific rules for functionality
- Avoid conflicting rules between global and kind-specific policies
## Security Considerations
- Policy scripts run with the same privileges as the relay process
- Scripts should be carefully reviewed and tested
- Consider using read-only filesystems for policy scripts in production
- Monitor script execution time to prevent DoS attacks
- Age validation helps prevent replay and clock skew attacks
- Global rules provide defense-in-depth security

View File

@@ -1,9 +1,33 @@
{
"kind": {
"whitelist": [1, 3, 5, 7, 9735],
"whitelist": [0, 1, 3, 4, 5, 6, 7, 40, 41, 42, 43, 44, 9735],
"blacklist": []
},
"global": {
"description": "Global security rules applied to all events",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 100000,
"content_limit": 50000,
"max_age_of_event": 86400,
"max_age_event_in_future": 300,
"privileged": false
},
"rules": {
"0": {
"description": "Metadata events - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 16000,
"content_limit": 8000,
"max_age_of_event": 604800,
"max_age_event_in_future": 60,
"privileged": false
},
"1": {
"description": "Text notes - allow all authenticated users",
"write_allow": [],
@@ -11,31 +35,142 @@
"read_allow": [],
"read_deny": [],
"size_limit": 32000,
"content_limit": 10000
"content_limit": 10000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60,
"privileged": false
},
"3": {
"description": "Contacts - only allow specific users",
"write_allow": ["npub1example1", "npub1example2"],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"script": "policy.sh"
},
"5": {
"description": "Deletion events - require authentication",
"description": "Contact lists - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 50000,
"content_limit": 20000,
"max_age_of_event": 2592000,
"max_age_event_in_future": 60,
"privileged": false
},
"4": {
"description": "Direct messages - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 16000,
"content_limit": 8000,
"max_age_of_event": 604800,
"max_age_event_in_future": 60,
"privileged": true
},
"9735": {
"description": "Zap receipts - allow all",
"5": {
"description": "Event deletion - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 10000
"size_limit": 8000,
"content_limit": 4000,
"max_age_of_event": 86400,
"max_age_event_in_future": 60,
"privileged": false
},
"6": {
"description": "Reposts - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 8000,
"content_limit": 4000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60,
"privileged": false
},
"7": {
"description": "Reaction events - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 8000,
"content_limit": 4000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60,
"privileged": false
},
"40": {
"description": "Channel creation - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 16000,
"content_limit": 8000,
"max_age_of_event": 604800,
"max_age_event_in_future": 60,
"privileged": false
},
"41": {
"description": "Channel metadata - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 16000,
"content_limit": 8000,
"max_age_of_event": 604800,
"max_age_event_in_future": 60,
"privileged": false
},
"42": {
"description": "Channel messages - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 32000,
"content_limit": 10000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60,
"privileged": false
},
"43": {
"description": "Channel hide message - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 8000,
"content_limit": 4000,
"max_age_of_event": 86400,
"max_age_event_in_future": 60,
"privileged": false
},
"44": {
"description": "Channel mute user - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 8000,
"content_limit": 4000,
"max_age_of_event": 604800,
"max_age_event_in_future": 60,
"privileged": false
},
"9735": {
"description": "Zap receipts - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 16000,
"content_limit": 8000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60,
"privileged": false
}
}
}
}

View File

@@ -1,48 +1,152 @@
#!/bin/bash
# Policy script example for ORLY relay
# This script receives JSON events via stdin and outputs JSON responses via stdout
# Each event includes the original event data plus logged_in_pubkey and ip_address fields
# ORLY Policy Script Example
# This script demonstrates advanced policy logic including:
# - IP address blocking
# - Content filtering
# - Authentication requirements
# - User-specific permissions
# - Age validation (complementing built-in age checks)
# Read events from stdin (JSONL format)
# Configuration
BLOCKED_IPS=("127.0.0.1" "192.168.1.100")
BLOCKED_WORDS=("spam" "scam" "phishing")
TRUSTED_USERS=("746573742d7075626b6579" "abcdef1234567890abcdef1234567890abcdef12")
ADMIN_USERS=("746573742d7075626b6579")
# Function to check if IP is blocked
is_ip_blocked() {
local ip="$1"
for blocked_ip in "${BLOCKED_IPS[@]}"; do
if [[ "$ip" == "$blocked_ip" ]]; then
return 0
fi
done
return 1
}
# Function to check for blocked words
contains_blocked_words() {
local content="$1"
local lower_content=$(echo "$content" | tr '[:upper:]' '[:lower:]')
for word in "${BLOCKED_WORDS[@]}"; do
if [[ "$lower_content" == *"$word"* ]]; then
return 0
fi
done
return 1
}
# Function to check if user is trusted
is_trusted_user() {
local pubkey="$1"
for trusted_user in "${TRUSTED_USERS[@]}"; do
if [[ "$pubkey" == "$trusted_user" ]]; then
return 0
fi
done
return 1
}
# Function to check if user is admin
is_admin_user() {
local pubkey="$1"
for admin_user in "${ADMIN_USERS[@]}"; do
if [[ "$pubkey" == "$admin_user" ]]; then
return 0
fi
done
return 1
}
# Function to validate event age (additional to built-in checks)
validate_event_age() {
local created_at="$1"
local current_time=$(date +%s)
local age=$((current_time - created_at))
# Additional age validation beyond built-in checks
# Reject events older than 7 days for certain kinds
if [[ $age -gt 604800 ]]; then
return 1
fi
return 0
}
# Main policy logic
while IFS= read -r line; do
# Parse the JSON event
# Parse JSON input
event_id=$(echo "$line" | jq -r '.id // empty')
event_kind=$(echo "$line" | jq -r '.kind // empty')
event_pubkey=$(echo "$line" | jq -r '.pubkey // empty')
event_content=$(echo "$line" | jq -r '.content // empty')
pubkey=$(echo "$line" | jq -r '.pubkey // empty')
kind=$(echo "$line" | jq -r '.kind // empty')
content=$(echo "$line" | jq -r '.content // empty')
created_at=$(echo "$line" | jq -r '.created_at // empty')
logged_in_pubkey=$(echo "$line" | jq -r '.logged_in_pubkey // empty')
ip_address=$(echo "$line" | jq -r '.ip_address // empty')
# Default action
# Default to accept
action="accept"
message=""
msg=""
# Example policy logic:
# 1. Block events from specific IP addresses
if [[ "$ip_address" == "192.168.1.100" ]]; then
# Check IP blocking
if is_ip_blocked "$ip_address"; then
action="reject"
message="blocked IP address"
msg="IP address blocked"
echo "{\"id\":\"$event_id\",\"action\":\"$action\",\"msg\":\"$msg\"}"
continue
fi
# 2. Block events with certain content patterns
if [[ "$event_content" =~ "spam" ]]; then
# Check for blocked words in content
if contains_blocked_words "$content"; then
action="reject"
message="spam content detected"
msg="Content contains blocked words"
echo "{\"id\":\"$event_id\",\"action\":\"$action\",\"msg\":\"$msg\"}"
continue
fi
# 3. Require authentication for certain kinds
if [[ "$event_kind" == "3" && -z "$logged_in_pubkey" ]]; then
# Additional age validation
if ! validate_event_age "$created_at"; then
action="reject"
message="authentication required for kind 3"
msg="Event too old (additional validation)"
echo "{\"id\":\"$event_id\",\"action\":\"$action\",\"msg\":\"$msg\"}"
continue
fi
# 4. Allow only specific users for kind 3
if [[ "$event_kind" == "3" && "$event_pubkey" != "npub1example1" && "$event_pubkey" != "npub1example2" ]]; then
action="reject"
message="unauthorized user for kind 3"
# Kind-specific rules
case "$kind" in
"4") # Direct messages
# Require authentication for DMs
if [[ -z "$logged_in_pubkey" ]]; then
action="reject"
msg="Authentication required for direct messages"
fi
;;
"40"|"41"|"42"|"43"|"44") # Channel events
# Require authentication for channel events
if [[ -z "$logged_in_pubkey" ]]; then
action="reject"
msg="Authentication required for channel events"
fi
;;
"9735") # Zap receipts
# Only allow trusted users to post zap receipts
if ! is_trusted_user "$pubkey"; then
action="reject"
msg="Only trusted users can post zap receipts"
fi
;;
esac
# Admin bypass for certain operations
if is_admin_user "$pubkey"; then
# Admins can bypass most restrictions
action="accept"
msg="Admin bypass"
fi
# Output JSON response
echo "{\"id\":\"$event_id\",\"action\":\"$action\",\"msg\":\"$message\"}"
done
# Output decision
echo "{\"id\":\"$event_id\",\"action\":\"$action\",\"msg\":\"$msg\"}"
done

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

View File

@@ -1 +1 @@
v0.16.1
v0.16.2