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