policy.go raw
1 package policy
2
3 import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "regexp"
14 "strconv"
15 "strings"
16 "sync"
17 "time"
18
19 "next.orly.dev/pkg/nostr/encoders/event"
20 "next.orly.dev/pkg/nostr/encoders/hex"
21 "github.com/adrg/xdg"
22 "github.com/sosodev/duration"
23 "next.orly.dev/pkg/lol/chk"
24 "next.orly.dev/pkg/lol/log"
25 "next.orly.dev/pkg/utils"
26 )
27
28 // parseDuration parses an ISO-8601 duration string into seconds.
29 // ISO-8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S
30 // Examples: "P1D" (1 day), "PT1H" (1 hour), "P7DT12H" (7 days 12 hours), "PT30M" (30 minutes)
31 // Uses the github.com/sosodev/duration library for strict ISO-8601 compliance.
32 // Note: Years and Months are converted to approximate time.Duration values
33 // (1 year ≈ 365.25 days, 1 month ≈ 30.44 days).
34 func parseDuration(s string) (int64, error) {
35 if s == "" {
36 return 0, fmt.Errorf("empty duration string")
37 }
38
39 s = strings.TrimSpace(s)
40 if s == "" {
41 return 0, fmt.Errorf("empty duration string")
42 }
43
44 // Parse using the ISO-8601 duration library
45 d, err := duration.Parse(s)
46 if err != nil {
47 return 0, fmt.Errorf("invalid ISO-8601 duration %q: %v", s, err)
48 }
49
50 // Convert to time.Duration and then to seconds
51 timeDur := d.ToTimeDuration()
52 return int64(timeDur.Seconds()), nil
53 }
54
55 // Kinds defines whitelist and blacklist policies for event kinds.
56 // Whitelist takes precedence over blacklist - if whitelist is present, only whitelisted kinds are allowed.
57 // If only blacklist is present, all kinds except blacklisted ones are allowed.
58 type Kinds struct {
59 // Whitelist is a list of event kinds that are allowed to be written to the relay. If any are present, implicitly all others are denied.
60 Whitelist []int `json:"whitelist,omitempty"`
61 // Blacklist is a list of event kinds that are not allowed to be written to the relay. If any are present, implicitly all others are allowed. Only takes effect in the absence of a Whitelist.
62 Blacklist []int `json:"blacklist,omitempty"`
63 }
64
65 // Rule defines policy criteria for a specific event kind.
66 //
67 // Rules are evaluated in the following order:
68 // 1. If Script is present and running, it determines the outcome
69 // 2. If Script fails or is not running, falls back to default_policy
70 // 3. Otherwise, all specified criteria are evaluated as AND operations
71 //
72 // For pubkey allow/deny lists: whitelist takes precedence over blacklist.
73 // If whitelist has entries, only whitelisted pubkeys are allowed.
74 // If only blacklist has entries, all pubkeys except blacklisted ones are allowed.
75 // =============================================================================
76 // Rule Sub-Components (Value Objects)
77 // =============================================================================
78
79 // AccessControl defines who can read/write events.
80 // This is a value object that encapsulates access control configuration.
81 type AccessControl struct {
82 // WriteAllow is a list of pubkeys allowed to write. If any present, all others denied.
83 WriteAllow []string `json:"write_allow,omitempty"`
84 // WriteDeny is a list of pubkeys denied write. Only effective without WriteAllow.
85 WriteDeny []string `json:"write_deny,omitempty"`
86 // ReadAllow is a list of pubkeys allowed to read. If any present, all others denied.
87 ReadAllow []string `json:"read_allow,omitempty"`
88 // ReadDeny is a list of pubkeys denied read. Only effective without ReadAllow.
89 ReadDeny []string `json:"read_deny,omitempty"`
90 // WriteAllowFollows grants access to policy admin follows when enabled.
91 WriteAllowFollows bool `json:"write_allow_follows,omitempty"`
92 // FollowsWhitelistAdmins specifies admin pubkeys whose follows are whitelisted.
93 // DEPRECATED: Use ReadFollowsWhitelist and WriteFollowsWhitelist instead.
94 FollowsWhitelistAdmins []string `json:"follows_whitelist_admins,omitempty"`
95 // ReadFollowsWhitelist specifies pubkeys whose follows can READ events.
96 ReadFollowsWhitelist []string `json:"read_follows_whitelist,omitempty"`
97 // WriteFollowsWhitelist specifies pubkeys whose follows can WRITE events.
98 WriteFollowsWhitelist []string `json:"write_follows_whitelist,omitempty"`
99 // ReadAllowPermissive allows read access for ALL kinds on GLOBAL rule.
100 ReadAllowPermissive bool `json:"read_allow_permissive,omitempty"`
101 // WriteAllowPermissive allows write access bypassing kind whitelist on GLOBAL rule.
102 WriteAllowPermissive bool `json:"write_allow_permissive,omitempty"`
103 // WriteAllowIfTagged is a list of pubkeys. Authors not in WriteAllow can still
104 // publish if their event contains a p-tag referencing one of these pubkeys.
105 WriteAllowIfTagged []string `json:"write_allow_if_tagged,omitempty"`
106
107 // Binary caches (internal, not serialized)
108 writeAllowBin [][]byte
109 writeDenyBin [][]byte
110 readAllowBin [][]byte
111 readDenyBin [][]byte
112 followsWhitelistAdminsBin [][]byte
113 followsWhitelistFollowsBin [][]byte
114 readFollowsWhitelistBin [][]byte
115 writeFollowsWhitelistBin [][]byte
116 readFollowsFollowsBin [][]byte
117 writeFollowsFollowsBin [][]byte
118 writeAllowIfTaggedBin [][]byte
119 }
120
121 // Constraints defines limits and restrictions on events.
122 // This is a value object that encapsulates event constraints.
123 type Constraints struct {
124 // MaxExpiry is the maximum expiry time in seconds.
125 // Deprecated: Use MaxExpiryDuration instead.
126 MaxExpiry *int64 `json:"max_expiry,omitempty"` //nolint:staticcheck
127 // MaxExpiryDuration is the max expiry in ISO-8601 duration format.
128 MaxExpiryDuration string `json:"max_expiry_duration,omitempty"`
129 // SizeLimit is the maximum total serialized size in bytes.
130 SizeLimit *int64 `json:"size_limit,omitempty"`
131 // ContentLimit is the maximum content field size in bytes.
132 ContentLimit *int64 `json:"content_limit,omitempty"`
133 // RateLimit is the write rate limit in bytes per second.
134 RateLimit *int64 `json:"rate_limit,omitempty"`
135 // MaxAgeOfEvent is the max age in seconds for created_at timestamps.
136 MaxAgeOfEvent *int64 `json:"max_age_of_event,omitempty"`
137 // MaxAgeEventInFuture is the max future offset for created_at timestamps.
138 MaxAgeEventInFuture *int64 `json:"max_age_event_in_future,omitempty"`
139 // ProtectedRequired requires events to have a "-" tag (NIP-70).
140 ProtectedRequired bool `json:"protected_required,omitempty"`
141 // Privileged means event is only sent to authenticated parties.
142 Privileged bool `json:"privileged,omitempty"`
143
144 // Parsed cache (internal, not serialized)
145 maxExpirySeconds *int64
146 }
147
148 // TagValidationConfig defines tag validation rules.
149 // This is a value object that encapsulates tag validation configuration.
150 type TagValidationConfig struct {
151 // MustHaveTags is a list of tag key letters that must be present.
152 MustHaveTags []string `json:"must_have_tags,omitempty"`
153 // TagValidation is a map of tag_name -> regex pattern for validation.
154 TagValidation map[string]string `json:"tag_validation,omitempty"`
155 // IdentifierRegex is a regex pattern for "d" tag identifiers.
156 IdentifierRegex string `json:"identifier_regex,omitempty"`
157
158 // Compiled cache (internal, not serialized)
159 identifierRegexCache *regexp.Regexp
160 }
161
162 // =============================================================================
163 // Rule (Composed from Sub-Components)
164 // =============================================================================
165
166 // Rule defines policies for a specific event kind or as a global default.
167 // It is composed of sub-value objects for cleaner organization.
168 type Rule struct {
169 // Description is a human-readable description of the rule.
170 Description string `json:"description"`
171 // Script is a path to a validation script.
172 Script string `json:"script,omitempty"`
173
174 // Embedded sub-components (fields are flattened in JSON for backward compatibility)
175 AccessControl
176 Constraints
177 TagValidationConfig
178 }
179
180 // hasAnyRules checks if the rule has any constraints configured
181 func (r *Rule) hasAnyRules() bool {
182 // Check for any configured constraints
183 return len(r.WriteAllow) > 0 || len(r.WriteDeny) > 0 ||
184 len(r.ReadAllow) > 0 || len(r.ReadDeny) > 0 ||
185 len(r.writeAllowBin) > 0 || len(r.writeDenyBin) > 0 ||
186 len(r.readAllowBin) > 0 || len(r.readDenyBin) > 0 ||
187 r.SizeLimit != nil || r.ContentLimit != nil ||
188 r.MaxAgeOfEvent != nil || r.MaxAgeEventInFuture != nil ||
189 r.MaxExpiry != nil || r.MaxExpiryDuration != "" || r.maxExpirySeconds != nil || //nolint:staticcheck // Backward compat
190 len(r.MustHaveTags) > 0 ||
191 r.Script != "" || r.Privileged ||
192 r.WriteAllowFollows || len(r.FollowsWhitelistAdmins) > 0 ||
193 len(r.ReadFollowsWhitelist) > 0 || len(r.WriteFollowsWhitelist) > 0 ||
194 len(r.readFollowsWhitelistBin) > 0 || len(r.writeFollowsWhitelistBin) > 0 ||
195 len(r.TagValidation) > 0 ||
196 r.ProtectedRequired || r.IdentifierRegex != "" ||
197 r.ReadAllowPermissive || r.WriteAllowPermissive ||
198 len(r.WriteAllowIfTagged) > 0 || len(r.writeAllowIfTaggedBin) > 0
199 }
200
201 // populateBinaryCache converts hex-encoded pubkey strings to binary for faster comparison.
202 // This should be called after unmarshaling the policy from JSON.
203 func (r *Rule) populateBinaryCache() error {
204 var err error
205
206 // Convert WriteAllow hex strings to binary
207 if len(r.WriteAllow) > 0 {
208 r.writeAllowBin = make([][]byte, 0, len(r.WriteAllow))
209 for _, hexPubkey := range r.WriteAllow {
210 binPubkey, decErr := hex.Dec(hexPubkey)
211 if decErr != nil {
212 log.W.F("failed to decode WriteAllow pubkey %q: %v", hexPubkey, decErr)
213 continue
214 }
215 r.writeAllowBin = append(r.writeAllowBin, binPubkey)
216 }
217 }
218
219 // Convert WriteDeny hex strings to binary
220 if len(r.WriteDeny) > 0 {
221 r.writeDenyBin = make([][]byte, 0, len(r.WriteDeny))
222 for _, hexPubkey := range r.WriteDeny {
223 binPubkey, decErr := hex.Dec(hexPubkey)
224 if decErr != nil {
225 log.W.F("failed to decode WriteDeny pubkey %q: %v", hexPubkey, decErr)
226 continue
227 }
228 r.writeDenyBin = append(r.writeDenyBin, binPubkey)
229 }
230 }
231
232 // Convert ReadAllow hex strings to binary
233 if len(r.ReadAllow) > 0 {
234 r.readAllowBin = make([][]byte, 0, len(r.ReadAllow))
235 for _, hexPubkey := range r.ReadAllow {
236 binPubkey, decErr := hex.Dec(hexPubkey)
237 if decErr != nil {
238 log.W.F("failed to decode ReadAllow pubkey %q: %v", hexPubkey, decErr)
239 continue
240 }
241 r.readAllowBin = append(r.readAllowBin, binPubkey)
242 }
243 }
244
245 // Convert ReadDeny hex strings to binary
246 if len(r.ReadDeny) > 0 {
247 r.readDenyBin = make([][]byte, 0, len(r.ReadDeny))
248 for _, hexPubkey := range r.ReadDeny {
249 binPubkey, decErr := hex.Dec(hexPubkey)
250 if decErr != nil {
251 log.W.F("failed to decode ReadDeny pubkey %q: %v", hexPubkey, decErr)
252 continue
253 }
254 r.readDenyBin = append(r.readDenyBin, binPubkey)
255 }
256 }
257
258 // Parse MaxExpiryDuration into maxExpirySeconds
259 // MaxExpiryDuration takes precedence over MaxExpiry if both are set
260 if r.MaxExpiryDuration != "" {
261 seconds, parseErr := parseDuration(r.MaxExpiryDuration)
262 if parseErr != nil {
263 log.W.F("failed to parse MaxExpiryDuration %q: %v", r.MaxExpiryDuration, parseErr)
264 } else {
265 r.maxExpirySeconds = &seconds
266 }
267 } else if r.MaxExpiry != nil { //nolint:staticcheck // Backward compatibility
268 // Fall back to MaxExpiry (raw seconds) if MaxExpiryDuration not set
269 r.maxExpirySeconds = r.MaxExpiry //nolint:staticcheck // Backward compatibility
270 }
271
272 // Compile IdentifierRegex pattern
273 if r.IdentifierRegex != "" {
274 compiled, compileErr := regexp.Compile(r.IdentifierRegex)
275 if compileErr != nil {
276 log.W.F("failed to compile IdentifierRegex %q: %v", r.IdentifierRegex, compileErr)
277 } else {
278 r.identifierRegexCache = compiled
279 }
280 }
281
282 // Convert FollowsWhitelistAdmins hex strings to binary (DEPRECATED)
283 if len(r.FollowsWhitelistAdmins) > 0 {
284 r.followsWhitelistAdminsBin = make([][]byte, 0, len(r.FollowsWhitelistAdmins))
285 for _, hexPubkey := range r.FollowsWhitelistAdmins {
286 binPubkey, decErr := hex.Dec(hexPubkey)
287 if decErr != nil {
288 log.W.F("failed to decode FollowsWhitelistAdmins pubkey %q: %v", hexPubkey, decErr)
289 continue
290 }
291 r.followsWhitelistAdminsBin = append(r.followsWhitelistAdminsBin, binPubkey)
292 }
293 }
294
295 // Convert ReadFollowsWhitelist hex strings to binary
296 if len(r.ReadFollowsWhitelist) > 0 {
297 r.readFollowsWhitelistBin = make([][]byte, 0, len(r.ReadFollowsWhitelist))
298 for _, hexPubkey := range r.ReadFollowsWhitelist {
299 binPubkey, decErr := hex.Dec(hexPubkey)
300 if decErr != nil {
301 log.W.F("failed to decode ReadFollowsWhitelist pubkey %q: %v", hexPubkey, decErr)
302 continue
303 }
304 r.readFollowsWhitelistBin = append(r.readFollowsWhitelistBin, binPubkey)
305 }
306 }
307
308 // Convert WriteFollowsWhitelist hex strings to binary
309 if len(r.WriteFollowsWhitelist) > 0 {
310 r.writeFollowsWhitelistBin = make([][]byte, 0, len(r.WriteFollowsWhitelist))
311 for _, hexPubkey := range r.WriteFollowsWhitelist {
312 binPubkey, decErr := hex.Dec(hexPubkey)
313 if decErr != nil {
314 log.W.F("failed to decode WriteFollowsWhitelist pubkey %q: %v", hexPubkey, decErr)
315 continue
316 }
317 r.writeFollowsWhitelistBin = append(r.writeFollowsWhitelistBin, binPubkey)
318 }
319 }
320
321 // Convert WriteAllowIfTagged hex strings to binary
322 if len(r.WriteAllowIfTagged) > 0 {
323 r.writeAllowIfTaggedBin = make([][]byte, 0, len(r.WriteAllowIfTagged))
324 for _, hexPubkey := range r.WriteAllowIfTagged {
325 binPubkey, decErr := hex.Dec(hexPubkey)
326 if decErr != nil {
327 log.W.F("failed to decode WriteAllowIfTagged pubkey %q: %v", hexPubkey, decErr)
328 continue
329 }
330 r.writeAllowIfTaggedBin = append(r.writeAllowIfTaggedBin, binPubkey)
331 }
332 }
333
334 return err
335 }
336
337 // IsInFollowsWhitelist checks if the given pubkey is in this rule's follows whitelist.
338 // The pubkey parameter should be binary ([]byte), not hex-encoded.
339 func (r *Rule) IsInFollowsWhitelist(pubkey []byte) bool {
340 if len(pubkey) == 0 || len(r.followsWhitelistFollowsBin) == 0 {
341 return false
342 }
343 for _, follow := range r.followsWhitelistFollowsBin {
344 if utils.FastEqual(pubkey, follow) {
345 return true
346 }
347 }
348 return false
349 }
350
351 // UpdateFollowsWhitelist sets the follows list for this rule's FollowsWhitelistAdmins.
352 // The follows should be binary pubkeys ([]byte), not hex-encoded.
353 func (r *Rule) UpdateFollowsWhitelist(follows [][]byte) {
354 r.followsWhitelistFollowsBin = follows
355 }
356
357 // GetFollowsWhitelistAdminsBin returns the binary-encoded admin pubkeys for this rule.
358 func (r *Rule) GetFollowsWhitelistAdminsBin() [][]byte {
359 return r.followsWhitelistAdminsBin
360 }
361
362 // HasFollowsWhitelistAdmins returns true if this rule has FollowsWhitelistAdmins configured.
363 // DEPRECATED: Use HasReadFollowsWhitelist and HasWriteFollowsWhitelist instead.
364 func (r *Rule) HasFollowsWhitelistAdmins() bool {
365 return len(r.FollowsWhitelistAdmins) > 0
366 }
367
368 // HasReadFollowsWhitelist returns true if this rule has ReadFollowsWhitelist configured.
369 func (r *Rule) HasReadFollowsWhitelist() bool {
370 return len(r.ReadFollowsWhitelist) > 0
371 }
372
373 // HasWriteFollowsWhitelist returns true if this rule has WriteFollowsWhitelist configured.
374 func (r *Rule) HasWriteFollowsWhitelist() bool {
375 return len(r.WriteFollowsWhitelist) > 0
376 }
377
378 // GetReadFollowsWhitelistBin returns the binary-encoded pubkeys for ReadFollowsWhitelist.
379 func (r *Rule) GetReadFollowsWhitelistBin() [][]byte {
380 return r.readFollowsWhitelistBin
381 }
382
383 // GetWriteFollowsWhitelistBin returns the binary-encoded pubkeys for WriteFollowsWhitelist.
384 func (r *Rule) GetWriteFollowsWhitelistBin() [][]byte {
385 return r.writeFollowsWhitelistBin
386 }
387
388 // UpdateReadFollowsWhitelist sets the follows list for this rule's ReadFollowsWhitelist.
389 // The follows should be binary pubkeys ([]byte), not hex-encoded.
390 func (r *Rule) UpdateReadFollowsWhitelist(follows [][]byte) {
391 r.readFollowsFollowsBin = follows
392 }
393
394 // UpdateWriteFollowsWhitelist sets the follows list for this rule's WriteFollowsWhitelist.
395 // The follows should be binary pubkeys ([]byte), not hex-encoded.
396 func (r *Rule) UpdateWriteFollowsWhitelist(follows [][]byte) {
397 r.writeFollowsFollowsBin = follows
398 }
399
400 // IsInReadFollowsWhitelist checks if the given pubkey is in this rule's read follows whitelist.
401 // The pubkey parameter should be binary ([]byte), not hex-encoded.
402 // Returns true if either:
403 // 1. The pubkey is one of the ReadFollowsWhitelist pubkeys themselves, OR
404 // 2. The pubkey is in the follows list of the ReadFollowsWhitelist pubkeys.
405 func (r *Rule) IsInReadFollowsWhitelist(pubkey []byte) bool {
406 if len(pubkey) == 0 {
407 return false
408 }
409 // Check if pubkey is one of the whitelist pubkeys themselves
410 for _, wlPubkey := range r.readFollowsWhitelistBin {
411 if utils.FastEqual(pubkey, wlPubkey) {
412 return true
413 }
414 }
415 // Check if pubkey is in the follows list
416 for _, follow := range r.readFollowsFollowsBin {
417 if utils.FastEqual(pubkey, follow) {
418 return true
419 }
420 }
421 return false
422 }
423
424 // IsInWriteFollowsWhitelist checks if the given pubkey is in this rule's write follows whitelist.
425 // The pubkey parameter should be binary ([]byte), not hex-encoded.
426 // Returns true if either:
427 // 1. The pubkey is one of the WriteFollowsWhitelist pubkeys themselves, OR
428 // 2. The pubkey is in the follows list of the WriteFollowsWhitelist pubkeys.
429 func (r *Rule) IsInWriteFollowsWhitelist(pubkey []byte) bool {
430 if len(pubkey) == 0 {
431 return false
432 }
433 // Check if pubkey is one of the whitelist pubkeys themselves
434 for _, wlPubkey := range r.writeFollowsWhitelistBin {
435 if utils.FastEqual(pubkey, wlPubkey) {
436 return true
437 }
438 }
439 // Check if pubkey is in the follows list
440 for _, follow := range r.writeFollowsFollowsBin {
441 if utils.FastEqual(pubkey, follow) {
442 return true
443 }
444 }
445 return false
446 }
447
448 // PolicyEvent represents an event with additional context for policy scripts.
449 // It embeds the Nostr event and adds authentication and network context.
450 type PolicyEvent struct {
451 *event.E
452 LoggedInPubkey string `json:"logged_in_pubkey,omitempty"`
453 IPAddress string `json:"ip_address,omitempty"`
454 AccessType string `json:"access_type,omitempty"` // "read" or "write"
455 }
456
457 // MarshalJSON implements custom JSON marshaling for PolicyEvent.
458 // It safely serializes the embedded event and additional context fields.
459 func (pe *PolicyEvent) MarshalJSON() ([]byte, error) {
460 if pe.E == nil {
461 return json.Marshal(
462 map[string]interface{}{
463 "logged_in_pubkey": pe.LoggedInPubkey,
464 "ip_address": pe.IPAddress,
465 },
466 )
467 }
468
469 // Create a safe copy of the event for JSON marshaling
470 safeEvent := map[string]interface{}{
471 "id": hex.Enc(pe.E.ID),
472 "pubkey": hex.Enc(pe.E.Pubkey),
473 "created_at": pe.E.CreatedAt,
474 "kind": pe.E.Kind,
475 "content": string(pe.E.Content),
476 "tags": pe.E.Tags,
477 "sig": hex.Enc(pe.E.Sig),
478 }
479
480 // Add policy-specific fields
481 if pe.LoggedInPubkey != "" {
482 safeEvent["logged_in_pubkey"] = pe.LoggedInPubkey
483 }
484 if pe.IPAddress != "" {
485 safeEvent["ip_address"] = pe.IPAddress
486 }
487 if pe.AccessType != "" {
488 safeEvent["access_type"] = pe.AccessType
489 }
490
491 return json.Marshal(safeEvent)
492 }
493
494 // PolicyResponse represents a response from the policy script.
495 // The script should return JSON with these fields to indicate its decision.
496 type PolicyResponse struct {
497 ID string `json:"id"`
498 Action string `json:"action"` // accept, reject, or shadowReject
499 Msg string `json:"msg"` // NIP-20 response message (only used for reject)
500 }
501
502 // ScriptRunner manages a single policy script process.
503 // Each unique script path gets its own independent runner with its own goroutine.
504 type ScriptRunner struct {
505 ctx context.Context
506 cancel context.CancelFunc
507 configDir string
508 scriptPath string
509 currentCmd *exec.Cmd
510 currentCancel context.CancelFunc
511 mutex sync.RWMutex
512 isRunning bool
513 isStarting bool
514 stdin io.WriteCloser
515 stdout io.ReadCloser
516 stderr io.ReadCloser
517 responseChan chan PolicyResponse
518 startupChan chan error
519 }
520
521 // PolicyManager handles multiple policy script runners.
522 // It manages the lifecycle of policy scripts, handles communication with them,
523 // and provides resilient operation with automatic restart capabilities.
524 // Each unique script path gets its own ScriptRunner instance.
525 type PolicyManager struct {
526 ctx context.Context
527 cancel context.CancelFunc
528 configDir string
529 configPath string // Path to policy.json file
530 scriptPath string // Default script path for backward compatibility
531 enabled bool
532 mutex sync.RWMutex
533 runners map[string]*ScriptRunner // Map of script path -> runner
534 }
535
536 // ConfigPath returns the path to the policy configuration file.
537 // This is used by hot-reload handlers to know where to save updated policy.
538 func (pm *PolicyManager) ConfigPath() string {
539 return pm.configPath
540 }
541
542 // P represents a complete policy configuration for a Nostr relay.
543 // It defines access control rules, kind filtering, and default behavior.
544 // Policies are evaluated in order: global rules, kind filtering, specific rules, then default policy.
545 type P struct {
546 // Kind is policies for accepting or rejecting events by kind number.
547 Kind Kinds `json:"kind"`
548 // rules is a map of rules for criteria that must be met for the event to be allowed to be written to the relay.
549 // Unexported to enforce use of public API methods (CheckPolicy, IsEnabled).
550 rules map[int]Rule
551 // Global is a rule set that applies to all events.
552 Global Rule `json:"global"`
553 // DefaultPolicy determines the default behavior when no rules deny an event ("allow" or "deny", defaults to "allow")
554 DefaultPolicy string `json:"default_policy"`
555
556 // PolicyAdmins is a list of hex-encoded pubkeys that can update policy configuration via kind 12345 events.
557 // These are SEPARATE from ACL relay admins - policy admins manage policy only.
558 PolicyAdmins []string `json:"policy_admins,omitempty"`
559 // PolicyFollowWhitelistEnabled enables automatic whitelisting of pubkeys followed by policy admins.
560 // When true and a rule has WriteAllowFollows=true, policy admin follows get read+write access.
561 PolicyFollowWhitelistEnabled bool `json:"policy_follow_whitelist_enabled,omitempty"`
562
563 // Owners is a list of hex-encoded pubkeys that have full control of the relay.
564 // These are merged with owners from the ORLY_OWNERS environment variable.
565 // Useful for cloud deployments where environment variables cannot be modified.
566 Owners []string `json:"owners,omitempty"`
567
568 // Unexported binary caches for faster comparison (populated from hex strings above)
569 policyAdminsBin [][]byte // Binary cache for policy admin pubkeys
570 policyFollows [][]byte // Cached follow list from policy admins (kind 3 events)
571 ownersBin [][]byte // Binary cache for policy-defined owner pubkeys
572
573 // followsMx protects all follows-related caches from concurrent access.
574 // This includes policyFollows, Global.readFollowsFollowsBin, Global.writeFollowsFollowsBin,
575 // and rule-specific follows whitelists.
576 // Use RLock for reads (CheckPolicy) and Lock for writes (Update*Follows*).
577 followsMx sync.RWMutex
578
579 // manager handles policy script execution.
580 // Unexported to enforce use of public API methods (CheckPolicy, IsEnabled).
581 manager *PolicyManager
582 }
583
584 // pJSON is a shadow struct for JSON unmarshalling with exported fields.
585 type pJSON struct {
586 Kind Kinds `json:"kind"`
587 Rules map[int]Rule `json:"rules"`
588 Global Rule `json:"global"`
589 DefaultPolicy string `json:"default_policy"`
590 PolicyAdmins []string `json:"policy_admins,omitempty"`
591 PolicyFollowWhitelistEnabled bool `json:"policy_follow_whitelist_enabled,omitempty"`
592 Owners []string `json:"owners,omitempty"`
593 }
594
595 // UnmarshalJSON implements custom JSON unmarshalling to handle unexported fields.
596 func (p *P) UnmarshalJSON(data []byte) error {
597 var shadow pJSON
598 if err := json.Unmarshal(data, &shadow); err != nil {
599 return err
600 }
601 p.Kind = shadow.Kind
602 p.rules = shadow.Rules
603 p.Global = shadow.Global
604 p.DefaultPolicy = shadow.DefaultPolicy
605 p.PolicyAdmins = shadow.PolicyAdmins
606 p.PolicyFollowWhitelistEnabled = shadow.PolicyFollowWhitelistEnabled
607 p.Owners = shadow.Owners
608
609 // Populate binary cache for policy admins
610 if len(p.PolicyAdmins) > 0 {
611 p.policyAdminsBin = make([][]byte, 0, len(p.PolicyAdmins))
612 for _, hexPubkey := range p.PolicyAdmins {
613 binPubkey, err := hex.Dec(hexPubkey)
614 if err != nil {
615 log.W.F("failed to decode PolicyAdmin pubkey %q: %v", hexPubkey, err)
616 continue
617 }
618 p.policyAdminsBin = append(p.policyAdminsBin, binPubkey)
619 }
620 }
621
622 // Populate binary cache for policy-defined owners
623 if len(p.Owners) > 0 {
624 p.ownersBin = make([][]byte, 0, len(p.Owners))
625 for _, hexPubkey := range p.Owners {
626 binPubkey, err := hex.Dec(hexPubkey)
627 if err != nil {
628 log.W.F("failed to decode owner pubkey %q: %v", hexPubkey, err)
629 continue
630 }
631 p.ownersBin = append(p.ownersBin, binPubkey)
632 }
633 }
634
635 return nil
636 }
637
638 // New creates a new policy from JSON configuration.
639 // If policyJSON is empty, returns a policy with default settings.
640 // The default_policy field defaults to "allow" if not specified.
641 // Returns an error if the policy JSON contains invalid values (e.g., invalid
642 // ISO-8601 duration format for max_expiry_duration, invalid regex patterns, etc.).
643 func New(policyJSON []byte) (p *P, err error) {
644 p = &P{
645 DefaultPolicy: "allow", // Set default value
646 }
647 if len(policyJSON) > 0 {
648 // Validate JSON before loading to fail fast on invalid configurations.
649 // This prevents silent failures where invalid values (like "T10M" instead
650 // of "PT10M" for max_expiry_duration) are ignored and constraints don't apply.
651 if err = p.ValidateJSON(policyJSON); err != nil {
652 return nil, fmt.Errorf("policy validation failed: %v", err)
653 }
654 if err = json.Unmarshal(policyJSON, p); chk.E(err) {
655 return nil, fmt.Errorf("failed to unmarshal policy JSON: %v", err)
656 }
657 }
658 // Ensure default policy is valid
659 if p.DefaultPolicy == "" {
660 p.DefaultPolicy = "allow"
661 }
662
663 // Populate binary caches for all rules (including global rule)
664 p.Global.populateBinaryCache()
665 for kind := range p.rules {
666 rule := p.rules[kind] // Get a copy
667 rule.populateBinaryCache()
668 p.rules[kind] = rule // Store the modified copy back
669 }
670
671 return
672 }
673
674 // IsPartyInvolved checks if the given pubkey is a party involved in the event.
675 // A party is involved if they are either:
676 // 1. The author of the event (ev.Pubkey == userPubkey)
677 // 2. Mentioned in a p-tag of the event
678 //
679 // Both ev.Pubkey and userPubkey must be binary ([]byte), not hex-encoded.
680 // P-tags may be stored in either binary-optimized format (33 bytes) or hex format.
681 //
682 // This is the single source of truth for "parties_involved" / "privileged" checks.
683 func IsPartyInvolved(ev *event.E, userPubkey []byte) bool {
684 // Must be authenticated
685 if len(userPubkey) == 0 {
686 return false
687 }
688
689 // Check if user is the author
690 if bytes.Equal(ev.Pubkey, userPubkey) {
691 return true
692 }
693
694 // Check if user is in p tags
695 pTags := ev.Tags.GetAll([]byte("p"))
696 for _, pTag := range pTags {
697 // ValueHex() handles both binary and hex storage formats automatically
698 pt, err := hex.Dec(string(pTag.ValueHex()))
699 if err != nil {
700 // Skip malformed tags
701 continue
702 }
703 if bytes.Equal(pt, userPubkey) {
704 return true
705 }
706 }
707
708 return false
709 }
710
711 // IsEnabled returns whether the policy system is enabled and ready to process events.
712 // This is the public API for checking if policy filtering should be applied.
713 func (p *P) IsEnabled() bool {
714 return p != nil && p.manager != nil && p.manager.IsEnabled()
715 }
716
717 // ConfigPath returns the path to the policy configuration file.
718 // Delegates to the internal PolicyManager.
719 func (p *P) ConfigPath() string {
720 if p == nil || p.manager == nil {
721 return ""
722 }
723 return p.manager.ConfigPath()
724 }
725
726 // getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
727 func (p *P) getDefaultPolicyAction() (allowed bool) {
728 switch p.DefaultPolicy {
729 case "deny":
730 return false
731 case "allow", "":
732 return true
733 default:
734 // Invalid value, default to allow
735 return true
736 }
737 }
738
739 // NewWithManager creates a new policy with a policy manager for script execution.
740 // It initializes the policy manager, loads configuration from files, and starts
741 // background processes for script management and periodic health checks.
742 //
743 // The customPolicyPath parameter allows overriding the default policy file location.
744 // If empty, uses the default path: $HOME/.config/{appName}/policy.json
745 // If provided, it MUST be an absolute path (starting with /) or the function will panic.
746 func NewWithManager(ctx context.Context, appName string, enabled bool, customPolicyPath string) *P {
747 configDir := filepath.Join(xdg.ConfigHome, appName)
748 scriptPath := filepath.Join(configDir, "policy.sh")
749
750 // Determine the policy config path
751 var configPath string
752 if customPolicyPath != "" {
753 // Validate that custom path is absolute
754 if !filepath.IsAbs(customPolicyPath) {
755 panic(fmt.Sprintf("FATAL: ORLY_POLICY_PATH must be an ABSOLUTE path (starting with /), got: %q", customPolicyPath))
756 }
757 configPath = customPolicyPath
758 // Update configDir to match the custom path's directory for script resolution
759 configDir = filepath.Dir(customPolicyPath)
760 scriptPath = filepath.Join(configDir, "policy.sh")
761 log.I.F("using custom policy path: %s", configPath)
762 } else {
763 configPath = filepath.Join(configDir, "policy.json")
764 }
765
766 ctx, cancel := context.WithCancel(ctx)
767
768 manager := &PolicyManager{
769 ctx: ctx,
770 cancel: cancel,
771 configDir: configDir,
772 configPath: configPath,
773 scriptPath: scriptPath,
774 enabled: enabled,
775 runners: make(map[string]*ScriptRunner),
776 }
777
778 // Load policy configuration from JSON file
779 policy := &P{
780 DefaultPolicy: "allow", // Set default value
781 manager: manager,
782 }
783
784 if enabled {
785 if err := policy.LoadFromFile(configPath); err != nil {
786 log.W.F(
787 "policy enabled but config failed to load from %s: %v — disabling policy",
788 configPath, err,
789 )
790 } else {
791 log.I.F("loaded policy configuration from %s", configPath)
792
793 // Start the policy script if it exists and is enabled
794 go manager.startPolicyIfExists()
795 // Start periodic check for policy script availability
796 go manager.periodicCheck()
797 }
798 }
799
800 return policy
801 }
802
803 // getOrCreateRunner gets an existing runner for the script path or creates a new one.
804 // This method is thread-safe and ensures only one runner exists per unique script path.
805 func (pm *PolicyManager) getOrCreateRunner(scriptPath string) *ScriptRunner {
806 pm.mutex.Lock()
807 defer pm.mutex.Unlock()
808
809 // Check if runner already exists
810 if runner, exists := pm.runners[scriptPath]; exists {
811 return runner
812 }
813
814 // Create new runner
815 runnerCtx, runnerCancel := context.WithCancel(pm.ctx)
816 runner := &ScriptRunner{
817 ctx: runnerCtx,
818 cancel: runnerCancel,
819 configDir: pm.configDir,
820 scriptPath: scriptPath,
821 responseChan: make(chan PolicyResponse, 100),
822 startupChan: make(chan error, 1),
823 }
824
825 pm.runners[scriptPath] = runner
826
827 // Start periodic check for this runner
828 go runner.periodicCheck()
829
830 return runner
831 }
832
833 // ScriptRunner methods
834
835 // IsRunning returns whether the script is currently running.
836 func (sr *ScriptRunner) IsRunning() bool {
837 sr.mutex.RLock()
838 defer sr.mutex.RUnlock()
839 return sr.isRunning
840 }
841
842 // ensureRunning ensures the script is running, starting it if necessary.
843 func (sr *ScriptRunner) ensureRunning() error {
844 sr.mutex.Lock()
845 // Check if already running
846 if sr.isRunning {
847 sr.mutex.Unlock()
848 return nil
849 }
850
851 // Check if already starting
852 if sr.isStarting {
853 sr.mutex.Unlock()
854 // Wait for startup to complete
855 select {
856 case err := <-sr.startupChan:
857 if err != nil {
858 return fmt.Errorf("script startup failed: %v", err)
859 }
860 // Double-check it's actually running after receiving signal
861 sr.mutex.RLock()
862 running := sr.isRunning
863 sr.mutex.RUnlock()
864 if !running {
865 return fmt.Errorf("script startup completed but process is not running")
866 }
867 return nil
868 case <-time.After(10 * time.Second):
869 return fmt.Errorf("script startup timeout")
870 case <-sr.ctx.Done():
871 return fmt.Errorf("script context cancelled")
872 }
873 }
874
875 // Mark as starting
876 sr.isStarting = true
877 sr.mutex.Unlock()
878
879 // Start the script in a goroutine
880 go func() {
881 err := sr.Start()
882 sr.mutex.Lock()
883 sr.isStarting = false
884 sr.mutex.Unlock()
885 // Signal startup completion (non-blocking)
886 // Drain any stale value first, then send
887 select {
888 case <-sr.startupChan:
889 default:
890 }
891 select {
892 case sr.startupChan <- err:
893 default:
894 // Channel should be empty now, but if it's full, try again
895 sr.startupChan <- err
896 }
897 }()
898
899 // Wait for startup to complete
900 select {
901 case err := <-sr.startupChan:
902 if err != nil {
903 return fmt.Errorf("script startup failed: %v", err)
904 }
905 // Double-check it's actually running after receiving signal
906 sr.mutex.RLock()
907 running := sr.isRunning
908 sr.mutex.RUnlock()
909 if !running {
910 return fmt.Errorf("script startup completed but process is not running")
911 }
912 return nil
913 case <-time.After(10 * time.Second):
914 sr.mutex.Lock()
915 sr.isStarting = false
916 sr.mutex.Unlock()
917 return fmt.Errorf("script startup timeout")
918 case <-sr.ctx.Done():
919 sr.mutex.Lock()
920 sr.isStarting = false
921 sr.mutex.Unlock()
922 return fmt.Errorf("script context cancelled")
923 }
924 }
925
926 // Start starts the script process.
927 func (sr *ScriptRunner) Start() error {
928 sr.mutex.Lock()
929 defer sr.mutex.Unlock()
930
931 if sr.isRunning {
932 return fmt.Errorf("script is already running")
933 }
934
935 if _, err := os.Stat(sr.scriptPath); os.IsNotExist(err) {
936 return fmt.Errorf("script does not exist at %s", sr.scriptPath)
937 }
938
939 // Create a new context for this command
940 cmdCtx, cmdCancel := context.WithCancel(sr.ctx)
941
942 // Make the script executable
943 if err := os.Chmod(sr.scriptPath, 0755); chk.E(err) {
944 cmdCancel()
945 return fmt.Errorf("failed to make script executable: %v", err)
946 }
947
948 // Start the script
949 cmd := exec.CommandContext(cmdCtx, sr.scriptPath)
950 cmd.Dir = sr.configDir
951
952 // Set up stdio pipes for communication
953 stdin, err := cmd.StdinPipe()
954 if chk.E(err) {
955 cmdCancel()
956 return fmt.Errorf("failed to create stdin pipe: %v", err)
957 }
958
959 stdout, err := cmd.StdoutPipe()
960 if chk.E(err) {
961 cmdCancel()
962 stdin.Close()
963 return fmt.Errorf("failed to create stdout pipe: %v", err)
964 }
965
966 stderr, err := cmd.StderrPipe()
967 if chk.E(err) {
968 cmdCancel()
969 stdin.Close()
970 stdout.Close()
971 return fmt.Errorf("failed to create stderr pipe: %v", err)
972 }
973
974 // Start the command
975 if err := cmd.Start(); chk.E(err) {
976 cmdCancel()
977 stdin.Close()
978 stdout.Close()
979 stderr.Close()
980 return fmt.Errorf("failed to start script: %v", err)
981 }
982
983 sr.currentCmd = cmd
984 sr.currentCancel = cmdCancel
985 sr.stdin = stdin
986 sr.stdout = stdout
987 sr.stderr = stderr
988 sr.isRunning = true
989
990 // Start response reader in background
991 go sr.readResponses()
992
993 // Log stderr output in background
994 go sr.logOutput(stdout, stderr)
995
996 // Monitor the process
997 go sr.monitorProcess()
998
999 log.I.F(
1000 "policy script started: %s (pid=%d)", sr.scriptPath, cmd.Process.Pid,
1001 )
1002 return nil
1003 }
1004
1005 // Stop stops the script gracefully.
1006 func (sr *ScriptRunner) Stop() error {
1007 sr.mutex.Lock()
1008
1009 if !sr.isRunning || sr.currentCmd == nil {
1010 sr.mutex.Unlock()
1011 return fmt.Errorf("script is not running")
1012 }
1013
1014 // Close stdin first to signal the script to exit
1015 if sr.stdin != nil {
1016 sr.stdin.Close()
1017 }
1018
1019 // Cancel the context
1020 if sr.currentCancel != nil {
1021 sr.currentCancel()
1022 }
1023
1024 // Get the process reference before releasing the lock
1025 process := sr.currentCmd.Process
1026 sr.mutex.Unlock()
1027
1028 // Wait for graceful shutdown with timeout
1029 // Note: monitorProcess() is the one that calls cmd.Wait() and cleans up
1030 // We just wait for it to finish by polling isRunning
1031 gracefulShutdown := false
1032 for i := 0; i < 50; i++ { // 5 seconds total (50 * 100ms)
1033 time.Sleep(100 * time.Millisecond)
1034 sr.mutex.RLock()
1035 running := sr.isRunning
1036 sr.mutex.RUnlock()
1037 if !running {
1038 gracefulShutdown = true
1039 log.I.F("policy script stopped gracefully: %s", sr.scriptPath)
1040 break
1041 }
1042 }
1043
1044 if !gracefulShutdown {
1045 // Force kill after timeout
1046 log.W.F(
1047 "policy script did not stop gracefully, sending SIGKILL: %s",
1048 sr.scriptPath,
1049 )
1050 if process != nil {
1051 if err := process.Kill(); chk.E(err) {
1052 log.E.F("failed to kill script process: %v", err)
1053 }
1054 }
1055
1056 // Wait a bit more for monitorProcess to clean up
1057 for i := 0; i < 30; i++ { // 3 more seconds
1058 time.Sleep(100 * time.Millisecond)
1059 sr.mutex.RLock()
1060 running := sr.isRunning
1061 sr.mutex.RUnlock()
1062 if !running {
1063 break
1064 }
1065 }
1066 }
1067
1068 return nil
1069 }
1070
1071 // ProcessEvent sends an event to the script and waits for a response.
1072 func (sr *ScriptRunner) ProcessEvent(evt *PolicyEvent) (
1073 *PolicyResponse, error,
1074 ) {
1075 log.D.F("processing event: %s", evt.Serialize())
1076 sr.mutex.RLock()
1077 if !sr.isRunning || sr.stdin == nil {
1078 sr.mutex.RUnlock()
1079 return nil, fmt.Errorf("script is not running")
1080 }
1081 stdin := sr.stdin
1082 sr.mutex.RUnlock()
1083
1084 // Serialize the event to JSON
1085 eventJSON, err := json.Marshal(evt)
1086 if chk.E(err) {
1087 return nil, fmt.Errorf("failed to serialize event: %v", err)
1088 }
1089
1090 // Send the event JSON to the script (newline-terminated)
1091 if _, err := stdin.Write(append(eventJSON, '\n')); chk.E(err) {
1092 // Check if it's a broken pipe error, which means the script has died
1093 if strings.Contains(err.Error(), "broken pipe") || strings.Contains(err.Error(), "closed pipe") {
1094 log.E.F(
1095 "policy script %s stdin closed (broken pipe) - script may have crashed or exited prematurely",
1096 sr.scriptPath,
1097 )
1098 // Mark as not running so it will be restarted on next periodic check
1099 sr.mutex.Lock()
1100 sr.isRunning = false
1101 sr.mutex.Unlock()
1102 }
1103 return nil, fmt.Errorf("failed to write event to script: %v", err)
1104 }
1105
1106 // Wait for response with timeout
1107 select {
1108 case response := <-sr.responseChan:
1109 log.D.S("response", response)
1110 return &response, nil
1111 case <-time.After(5 * time.Second):
1112 log.W.F(
1113 "policy script %s response timeout - script may not be responding correctly (check for debug output on stdout)",
1114 sr.scriptPath,
1115 )
1116 return nil, fmt.Errorf("script response timeout")
1117 case <-sr.ctx.Done():
1118 return nil, fmt.Errorf("script context cancelled")
1119 }
1120 }
1121
1122 // readResponses reads JSONL responses from the script
1123 func (sr *ScriptRunner) readResponses() {
1124 if sr.stdout == nil {
1125 return
1126 }
1127
1128 scanner := bufio.NewScanner(sr.stdout)
1129 nonJSONLineCount := 0
1130 for scanner.Scan() {
1131 line := scanner.Text()
1132 if line == "" {
1133 continue
1134 }
1135 log.D.F("policy response: %s", line)
1136 var response PolicyResponse
1137 if err := json.Unmarshal([]byte(line), &response); chk.E(err) {
1138 // Check if this looks like debug output
1139 if strings.HasPrefix(line, "{") {
1140 // Looks like JSON but failed to parse
1141 log.E.F(
1142 "failed to parse policy response from %s: %v\nLine: %s",
1143 sr.scriptPath, err, line,
1144 )
1145 } else {
1146 // Definitely not JSON - probably debug output
1147 nonJSONLineCount++
1148 if nonJSONLineCount <= 3 {
1149 log.W.F(
1150 "policy script %s produced non-JSON output on stdout (should only output JSONL): %q",
1151 sr.scriptPath, line,
1152 )
1153 } else if nonJSONLineCount == 4 {
1154 log.W.F(
1155 "policy script %s continues to produce non-JSON output - suppressing further warnings",
1156 sr.scriptPath,
1157 )
1158 }
1159 log.W.F(
1160 "IMPORTANT: Policy scripts must ONLY write JSON responses to stdout. Use stderr or a log file for debug output.",
1161 )
1162 }
1163 continue
1164 }
1165
1166 // Send response to channel (non-blocking)
1167 select {
1168 case sr.responseChan <- response:
1169 default:
1170 log.W.F(
1171 "policy response channel full for %s, dropping response",
1172 sr.scriptPath,
1173 )
1174 }
1175 }
1176
1177 if err := scanner.Err(); chk.E(err) {
1178 log.E.F(
1179 "error reading policy responses from %s: %v", sr.scriptPath, err,
1180 )
1181 }
1182 }
1183
1184 // logOutput logs the output from stderr
1185 func (sr *ScriptRunner) logOutput(_ /* stdout */, stderr io.ReadCloser) {
1186 defer stderr.Close()
1187
1188 // Only log stderr, stdout is used by readResponses
1189 go func() {
1190 scanner := bufio.NewScanner(stderr)
1191 for scanner.Scan() {
1192 line := scanner.Text()
1193 if line != "" {
1194 // Log script stderr output through relay logging system
1195 log.I.F("[policy script %s] %s", sr.scriptPath, line)
1196 }
1197 }
1198 if err := scanner.Err(); chk.E(err) {
1199 log.E.F("error reading stderr from policy script %s: %v", sr.scriptPath, err)
1200 }
1201 }()
1202 }
1203
1204 // monitorProcess monitors the script process and cleans up when it exits
1205 func (sr *ScriptRunner) monitorProcess() {
1206 if sr.currentCmd == nil {
1207 return
1208 }
1209
1210 err := sr.currentCmd.Wait()
1211
1212 sr.mutex.Lock()
1213 defer sr.mutex.Unlock()
1214
1215 // Clean up pipes
1216 if sr.stdin != nil {
1217 sr.stdin.Close()
1218 sr.stdin = nil
1219 }
1220 if sr.stdout != nil {
1221 sr.stdout.Close()
1222 sr.stdout = nil
1223 }
1224 if sr.stderr != nil {
1225 sr.stderr.Close()
1226 sr.stderr = nil
1227 }
1228
1229 sr.isRunning = false
1230 sr.currentCmd = nil
1231 sr.currentCancel = nil
1232
1233 if err != nil {
1234 log.E.F(
1235 "policy script exited with error: %s: %v, will retry periodically",
1236 sr.scriptPath, err,
1237 )
1238 } else {
1239 log.I.F("policy script exited normally: %s", sr.scriptPath)
1240 }
1241 }
1242
1243 // periodicCheck periodically checks if script becomes available and attempts to restart failed scripts.
1244 func (sr *ScriptRunner) periodicCheck() {
1245 ticker := time.NewTicker(60 * time.Second)
1246 defer ticker.Stop()
1247
1248 for {
1249 select {
1250 case <-sr.ctx.Done():
1251 return
1252 case <-ticker.C:
1253 sr.mutex.RLock()
1254 running := sr.isRunning
1255 sr.mutex.RUnlock()
1256
1257 // Check if script is not running and try to start it
1258 if !running {
1259 if _, err := os.Stat(sr.scriptPath); err == nil {
1260 // Script exists but not running, try to start
1261 go func() {
1262 if err := sr.Start(); err != nil {
1263 log.E.F(
1264 "failed to restart policy script %s: %v, will retry in next cycle",
1265 sr.scriptPath, err,
1266 )
1267 } else {
1268 log.I.F(
1269 "policy script restarted successfully: %s",
1270 sr.scriptPath,
1271 )
1272 }
1273 }()
1274 }
1275 }
1276 }
1277 }
1278 }
1279
1280 // LoadFromFile loads policy configuration from a JSON file.
1281 // Returns an error if the file doesn't exist, can't be read, or contains invalid JSON.
1282 func (p *P) LoadFromFile(configPath string) error {
1283 if _, err := os.Stat(configPath); os.IsNotExist(err) {
1284 return fmt.Errorf(
1285 "policy configuration file does not exist: %s", configPath,
1286 )
1287 }
1288
1289 configData, err := os.ReadFile(configPath)
1290 if err != nil {
1291 return fmt.Errorf("failed to read policy configuration file: %v", err)
1292 }
1293
1294 if len(configData) == 0 {
1295 return fmt.Errorf("policy configuration file is empty")
1296 }
1297
1298 if err := json.Unmarshal(configData, p); err != nil {
1299 return fmt.Errorf("failed to parse policy configuration JSON: %v", err)
1300 }
1301
1302 // Populate binary caches for all rules (including global rule)
1303 p.Global.populateBinaryCache()
1304 for kind, rule := range p.rules {
1305 rule.populateBinaryCache()
1306 p.rules[kind] = rule // Update the map with the modified rule
1307 }
1308
1309 return nil
1310 }
1311
1312 // CheckPolicy checks if an event is allowed based on the policy configuration.
1313 // The access parameter should be "write" for accepting events or "read" for filtering events.
1314 // Returns true if the event is allowed, false if denied, and an error if validation fails.
1315 //
1316 // Policy evaluation order (more specific rules take precedence):
1317 // 1. Kinds whitelist/blacklist - if kind is blocked, deny immediately
1318 // 2. Kind-specific rule - if exists for this kind, use it exclusively
1319 // 3. Global rule - fallback if no kind-specific rule exists
1320 // 4. Default policy - fallback if no rules apply
1321 //
1322 // Thread-safety: Uses followsMx.RLock to protect reads of follows whitelists during policy checks.
1323 // Write operations (Update*) acquire the write lock, which blocks concurrent reads.
1324 func (p *P) CheckPolicy(
1325 access string, ev *event.E, loggedInPubkey []byte, ipAddress string,
1326 ) (allowed bool, err error) {
1327 // Handle nil policy - this should not happen if policy is enabled
1328 // If policy is enabled but p is nil, it's a configuration error
1329 if p == nil {
1330 log.F.Ln("FATAL: CheckPolicy called on nil policy - this indicates misconfiguration. " +
1331 "If ORLY_POLICY_ENABLED=true, ensure policy configuration is valid.")
1332 return false, fmt.Errorf("policy is nil but policy checking is enabled - check configuration")
1333 }
1334
1335 // Handle nil event
1336 if ev == nil {
1337 return false, fmt.Errorf("event cannot be nil")
1338 }
1339
1340 // Acquire read lock to protect follows whitelists during policy check
1341 p.followsMx.RLock()
1342 defer p.followsMx.RUnlock()
1343
1344 // ==========================================================================
1345 // STEP 1: Check kinds whitelist/blacklist (applies before any rule checks)
1346 // ==========================================================================
1347 if !p.checkKindsPolicy(access, ev.Kind) {
1348 return false, nil
1349 }
1350
1351 // ==========================================================================
1352 // STEP 2: Check KIND-SPECIFIC rule FIRST (more specific = higher priority)
1353 // ==========================================================================
1354 // If kind-specific rule exists and accepts, that's final - global is ignored.
1355 rule, hasKindRule := p.rules[int(ev.Kind)]
1356 if hasKindRule {
1357 // Check if script is present and enabled for this kind
1358 if rule.Script != "" && p.manager != nil {
1359 if p.manager.IsEnabled() {
1360 // Check if script file exists before trying to use it
1361 if _, err := os.Stat(rule.Script); err == nil {
1362 // Script exists, try to use it
1363 log.D.F("using policy script for kind %d: %s", ev.Kind, rule.Script)
1364 allowed, err := p.checkScriptPolicy(
1365 access, ev, rule.Script, loggedInPubkey, ipAddress,
1366 )
1367 if err == nil {
1368 // Script ran successfully, return its decision
1369 return allowed, nil
1370 }
1371 // Script failed, fall through to apply other criteria
1372 log.W.F("policy script check failed for kind %d: %v, applying other criteria",
1373 ev.Kind, err)
1374 } else {
1375 // Script configured but doesn't exist
1376 log.W.F("policy script configured for kind %d but not found at %s: %v, applying other criteria",
1377 ev.Kind, rule.Script, err)
1378 }
1379 // Script doesn't exist or failed, fall through to apply other criteria
1380 } else {
1381 // Policy manager is disabled, fall back to default policy
1382 log.D.F("policy manager is disabled for kind %d, falling back to default policy (%s)",
1383 ev.Kind, p.DefaultPolicy)
1384 return p.getDefaultPolicyAction(), nil
1385 }
1386 }
1387
1388 // Apply kind-specific rule-based filtering
1389 return p.checkRulePolicy(access, ev, rule, loggedInPubkey)
1390 }
1391
1392 // ==========================================================================
1393 // STEP 3: No kind-specific rule - check GLOBAL rule as fallback
1394 // ==========================================================================
1395
1396 // Check if global rule has any configuration
1397 if p.Global.hasAnyRules() {
1398 // Apply global rule filtering
1399 return p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
1400 }
1401
1402 // ==========================================================================
1403 // STEP 4: No kind-specific or global rules - use default policy
1404 // ==========================================================================
1405 return p.getDefaultPolicyAction(), nil
1406 }
1407
1408 // checkKindsPolicy checks if the event kind is allowed for the given access type.
1409 // Logic:
1410 // 1. If explicit whitelist exists, use it (but respect permissive flags for read/write)
1411 // 2. If explicit blacklist exists, use it (but respect permissive flags for read/write)
1412 // 3. Otherwise, kinds with defined rules are implicitly allowed, others denied (with permissive overrides)
1413 //
1414 // Permissive flags (set on Global rule):
1415 // - ReadAllowPermissive: Allows READ access for kinds not in whitelist (write still restricted)
1416 // - WriteAllowPermissive: Allows WRITE access for kinds not in whitelist (uses global rule constraints)
1417 func (p *P) checkKindsPolicy(access string, kind uint16) bool {
1418 // If whitelist is present, only allow whitelisted kinds (with permissive overrides)
1419 if len(p.Kind.Whitelist) > 0 {
1420 for _, allowedKind := range p.Kind.Whitelist {
1421 if kind == uint16(allowedKind) {
1422 return true
1423 }
1424 }
1425 // Kind not in whitelist - check permissive flags
1426 if access == "read" && p.Global.ReadAllowPermissive {
1427 log.D.F("read_allow_permissive: allowing read for kind %d not in whitelist", kind)
1428 return true // Allow read even though kind not whitelisted
1429 }
1430 if access == "write" && p.Global.WriteAllowPermissive {
1431 log.D.F("write_allow_permissive: allowing write for kind %d not in whitelist (global rules apply)", kind)
1432 return true // Allow write even though kind not whitelisted, global rule will be applied
1433 }
1434 return false
1435 }
1436
1437 // If blacklist is present, deny blacklisted kinds
1438 if len(p.Kind.Blacklist) > 0 {
1439 for _, deniedKind := range p.Kind.Blacklist {
1440 if kind == uint16(deniedKind) {
1441 // Kind is explicitly blacklisted - permissive flags don't override blacklist
1442 return false
1443 }
1444 }
1445 // Not in blacklist - check if rule exists for implicit whitelist
1446 _, hasRule := p.rules[int(kind)]
1447 if hasRule {
1448 return true
1449 }
1450 // No kind-specific rule - check permissive flags
1451 if access == "read" && p.Global.ReadAllowPermissive {
1452 log.D.F("read_allow_permissive: allowing read for kind %d (not blacklisted, no rule)", kind)
1453 return true
1454 }
1455 if access == "write" && p.Global.WriteAllowPermissive {
1456 log.D.F("write_allow_permissive: allowing write for kind %d (not blacklisted, no rule)", kind)
1457 return true
1458 }
1459 return false // Only allow if there's a rule defined
1460 }
1461
1462 // No explicit whitelist or blacklist
1463 // Behavior depends on whether default_policy is explicitly set:
1464 // - If default_policy is explicitly "allow", allow all kinds (rules add constraints, not restrictions)
1465 // - If default_policy is unset or "deny", use implicit whitelist (only allow kinds with rules)
1466 // - If global rule has any configuration, allow kinds through for global rule checking
1467 // - Permissive flags can override implicit whitelist behavior
1468 if len(p.rules) > 0 {
1469 // If default_policy is explicitly "allow", don't use implicit whitelist
1470 if p.DefaultPolicy == "allow" {
1471 return true
1472 }
1473 // Implicit whitelist mode - only allow kinds with specific rules
1474 _, hasRule := p.rules[int(kind)]
1475 if hasRule {
1476 return true
1477 }
1478 // No kind-specific rule, but check if global rule exists
1479 if p.Global.hasAnyRules() {
1480 return true // Allow through for global rule check
1481 }
1482 // Check permissive flags for implicit whitelist override
1483 if access == "read" && p.Global.ReadAllowPermissive {
1484 log.D.F("read_allow_permissive: allowing read for kind %d (implicit whitelist override)", kind)
1485 return true
1486 }
1487 if access == "write" && p.Global.WriteAllowPermissive {
1488 log.D.F("write_allow_permissive: allowing write for kind %d (implicit whitelist override)", kind)
1489 return true
1490 }
1491 return false
1492 }
1493 // No kind-specific rules - check if global rule exists
1494 if p.Global.hasAnyRules() {
1495 return true // Allow through for global rule check
1496 }
1497 // No rules at all - fall back to default policy
1498 return p.getDefaultPolicyAction()
1499 }
1500
1501 // checkGlobalFollowsWhitelistAccess checks if the user is explicitly granted access
1502 // via the global rule's follows whitelists (read_follows_whitelist or write_follows_whitelist).
1503 // This grants access that bypasses the default policy for kinds without specific rules.
1504 // Note: p should never be nil here - caller (CheckPolicy) already validates this.
1505 func (p *P) checkGlobalFollowsWhitelistAccess(access string, loggedInPubkey []byte) bool {
1506 if len(loggedInPubkey) == 0 {
1507 return false
1508 }
1509
1510 if access == "read" {
1511 // Check if user is in global read follows whitelist
1512 if p.Global.HasReadFollowsWhitelist() && p.Global.IsInReadFollowsWhitelist(loggedInPubkey) {
1513 return true
1514 }
1515 // Also check legacy WriteAllowFollows and FollowsWhitelistAdmins for read access
1516 if p.Global.WriteAllowFollows && p.PolicyFollowWhitelistEnabled && p.IsPolicyFollow(loggedInPubkey) {
1517 return true
1518 }
1519 if p.Global.HasFollowsWhitelistAdmins() && p.Global.IsInFollowsWhitelist(loggedInPubkey) {
1520 return true
1521 }
1522 } else if access == "write" {
1523 // Check if user is in global write follows whitelist
1524 if p.Global.HasWriteFollowsWhitelist() && p.Global.IsInWriteFollowsWhitelist(loggedInPubkey) {
1525 return true
1526 }
1527 // Also check legacy WriteAllowFollows and FollowsWhitelistAdmins for write access
1528 if p.Global.WriteAllowFollows && p.PolicyFollowWhitelistEnabled && p.IsPolicyFollow(loggedInPubkey) {
1529 return true
1530 }
1531 if p.Global.HasFollowsWhitelistAdmins() && p.Global.IsInFollowsWhitelist(loggedInPubkey) {
1532 return true
1533 }
1534 }
1535
1536 return false
1537 }
1538
1539 // checkGlobalRulePolicy checks if the event passes the global rule filter
1540 // Note: p should never be nil here - caller (CheckPolicy) already validates this.
1541 func (p *P) checkGlobalRulePolicy(
1542 access string, ev *event.E, loggedInPubkey []byte,
1543 ) bool {
1544 // Skip if no global rules are configured
1545 if !p.Global.hasAnyRules() {
1546 return true
1547 }
1548
1549 // Apply global rule filtering
1550 allowed, err := p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
1551 if err != nil {
1552 log.E.F("global rule policy check failed: %v", err)
1553 return false
1554 }
1555 return allowed
1556 }
1557
1558 // checkRulePolicy evaluates rule-based access control with the following logic:
1559 //
1560 // READ ACCESS (default-permissive):
1561 // - Denied if in read_deny list
1562 // - If read_allow, read_follows_whitelist, or privileged is set, user must pass one of those checks
1563 // - Otherwise, read is allowed by default
1564 //
1565 // WRITE ACCESS (default-permissive):
1566 // - Denied if in write_deny list
1567 // - Universal constraints (size, tags, age) apply to writes only
1568 // - If write_allow or write_follows_whitelist is set, user must pass one of those checks
1569 // - Otherwise, write is allowed by default
1570 //
1571 // PRIVILEGED: Only applies to READ operations (party-involved check)
1572 func (p *P) checkRulePolicy(
1573 access string, ev *event.E, rule Rule, loggedInPubkey []byte,
1574 ) (allowed bool, err error) {
1575 log.T.F("checkRulePolicy: access=%s kind=%d readFollowsFollowsBin_len=%d readFollowsWhitelistBin_len=%d HasReadFollowsWhitelist=%v",
1576 access, ev.Kind, len(rule.readFollowsFollowsBin), len(rule.readFollowsWhitelistBin), rule.HasReadFollowsWhitelist())
1577
1578 // ===================================================================
1579 // STEP 1: Universal Constraints (WRITE ONLY - apply to everyone)
1580 // ===================================================================
1581
1582 if access == "write" {
1583 // Check size limits
1584 if rule.SizeLimit != nil {
1585 eventSize := int64(len(ev.Serialize()))
1586 if eventSize > *rule.SizeLimit {
1587 return false, nil
1588 }
1589 }
1590
1591 if rule.ContentLimit != nil {
1592 contentSize := int64(len(ev.Content))
1593 if contentSize > *rule.ContentLimit {
1594 return false, nil
1595 }
1596 }
1597
1598 // Check required tags
1599 if len(rule.MustHaveTags) > 0 {
1600 for _, requiredTag := range rule.MustHaveTags {
1601 if ev.Tags.GetFirst([]byte(requiredTag)) == nil {
1602 return false, nil
1603 }
1604 }
1605 }
1606
1607 // Check expiry time (uses maxExpirySeconds which is parsed from MaxExpiryDuration or MaxExpiry)
1608 if rule.maxExpirySeconds != nil && *rule.maxExpirySeconds > 0 {
1609 expiryTag := ev.Tags.GetFirst([]byte("expiration"))
1610 if expiryTag == nil {
1611 return false, nil // Must have expiry if max_expiry is set
1612 }
1613 // Parse expiry timestamp and validate it's within allowed duration from created_at
1614 expiryStr := string(expiryTag.Value())
1615 expiryTs, parseErr := strconv.ParseInt(expiryStr, 10, 64)
1616 if parseErr != nil {
1617 log.D.F("invalid expiration tag value %q: %v", expiryStr, parseErr)
1618 return false, nil // Invalid expiry format
1619 }
1620 maxAllowedExpiry := ev.CreatedAt + *rule.maxExpirySeconds
1621 if expiryTs >= maxAllowedExpiry {
1622 log.D.F("expiration %d exceeds max allowed %d (created_at %d + max_expiry %d)",
1623 expiryTs, maxAllowedExpiry, ev.CreatedAt, *rule.maxExpirySeconds)
1624 return false, nil // Expiry too far in the future
1625 }
1626 }
1627
1628 // Check ProtectedRequired (NIP-70: events must have "-" tag)
1629 if rule.ProtectedRequired {
1630 protectedTag := ev.Tags.GetFirst([]byte("-"))
1631 if protectedTag == nil {
1632 log.D.F("protected_required: event missing '-' tag (NIP-70)")
1633 return false, nil // Must have protected tag
1634 }
1635 }
1636
1637 // Check IdentifierRegex (validates "d" tag values)
1638 if rule.identifierRegexCache != nil {
1639 dTags := ev.Tags.GetAll([]byte("d"))
1640 if len(dTags) == 0 {
1641 log.D.F("identifier_regex: event missing 'd' tag")
1642 return false, nil // Must have d tag if identifier_regex is set
1643 }
1644 for _, dTag := range dTags {
1645 value := string(dTag.Value())
1646 if !rule.identifierRegexCache.MatchString(value) {
1647 log.D.F("identifier_regex: d tag value %q does not match pattern %q",
1648 value, rule.IdentifierRegex)
1649 return false, nil
1650 }
1651 }
1652 }
1653
1654 // Check MaxAgeOfEvent (maximum age of event in seconds)
1655 if rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 {
1656 currentTime := time.Now().Unix()
1657 maxAllowedTime := currentTime - *rule.MaxAgeOfEvent
1658 if ev.CreatedAt < maxAllowedTime {
1659 return false, nil // Event is too old
1660 }
1661 }
1662
1663 // Check MaxAgeEventInFuture (maximum time event can be in the future in seconds)
1664 if rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 {
1665 currentTime := time.Now().Unix()
1666 maxFutureTime := currentTime + *rule.MaxAgeEventInFuture
1667 if ev.CreatedAt > maxFutureTime {
1668 return false, nil // Event is too far in the future
1669 }
1670 }
1671
1672 // Check tag validation rules (regex patterns)
1673 // NOTE: TagValidation only validates tags that ARE present on the event.
1674 // To REQUIRE a tag to exist, use MustHaveTags instead.
1675 if len(rule.TagValidation) > 0 {
1676 for tagName, regexPattern := range rule.TagValidation {
1677 // Compile regex pattern (errors should have been caught in ValidateJSON)
1678 regex, compileErr := regexp.Compile(regexPattern)
1679 if compileErr != nil {
1680 log.E.F("invalid regex pattern for tag %q: %v (skipping validation)", tagName, compileErr)
1681 continue
1682 }
1683
1684 // Get all tags with this name
1685 tags := ev.Tags.GetAll([]byte(tagName))
1686
1687 // If no tags found, skip validation for this tag type
1688 // (TagValidation validates format, not presence - use MustHaveTags for presence)
1689 if len(tags) == 0 {
1690 continue
1691 }
1692
1693 // Validate each tag value against regex
1694 for _, t := range tags {
1695 value := string(t.Value())
1696 if !regex.MatchString(value) {
1697 log.D.F("tag validation failed: tag %q value %q does not match pattern %q",
1698 tagName, value, regexPattern)
1699 return false, nil
1700 }
1701 }
1702 }
1703 }
1704 }
1705
1706 // ===================================================================
1707 // STEP 2: Explicit Denials (highest priority blacklist)
1708 // ===================================================================
1709
1710 if access == "write" {
1711 // Check write deny list - deny specific users from submitting events
1712 if len(rule.writeDenyBin) > 0 {
1713 for _, deniedPubkey := range rule.writeDenyBin {
1714 if utils.FastEqual(loggedInPubkey, deniedPubkey) {
1715 return false, nil // Submitter explicitly denied
1716 }
1717 }
1718 } else if len(rule.WriteDeny) > 0 {
1719 // Fallback: binary cache not populated, use hex comparison
1720 loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1721 for _, deniedPubkey := range rule.WriteDeny {
1722 if loggedInPubkeyHex == deniedPubkey {
1723 return false, nil // Submitter explicitly denied
1724 }
1725 }
1726 }
1727 } else if access == "read" {
1728 // Check read deny list
1729 if len(rule.readDenyBin) > 0 {
1730 for _, deniedPubkey := range rule.readDenyBin {
1731 if utils.FastEqual(loggedInPubkey, deniedPubkey) {
1732 return false, nil // Explicitly denied
1733 }
1734 }
1735 } else if len(rule.ReadDeny) > 0 {
1736 // Fallback: binary cache not populated, use hex comparison
1737 loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1738 for _, deniedPubkey := range rule.ReadDeny {
1739 if loggedInPubkeyHex == deniedPubkey {
1740 return false, nil // Explicitly denied
1741 }
1742 }
1743 }
1744 }
1745
1746 // ===================================================================
1747 // STEP 3: Legacy WriteAllowFollows (grants BOTH read AND write access)
1748 // ===================================================================
1749
1750 // WriteAllowFollows grants both read and write access to policy admin follows
1751 // This check applies to BOTH read and write access types (legacy behavior)
1752 if rule.WriteAllowFollows && p.PolicyFollowWhitelistEnabled {
1753 if p.IsPolicyFollow(loggedInPubkey) {
1754 log.D.F("policy admin follow granted %s access for kind %d", access, ev.Kind)
1755 return true, nil // Allow access from policy admin follow
1756 }
1757 }
1758
1759 // FollowsWhitelistAdmins grants access to follows of specific admin pubkeys for this rule
1760 // This is a per-rule alternative to WriteAllowFollows which uses global PolicyAdmins (DEPRECATED)
1761 if rule.HasFollowsWhitelistAdmins() {
1762 if rule.IsInFollowsWhitelist(loggedInPubkey) {
1763 log.D.F("follows_whitelist_admins granted %s access for kind %d", access, ev.Kind)
1764 return true, nil // Allow access from rule-specific admin follow
1765 }
1766 }
1767
1768 // ===================================================================
1769 // STEP 4: New Follows Whitelist Checks (separate read/write)
1770 // ===================================================================
1771
1772 if access == "read" {
1773 // Check ReadFollowsWhitelist - if set, it acts as a whitelist
1774 if rule.HasReadFollowsWhitelist() {
1775 if rule.IsInReadFollowsWhitelist(loggedInPubkey) {
1776 log.D.F("read_follows_whitelist granted read access for kind %d", ev.Kind)
1777 return true, nil
1778 }
1779 // ReadFollowsWhitelist is set but user is not in it
1780 // Continue to check other access methods (privileged, read_allow)
1781 }
1782 } else if access == "write" {
1783 // Check WriteFollowsWhitelist - if set, it acts as a whitelist
1784 if rule.HasWriteFollowsWhitelist() {
1785 if rule.IsInWriteFollowsWhitelist(loggedInPubkey) {
1786 log.D.F("write_follows_whitelist granted write access for kind %d", ev.Kind)
1787 return true, nil
1788 }
1789 // WriteFollowsWhitelist is set but user is not in it - must check write_allow too
1790 }
1791 }
1792
1793 // ===================================================================
1794 // STEP 5: Read Access Control
1795 // ===================================================================
1796
1797 if access == "read" {
1798 hasReadAllowList := len(rule.readAllowBin) > 0 || len(rule.ReadAllow) > 0
1799 hasReadFollowsWhitelist := rule.HasReadFollowsWhitelist()
1800 // Include deprecated FollowsWhitelistAdmins for backward compatibility (it grants read+write)
1801 hasLegacyFollowsWhitelist := rule.HasFollowsWhitelistAdmins()
1802 userIsPrivileged := rule.Privileged && IsPartyInvolved(ev, loggedInPubkey)
1803
1804 // Check if user is in read allow list
1805 userInAllowList := false
1806 if len(rule.readAllowBin) > 0 {
1807 for _, allowedPubkey := range rule.readAllowBin {
1808 if utils.FastEqual(loggedInPubkey, allowedPubkey) {
1809 userInAllowList = true
1810 break
1811 }
1812 }
1813 } else if len(rule.ReadAllow) > 0 {
1814 loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1815 for _, allowedPubkey := range rule.ReadAllow {
1816 if loggedInPubkeyHex == allowedPubkey {
1817 userInAllowList = true
1818 break
1819 }
1820 }
1821 }
1822
1823 // Determine if any read whitelist restriction is active
1824 // Note: Legacy FollowsWhitelistAdmins also counts as a read restriction for backward compatibility
1825 hasReadRestriction := hasReadAllowList || hasReadFollowsWhitelist || hasLegacyFollowsWhitelist || rule.Privileged
1826
1827 if hasReadRestriction {
1828 // User must pass one of the configured access methods
1829 if userInAllowList {
1830 return true, nil
1831 }
1832 if userIsPrivileged {
1833 return true, nil
1834 }
1835 // User is in ReadFollowsWhitelist was already checked in STEP 4
1836 // User in legacy FollowsWhitelistAdmins was already checked in STEP 3
1837 // If we reach here with a read restriction, deny access
1838 return false, nil
1839 }
1840
1841 // No read restriction configured - read is permissive by default
1842 return true, nil
1843 }
1844
1845 // ===================================================================
1846 // STEP 6: Write Access Control
1847 // ===================================================================
1848
1849 if access == "write" {
1850 hasWriteAllowList := len(rule.writeAllowBin) > 0 || len(rule.WriteAllow) > 0
1851 hasWriteFollowsWhitelist := rule.HasWriteFollowsWhitelist()
1852 // Include deprecated FollowsWhitelistAdmins for backward compatibility
1853 hasLegacyFollowsWhitelist := rule.HasFollowsWhitelistAdmins()
1854
1855 // Check if user is in write allow list
1856 userInAllowList := false
1857 if len(rule.writeAllowBin) > 0 {
1858 for _, allowedPubkey := range rule.writeAllowBin {
1859 if utils.FastEqual(loggedInPubkey, allowedPubkey) {
1860 userInAllowList = true
1861 break
1862 }
1863 }
1864 } else if len(rule.WriteAllow) > 0 {
1865 loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1866 for _, allowedPubkey := range rule.WriteAllow {
1867 if loggedInPubkeyHex == allowedPubkey {
1868 userInAllowList = true
1869 break
1870 }
1871 }
1872 }
1873
1874 // Determine if any write whitelist restriction is active
1875 // Note: Legacy FollowsWhitelistAdmins also counts as a write restriction for backward compatibility
1876 hasWriteIfTagged := len(rule.writeAllowIfTaggedBin) > 0 || len(rule.WriteAllowIfTagged) > 0
1877 hasWriteRestriction := hasWriteAllowList || hasWriteFollowsWhitelist || hasLegacyFollowsWhitelist || hasWriteIfTagged
1878
1879 if hasWriteRestriction {
1880 // User must pass one of the configured access methods
1881 if userInAllowList {
1882 return true, nil
1883 }
1884 // User in WriteFollowsWhitelist was already checked in STEP 4
1885 // User in legacy FollowsWhitelistAdmins was already checked in STEP 3
1886
1887 // Check write_allow_if_tagged: allow if event p-tags a listed pubkey
1888 if hasWriteIfTagged {
1889 pTags := ev.Tags.GetAll([]byte("p"))
1890 for _, pTag := range pTags {
1891 pt, decErr := hex.Dec(string(pTag.ValueHex()))
1892 if decErr != nil {
1893 continue
1894 }
1895 for _, allowed := range rule.writeAllowIfTaggedBin {
1896 if utils.FastEqual(pt, allowed) {
1897 return true, nil
1898 }
1899 }
1900 }
1901 }
1902
1903 // If we reach here with a write restriction, deny access
1904 return false, nil
1905 }
1906
1907 // No write restriction configured - write is permissive by default
1908 return true, nil
1909 }
1910
1911 // ===================================================================
1912 // STEP 7: Default Policy (fallback)
1913 // ===================================================================
1914
1915 // If no specific rules matched, use the configured default policy
1916 return p.getDefaultPolicyAction(), nil
1917 }
1918
1919 // checkScriptPolicy runs the policy script to determine if event should be allowed
1920 func (p *P) checkScriptPolicy(
1921 access string, ev *event.E, scriptPath string, loggedInPubkey []byte,
1922 ipAddress string,
1923 ) (allowed bool, err error) {
1924 if p.manager == nil {
1925 return false, fmt.Errorf("policy manager is not initialized")
1926 }
1927
1928 // If policy is disabled, fall back to default policy immediately
1929 if !p.manager.IsEnabled() {
1930 log.W.F(
1931 "policy rule for kind %d is inactive (policy disabled), falling back to default policy (%s)",
1932 ev.Kind, p.DefaultPolicy,
1933 )
1934 return p.getDefaultPolicyAction(), nil
1935 }
1936
1937 // Check if script file exists
1938 if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
1939 // Script doesn't exist, return error so caller can fall back to other criteria
1940 return false, fmt.Errorf(
1941 "policy script does not exist at %s", scriptPath,
1942 )
1943 }
1944
1945 // Get or create a runner for this specific script path
1946 runner := p.manager.getOrCreateRunner(scriptPath)
1947
1948 // Policy is enabled, check if this runner is running
1949 if !runner.IsRunning() {
1950 // Try to start this runner and wait for it
1951 log.D.F("starting policy script for kind %d: %s", ev.Kind, scriptPath)
1952 if err := runner.ensureRunning(); err != nil {
1953 // Startup failed, return error so caller can fall back to other criteria
1954 return false, fmt.Errorf(
1955 "failed to start policy script %s: %v", scriptPath, err,
1956 )
1957 }
1958 log.I.F("policy script started for kind %d: %s", ev.Kind, scriptPath)
1959 }
1960
1961 // Create policy event with additional context
1962 policyEvent := &PolicyEvent{
1963 E: ev,
1964 LoggedInPubkey: hex.Enc(loggedInPubkey),
1965 IPAddress: ipAddress,
1966 AccessType: access,
1967 }
1968
1969 // Process event through policy script
1970 response, scriptErr := runner.ProcessEvent(policyEvent)
1971 if chk.E(scriptErr) {
1972 log.E.F(
1973 "policy rule for kind %d failed (script processing error: %v), falling back to default policy (%s)",
1974 ev.Kind, scriptErr, p.DefaultPolicy,
1975 )
1976 // Fall back to default policy on script failure
1977 return p.getDefaultPolicyAction(), nil
1978 }
1979
1980 // Handle script response
1981 switch response.Action {
1982 case "accept":
1983 return true, nil
1984 case "reject":
1985 return false, nil
1986 case "shadowReject":
1987 return false, nil // Treat as reject for policy purposes
1988 default:
1989 log.W.F(
1990 "policy rule for kind %d returned unknown action '%s', falling back to default policy (%s)",
1991 ev.Kind, response.Action, p.DefaultPolicy,
1992 )
1993 // Fall back to default policy for unknown actions
1994 return p.getDefaultPolicyAction(), nil
1995 }
1996 }
1997
1998 // PolicyManager methods
1999
2000 // periodicCheck periodically checks if the default policy script becomes available.
2001 // This is for backward compatibility with the default script path.
2002 func (pm *PolicyManager) periodicCheck() {
2003 // Get or create runner for the default script path
2004 // This will also start its own periodic check
2005 pm.getOrCreateRunner(pm.scriptPath)
2006 }
2007
2008 // startPolicyIfExists starts the default policy script if the file exists.
2009 // This is for backward compatibility with the default script path.
2010 // Only logs if the default script actually exists - missing default scripts are normal
2011 // when users configure rule-specific scripts.
2012 func (pm *PolicyManager) startPolicyIfExists() {
2013 if _, err := os.Stat(pm.scriptPath); err == nil {
2014 // Default script exists, try to start it
2015 log.I.F("found default policy script at %s, starting...", pm.scriptPath)
2016 runner := pm.getOrCreateRunner(pm.scriptPath)
2017 if err := runner.Start(); err != nil {
2018 log.E.F(
2019 "failed to start default policy script: %v, will retry periodically",
2020 err,
2021 )
2022 }
2023 }
2024 // Silently ignore if default script doesn't exist - it's fine if rules use custom scripts
2025 }
2026
2027 // IsEnabled returns whether the policy manager is enabled.
2028 func (pm *PolicyManager) IsEnabled() bool {
2029 return pm.enabled
2030 }
2031
2032 // IsRunning returns whether the default policy script is currently running.
2033 // Deprecated: Use getOrCreateRunner(scriptPath).IsRunning() for specific scripts.
2034 func (pm *PolicyManager) IsRunning() bool {
2035 pm.mutex.RLock()
2036 defer pm.mutex.RUnlock()
2037
2038 // Check if default script runner exists and is running
2039 if runner, exists := pm.runners[pm.scriptPath]; exists {
2040 return runner.IsRunning()
2041 }
2042 return false
2043 }
2044
2045 // GetScriptPath returns the default script path.
2046 func (pm *PolicyManager) GetScriptPath() string {
2047 return pm.scriptPath
2048 }
2049
2050 // Shutdown gracefully shuts down the policy manager and all running scripts.
2051 func (pm *PolicyManager) Shutdown() {
2052 pm.cancel()
2053
2054 pm.mutex.Lock()
2055 defer pm.mutex.Unlock()
2056
2057 // Stop all running scripts
2058 for path, runner := range pm.runners {
2059 if runner.IsRunning() {
2060 log.I.F("stopping policy script: %s", path)
2061 runner.Stop()
2062 }
2063 // Cancel the runner's context
2064 runner.cancel()
2065 }
2066
2067 // Clear runners map
2068 pm.runners = make(map[string]*ScriptRunner)
2069 }
2070
2071 // =============================================================================
2072 // Policy Hot Reload Methods
2073 // =============================================================================
2074
2075 // ValidateJSON validates policy JSON without applying changes.
2076 // This is called BEFORE any modifications to ensure JSON is valid.
2077 // Returns error if validation fails - no changes are made to current policy.
2078 func (p *P) ValidateJSON(policyJSON []byte) error {
2079 // Try to unmarshal into a temporary policy struct
2080 tempPolicy := &P{}
2081 if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2082 return fmt.Errorf("invalid JSON syntax: %v", err)
2083 }
2084
2085 // Validate policy_admins are valid hex pubkeys (64 characters)
2086 for _, admin := range tempPolicy.PolicyAdmins {
2087 if len(admin) != 64 {
2088 return fmt.Errorf("invalid policy_admin pubkey length: %q (expected 64 hex characters)", admin)
2089 }
2090 if _, err := hex.Dec(admin); err != nil {
2091 return fmt.Errorf("invalid policy_admin pubkey format: %q: %v", admin, err)
2092 }
2093 }
2094
2095 // Validate owners are valid hex pubkeys (64 characters)
2096 for _, owner := range tempPolicy.Owners {
2097 if len(owner) != 64 {
2098 return fmt.Errorf("invalid owner pubkey length: %q (expected 64 hex characters)", owner)
2099 }
2100 if _, err := hex.Dec(owner); err != nil {
2101 return fmt.Errorf("invalid owner pubkey format: %q: %v", owner, err)
2102 }
2103 }
2104
2105 // Note: Owner-specific validation (non-empty owners) is done in ValidateOwnerPolicyUpdate
2106
2107 // Validate regex patterns in tag_validation rules and new fields
2108 for kind, rule := range tempPolicy.rules {
2109 for tagName, pattern := range rule.TagValidation {
2110 if _, err := regexp.Compile(pattern); err != nil {
2111 return fmt.Errorf("invalid regex pattern for tag %q in kind %d: %v", tagName, kind, err)
2112 }
2113 }
2114 // Validate IdentifierRegex pattern
2115 if rule.IdentifierRegex != "" {
2116 if _, err := regexp.Compile(rule.IdentifierRegex); err != nil {
2117 return fmt.Errorf("invalid identifier_regex pattern in kind %d: %v", kind, err)
2118 }
2119 }
2120 // Validate MaxExpiryDuration format
2121 if rule.MaxExpiryDuration != "" {
2122 if _, err := parseDuration(rule.MaxExpiryDuration); err != nil {
2123 return fmt.Errorf("invalid max_expiry_duration %q in kind %d: %v (format must be ISO-8601 duration, e.g. \"PT10M\" for 10 minutes, \"P7D\" for 7 days, \"P1DT12H\" for 1 day 12 hours)", rule.MaxExpiryDuration, kind, err)
2124 }
2125 }
2126 // Validate FollowsWhitelistAdmins pubkeys
2127 for _, admin := range rule.FollowsWhitelistAdmins {
2128 if len(admin) != 64 {
2129 return fmt.Errorf("invalid follows_whitelist_admins pubkey length in kind %d: %q (expected 64 hex characters)", kind, admin)
2130 }
2131 if _, err := hex.Dec(admin); err != nil {
2132 return fmt.Errorf("invalid follows_whitelist_admins pubkey format in kind %d: %q: %v", kind, admin, err)
2133 }
2134 }
2135 }
2136
2137 // Validate global rule tag_validation patterns
2138 for tagName, pattern := range tempPolicy.Global.TagValidation {
2139 if _, err := regexp.Compile(pattern); err != nil {
2140 return fmt.Errorf("invalid regex pattern for tag %q in global rule: %v", tagName, err)
2141 }
2142 }
2143
2144 // Validate global rule IdentifierRegex pattern
2145 if tempPolicy.Global.IdentifierRegex != "" {
2146 if _, err := regexp.Compile(tempPolicy.Global.IdentifierRegex); err != nil {
2147 return fmt.Errorf("invalid identifier_regex pattern in global rule: %v", err)
2148 }
2149 }
2150
2151 // Validate global rule MaxExpiryDuration format
2152 if tempPolicy.Global.MaxExpiryDuration != "" {
2153 if _, err := parseDuration(tempPolicy.Global.MaxExpiryDuration); err != nil {
2154 return fmt.Errorf("invalid max_expiry_duration %q in global rule: %v (format must be ISO-8601 duration, e.g. \"PT10M\" for 10 minutes, \"P7D\" for 7 days, \"P1DT12H\" for 1 day 12 hours)", tempPolicy.Global.MaxExpiryDuration, err)
2155 }
2156 }
2157
2158 // Validate global rule FollowsWhitelistAdmins pubkeys
2159 for _, admin := range tempPolicy.Global.FollowsWhitelistAdmins {
2160 if len(admin) != 64 {
2161 return fmt.Errorf("invalid follows_whitelist_admins pubkey length in global rule: %q (expected 64 hex characters)", admin)
2162 }
2163 if _, err := hex.Dec(admin); err != nil {
2164 return fmt.Errorf("invalid follows_whitelist_admins pubkey format in global rule: %q: %v", admin, err)
2165 }
2166 }
2167
2168 // Validate default_policy value
2169 if tempPolicy.DefaultPolicy != "" && tempPolicy.DefaultPolicy != "allow" && tempPolicy.DefaultPolicy != "deny" {
2170 return fmt.Errorf("invalid default_policy value: %q (must be \"allow\" or \"deny\")", tempPolicy.DefaultPolicy)
2171 }
2172
2173 // Validate permissive flags: if both read_allow_permissive AND write_allow_permissive are set
2174 // with a kind whitelist or blacklist, this makes the whitelist/blacklist meaningless
2175 hasKindRestriction := len(tempPolicy.Kind.Whitelist) > 0 || len(tempPolicy.Kind.Blacklist) > 0
2176 if hasKindRestriction && tempPolicy.Global.ReadAllowPermissive && tempPolicy.Global.WriteAllowPermissive {
2177 return fmt.Errorf("invalid policy: both read_allow_permissive and write_allow_permissive cannot be enabled together with a kind whitelist or blacklist (this would make the kind restriction meaningless)")
2178 }
2179
2180 log.D.F("policy JSON validation passed")
2181 return nil
2182 }
2183
2184 // Reload loads policy from JSON bytes and applies it to the existing policy instance.
2185 // This validates JSON FIRST, then pauses the policy manager, updates configuration, and resumes.
2186 // Returns error if validation fails - no changes are made on validation failure.
2187 func (p *P) Reload(policyJSON []byte, configPath string) error {
2188 // Step 1: Validate JSON FIRST (before making any changes)
2189 if err := p.ValidateJSON(policyJSON); err != nil {
2190 return fmt.Errorf("validation failed: %v", err)
2191 }
2192
2193 // Step 2: Pause policy manager (stop script runners)
2194 if err := p.Pause(); err != nil {
2195 log.W.F("failed to pause policy manager (continuing anyway): %v", err)
2196 }
2197
2198 // Step 3: Unmarshal JSON into a temporary struct
2199 tempPolicy := &P{}
2200 if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2201 // Resume before returning error
2202 p.Resume()
2203 return fmt.Errorf("failed to unmarshal policy JSON: %v", err)
2204 }
2205
2206 // Step 4: Apply the new configuration (preserve manager reference)
2207 p.followsMx.Lock()
2208 p.Kind = tempPolicy.Kind
2209 p.rules = tempPolicy.rules
2210 p.Global = tempPolicy.Global
2211 p.DefaultPolicy = tempPolicy.DefaultPolicy
2212 p.PolicyAdmins = tempPolicy.PolicyAdmins
2213 p.PolicyFollowWhitelistEnabled = tempPolicy.PolicyFollowWhitelistEnabled
2214 p.Owners = tempPolicy.Owners
2215 p.policyAdminsBin = tempPolicy.policyAdminsBin
2216 p.ownersBin = tempPolicy.ownersBin
2217 // Note: policyFollows is NOT reset here - it will be refreshed separately
2218 p.followsMx.Unlock()
2219
2220 // Step 5: Populate binary caches for all rules
2221 p.Global.populateBinaryCache()
2222 for kind := range p.rules {
2223 rule := p.rules[kind]
2224 rule.populateBinaryCache()
2225 p.rules[kind] = rule
2226 }
2227
2228 // Step 6: Save to file (atomic write)
2229 if err := p.SaveToFile(configPath); err != nil {
2230 log.E.F("failed to persist policy to disk: %v (policy was updated in memory)", err)
2231 // Continue anyway - policy is loaded in memory
2232 }
2233
2234 // Step 7: Resume policy manager (restart script runners)
2235 if err := p.Resume(); err != nil {
2236 log.W.F("failed to resume policy manager: %v", err)
2237 }
2238
2239 log.I.F("policy configuration reloaded successfully")
2240 return nil
2241 }
2242
2243 // Pause pauses the policy manager and stops all script runners.
2244 func (p *P) Pause() error {
2245 if p.manager == nil {
2246 return fmt.Errorf("policy manager is not initialized")
2247 }
2248
2249 p.manager.mutex.Lock()
2250 defer p.manager.mutex.Unlock()
2251
2252 // Stop all running scripts
2253 for path, runner := range p.manager.runners {
2254 if runner.IsRunning() {
2255 log.I.F("pausing policy script: %s", path)
2256 if err := runner.Stop(); err != nil {
2257 log.W.F("failed to stop runner %s: %v", path, err)
2258 }
2259 }
2260 }
2261
2262 log.I.F("policy manager paused")
2263 return nil
2264 }
2265
2266 // Resume resumes the policy manager and restarts script runners.
2267 func (p *P) Resume() error {
2268 if p.manager == nil {
2269 return fmt.Errorf("policy manager is not initialized")
2270 }
2271
2272 // Restart the default policy script if it exists
2273 go p.manager.startPolicyIfExists()
2274
2275 // Restart rule-specific scripts
2276 for _, rule := range p.rules {
2277 if rule.Script != "" {
2278 if _, err := os.Stat(rule.Script); err == nil {
2279 runner := p.manager.getOrCreateRunner(rule.Script)
2280 go func(r *ScriptRunner, script string) {
2281 if err := r.Start(); err != nil {
2282 log.W.F("failed to restart policy script %s: %v", script, err)
2283 }
2284 }(runner, rule.Script)
2285 }
2286 }
2287 }
2288
2289 log.I.F("policy manager resumed")
2290 return nil
2291 }
2292
2293 // SaveToFile persists the current policy configuration to disk using atomic write.
2294 // Uses temp file + rename pattern to ensure atomic writes.
2295 func (p *P) SaveToFile(configPath string) error {
2296 // Create shadow struct for JSON marshalling
2297 shadow := pJSON{
2298 Kind: p.Kind,
2299 Rules: p.rules,
2300 Global: p.Global,
2301 DefaultPolicy: p.DefaultPolicy,
2302 PolicyAdmins: p.PolicyAdmins,
2303 PolicyFollowWhitelistEnabled: p.PolicyFollowWhitelistEnabled,
2304 Owners: p.Owners,
2305 }
2306
2307 // Marshal to JSON with indentation for readability
2308 jsonData, err := json.MarshalIndent(shadow, "", " ")
2309 if err != nil {
2310 return fmt.Errorf("failed to marshal policy to JSON: %v", err)
2311 }
2312
2313 // Write to temp file first (atomic write pattern)
2314 tempPath := configPath + ".tmp"
2315 if err := os.WriteFile(tempPath, jsonData, 0644); err != nil {
2316 return fmt.Errorf("failed to write temp file: %v", err)
2317 }
2318
2319 // Rename temp file to actual config file (atomic on most filesystems)
2320 if err := os.Rename(tempPath, configPath); err != nil {
2321 // Clean up temp file on failure
2322 os.Remove(tempPath)
2323 return fmt.Errorf("failed to rename temp file: %v", err)
2324 }
2325
2326 log.I.F("policy configuration saved to %s", configPath)
2327 return nil
2328 }
2329
2330 // =============================================================================
2331 // Policy Admin and Follow Checking Methods
2332 // =============================================================================
2333
2334 // IsPolicyAdmin checks if the given pubkey is in the policy_admins list.
2335 // The pubkey parameter should be binary ([]byte), not hex-encoded.
2336 func (p *P) IsPolicyAdmin(pubkey []byte) bool {
2337 if len(pubkey) == 0 {
2338 return false
2339 }
2340
2341 p.followsMx.RLock()
2342 defer p.followsMx.RUnlock()
2343
2344 for _, admin := range p.policyAdminsBin {
2345 if utils.FastEqual(admin, pubkey) {
2346 return true
2347 }
2348 }
2349 return false
2350 }
2351
2352 // IsPolicyFollow checks if the given pubkey is in the policy admin follows list.
2353 // The pubkey parameter should be binary ([]byte), not hex-encoded.
2354 func (p *P) IsPolicyFollow(pubkey []byte) bool {
2355 if len(pubkey) == 0 {
2356 return false
2357 }
2358
2359 p.followsMx.RLock()
2360 defer p.followsMx.RUnlock()
2361
2362 for _, follow := range p.policyFollows {
2363 if utils.FastEqual(pubkey, follow) {
2364 return true
2365 }
2366 }
2367 return false
2368 }
2369
2370 // UpdatePolicyFollows replaces the policy follows list with a new set of pubkeys.
2371 // This is called when policy admins update their follow lists (kind 3 events).
2372 // The pubkeys should be binary ([]byte), not hex-encoded.
2373 func (p *P) UpdatePolicyFollows(follows [][]byte) {
2374 p.followsMx.Lock()
2375 defer p.followsMx.Unlock()
2376
2377 p.policyFollows = follows
2378 log.I.F("policy follows list updated with %d pubkeys", len(follows))
2379 }
2380
2381 // GetPolicyAdminsBin returns a copy of the binary policy admin pubkeys.
2382 // Used for checking if an event author is a policy admin.
2383 func (p *P) GetPolicyAdminsBin() [][]byte {
2384 p.followsMx.RLock()
2385 defer p.followsMx.RUnlock()
2386
2387 // Return a copy to prevent external modification
2388 result := make([][]byte, len(p.policyAdminsBin))
2389 for i, admin := range p.policyAdminsBin {
2390 adminCopy := make([]byte, len(admin))
2391 copy(adminCopy, admin)
2392 result[i] = adminCopy
2393 }
2394 return result
2395 }
2396
2397 // GetOwnersBin returns a copy of the binary owner pubkeys defined in the policy.
2398 // These are merged with environment-defined owners by the application layer.
2399 // Useful for cloud deployments where environment variables cannot be modified.
2400 func (p *P) GetOwnersBin() [][]byte {
2401 if p == nil {
2402 return nil
2403 }
2404
2405 p.followsMx.RLock()
2406 defer p.followsMx.RUnlock()
2407
2408 // Return a copy to prevent external modification
2409 result := make([][]byte, len(p.ownersBin))
2410 for i, owner := range p.ownersBin {
2411 ownerCopy := make([]byte, len(owner))
2412 copy(ownerCopy, owner)
2413 result[i] = ownerCopy
2414 }
2415 return result
2416 }
2417
2418 // GetOwners returns the hex-encoded owner pubkeys defined in the policy.
2419 // These are merged with environment-defined owners by the application layer.
2420 func (p *P) GetOwners() []string {
2421 if p == nil {
2422 return nil
2423 }
2424 return p.Owners
2425 }
2426
2427 // IsPolicyFollowWhitelistEnabled returns whether the policy follow whitelist feature is enabled.
2428 // When enabled, pubkeys followed by policy admins are automatically whitelisted for access
2429 // when rules have WriteAllowFollows=true.
2430 func (p *P) IsPolicyFollowWhitelistEnabled() bool {
2431 if p == nil {
2432 return false
2433 }
2434 return p.PolicyFollowWhitelistEnabled
2435 }
2436
2437 // =============================================================================
2438 // FollowsWhitelistAdmins Methods
2439 // =============================================================================
2440
2441 // GetAllFollowsWhitelistAdmins returns all unique admin pubkeys from FollowsWhitelistAdmins
2442 // across all rules (including global). Returns hex-encoded pubkeys.
2443 // This is used at startup to validate that kind 3 events exist for these admins.
2444 func (p *P) GetAllFollowsWhitelistAdmins() []string {
2445 if p == nil {
2446 return nil
2447 }
2448
2449 // Use map to deduplicate
2450 admins := make(map[string]struct{})
2451
2452 // Check global rule
2453 for _, admin := range p.Global.FollowsWhitelistAdmins {
2454 admins[admin] = struct{}{}
2455 }
2456
2457 // Check all kind-specific rules
2458 for _, rule := range p.rules {
2459 for _, admin := range rule.FollowsWhitelistAdmins {
2460 admins[admin] = struct{}{}
2461 }
2462 }
2463
2464 // Convert map to slice
2465 result := make([]string, 0, len(admins))
2466 for admin := range admins {
2467 result = append(result, admin)
2468 }
2469 return result
2470 }
2471
2472 // GetRuleForKind returns the Rule for a specific kind, or nil if no rule exists.
2473 // This allows external code to access and modify rule-specific follows whitelists.
2474 func (p *P) GetRuleForKind(kind int) *Rule {
2475 if p == nil || p.rules == nil {
2476 return nil
2477 }
2478 if rule, exists := p.rules[kind]; exists {
2479 return &rule
2480 }
2481 return nil
2482 }
2483
2484 // UpdateRuleFollowsWhitelist updates the follows whitelist for a specific kind's rule.
2485 // The follows should be binary pubkeys ([]byte), not hex-encoded.
2486 // Thread-safe: uses followsMx to protect concurrent access.
2487 func (p *P) UpdateRuleFollowsWhitelist(kind int, follows [][]byte) {
2488 if p == nil || p.rules == nil {
2489 return
2490 }
2491 p.followsMx.Lock()
2492 defer p.followsMx.Unlock()
2493 if rule, exists := p.rules[kind]; exists {
2494 rule.UpdateFollowsWhitelist(follows)
2495 p.rules[kind] = rule
2496 }
2497 }
2498
2499 // UpdateGlobalFollowsWhitelist updates the follows whitelist for the global rule.
2500 // The follows should be binary pubkeys ([]byte), not hex-encoded.
2501 // Note: We directly modify p.Global's unexported field because Global is a value type (not *Rule),
2502 // so calling p.Global.UpdateFollowsWhitelist() would operate on a copy and discard changes.
2503 // Thread-safe: uses followsMx to protect concurrent access.
2504 func (p *P) UpdateGlobalFollowsWhitelist(follows [][]byte) {
2505 if p == nil {
2506 return
2507 }
2508 p.followsMx.Lock()
2509 defer p.followsMx.Unlock()
2510 p.Global.followsWhitelistFollowsBin = follows
2511 }
2512
2513 // GetGlobalRule returns a pointer to the global rule for modification.
2514 func (p *P) GetGlobalRule() *Rule {
2515 if p == nil {
2516 return nil
2517 }
2518 return &p.Global
2519 }
2520
2521 // GetRules returns the rules map for iteration.
2522 // Note: Returns a copy of the map keys to prevent modification.
2523 func (p *P) GetRulesKinds() []int {
2524 if p == nil || p.rules == nil {
2525 return nil
2526 }
2527 kinds := make([]int, 0, len(p.rules))
2528 for kind := range p.rules {
2529 kinds = append(kinds, kind)
2530 }
2531 return kinds
2532 }
2533
2534 // =============================================================================
2535 // ReadFollowsWhitelist and WriteFollowsWhitelist Methods
2536 // =============================================================================
2537
2538 // GetAllReadFollowsWhitelistPubkeys returns all unique pubkeys from ReadFollowsWhitelist
2539 // across all rules (including global). Returns hex-encoded pubkeys.
2540 // This is used at startup to validate that kind 3 events exist for these pubkeys.
2541 func (p *P) GetAllReadFollowsWhitelistPubkeys() []string {
2542 if p == nil {
2543 return nil
2544 }
2545
2546 // Use map to deduplicate
2547 pubkeys := make(map[string]struct{})
2548
2549 // Check global rule
2550 for _, pk := range p.Global.ReadFollowsWhitelist {
2551 pubkeys[pk] = struct{}{}
2552 }
2553
2554 // Check all kind-specific rules
2555 for _, rule := range p.rules {
2556 for _, pk := range rule.ReadFollowsWhitelist {
2557 pubkeys[pk] = struct{}{}
2558 }
2559 }
2560
2561 // Convert map to slice
2562 result := make([]string, 0, len(pubkeys))
2563 for pk := range pubkeys {
2564 result = append(result, pk)
2565 }
2566 return result
2567 }
2568
2569 // GetAllWriteFollowsWhitelistPubkeys returns all unique pubkeys from WriteFollowsWhitelist
2570 // across all rules (including global). Returns hex-encoded pubkeys.
2571 // This is used at startup to validate that kind 3 events exist for these pubkeys.
2572 func (p *P) GetAllWriteFollowsWhitelistPubkeys() []string {
2573 if p == nil {
2574 return nil
2575 }
2576
2577 // Use map to deduplicate
2578 pubkeys := make(map[string]struct{})
2579
2580 // Check global rule
2581 for _, pk := range p.Global.WriteFollowsWhitelist {
2582 pubkeys[pk] = struct{}{}
2583 }
2584
2585 // Check all kind-specific rules
2586 for _, rule := range p.rules {
2587 for _, pk := range rule.WriteFollowsWhitelist {
2588 pubkeys[pk] = struct{}{}
2589 }
2590 }
2591
2592 // Convert map to slice
2593 result := make([]string, 0, len(pubkeys))
2594 for pk := range pubkeys {
2595 result = append(result, pk)
2596 }
2597 return result
2598 }
2599
2600 // GetAllFollowsWhitelistPubkeys returns all unique pubkeys from both ReadFollowsWhitelist
2601 // and WriteFollowsWhitelist across all rules (including global). Returns hex-encoded pubkeys.
2602 // This is a convenience method for startup validation to check all required kind 3 events.
2603 func (p *P) GetAllFollowsWhitelistPubkeys() []string {
2604 if p == nil {
2605 return nil
2606 }
2607
2608 // Use map to deduplicate
2609 pubkeys := make(map[string]struct{})
2610
2611 // Get read follows whitelist pubkeys
2612 for _, pk := range p.GetAllReadFollowsWhitelistPubkeys() {
2613 pubkeys[pk] = struct{}{}
2614 }
2615
2616 // Get write follows whitelist pubkeys
2617 for _, pk := range p.GetAllWriteFollowsWhitelistPubkeys() {
2618 pubkeys[pk] = struct{}{}
2619 }
2620
2621 // Also include deprecated FollowsWhitelistAdmins for backward compatibility
2622 for _, pk := range p.GetAllFollowsWhitelistAdmins() {
2623 pubkeys[pk] = struct{}{}
2624 }
2625
2626 // Convert map to slice
2627 result := make([]string, 0, len(pubkeys))
2628 for pk := range pubkeys {
2629 result = append(result, pk)
2630 }
2631 return result
2632 }
2633
2634 // UpdateRuleReadFollowsWhitelist updates the read follows whitelist for a specific kind's rule.
2635 // The follows should be binary pubkeys ([]byte), not hex-encoded.
2636 // Thread-safe: uses followsMx to protect concurrent access.
2637 func (p *P) UpdateRuleReadFollowsWhitelist(kind int, follows [][]byte) {
2638 if p == nil || p.rules == nil {
2639 return
2640 }
2641 p.followsMx.Lock()
2642 defer p.followsMx.Unlock()
2643 if rule, exists := p.rules[kind]; exists {
2644 rule.UpdateReadFollowsWhitelist(follows)
2645 p.rules[kind] = rule
2646 }
2647 }
2648
2649 // UpdateRuleWriteFollowsWhitelist updates the write follows whitelist for a specific kind's rule.
2650 // The follows should be binary pubkeys ([]byte), not hex-encoded.
2651 // Thread-safe: uses followsMx to protect concurrent access.
2652 func (p *P) UpdateRuleWriteFollowsWhitelist(kind int, follows [][]byte) {
2653 if p == nil || p.rules == nil {
2654 return
2655 }
2656 p.followsMx.Lock()
2657 defer p.followsMx.Unlock()
2658 if rule, exists := p.rules[kind]; exists {
2659 rule.UpdateWriteFollowsWhitelist(follows)
2660 p.rules[kind] = rule
2661 }
2662 }
2663
2664 // UpdateGlobalReadFollowsWhitelist updates the read follows whitelist for the global rule.
2665 // The follows should be binary pubkeys ([]byte), not hex-encoded.
2666 // Note: We directly modify p.Global's unexported field because Global is a value type (not *Rule),
2667 // so calling p.Global.UpdateReadFollowsWhitelist() would operate on a copy and discard changes.
2668 // Thread-safe: uses followsMx to protect concurrent access.
2669 func (p *P) UpdateGlobalReadFollowsWhitelist(follows [][]byte) {
2670 if p == nil {
2671 return
2672 }
2673 p.followsMx.Lock()
2674 defer p.followsMx.Unlock()
2675 p.Global.readFollowsFollowsBin = follows
2676 }
2677
2678 // UpdateGlobalWriteFollowsWhitelist updates the write follows whitelist for the global rule.
2679 // The follows should be binary pubkeys ([]byte), not hex-encoded.
2680 // Note: We directly modify p.Global's unexported field because Global is a value type (not *Rule),
2681 // so calling p.Global.UpdateWriteFollowsWhitelist() would operate on a copy and discard changes.
2682 // Thread-safe: uses followsMx to protect concurrent access.
2683 func (p *P) UpdateGlobalWriteFollowsWhitelist(follows [][]byte) {
2684 if p == nil {
2685 return
2686 }
2687 p.followsMx.Lock()
2688 defer p.followsMx.Unlock()
2689 p.Global.writeFollowsFollowsBin = follows
2690 }
2691
2692 // =============================================================================
2693 // Owner vs Policy Admin Update Validation
2694 // =============================================================================
2695
2696 // ValidateOwnerPolicyUpdate validates a full policy update from an owner.
2697 // Owners can modify all fields but the owners list must be non-empty.
2698 func (p *P) ValidateOwnerPolicyUpdate(policyJSON []byte) error {
2699 // First run standard validation
2700 if err := p.ValidateJSON(policyJSON); err != nil {
2701 return err
2702 }
2703
2704 // Parse the new policy
2705 tempPolicy := &P{}
2706 if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2707 return fmt.Errorf("failed to parse policy JSON: %v", err)
2708 }
2709
2710 // Owner-specific validation: owners list cannot be empty
2711 if len(tempPolicy.Owners) == 0 {
2712 return fmt.Errorf("owners list cannot be empty: at least one owner must be defined to prevent lockout")
2713 }
2714
2715 return nil
2716 }
2717
2718 // ValidatePolicyAdminUpdate validates a policy update from a policy admin.
2719 // Policy admins CANNOT modify: owners, policy_admins
2720 // Policy admins CAN: extend rules, add blacklists, add new kind rules
2721 func (p *P) ValidatePolicyAdminUpdate(policyJSON []byte, adminPubkey []byte) error {
2722 // First run standard validation
2723 if err := p.ValidateJSON(policyJSON); err != nil {
2724 return err
2725 }
2726
2727 // Parse the new policy
2728 tempPolicy := &P{}
2729 if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2730 return fmt.Errorf("failed to parse policy JSON: %v", err)
2731 }
2732
2733 // Protected field check: owners must match current
2734 if !stringSliceEqual(tempPolicy.Owners, p.Owners) {
2735 return fmt.Errorf("policy admins cannot modify the 'owners' field: this is a protected field that only owners can change")
2736 }
2737
2738 // Protected field check: policy_admins must match current
2739 if !stringSliceEqual(tempPolicy.PolicyAdmins, p.PolicyAdmins) {
2740 return fmt.Errorf("policy admins cannot modify the 'policy_admins' field: this is a protected field that only owners can change")
2741 }
2742
2743 // Validate that the admin is not reducing owner-granted permissions
2744 // This check ensures policy admins can only extend, not restrict
2745 if err := p.validateNoPermissionReduction(tempPolicy); err != nil {
2746 return fmt.Errorf("policy admins cannot reduce owner-granted permissions: %v", err)
2747 }
2748
2749 return nil
2750 }
2751
2752 // validateNoPermissionReduction checks that the new policy doesn't reduce
2753 // permissions that were granted in the current (owner) policy.
2754 //
2755 // Policy admins CAN:
2756 // - ADD to allow lists (write_allow, read_allow)
2757 // - ADD to deny lists (write_deny, read_deny) to blacklist non-admin users
2758 // - INCREASE limits (size_limit, content_limit, max_age_of_event)
2759 // - ADD new kinds to whitelist or blacklist
2760 // - ADD new rules for kinds not defined by owner
2761 //
2762 // Policy admins CANNOT:
2763 // - REMOVE from allow lists
2764 // - DECREASE limits
2765 // - REMOVE kinds from whitelist
2766 // - REMOVE rules defined by owner
2767 // - ADD new required tags (restrictions)
2768 // - BLACKLIST owners or other policy admins
2769 func (p *P) validateNoPermissionReduction(newPolicy *P) error {
2770 // Check kind whitelist - new policy must include all current whitelisted kinds
2771 for _, kind := range p.Kind.Whitelist {
2772 found := false
2773 for _, newKind := range newPolicy.Kind.Whitelist {
2774 if kind == newKind {
2775 found = true
2776 break
2777 }
2778 }
2779 if !found {
2780 return fmt.Errorf("cannot remove kind %d from whitelist", kind)
2781 }
2782 }
2783
2784 // Check each rule in the current policy
2785 for kind, currentRule := range p.rules {
2786 newRule, exists := newPolicy.rules[kind]
2787 if !exists {
2788 return fmt.Errorf("cannot remove rule for kind %d", kind)
2789 }
2790
2791 // Check write_allow - new rule must include all current pubkeys
2792 for _, pk := range currentRule.WriteAllow {
2793 if !containsString(newRule.WriteAllow, pk) {
2794 return fmt.Errorf("cannot remove pubkey %s from write_allow for kind %d", pk, kind)
2795 }
2796 }
2797
2798 // Check read_allow - new rule must include all current pubkeys
2799 for _, pk := range currentRule.ReadAllow {
2800 if !containsString(newRule.ReadAllow, pk) {
2801 return fmt.Errorf("cannot remove pubkey %s from read_allow for kind %d", pk, kind)
2802 }
2803 }
2804
2805 // Check write_deny - cannot blacklist owners or policy admins
2806 for _, pk := range newRule.WriteDeny {
2807 if containsString(p.Owners, pk) {
2808 return fmt.Errorf("cannot blacklist owner %s in write_deny for kind %d", pk, kind)
2809 }
2810 if containsString(p.PolicyAdmins, pk) {
2811 return fmt.Errorf("cannot blacklist policy admin %s in write_deny for kind %d", pk, kind)
2812 }
2813 }
2814
2815 // Check read_deny - cannot blacklist owners or policy admins
2816 for _, pk := range newRule.ReadDeny {
2817 if containsString(p.Owners, pk) {
2818 return fmt.Errorf("cannot blacklist owner %s in read_deny for kind %d", pk, kind)
2819 }
2820 if containsString(p.PolicyAdmins, pk) {
2821 return fmt.Errorf("cannot blacklist policy admin %s in read_deny for kind %d", pk, kind)
2822 }
2823 }
2824
2825 // Check size limits - new limit cannot be smaller
2826 if currentRule.SizeLimit != nil && newRule.SizeLimit != nil {
2827 if *newRule.SizeLimit < *currentRule.SizeLimit {
2828 return fmt.Errorf("cannot reduce size_limit for kind %d from %d to %d", kind, *currentRule.SizeLimit, *newRule.SizeLimit)
2829 }
2830 }
2831
2832 // Check content limits - new limit cannot be smaller
2833 if currentRule.ContentLimit != nil && newRule.ContentLimit != nil {
2834 if *newRule.ContentLimit < *currentRule.ContentLimit {
2835 return fmt.Errorf("cannot reduce content_limit for kind %d from %d to %d", kind, *currentRule.ContentLimit, *newRule.ContentLimit)
2836 }
2837 }
2838
2839 // Check max_age_of_event - new limit cannot be smaller (smaller = more restrictive)
2840 if currentRule.MaxAgeOfEvent != nil && newRule.MaxAgeOfEvent != nil {
2841 if *newRule.MaxAgeOfEvent < *currentRule.MaxAgeOfEvent {
2842 return fmt.Errorf("cannot reduce max_age_of_event for kind %d from %d to %d", kind, *currentRule.MaxAgeOfEvent, *newRule.MaxAgeOfEvent)
2843 }
2844 }
2845
2846 // Check must_have_tags - cannot add new required tags (more restrictive)
2847 for _, tag := range newRule.MustHaveTags {
2848 found := false
2849 for _, currentTag := range currentRule.MustHaveTags {
2850 if tag == currentTag {
2851 found = true
2852 break
2853 }
2854 }
2855 if !found {
2856 return fmt.Errorf("cannot add required tag %q for kind %d (only owners can add restrictions)", tag, kind)
2857 }
2858 }
2859 }
2860
2861 // Check global rule write_deny - cannot blacklist owners or policy admins
2862 for _, pk := range newPolicy.Global.WriteDeny {
2863 if containsString(p.Owners, pk) {
2864 return fmt.Errorf("cannot blacklist owner %s in global write_deny", pk)
2865 }
2866 if containsString(p.PolicyAdmins, pk) {
2867 return fmt.Errorf("cannot blacklist policy admin %s in global write_deny", pk)
2868 }
2869 }
2870
2871 // Check global rule read_deny - cannot blacklist owners or policy admins
2872 for _, pk := range newPolicy.Global.ReadDeny {
2873 if containsString(p.Owners, pk) {
2874 return fmt.Errorf("cannot blacklist owner %s in global read_deny", pk)
2875 }
2876 if containsString(p.PolicyAdmins, pk) {
2877 return fmt.Errorf("cannot blacklist policy admin %s in global read_deny", pk)
2878 }
2879 }
2880
2881 // Check global rule size limits
2882 if p.Global.SizeLimit != nil && newPolicy.Global.SizeLimit != nil {
2883 if *newPolicy.Global.SizeLimit < *p.Global.SizeLimit {
2884 return fmt.Errorf("cannot reduce global size_limit from %d to %d", *p.Global.SizeLimit, *newPolicy.Global.SizeLimit)
2885 }
2886 }
2887
2888 return nil
2889 }
2890
2891 // ReloadAsOwner reloads the policy from an owner's kind 12345 event.
2892 // Owners can modify all fields but the owners list must be non-empty.
2893 func (p *P) ReloadAsOwner(policyJSON []byte, configPath string) error {
2894 // Validate as owner update
2895 if err := p.ValidateOwnerPolicyUpdate(policyJSON); err != nil {
2896 return fmt.Errorf("owner policy validation failed: %v", err)
2897 }
2898
2899 // Use existing Reload logic
2900 return p.Reload(policyJSON, configPath)
2901 }
2902
2903 // ReloadAsPolicyAdmin reloads the policy from a policy admin's kind 12345 event.
2904 // Policy admins cannot modify protected fields (owners, policy_admins) and
2905 // cannot reduce owner-granted permissions.
2906 func (p *P) ReloadAsPolicyAdmin(policyJSON []byte, configPath string, adminPubkey []byte) error {
2907 // Validate as policy admin update
2908 if err := p.ValidatePolicyAdminUpdate(policyJSON, adminPubkey); err != nil {
2909 return fmt.Errorf("policy admin validation failed: %v", err)
2910 }
2911
2912 // Use existing Reload logic
2913 return p.Reload(policyJSON, configPath)
2914 }
2915
2916 // stringSliceEqual checks if two string slices are equal (order-independent).
2917 func stringSliceEqual(a, b []string) bool {
2918 if len(a) != len(b) {
2919 return false
2920 }
2921
2922 // Create maps for comparison
2923 aMap := make(map[string]int)
2924 for _, v := range a {
2925 aMap[v]++
2926 }
2927
2928 bMap := make(map[string]int)
2929 for _, v := range b {
2930 bMap[v]++
2931 }
2932
2933 // Compare maps
2934 for k, v := range aMap {
2935 if bMap[k] != v {
2936 return false
2937 }
2938 }
2939
2940 return true
2941 }
2942