Browse Source
Introduce a comprehensive reference guide for ORLY policy configuration. This document outlines policy options, validation rules, access control, and debugging methods, serving as the authoritative resource for policy-related behavior.main
1 changed files with 615 additions and 0 deletions
@ -0,0 +1,615 @@
@@ -0,0 +1,615 @@
|
||||
# ORLY Policy Configuration Reference |
||||
|
||||
This document provides a definitive reference for all policy configuration options and when each rule applies. Use this as the authoritative source for understanding policy behavior. |
||||
|
||||
## Quick Reference: Read vs Write Applicability |
||||
|
||||
| Rule Field | Write (EVENT) | Read (REQ) | Notes | |
||||
|------------|:-------------:|:----------:|-------| |
||||
| `size_limit` | ✅ | ❌ | Validates incoming events only | |
||||
| `content_limit` | ✅ | ❌ | Validates incoming events only | |
||||
| `max_age_of_event` | ✅ | ❌ | Prevents replay attacks | |
||||
| `max_age_event_in_future` | ✅ | ❌ | Prevents future-dated events | |
||||
| `max_expiry_duration` | ✅ | ❌ | Requires expiration tag | |
||||
| `must_have_tags` | ✅ | ❌ | Validates required tags | |
||||
| `protected_required` | ✅ | ❌ | Requires NIP-70 "-" tag | |
||||
| `identifier_regex` | ✅ | ❌ | Validates "d" tag format | |
||||
| `tag_validation` | ✅ | ❌ | Validates tag values with regex | |
||||
| `write_allow` | ✅ | ❌ | Pubkey whitelist for writing | |
||||
| `write_deny` | ✅ | ❌ | Pubkey blacklist for writing | |
||||
| `read_allow` | ❌ | ✅ | Pubkey whitelist for reading | |
||||
| `read_deny` | ❌ | ✅ | Pubkey blacklist for reading | |
||||
| `privileged` | ❌ | ✅ | Party-involved access control | |
||||
| `write_allow_follows` | ✅ | ✅ | Grants **both** read AND write | |
||||
| `follows_whitelist_admins` | ✅ | ✅ | Grants **both** read AND write | |
||||
| `script` | ✅ | ❌ | Scripts only run for writes | |
||||
|
||||
--- |
||||
|
||||
## Core Principle: Validation vs Filtering |
||||
|
||||
The policy system has two distinct modes of operation: |
||||
|
||||
### Write Operations (EVENT messages) |
||||
- **Purpose**: Validate and accept/reject incoming events |
||||
- **All rules apply** except `read_allow`, `read_deny`, and `privileged` |
||||
- Events are checked **before storage** |
||||
- Rejected events are never stored |
||||
|
||||
### Read Operations (REQ messages) |
||||
- **Purpose**: Filter which stored events a user can retrieve |
||||
- **Only access control rules apply**: `read_allow`, `read_deny`, `privileged`, `write_allow_follows`, `follows_whitelist_admins` |
||||
- Validation rules (size, age, tags) do NOT apply |
||||
- Scripts are NOT executed for reads |
||||
- Filtering happens **after database query** |
||||
|
||||
--- |
||||
|
||||
## Configuration Structure |
||||
|
||||
```json |
||||
{ |
||||
"default_policy": "allow|deny", |
||||
"kind": { |
||||
"whitelist": [1, 3, 7], |
||||
"blacklist": [4, 42] |
||||
}, |
||||
"owners": ["hex_pubkey_64_chars"], |
||||
"policy_admins": ["hex_pubkey_64_chars"], |
||||
"policy_follow_whitelist_enabled": true, |
||||
"global": { /* Rule object */ }, |
||||
"rules": { |
||||
"1": { /* Rule object for kind 1 */ }, |
||||
"30023": { /* Rule object for kind 30023 */ } |
||||
} |
||||
} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Top-Level Configuration Fields |
||||
|
||||
### `default_policy` |
||||
**Type**: `string` |
||||
**Values**: `"allow"` (default) or `"deny"` |
||||
**Applies to**: Both read and write |
||||
|
||||
The fallback behavior when no specific rule makes a decision. |
||||
|
||||
```json |
||||
{ |
||||
"default_policy": "deny" |
||||
} |
||||
``` |
||||
|
||||
### `kind.whitelist` and `kind.blacklist` |
||||
**Type**: `[]int` |
||||
**Applies to**: Both read and write |
||||
|
||||
Controls which event kinds are processed at all. |
||||
|
||||
- **Whitelist** takes precedence: If present, ONLY whitelisted kinds are allowed |
||||
- **Blacklist**: If no whitelist, these kinds are denied |
||||
- **Neither**: Behavior depends on `default_policy` and whether rules exist |
||||
|
||||
```json |
||||
{ |
||||
"kind": { |
||||
"whitelist": [0, 1, 3, 7, 30023] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### `owners` |
||||
**Type**: `[]string` (64-character hex pubkeys) |
||||
**Applies to**: Policy administration |
||||
|
||||
Relay owners with full control. Merged with `ORLY_OWNERS` environment variable. |
||||
|
||||
```json |
||||
{ |
||||
"owners": ["4a93c5ac0c6f49d2c7e7a5b8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"] |
||||
} |
||||
``` |
||||
|
||||
### `policy_admins` |
||||
**Type**: `[]string` (64-character hex pubkeys) |
||||
**Applies to**: Policy administration |
||||
|
||||
Pubkeys that can update policy via kind 12345 events (with restrictions). |
||||
|
||||
### `policy_follow_whitelist_enabled` |
||||
**Type**: `boolean` |
||||
**Default**: `false` |
||||
**Applies to**: Both read and write (when `write_allow_follows` is true) |
||||
|
||||
When enabled, allows `write_allow_follows` rules to grant access to policy admin follows. |
||||
|
||||
--- |
||||
|
||||
## Rule Object Fields |
||||
|
||||
Rules can be defined in `global` (applies to all events) or `rules[kind]` (applies to specific kind). |
||||
|
||||
### Access Control Fields |
||||
|
||||
#### `write_allow` |
||||
**Type**: `[]string` (hex pubkeys) |
||||
**Applies to**: Write only |
||||
**Behavior**: Exclusive whitelist |
||||
|
||||
When present with entries, ONLY these pubkeys can write events of this kind. All others are denied. |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"1": { |
||||
"write_allow": ["pubkey1_hex", "pubkey2_hex"] |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
**Special case**: Empty array `[]` explicitly allows all writers. |
||||
|
||||
#### `write_deny` |
||||
**Type**: `[]string` (hex pubkeys) |
||||
**Applies to**: Write only |
||||
**Behavior**: Blacklist (highest priority) |
||||
|
||||
These pubkeys cannot write events of this kind. **Checked before allow lists.** |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"1": { |
||||
"write_deny": ["banned_pubkey_hex"] |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `read_allow` |
||||
**Type**: `[]string` (hex pubkeys) |
||||
**Applies to**: Read only |
||||
**Behavior**: Exclusive whitelist (with OR logic for privileged) |
||||
|
||||
When present with entries: |
||||
- If `privileged: false`: ONLY these pubkeys can read |
||||
- If `privileged: true`: These pubkeys OR parties involved can read |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"4": { |
||||
"read_allow": ["trusted_pubkey_hex"], |
||||
"privileged": true |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `read_deny` |
||||
**Type**: `[]string` (hex pubkeys) |
||||
**Applies to**: Read only |
||||
**Behavior**: Blacklist (highest priority) |
||||
|
||||
These pubkeys cannot read events of this kind. **Checked before allow lists.** |
||||
|
||||
#### `privileged` |
||||
**Type**: `boolean` |
||||
**Default**: `false` |
||||
**Applies to**: Read only |
||||
|
||||
When `true`, events are only readable by "parties involved": |
||||
- The event author (`event.pubkey`) |
||||
- Users mentioned in `p` tags |
||||
|
||||
**Interaction with `read_allow`**: |
||||
- `read_allow` present + `privileged: true` = OR logic (in list OR party involved) |
||||
- `read_allow` empty + `privileged: true` = Only parties involved |
||||
- `privileged: true` alone = Only parties involved |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"4": { |
||||
"description": "DMs - only sender and recipient can read", |
||||
"privileged": true |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `write_allow_follows` |
||||
**Type**: `boolean` |
||||
**Default**: `false` |
||||
**Applies to**: Both read AND write |
||||
**Requires**: `policy_follow_whitelist_enabled: true` at top level |
||||
|
||||
Grants **both read and write access** to pubkeys followed by policy admins. |
||||
|
||||
> **Important**: Despite the name, this grants BOTH read and write access. |
||||
|
||||
```json |
||||
{ |
||||
"policy_follow_whitelist_enabled": true, |
||||
"rules": { |
||||
"1": { |
||||
"write_allow_follows": true |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `follows_whitelist_admins` |
||||
**Type**: `[]string` (hex pubkeys) |
||||
**Applies to**: Both read AND write |
||||
|
||||
Alternative to `write_allow_follows` that specifies which admin pubkeys' follows are whitelisted for this specific rule. |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"30023": { |
||||
"follows_whitelist_admins": ["curator_pubkey_hex"] |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### Validation Fields (Write-Only) |
||||
|
||||
These fields validate incoming events and are **completely ignored for read operations**. |
||||
|
||||
#### `size_limit` |
||||
**Type**: `int64` (bytes) |
||||
**Applies to**: Write only |
||||
|
||||
Maximum total serialized event size. |
||||
|
||||
```json |
||||
{ |
||||
"global": { |
||||
"size_limit": 100000 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `content_limit` |
||||
**Type**: `int64` (bytes) |
||||
**Applies to**: Write only |
||||
|
||||
Maximum size of the `content` field. |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"1": { |
||||
"content_limit": 10000 |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `max_age_of_event` |
||||
**Type**: `int64` (seconds) |
||||
**Applies to**: Write only |
||||
|
||||
Maximum age of events. Events with `created_at` older than `now - max_age_of_event` are rejected. |
||||
|
||||
```json |
||||
{ |
||||
"global": { |
||||
"max_age_of_event": 86400 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `max_age_event_in_future` |
||||
**Type**: `int64` (seconds) |
||||
**Applies to**: Write only |
||||
|
||||
Maximum time events can be dated in the future. Events with `created_at` later than `now + max_age_event_in_future` are rejected. |
||||
|
||||
```json |
||||
{ |
||||
"global": { |
||||
"max_age_event_in_future": 300 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `max_expiry_duration` |
||||
**Type**: `string` (ISO-8601 duration) |
||||
**Applies to**: Write only |
||||
|
||||
Maximum allowed expiry time from event creation. Events **must** have an `expiration` tag when this is set. |
||||
|
||||
**Format**: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S` |
||||
|
||||
**Examples**: |
||||
- `P7D` = 7 days |
||||
- `PT1H` = 1 hour |
||||
- `P1DT12H` = 1 day 12 hours |
||||
- `PT30M` = 30 minutes |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"20": { |
||||
"description": "Ephemeral events must expire within 24 hours", |
||||
"max_expiry_duration": "P1D" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `must_have_tags` |
||||
**Type**: `[]string` (tag names) |
||||
**Applies to**: Write only |
||||
|
||||
Required tags that must be present on the event. |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"1": { |
||||
"must_have_tags": ["p", "e"] |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `protected_required` |
||||
**Type**: `boolean` |
||||
**Default**: `false` |
||||
**Applies to**: Write only |
||||
|
||||
Requires events to have a `-` tag (NIP-70 protected events). |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"4": { |
||||
"protected_required": true |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `identifier_regex` |
||||
**Type**: `string` (regex pattern) |
||||
**Applies to**: Write only |
||||
|
||||
Regex pattern that `d` tag values must match. Events **must** have a `d` tag when this is set. |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"30023": { |
||||
"identifier_regex": "^[a-z0-9-]{1,64}$" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### `tag_validation` |
||||
**Type**: `map[string]string` (tag name → regex pattern) |
||||
**Applies to**: Write only |
||||
|
||||
Regex patterns for validating specific tag values. Only validates tags that are **present** on the event. |
||||
|
||||
> **Note**: To require a tag to exist, use `must_have_tags`. `tag_validation` only validates format. |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"30023": { |
||||
"tag_validation": { |
||||
"t": "^[a-z0-9-]{1,32}$", |
||||
"d": "^[a-z0-9-]+$" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### Script Configuration |
||||
|
||||
#### `script` |
||||
**Type**: `string` (file path) |
||||
**Applies to**: Write only |
||||
|
||||
Path to a custom validation script. **Scripts are NOT executed for read operations.** |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"1": { |
||||
"script": "/etc/orly/scripts/spam-filter.py" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Policy Evaluation Order |
||||
|
||||
### For Write Operations |
||||
|
||||
``` |
||||
1. Global Rule Check (all fields apply) |
||||
├─ Universal constraints (size, tags, age, etc.) |
||||
├─ write_deny check |
||||
├─ write_allow_follows / follows_whitelist_admins check |
||||
└─ write_allow check |
||||
|
||||
2. Kind Filtering (whitelist/blacklist) |
||||
|
||||
3. Kind-Specific Rule Check (same as global) |
||||
├─ Universal constraints |
||||
├─ write_deny check |
||||
├─ write_allow_follows / follows_whitelist_admins check |
||||
├─ write_allow check |
||||
└─ Script execution (if configured) |
||||
|
||||
4. Default Policy (if no rules matched) |
||||
``` |
||||
|
||||
### For Read Operations |
||||
|
||||
``` |
||||
1. Global Rule Check (access control only) |
||||
├─ read_deny check |
||||
├─ write_allow_follows / follows_whitelist_admins check |
||||
├─ read_allow check |
||||
└─ privileged check (party involved) |
||||
|
||||
2. Kind Filtering (whitelist/blacklist) |
||||
|
||||
3. Kind-Specific Rule Check (access control only) |
||||
├─ read_deny check |
||||
├─ write_allow_follows / follows_whitelist_admins check |
||||
├─ read_allow + privileged (OR logic) |
||||
└─ privileged-only check |
||||
|
||||
4. Default Policy (if no rules matched) |
||||
|
||||
NOTE: Scripts are NOT executed for read operations |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Common Configuration Patterns |
||||
|
||||
### Private Relay (Whitelist Only) |
||||
|
||||
```json |
||||
{ |
||||
"default_policy": "deny", |
||||
"global": { |
||||
"write_allow": ["trusted_pubkey_1", "trusted_pubkey_2"], |
||||
"read_allow": ["trusted_pubkey_1", "trusted_pubkey_2"] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Open Relay with Spam Protection |
||||
|
||||
```json |
||||
{ |
||||
"default_policy": "allow", |
||||
"global": { |
||||
"size_limit": 100000, |
||||
"max_age_of_event": 86400, |
||||
"max_age_event_in_future": 300 |
||||
}, |
||||
"rules": { |
||||
"1": { |
||||
"script": "/etc/orly/scripts/spam-filter.sh" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Community Relay (Follows-Based) |
||||
|
||||
```json |
||||
{ |
||||
"default_policy": "deny", |
||||
"policy_admins": ["community_admin_pubkey"], |
||||
"policy_follow_whitelist_enabled": true, |
||||
"global": { |
||||
"write_allow_follows": true |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Encrypted DMs (Privileged Access) |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"4": { |
||||
"description": "Encrypted DMs - only sender/recipient", |
||||
"privileged": true, |
||||
"protected_required": true |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Long-Form Content with Validation |
||||
|
||||
```json |
||||
{ |
||||
"rules": { |
||||
"30023": { |
||||
"description": "Long-form articles", |
||||
"size_limit": 100000, |
||||
"content_limit": 50000, |
||||
"max_expiry_duration": "P30D", |
||||
"identifier_regex": "^[a-z0-9-]{1,64}$", |
||||
"tag_validation": { |
||||
"t": "^[a-z0-9-]{1,32}$" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Important Behaviors |
||||
|
||||
### Whitelist vs Blacklist Precedence |
||||
|
||||
1. **Deny lists** (`write_deny`, `read_deny`) are checked **first** and have highest priority |
||||
2. **Allow lists** are exclusive when populated - ONLY listed pubkeys are allowed |
||||
3. **Deny-only configuration**: If only deny list exists (no allow list), all non-denied pubkeys are allowed |
||||
|
||||
### Empty Arrays vs Null |
||||
|
||||
- `[]` (empty array explicitly set) = Allow all |
||||
- `null` or field omitted = No list configured, use other rules |
||||
|
||||
### Global Rules Are Additive |
||||
|
||||
Global rules are always evaluated **in addition to** kind-specific rules. They cannot be overridden at the kind level. |
||||
|
||||
### Implicit Kind Whitelist |
||||
|
||||
When rules are defined but no explicit `kind.whitelist`: |
||||
- If `default_policy: "allow"`: All kinds allowed |
||||
- If `default_policy: "deny"` or unset: Only kinds with rules allowed |
||||
|
||||
--- |
||||
|
||||
## Debugging Policy Issues |
||||
|
||||
Enable debug logging to see policy decisions: |
||||
|
||||
```bash |
||||
export ORLY_LOG_LEVEL=debug |
||||
``` |
||||
|
||||
Log messages include: |
||||
- Policy evaluation steps |
||||
- Rule matching |
||||
- Access decisions with reasons |
||||
|
||||
--- |
||||
|
||||
## Source Code Reference |
||||
|
||||
- Policy struct definition: `pkg/policy/policy.go:75-144` (Rule struct) |
||||
- Policy struct definition: `pkg/policy/policy.go:380-412` (P struct) |
||||
- Check evaluation: `pkg/policy/policy.go:1260-1595` (checkRulePolicy) |
||||
- Write handler: `app/handle-event.go:114-138` |
||||
- Read handler: `app/handle-req.go:420-438` |
||||
Loading…
Reference in new issue