handle-policy-config.go raw
1 package app
2
3 import (
4 "bytes"
5 "fmt"
6
7 "next.orly.dev/pkg/lol/log"
8 "next.orly.dev/pkg/nostr/encoders/event"
9 "next.orly.dev/pkg/nostr/encoders/filter"
10 "next.orly.dev/pkg/nostr/encoders/hex"
11 "next.orly.dev/pkg/nostr/encoders/kind"
12 "next.orly.dev/pkg/nostr/encoders/tag"
13 )
14
15 // HandlePolicyConfigUpdate processes kind 12345 policy configuration events.
16 // Owners and policy admins can update policy configuration, with different permissions:
17 //
18 // OWNERS can:
19 // - Modify all fields including owners and policy_admins
20 // - But owners list must remain non-empty (to prevent lockout)
21 //
22 // POLICY ADMINS can:
23 // - Extend rules (add to allow lists, add new kinds, add blacklists)
24 // - CANNOT modify owners or policy_admins (protected fields)
25 // - CANNOT reduce owner-granted permissions
26 //
27 // Process flow:
28 // 1. Check if sender is owner or policy admin
29 // 2. Validate JSON with appropriate rules for the sender type
30 // 3. Pause ALL message processing (lock mutex)
31 // 4. Reload policy (pause policy engine, update, save, resume)
32 // 5. Resume message processing (unlock mutex)
33 //
34 // The message processing mutex is already released by the caller (HandleEvent),
35 // so we acquire it ourselves for the critical section.
36 func (l *Listener) HandlePolicyConfigUpdate(ev *event.E) error {
37 log.I.F("received policy config update from pubkey: %s", hex.Enc(ev.Pubkey))
38
39 // 1. Verify sender is owner or policy admin
40 if l.policyManager == nil {
41 return fmt.Errorf("policy system is not enabled")
42 }
43
44 isOwner := l.policyManager.IsOwner(ev.Pubkey)
45 isAdmin := l.policyManager.IsPolicyAdmin(ev.Pubkey)
46
47 if !isOwner && !isAdmin {
48 log.W.F("policy config update rejected: pubkey %s is not an owner or policy admin", hex.Enc(ev.Pubkey))
49 return fmt.Errorf("only owners and policy administrators can update policy configuration")
50 }
51
52 if isOwner {
53 log.D.F("owner verified: %s", hex.Enc(ev.Pubkey))
54 } else {
55 log.D.F("policy admin verified: %s", hex.Enc(ev.Pubkey))
56 }
57
58 // 2. Parse and validate JSON with appropriate validation rules
59 policyJSON := []byte(ev.Content)
60 var validationErr error
61
62 if isOwner {
63 // Owners can modify all fields, but owners list must be non-empty
64 validationErr = l.policyManager.ValidateOwnerPolicyUpdate(policyJSON)
65 } else {
66 // Policy admins have restrictions: can't modify protected fields, can't reduce permissions
67 validationErr = l.policyManager.ValidatePolicyAdminUpdate(policyJSON, ev.Pubkey)
68 }
69
70 if validationErr != nil {
71 log.E.F("policy config update validation failed: %v", validationErr)
72 return fmt.Errorf("invalid policy configuration: %v", validationErr)
73 }
74
75 log.I.F("policy config validation passed")
76
77 // Get config path for saving (uses custom path if set, otherwise default)
78 configPath := l.policyManager.ConfigPath()
79
80 // 3. Pause ALL message processing (lock mutex)
81 // Note: We need to release the RLock first (which caller holds), then acquire exclusive Lock
82 // Actually, the HandleMessage already released the lock after calling HandleEvent
83 // So we can directly acquire the exclusive lock
84 log.I.F("pausing message processing for policy update")
85 l.Server.PauseMessageProcessing()
86 defer l.Server.ResumeMessageProcessing()
87
88 // 4. Reload policy (this will pause policy engine, update, save, and resume)
89 log.I.F("applying policy configuration update")
90 var reloadErr error
91 if isOwner {
92 reloadErr = l.policyManager.ReloadAsOwner(policyJSON, configPath)
93 } else {
94 reloadErr = l.policyManager.ReloadAsPolicyAdmin(policyJSON, configPath, ev.Pubkey)
95 }
96
97 if reloadErr != nil {
98 log.E.F("policy config update failed: %v", reloadErr)
99 return fmt.Errorf("failed to apply policy configuration: %v", reloadErr)
100 }
101
102 if isOwner {
103 log.I.F("policy configuration updated successfully by owner: %s", hex.Enc(ev.Pubkey))
104 } else {
105 log.I.F("policy configuration updated successfully by policy admin: %s", hex.Enc(ev.Pubkey))
106 }
107
108 // 5. Message processing mutex will be unlocked by defer
109 return nil
110 }
111
112 // HandlePolicyAdminFollowListUpdate processes kind 3 follow list events from policy admins.
113 // When a policy admin updates their follow list, we immediately refresh the policy follows cache.
114 //
115 // Process flow:
116 // 1. Check if sender is a policy admin
117 // 2. If yes, extract p-tags from the follow list
118 // 3. Pause message processing
119 // 4. Aggregate all policy admin follows and update cache
120 // 5. Resume message processing
121 func (l *Listener) HandlePolicyAdminFollowListUpdate(ev *event.E) error {
122 // Only process if policy system is enabled
123 if l.policyManager == nil || !l.policyManager.IsEnabled() {
124 return nil // Not an error, just ignore
125 }
126
127 // Check if sender is a policy admin
128 if !l.policyManager.IsPolicyAdmin(ev.Pubkey) {
129 return nil // Not a policy admin, ignore
130 }
131
132 log.I.F("policy admin %s updated their follow list, refreshing policy follows", hex.Enc(ev.Pubkey))
133
134 // Extract p-tags from this follow list event
135 newFollows := extractFollowsFromEvent(ev)
136
137 // Pause message processing for atomic update
138 log.D.F("pausing message processing for follow list update")
139 l.Server.PauseMessageProcessing()
140 defer l.Server.ResumeMessageProcessing()
141
142 // Get all current follows from database for all policy admins
143 // For now, we'll merge the new follows with existing ones
144 // A more complete implementation would re-fetch all admin follows from DB
145 allFollows, err := l.fetchAllPolicyAdminFollows()
146 if err != nil {
147 log.W.F("failed to fetch all policy admin follows: %v, using new follows only", err)
148 allFollows = newFollows
149 } else {
150 // Merge with the new follows (deduplicated)
151 allFollows = mergeFollows(allFollows, newFollows)
152 }
153
154 // Update the policy follows cache
155 l.policyManager.UpdatePolicyFollows(allFollows)
156
157 log.I.F("policy follows cache updated with %d total pubkeys", len(allFollows))
158 return nil
159 }
160
161 // extractFollowsFromEvent extracts p-tag pubkeys from a kind 3 follow list event.
162 // Returns binary pubkeys.
163 func extractFollowsFromEvent(ev *event.E) [][]byte {
164 var follows [][]byte
165
166 pTags := ev.Tags.GetAll([]byte("p"))
167 for _, pTag := range pTags {
168 // ValueHex() handles both binary and hex storage formats automatically
169 pt, err := hex.Dec(string(pTag.ValueHex()))
170 if err != nil {
171 continue
172 }
173 follows = append(follows, pt)
174 }
175
176 return follows
177 }
178
179 // fetchAllPolicyAdminFollows fetches kind 3 events for all policy admins from the database
180 // and aggregates their follows.
181 func (l *Listener) fetchAllPolicyAdminFollows() ([][]byte, error) {
182 var allFollows [][]byte
183 seen := make(map[string]bool)
184
185 // Get policy admin pubkeys
186 admins := l.policyManager.GetPolicyAdminsBin()
187 if len(admins) == 0 {
188 return nil, fmt.Errorf("no policy admins configured")
189 }
190
191 // For each admin, query their latest kind 3 event
192 for _, adminPubkey := range admins {
193 // Build proper filter for kind 3 from this admin
194 f := filter.New()
195 f.Authors = tag.NewFromAny(adminPubkey)
196 f.Kinds = kind.NewS(kind.FollowList)
197 limit := uint(1)
198 f.Limit = &limit
199
200 // Query the database for kind 3 events from this admin
201 events, err := l.DB.QueryEvents(l.ctx, f)
202 if err != nil {
203 log.W.F("failed to query follows for admin %s: %v", hex.Enc(adminPubkey), err)
204 continue
205 }
206
207 // events is []*event.E - iterate over the slice
208 for _, ev := range events {
209 // Extract p-tags from this follow list
210 follows := extractFollowsFromEvent(ev)
211 for _, follow := range follows {
212 key := string(follow)
213 if !seen[key] {
214 seen[key] = true
215 allFollows = append(allFollows, follow)
216 }
217 }
218 }
219 }
220
221 return allFollows, nil
222 }
223
224 // mergeFollows merges two follow lists, removing duplicates.
225 func mergeFollows(existing, newFollows [][]byte) [][]byte {
226 seen := make(map[string]bool)
227 var result [][]byte
228
229 for _, f := range existing {
230 key := string(f)
231 if !seen[key] {
232 seen[key] = true
233 result = append(result, f)
234 }
235 }
236
237 for _, f := range newFollows {
238 key := string(f)
239 if !seen[key] {
240 seen[key] = true
241 result = append(result, f)
242 }
243 }
244
245 return result
246 }
247
248 // IsPolicyConfigEvent returns true if the event is a policy configuration event (kind 12345)
249 func IsPolicyConfigEvent(ev *event.E) bool {
250 return ev.Kind == kind.PolicyConfig.K
251 }
252
253 // IsPolicyAdminFollowListEvent returns true if this is a follow list event from a policy admin.
254 // Used to detect when we need to refresh the policy follows cache.
255 func (l *Listener) IsPolicyAdminFollowListEvent(ev *event.E) bool {
256 // Must be kind 3 (follow list)
257 if ev.Kind != kind.FollowList.K {
258 return false
259 }
260
261 // Policy system must be enabled
262 if l.policyManager == nil || !l.policyManager.IsEnabled() {
263 return false
264 }
265
266 // Sender must be a policy admin
267 return l.policyManager.IsPolicyAdmin(ev.Pubkey)
268 }
269
270 // isPolicyAdmin checks if a pubkey is in the list of policy admins
271 func isPolicyAdmin(pubkey []byte, admins [][]byte) bool {
272 for _, admin := range admins {
273 if bytes.Equal(pubkey, admin) {
274 return true
275 }
276 }
277 return false
278 }
279
280 // InitializePolicyFollows loads the follow lists of all policy admins at startup.
281 // This should be called after the policy manager is initialized but before
282 // the relay starts accepting connections.
283 // It's a method on Server so it can be called from main.go during initialization.
284 func (s *Server) InitializePolicyFollows() error {
285 // Skip if policy system is not enabled
286 if s.policyManager == nil || !s.policyManager.IsEnabled() {
287 log.D.F("policy system not enabled, skipping follow list initialization")
288 return nil
289 }
290
291 // Skip if PolicyFollowWhitelistEnabled is false
292 if !s.policyManager.IsPolicyFollowWhitelistEnabled() {
293 log.D.F("policy follow whitelist not enabled, skipping follow list initialization")
294 return nil
295 }
296
297 log.I.F("initializing policy follows from database")
298
299 // Get policy admin pubkeys
300 admins := s.policyManager.GetPolicyAdminsBin()
301 if len(admins) == 0 {
302 log.W.F("no policy admins configured, skipping follow list initialization")
303 return nil
304 }
305
306 var allFollows [][]byte
307 seen := make(map[string]bool)
308
309 // For each admin, query their latest kind 3 event
310 for _, adminPubkey := range admins {
311 // Build proper filter for kind 3 from this admin
312 f := filter.New()
313 f.Authors = tag.NewFromAny(adminPubkey)
314 f.Kinds = kind.NewS(kind.FollowList)
315 limit := uint(1)
316 f.Limit = &limit
317
318 // Query the database for kind 3 events from this admin
319 events, err := s.DB.QueryEvents(s.Ctx, f)
320 if err != nil {
321 log.W.F("failed to query follows for admin %s: %v", hex.Enc(adminPubkey), err)
322 continue
323 }
324
325 // Extract p-tags from each follow list event
326 for _, ev := range events {
327 follows := extractFollowsFromEvent(ev)
328 for _, follow := range follows {
329 key := string(follow)
330 if !seen[key] {
331 seen[key] = true
332 allFollows = append(allFollows, follow)
333 }
334 }
335 }
336 }
337
338 // Update the policy follows cache
339 s.policyManager.UpdatePolicyFollows(allFollows)
340
341 log.I.F("policy follows initialized with %d pubkeys from %d admin(s)",
342 len(allFollows), len(admins))
343
344 return nil
345 }
346