Add ORLY_POLICY_PATH for custom policy file location
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add ORLY_POLICY_PATH environment variable to configure custom policy file path, overriding the default ~/.config/ORLY/policy.json location - Enforce ABSOLUTE paths only - relay panics on startup if relative path is provided, preventing common misconfiguration errors - Update PolicyManager to store and expose configPath for hot-reload saves - Add ConfigPath() method to P struct delegating to internal PolicyManager - Update NewWithManager() signature to accept optional custom path parameter - Add BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md with issue submission guidelines requiring environment details, reproduction steps, and logs - Update README.md with system requirements (500MB minimum memory) and link to bug report protocol - Update CLAUDE.md and README.md documentation for new ORLY_POLICY_PATH Files modified: - app/config/config.go: Add PolicyPath config field - pkg/policy/policy.go: Add configPath storage and validation - app/handle-policy-config.go: Use policyManager.ConfigPath() - app/main.go: Pass cfg.PolicyPath to NewWithManager - pkg/policy/*_test.go: Update test calls with new parameter - BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md: New file - README.md, CLAUDE.md: Documentation updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,8 @@
|
|||||||
"WebFetch(domain:eylenburg.github.io)",
|
"WebFetch(domain:eylenburg.github.io)",
|
||||||
"Bash(go run -exec '' -c 'package main; import \"\"git.mleku.dev/mleku/nostr/utils/normalize\"\"; import \"\"fmt\"\"; func main() { fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:443\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"ws://relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"wss://relay.example.com:3334\"\")))) }')",
|
"Bash(go run -exec '' -c 'package main; import \"\"git.mleku.dev/mleku/nostr/utils/normalize\"\"; import \"\"fmt\"\"; func main() { fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:443\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"ws://relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"wss://relay.example.com:3334\"\")))) }')",
|
||||||
"Bash(go run:*)",
|
"Bash(go run:*)",
|
||||||
"Bash(git commit -m \"$(cat <<''EOF''\nFix NIP-11 fetch URL scheme conversion for non-proxied relays\n\n- Convert wss:// to https:// and ws:// to http:// before fetching NIP-11\n documents, fixing failures for users not using HTTPS upgrade proxies\n- The fetchNIP11 function was using WebSocket URLs directly for HTTP\n requests, causing scheme mismatch errors\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")"
|
"Bash(git commit -m \"$(cat <<''EOF''\nFix NIP-11 fetch URL scheme conversion for non-proxied relays\n\n- Convert wss:// to https:// and ws:// to http:// before fetching NIP-11\n documents, fixing failures for users not using HTTPS upgrade proxies\n- The fetchNIP11 function was using WebSocket URLs directly for HTTP\n requests, causing scheme mismatch errors\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
|
||||||
|
"Bash(/tmp/orly help:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
254
BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md
Normal file
254
BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Feature Request and Bug Report Protocol
|
||||||
|
|
||||||
|
This document describes how to submit effective bug reports and feature requests for ORLY relay. Following these guidelines helps maintainers understand and resolve issues quickly.
|
||||||
|
|
||||||
|
## Before Submitting
|
||||||
|
|
||||||
|
1. **Search existing issues** - Your issue may already be reported or discussed
|
||||||
|
2. **Check documentation** - Review `CLAUDE.md`, `docs/`, and `pkg/*/README.md` files
|
||||||
|
3. **Verify with latest version** - Ensure the issue exists in the current release
|
||||||
|
4. **Test with default configuration** - Rule out configuration-specific problems
|
||||||
|
|
||||||
|
## Bug Reports
|
||||||
|
|
||||||
|
### Required Information
|
||||||
|
|
||||||
|
**Title**: Concise summary of the problem
|
||||||
|
- Good: "Kind 3 events with 8000+ follows truncated on save"
|
||||||
|
- Bad: "Events not saving" or "Bug in database"
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
```
|
||||||
|
ORLY version: (output of ./orly version)
|
||||||
|
OS: (e.g., Ubuntu 24.04, macOS 14.2)
|
||||||
|
Go version: (output of go version)
|
||||||
|
Database backend: (badger/neo4j/wasmdb)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration** (relevant settings only):
|
||||||
|
```bash
|
||||||
|
ORLY_DB_TYPE=badger
|
||||||
|
ORLY_POLICY_ENABLED=true
|
||||||
|
# Include any non-default settings
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps to Reproduce**:
|
||||||
|
1. Start relay with configuration X
|
||||||
|
2. Connect client and send event Y
|
||||||
|
3. Query for event with filter Z
|
||||||
|
4. Observe error/unexpected behavior
|
||||||
|
|
||||||
|
**Expected Behavior**: What should happen
|
||||||
|
|
||||||
|
**Actual Behavior**: What actually happens
|
||||||
|
|
||||||
|
**Logs**: Include relevant log output with `ORLY_LOG_LEVEL=debug` or `trace`
|
||||||
|
|
||||||
|
### Minimal Reproduction
|
||||||
|
|
||||||
|
The most effective bug reports include a minimal reproduction case:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: Script that demonstrates the issue
|
||||||
|
export ORLY_LOG_LEVEL=debug
|
||||||
|
./orly &
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Send problematic event
|
||||||
|
echo '["EVENT", {...}]' | websocat ws://localhost:3334
|
||||||
|
|
||||||
|
# Show the failure
|
||||||
|
echo '["REQ", "test", {"kinds": [1]}]' | websocat ws://localhost:3334
|
||||||
|
```
|
||||||
|
|
||||||
|
Or provide a failing test case:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestReproduceBug(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
db := setupTestDB(t)
|
||||||
|
|
||||||
|
// This should work but fails
|
||||||
|
event := createTestEvent(kind, content)
|
||||||
|
err := db.SaveEvent(ctx, event)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Query returns unexpected result
|
||||||
|
results, err := db.QueryEvents(ctx, filter)
|
||||||
|
assert.Len(t, results, 1) // Fails: got 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Requests
|
||||||
|
|
||||||
|
### Required Information
|
||||||
|
|
||||||
|
**Title**: Clear description of the feature
|
||||||
|
- Good: "Add WebSocket compression support (permessage-deflate)"
|
||||||
|
- Bad: "Make it faster" or "New feature idea"
|
||||||
|
|
||||||
|
**Problem Statement**: What problem does this solve?
|
||||||
|
```
|
||||||
|
Currently, clients with high-latency connections experience slow sync times
|
||||||
|
because event data is transmitted uncompressed. A typical session transfers
|
||||||
|
50MB of JSON that could be reduced to ~10MB with compression.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proposed Solution**: How should it work?
|
||||||
|
```
|
||||||
|
Add optional permessage-deflate WebSocket extension support:
|
||||||
|
- New config: ORLY_WS_COMPRESSION=true
|
||||||
|
- Negotiate compression during WebSocket handshake
|
||||||
|
- Apply to messages over configurable threshold (default 1KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**: Who benefits and how?
|
||||||
|
```
|
||||||
|
- Mobile clients on cellular connections
|
||||||
|
- Users syncing large follow lists
|
||||||
|
- Relays with bandwidth constraints
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternatives Considered** (optional):
|
||||||
|
```
|
||||||
|
- Application-level compression: Rejected because it requires client changes
|
||||||
|
- HTTP/2: Not applicable for WebSocket connections
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Notes (optional)
|
||||||
|
|
||||||
|
If you have implementation ideas:
|
||||||
|
|
||||||
|
```
|
||||||
|
Suggested approach:
|
||||||
|
1. Add compression config to app/config/config.go
|
||||||
|
2. Modify gorilla/websocket upgrader in app/handle-websocket.go
|
||||||
|
3. Add compression threshold check before WriteMessage()
|
||||||
|
|
||||||
|
Reference: gorilla/websocket has built-in permessage-deflate support
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Makes Reports Effective
|
||||||
|
|
||||||
|
**Do**:
|
||||||
|
- Be specific and factual
|
||||||
|
- Include version numbers and exact error messages
|
||||||
|
- Provide reproducible steps
|
||||||
|
- Attach relevant logs (redact sensitive data)
|
||||||
|
- Link to related issues or discussions
|
||||||
|
- Respond to follow-up questions promptly
|
||||||
|
|
||||||
|
**Avoid**:
|
||||||
|
- Vague descriptions ("it doesn't work")
|
||||||
|
- Multiple unrelated issues in one report
|
||||||
|
- Assuming the cause without evidence
|
||||||
|
- Demanding immediate fixes
|
||||||
|
- Duplicating existing issues
|
||||||
|
|
||||||
|
## Issue Labels
|
||||||
|
|
||||||
|
When applicable, suggest appropriate labels:
|
||||||
|
|
||||||
|
| Label | Use When |
|
||||||
|
|-------|----------|
|
||||||
|
| `bug` | Something isn't working as documented |
|
||||||
|
| `enhancement` | New feature or improvement |
|
||||||
|
| `performance` | Speed or resource usage issue |
|
||||||
|
| `documentation` | Docs are missing or incorrect |
|
||||||
|
| `question` | Clarification needed (not a bug) |
|
||||||
|
| `good first issue` | Suitable for new contributors |
|
||||||
|
|
||||||
|
## Response Expectations
|
||||||
|
|
||||||
|
- **Acknowledgment**: Within a few days
|
||||||
|
- **Triage**: Issue labeled and prioritized
|
||||||
|
- **Resolution**: Depends on complexity and priority
|
||||||
|
|
||||||
|
Complex features may require discussion before implementation. Bug fixes for critical issues are prioritized.
|
||||||
|
|
||||||
|
## Following Up
|
||||||
|
|
||||||
|
If your issue hasn't received attention:
|
||||||
|
|
||||||
|
1. **Check issue status** - It may be labeled or assigned
|
||||||
|
2. **Add new information** - If you've discovered more details
|
||||||
|
3. **Politely bump** - A single follow-up comment after 2 weeks is appropriate
|
||||||
|
4. **Consider contributing** - PRs that fix bugs or implement features are welcome
|
||||||
|
|
||||||
|
## Contributing Fixes
|
||||||
|
|
||||||
|
If you want to fix a bug or implement a feature yourself:
|
||||||
|
|
||||||
|
1. Comment on the issue to avoid duplicate work
|
||||||
|
2. Follow the coding patterns in `CLAUDE.md`
|
||||||
|
3. Include tests for your changes
|
||||||
|
4. Keep PRs focused on a single issue
|
||||||
|
5. Reference the issue number in your PR
|
||||||
|
|
||||||
|
## Security Issues
|
||||||
|
|
||||||
|
**Do not report security vulnerabilities in public issues.**
|
||||||
|
|
||||||
|
For security-sensitive bugs:
|
||||||
|
- Contact maintainers directly
|
||||||
|
- Provide detailed reproduction steps privately
|
||||||
|
- Allow reasonable time for a fix before disclosure
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Good Bug Report
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## WebSocket disconnects after 60 seconds of inactivity
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
- ORLY v0.34.5
|
||||||
|
- Ubuntu 22.04
|
||||||
|
- Go 1.25.3
|
||||||
|
- Badger backend
|
||||||
|
|
||||||
|
**Steps to Reproduce**:
|
||||||
|
1. Connect to relay: `websocat ws://localhost:3334`
|
||||||
|
2. Send subscription: `["REQ", "test", {"kinds": [1], "limit": 1}]`
|
||||||
|
3. Wait 60 seconds without sending messages
|
||||||
|
4. Observe connection closed
|
||||||
|
|
||||||
|
**Expected**: Connection remains open (Nostr relays should maintain persistent connections)
|
||||||
|
|
||||||
|
**Actual**: Connection closed with code 1000 after exactly 60 seconds
|
||||||
|
|
||||||
|
**Logs** (ORLY_LOG_LEVEL=debug):
|
||||||
|
```
|
||||||
|
1764783029014485🔎 client timeout, closing connection /app/handle-websocket.go:142
|
||||||
|
```
|
||||||
|
|
||||||
|
**Possible Cause**: May be related to read deadline not being extended on subscription activity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Good Feature Request
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Add rate limiting per pubkey
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
A single pubkey can flood the relay with events, consuming storage and
|
||||||
|
bandwidth. Currently there's no way to limit per-author submission rate.
|
||||||
|
|
||||||
|
**Proposed Solution**:
|
||||||
|
Add configurable rate limiting:
|
||||||
|
```bash
|
||||||
|
ORLY_RATE_LIMIT_EVENTS_PER_MINUTE=60
|
||||||
|
ORLY_RATE_LIMIT_BURST=10
|
||||||
|
```
|
||||||
|
|
||||||
|
When exceeded, return OK false with "rate-limited" message per NIP-20.
|
||||||
|
|
||||||
|
**Use Case**:
|
||||||
|
- Public relays protecting against spam
|
||||||
|
- Community relays with fair-use policies
|
||||||
|
- Paid relays enforcing subscription tiers
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
- IP-based limiting: Ineffective because users share IPs and use VPNs
|
||||||
|
- Global limiting: Punishes all users for one bad actor
|
||||||
|
```
|
||||||
@@ -147,6 +147,10 @@ export ORLY_SPROCKET_ENABLED=true
|
|||||||
# Enable policy system
|
# Enable policy system
|
||||||
export ORLY_POLICY_ENABLED=true
|
export ORLY_POLICY_ENABLED=true
|
||||||
|
|
||||||
|
# Custom policy file path (MUST be ABSOLUTE path starting with /)
|
||||||
|
# Default: ~/.config/ORLY/policy.json (or ~/.config/{ORLY_APP_NAME}/policy.json)
|
||||||
|
# export ORLY_POLICY_PATH=/etc/orly/policy.json
|
||||||
|
|
||||||
# Database backend selection (badger, neo4j, or wasmdb)
|
# Database backend selection (badger, neo4j, or wasmdb)
|
||||||
export ORLY_DB_TYPE=badger
|
export ORLY_DB_TYPE=badger
|
||||||
|
|
||||||
@@ -270,7 +274,8 @@ export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
|
|||||||
- `none.go` - Open relay (no restrictions)
|
- `none.go` - Open relay (no restrictions)
|
||||||
|
|
||||||
**`pkg/policy/`** - Event filtering and validation policies
|
**`pkg/policy/`** - Event filtering and validation policies
|
||||||
- Policy configuration loaded from `~/.config/ORLY/policy.json`
|
- Policy configuration loaded from `~/.config/ORLY/policy.json` by default
|
||||||
|
- Custom path via `ORLY_POLICY_PATH` (MUST be absolute path starting with `/`)
|
||||||
- Per-kind size limits, age restrictions, custom scripts
|
- Per-kind size limits, age restrictions, custom scripts
|
||||||
- **Write-Only Validation**: Size, age, tag, and expiry validations apply ONLY to write operations
|
- **Write-Only Validation**: Size, age, tag, and expiry validations apply ONLY to write operations
|
||||||
- **Read-Only Filtering**: `read_allow`, `read_deny`, `privileged` apply ONLY to read operations
|
- **Read-Only Filtering**: `read_allow`, `read_deny`, `privileged` apply ONLY to read operations
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -1,5 +1,7 @@
|
|||||||
# next.orly.dev
|
# next.orly.dev
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
@@ -10,6 +12,19 @@ zap me: <20>mlekudev@getalby.com
|
|||||||
|
|
||||||
follow me on [nostr](https://jumble.social/users/npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku)
|
follow me on [nostr](https://jumble.social/users/npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku)
|
||||||
|
|
||||||
|
## ⚠️ Bug Reports & Feature Requests
|
||||||
|
|
||||||
|
**Bug reports and feature requests that do not follow the protocol will not be accepted.**
|
||||||
|
|
||||||
|
Before submitting any issue, you must read and follow [BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md](./BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- **Bug reports**: Include environment details, reproduction steps, expected/actual behavior, and logs
|
||||||
|
- **Feature requests**: Include problem statement, proposed solution, and use cases
|
||||||
|
- **Both**: Search existing issues first, verify with latest version, provide minimal reproduction
|
||||||
|
|
||||||
|
Issues missing required information will be closed without review.
|
||||||
|
|
||||||
## ⚠️ System Requirements
|
## ⚠️ System Requirements
|
||||||
|
|
||||||
> **IMPORTANT: ORLY requires a minimum of 500MB of free memory to operate.**
|
> **IMPORTANT: ORLY requires a minimum of 500MB of free memory to operate.**
|
||||||
@@ -217,7 +232,12 @@ ORLY includes a comprehensive policy system for fine-grained control over event
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export ORLY_POLICY_ENABLED=true
|
export ORLY_POLICY_ENABLED=true
|
||||||
# Create policy file at ~/.config/ORLY/policy.json
|
# Default policy file: ~/.config/ORLY/policy.json
|
||||||
|
|
||||||
|
# OPTIONAL: Use a custom policy file location
|
||||||
|
# WARNING: ORLY_POLICY_PATH MUST be an ABSOLUTE path (starting with /)
|
||||||
|
# Relative paths will be REJECTED and the relay will fail to start
|
||||||
|
export ORLY_POLICY_PATH=/etc/orly/policy.json
|
||||||
```
|
```
|
||||||
|
|
||||||
For detailed configuration and examples, see the [Policy Usage Guide](docs/POLICY_USAGE_GUIDE.md).
|
For detailed configuration and examples, see the [Policy Usage Guide](docs/POLICY_USAGE_GUIDE.md).
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ type C struct {
|
|||||||
DirectorySpiderInterval time.Duration `env:"ORLY_DIRECTORY_SPIDER_INTERVAL" default:"24h" usage:"how often to run directory spider"`
|
DirectorySpiderInterval time.Duration `env:"ORLY_DIRECTORY_SPIDER_INTERVAL" default:"24h" usage:"how often to run directory spider"`
|
||||||
DirectorySpiderMaxHops int `env:"ORLY_DIRECTORY_SPIDER_HOPS" default:"3" usage:"maximum hops for relay discovery from seed users"`
|
DirectorySpiderMaxHops int `env:"ORLY_DIRECTORY_SPIDER_HOPS" default:"3" usage:"maximum hops for relay discovery from seed users"`
|
||||||
|
|
||||||
PolicyEnabled bool `env:"ORLY_POLICY_ENABLED" default:"false" usage:"enable policy-based event processing (configuration found in $HOME/.config/ORLY/policy.json)"`
|
PolicyEnabled bool `env:"ORLY_POLICY_ENABLED" default:"false" usage:"enable policy-based event processing (default config: $HOME/.config/ORLY/policy.json)"`
|
||||||
|
PolicyPath string `env:"ORLY_POLICY_PATH" usage:"ABSOLUTE path to policy configuration file (MUST start with /); overrides default location; relative paths are rejected"`
|
||||||
|
|
||||||
// NIP-43 Relay Access Metadata and Requests
|
// NIP-43 Relay Access Metadata and Requests
|
||||||
NIP43Enabled bool `env:"ORLY_NIP43_ENABLED" default:"false" usage:"enable NIP-43 relay access metadata and invite system"`
|
NIP43Enabled bool `env:"ORLY_NIP43_ENABLED" default:"false" usage:"enable NIP-43 relay access metadata and invite system"`
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package app
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
"lol.mleku.dev/log"
|
"lol.mleku.dev/log"
|
||||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||||
@@ -76,8 +74,8 @@ func (l *Listener) HandlePolicyConfigUpdate(ev *event.E) error {
|
|||||||
|
|
||||||
log.I.F("policy config validation passed")
|
log.I.F("policy config validation passed")
|
||||||
|
|
||||||
// Get config path for saving
|
// Get config path for saving (uses custom path if set, otherwise default)
|
||||||
configPath := filepath.Join(xdg.ConfigHome, l.Config.AppName, "policy.json")
|
configPath := l.policyManager.ConfigPath()
|
||||||
|
|
||||||
// 3. Pause ALL message processing (lock mutex)
|
// 3. Pause ALL message processing (lock mutex)
|
||||||
// Note: We need to release the RLock first (which caller holds), then acquire exclusive Lock
|
// Note: We need to release the RLock first (which caller holds), then acquire exclusive Lock
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create policy manager - now config file exists at XDG path
|
// Create policy manager - now config file exists at XDG path
|
||||||
policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled)
|
policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled, "")
|
||||||
|
|
||||||
server := &Server{
|
server := &Server{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func Run(
|
|||||||
l.sprocketManager = NewSprocketManager(ctx, cfg.AppName, cfg.SprocketEnabled)
|
l.sprocketManager = NewSprocketManager(ctx, cfg.AppName, cfg.SprocketEnabled)
|
||||||
|
|
||||||
// Initialize policy manager
|
// Initialize policy manager
|
||||||
l.policyManager = policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled)
|
l.policyManager = policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled, cfg.PolicyPath)
|
||||||
|
|
||||||
// Merge policy-defined owners with environment-defined owners
|
// Merge policy-defined owners with environment-defined owners
|
||||||
// This allows cloud deployments to add owners via policy.json when env vars cannot be modified
|
// This allows cloud deployments to add owners via policy.json when env vars cannot be modified
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,5 +1,5 @@
|
|||||||
git.mleku.dev/mleku/nostr v1.0.8 h1:YYREdIxobEqYkzxQ7/5ALACPzLkiHW+CTira+VvSQZk=
|
git.mleku.dev/mleku/nostr v1.0.9 h1:aiN0ihnXzEpboXjW4u8qr5XokLQqg4P0XSZ1Y273qM0=
|
||||||
git.mleku.dev/mleku/nostr v1.0.8/go.mod h1:iYTlg2WKJXJ0kcsM6QBGOJ0UDiJidMgL/i64cHyPjZc=
|
git.mleku.dev/mleku/nostr v1.0.9/go.mod h1:iYTlg2WKJXJ0kcsM6QBGOJ0UDiJidMgL/i64cHyPjZc=
|
||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func TestBugReproduction_WithPolicyManager(t *testing.T) {
|
|||||||
|
|
||||||
// Create policy with manager (enabled)
|
// Create policy with manager (enabled)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
policy := NewWithManager(ctx, "ORLY", true)
|
policy := NewWithManager(ctx, "ORLY", true, "")
|
||||||
|
|
||||||
// Load policy from file
|
// Load policy from file
|
||||||
if err := policy.LoadFromFile(policyPath); err != nil {
|
if err := policy.LoadFromFile(policyPath); err != nil {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func setupTestPolicy(t *testing.T, appName string) (*P, func()) {
|
|||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
|
||||||
policy := NewWithManager(ctx, appName, true)
|
policy := NewWithManager(ctx, appName, true, "")
|
||||||
if policy == nil {
|
if policy == nil {
|
||||||
cancel()
|
cancel()
|
||||||
os.RemoveAll(configDir)
|
os.RemoveAll(configDir)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func setupHotreloadTestPolicy(t *testing.T, appName string) (*P, func()) {
|
|||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
|
||||||
policy := NewWithManager(ctx, appName, true)
|
policy := NewWithManager(ctx, appName, true, "")
|
||||||
if policy == nil {
|
if policy == nil {
|
||||||
cancel()
|
cancel()
|
||||||
os.RemoveAll(configDir)
|
os.RemoveAll(configDir)
|
||||||
|
|||||||
@@ -514,12 +514,19 @@ type PolicyManager struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
configDir string
|
configDir string
|
||||||
|
configPath string // Path to policy.json file
|
||||||
scriptPath string // Default script path for backward compatibility
|
scriptPath string // Default script path for backward compatibility
|
||||||
enabled bool
|
enabled bool
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
runners map[string]*ScriptRunner // Map of script path -> runner
|
runners map[string]*ScriptRunner // Map of script path -> runner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigPath returns the path to the policy configuration file.
|
||||||
|
// This is used by hot-reload handlers to know where to save updated policy.
|
||||||
|
func (pm *PolicyManager) ConfigPath() string {
|
||||||
|
return pm.configPath
|
||||||
|
}
|
||||||
|
|
||||||
// P represents a complete policy configuration for a Nostr relay.
|
// P represents a complete policy configuration for a Nostr relay.
|
||||||
// It defines access control rules, kind filtering, and default behavior.
|
// It defines access control rules, kind filtering, and default behavior.
|
||||||
// Policies are evaluated in order: global rules, kind filtering, specific rules, then default policy.
|
// Policies are evaluated in order: global rules, kind filtering, specific rules, then default policy.
|
||||||
@@ -695,6 +702,15 @@ func (p *P) IsEnabled() bool {
|
|||||||
return p != nil && p.manager != nil && p.manager.IsEnabled()
|
return p != nil && p.manager != nil && p.manager.IsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigPath returns the path to the policy configuration file.
|
||||||
|
// Delegates to the internal PolicyManager.
|
||||||
|
func (p *P) ConfigPath() string {
|
||||||
|
if p == nil || p.manager == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return p.manager.ConfigPath()
|
||||||
|
}
|
||||||
|
|
||||||
// getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
|
// getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
|
||||||
func (p *P) getDefaultPolicyAction() (allowed bool) {
|
func (p *P) getDefaultPolicyAction() (allowed bool) {
|
||||||
switch p.DefaultPolicy {
|
switch p.DefaultPolicy {
|
||||||
@@ -711,10 +727,29 @@ func (p *P) getDefaultPolicyAction() (allowed bool) {
|
|||||||
// NewWithManager creates a new policy with a policy manager for script execution.
|
// NewWithManager creates a new policy with a policy manager for script execution.
|
||||||
// It initializes the policy manager, loads configuration from files, and starts
|
// It initializes the policy manager, loads configuration from files, and starts
|
||||||
// background processes for script management and periodic health checks.
|
// background processes for script management and periodic health checks.
|
||||||
func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
|
//
|
||||||
|
// The customPolicyPath parameter allows overriding the default policy file location.
|
||||||
|
// If empty, uses the default path: $HOME/.config/{appName}/policy.json
|
||||||
|
// If provided, it MUST be an absolute path (starting with /) or the function will panic.
|
||||||
|
func NewWithManager(ctx context.Context, appName string, enabled bool, customPolicyPath string) *P {
|
||||||
configDir := filepath.Join(xdg.ConfigHome, appName)
|
configDir := filepath.Join(xdg.ConfigHome, appName)
|
||||||
scriptPath := filepath.Join(configDir, "policy.sh")
|
scriptPath := filepath.Join(configDir, "policy.sh")
|
||||||
configPath := filepath.Join(configDir, "policy.json")
|
|
||||||
|
// Determine the policy config path
|
||||||
|
var configPath string
|
||||||
|
if customPolicyPath != "" {
|
||||||
|
// Validate that custom path is absolute
|
||||||
|
if !filepath.IsAbs(customPolicyPath) {
|
||||||
|
panic(fmt.Sprintf("FATAL: ORLY_POLICY_PATH must be an ABSOLUTE path (starting with /), got: %q", customPolicyPath))
|
||||||
|
}
|
||||||
|
configPath = customPolicyPath
|
||||||
|
// Update configDir to match the custom path's directory for script resolution
|
||||||
|
configDir = filepath.Dir(customPolicyPath)
|
||||||
|
scriptPath = filepath.Join(configDir, "policy.sh")
|
||||||
|
log.I.F("using custom policy path: %s", configPath)
|
||||||
|
} else {
|
||||||
|
configPath = filepath.Join(configDir, "policy.json")
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
@@ -722,6 +757,7 @@ func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
|
configPath: configPath,
|
||||||
scriptPath: scriptPath,
|
scriptPath: scriptPath,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
runners: make(map[string]*ScriptRunner),
|
runners: make(map[string]*ScriptRunner),
|
||||||
|
|||||||
@@ -825,7 +825,7 @@ func TestNewWithManager(t *testing.T) {
|
|||||||
// Test with disabled policy (doesn't require policy.json file)
|
// Test with disabled policy (doesn't require policy.json file)
|
||||||
t.Run("disabled policy", func(t *testing.T) {
|
t.Run("disabled policy", func(t *testing.T) {
|
||||||
enabled := false
|
enabled := false
|
||||||
policy := NewWithManager(ctx, appName, enabled)
|
policy := NewWithManager(ctx, appName, enabled, "")
|
||||||
|
|
||||||
if policy == nil {
|
if policy == nil {
|
||||||
t.Fatal("Expected policy but got nil")
|
t.Fatal("Expected policy but got nil")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func setupTagValidationTestPolicy(t *testing.T, appName string) (*P, func()) {
|
|||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
|
||||||
policy := NewWithManager(ctx, appName, true)
|
policy := NewWithManager(ctx, appName, true, "")
|
||||||
if policy == nil {
|
if policy == nil {
|
||||||
cancel()
|
cancel()
|
||||||
os.RemoveAll(configDir)
|
os.RemoveAll(configDir)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v0.35.2
|
v0.35.3
|
||||||
Reference in New Issue
Block a user