You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
11 KiB
347 lines
11 KiB
package app |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"path/filepath" |
|
|
|
"github.com/adrg/xdg" |
|
"lol.mleku.dev/log" |
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/kind" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
) |
|
|
|
// HandlePolicyConfigUpdate processes kind 12345 policy configuration events. |
|
// Owners and policy admins can update policy configuration, with different permissions: |
|
// |
|
// OWNERS can: |
|
// - Modify all fields including owners and policy_admins |
|
// - But owners list must remain non-empty (to prevent lockout) |
|
// |
|
// POLICY ADMINS can: |
|
// - Extend rules (add to allow lists, add new kinds, add blacklists) |
|
// - CANNOT modify owners or policy_admins (protected fields) |
|
// - CANNOT reduce owner-granted permissions |
|
// |
|
// Process flow: |
|
// 1. Check if sender is owner or policy admin |
|
// 2. Validate JSON with appropriate rules for the sender type |
|
// 3. Pause ALL message processing (lock mutex) |
|
// 4. Reload policy (pause policy engine, update, save, resume) |
|
// 5. Resume message processing (unlock mutex) |
|
// |
|
// The message processing mutex is already released by the caller (HandleEvent), |
|
// so we acquire it ourselves for the critical section. |
|
func (l *Listener) HandlePolicyConfigUpdate(ev *event.E) error { |
|
log.I.F("received policy config update from pubkey: %s", hex.Enc(ev.Pubkey)) |
|
|
|
// 1. Verify sender is owner or policy admin |
|
if l.policyManager == nil { |
|
return fmt.Errorf("policy system is not enabled") |
|
} |
|
|
|
isOwner := l.policyManager.IsOwner(ev.Pubkey) |
|
isAdmin := l.policyManager.IsPolicyAdmin(ev.Pubkey) |
|
|
|
if !isOwner && !isAdmin { |
|
log.W.F("policy config update rejected: pubkey %s is not an owner or policy admin", hex.Enc(ev.Pubkey)) |
|
return fmt.Errorf("only owners and policy administrators can update policy configuration") |
|
} |
|
|
|
if isOwner { |
|
log.I.F("owner verified: %s", hex.Enc(ev.Pubkey)) |
|
} else { |
|
log.I.F("policy admin verified: %s", hex.Enc(ev.Pubkey)) |
|
} |
|
|
|
// 2. Parse and validate JSON with appropriate validation rules |
|
policyJSON := []byte(ev.Content) |
|
var validationErr error |
|
|
|
if isOwner { |
|
// Owners can modify all fields, but owners list must be non-empty |
|
validationErr = l.policyManager.ValidateOwnerPolicyUpdate(policyJSON) |
|
} else { |
|
// Policy admins have restrictions: can't modify protected fields, can't reduce permissions |
|
validationErr = l.policyManager.ValidatePolicyAdminUpdate(policyJSON, ev.Pubkey) |
|
} |
|
|
|
if validationErr != nil { |
|
log.E.F("policy config update validation failed: %v", validationErr) |
|
return fmt.Errorf("invalid policy configuration: %v", validationErr) |
|
} |
|
|
|
log.I.F("policy config validation passed") |
|
|
|
// Get config path for saving |
|
configPath := filepath.Join(xdg.ConfigHome, l.Config.AppName, "policy.json") |
|
|
|
// 3. Pause ALL message processing (lock mutex) |
|
// Note: We need to release the RLock first (which caller holds), then acquire exclusive Lock |
|
// Actually, the HandleMessage already released the lock after calling HandleEvent |
|
// So we can directly acquire the exclusive lock |
|
log.I.F("pausing message processing for policy update") |
|
l.Server.PauseMessageProcessing() |
|
defer l.Server.ResumeMessageProcessing() |
|
|
|
// 4. Reload policy (this will pause policy engine, update, save, and resume) |
|
log.I.F("applying policy configuration update") |
|
var reloadErr error |
|
if isOwner { |
|
reloadErr = l.policyManager.ReloadAsOwner(policyJSON, configPath) |
|
} else { |
|
reloadErr = l.policyManager.ReloadAsPolicyAdmin(policyJSON, configPath, ev.Pubkey) |
|
} |
|
|
|
if reloadErr != nil { |
|
log.E.F("policy config update failed: %v", reloadErr) |
|
return fmt.Errorf("failed to apply policy configuration: %v", reloadErr) |
|
} |
|
|
|
if isOwner { |
|
log.I.F("policy configuration updated successfully by owner: %s", hex.Enc(ev.Pubkey)) |
|
} else { |
|
log.I.F("policy configuration updated successfully by policy admin: %s", hex.Enc(ev.Pubkey)) |
|
} |
|
|
|
// 5. Message processing mutex will be unlocked by defer |
|
return nil |
|
} |
|
|
|
// HandlePolicyAdminFollowListUpdate processes kind 3 follow list events from policy admins. |
|
// When a policy admin updates their follow list, we immediately refresh the policy follows cache. |
|
// |
|
// Process flow: |
|
// 1. Check if sender is a policy admin |
|
// 2. If yes, extract p-tags from the follow list |
|
// 3. Pause message processing |
|
// 4. Aggregate all policy admin follows and update cache |
|
// 5. Resume message processing |
|
func (l *Listener) HandlePolicyAdminFollowListUpdate(ev *event.E) error { |
|
// Only process if policy system is enabled |
|
if l.policyManager == nil || !l.policyManager.IsEnabled() { |
|
return nil // Not an error, just ignore |
|
} |
|
|
|
// Check if sender is a policy admin |
|
if !l.policyManager.IsPolicyAdmin(ev.Pubkey) { |
|
return nil // Not a policy admin, ignore |
|
} |
|
|
|
log.I.F("policy admin %s updated their follow list, refreshing policy follows", hex.Enc(ev.Pubkey)) |
|
|
|
// Extract p-tags from this follow list event |
|
newFollows := extractFollowsFromEvent(ev) |
|
|
|
// Pause message processing for atomic update |
|
log.D.F("pausing message processing for follow list update") |
|
l.Server.PauseMessageProcessing() |
|
defer l.Server.ResumeMessageProcessing() |
|
|
|
// Get all current follows from database for all policy admins |
|
// For now, we'll merge the new follows with existing ones |
|
// A more complete implementation would re-fetch all admin follows from DB |
|
allFollows, err := l.fetchAllPolicyAdminFollows() |
|
if err != nil { |
|
log.W.F("failed to fetch all policy admin follows: %v, using new follows only", err) |
|
allFollows = newFollows |
|
} else { |
|
// Merge with the new follows (deduplicated) |
|
allFollows = mergeFollows(allFollows, newFollows) |
|
} |
|
|
|
// Update the policy follows cache |
|
l.policyManager.UpdatePolicyFollows(allFollows) |
|
|
|
log.I.F("policy follows cache updated with %d total pubkeys", len(allFollows)) |
|
return nil |
|
} |
|
|
|
// extractFollowsFromEvent extracts p-tag pubkeys from a kind 3 follow list event. |
|
// Returns binary pubkeys. |
|
func extractFollowsFromEvent(ev *event.E) [][]byte { |
|
var follows [][]byte |
|
|
|
pTags := ev.Tags.GetAll([]byte("p")) |
|
for _, pTag := range pTags { |
|
// ValueHex() handles both binary and hex storage formats automatically |
|
pt, err := hex.Dec(string(pTag.ValueHex())) |
|
if err != nil { |
|
continue |
|
} |
|
follows = append(follows, pt) |
|
} |
|
|
|
return follows |
|
} |
|
|
|
// fetchAllPolicyAdminFollows fetches kind 3 events for all policy admins from the database |
|
// and aggregates their follows. |
|
func (l *Listener) fetchAllPolicyAdminFollows() ([][]byte, error) { |
|
var allFollows [][]byte |
|
seen := make(map[string]bool) |
|
|
|
// Get policy admin pubkeys |
|
admins := l.policyManager.GetPolicyAdminsBin() |
|
if len(admins) == 0 { |
|
return nil, fmt.Errorf("no policy admins configured") |
|
} |
|
|
|
// For each admin, query their latest kind 3 event |
|
for _, adminPubkey := range admins { |
|
// Build proper filter for kind 3 from this admin |
|
f := filter.New() |
|
f.Authors = tag.NewFromAny(adminPubkey) |
|
f.Kinds = kind.NewS(kind.FollowList) |
|
limit := uint(1) |
|
f.Limit = &limit |
|
|
|
// Query the database for kind 3 events from this admin |
|
events, err := l.DB.QueryEvents(l.ctx, f) |
|
if err != nil { |
|
log.W.F("failed to query follows for admin %s: %v", hex.Enc(adminPubkey), err) |
|
continue |
|
} |
|
|
|
// events is []*event.E - iterate over the slice |
|
for _, ev := range events { |
|
// Extract p-tags from this follow list |
|
follows := extractFollowsFromEvent(ev) |
|
for _, follow := range follows { |
|
key := string(follow) |
|
if !seen[key] { |
|
seen[key] = true |
|
allFollows = append(allFollows, follow) |
|
} |
|
} |
|
} |
|
} |
|
|
|
return allFollows, nil |
|
} |
|
|
|
// mergeFollows merges two follow lists, removing duplicates. |
|
func mergeFollows(existing, newFollows [][]byte) [][]byte { |
|
seen := make(map[string]bool) |
|
var result [][]byte |
|
|
|
for _, f := range existing { |
|
key := string(f) |
|
if !seen[key] { |
|
seen[key] = true |
|
result = append(result, f) |
|
} |
|
} |
|
|
|
for _, f := range newFollows { |
|
key := string(f) |
|
if !seen[key] { |
|
seen[key] = true |
|
result = append(result, f) |
|
} |
|
} |
|
|
|
return result |
|
} |
|
|
|
// IsPolicyConfigEvent returns true if the event is a policy configuration event (kind 12345) |
|
func IsPolicyConfigEvent(ev *event.E) bool { |
|
return ev.Kind == kind.PolicyConfig.K |
|
} |
|
|
|
// IsPolicyAdminFollowListEvent returns true if this is a follow list event from a policy admin. |
|
// Used to detect when we need to refresh the policy follows cache. |
|
func (l *Listener) IsPolicyAdminFollowListEvent(ev *event.E) bool { |
|
// Must be kind 3 (follow list) |
|
if ev.Kind != kind.FollowList.K { |
|
return false |
|
} |
|
|
|
// Policy system must be enabled |
|
if l.policyManager == nil || !l.policyManager.IsEnabled() { |
|
return false |
|
} |
|
|
|
// Sender must be a policy admin |
|
return l.policyManager.IsPolicyAdmin(ev.Pubkey) |
|
} |
|
|
|
// isPolicyAdmin checks if a pubkey is in the list of policy admins |
|
func isPolicyAdmin(pubkey []byte, admins [][]byte) bool { |
|
for _, admin := range admins { |
|
if bytes.Equal(pubkey, admin) { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
// InitializePolicyFollows loads the follow lists of all policy admins at startup. |
|
// This should be called after the policy manager is initialized but before |
|
// the relay starts accepting connections. |
|
// It's a method on Server so it can be called from main.go during initialization. |
|
func (s *Server) InitializePolicyFollows() error { |
|
// Skip if policy system is not enabled |
|
if s.policyManager == nil || !s.policyManager.IsEnabled() { |
|
log.D.F("policy system not enabled, skipping follow list initialization") |
|
return nil |
|
} |
|
|
|
// Skip if PolicyFollowWhitelistEnabled is false |
|
if !s.policyManager.IsPolicyFollowWhitelistEnabled() { |
|
log.D.F("policy follow whitelist not enabled, skipping follow list initialization") |
|
return nil |
|
} |
|
|
|
log.I.F("initializing policy follows from database") |
|
|
|
// Get policy admin pubkeys |
|
admins := s.policyManager.GetPolicyAdminsBin() |
|
if len(admins) == 0 { |
|
log.W.F("no policy admins configured, skipping follow list initialization") |
|
return nil |
|
} |
|
|
|
var allFollows [][]byte |
|
seen := make(map[string]bool) |
|
|
|
// For each admin, query their latest kind 3 event |
|
for _, adminPubkey := range admins { |
|
// Build proper filter for kind 3 from this admin |
|
f := filter.New() |
|
f.Authors = tag.NewFromAny(adminPubkey) |
|
f.Kinds = kind.NewS(kind.FollowList) |
|
limit := uint(1) |
|
f.Limit = &limit |
|
|
|
// Query the database for kind 3 events from this admin |
|
events, err := s.DB.QueryEvents(s.Ctx, f) |
|
if err != nil { |
|
log.W.F("failed to query follows for admin %s: %v", hex.Enc(adminPubkey), err) |
|
continue |
|
} |
|
|
|
// Extract p-tags from each follow list event |
|
for _, ev := range events { |
|
follows := extractFollowsFromEvent(ev) |
|
for _, follow := range follows { |
|
key := string(follow) |
|
if !seen[key] { |
|
seen[key] = true |
|
allFollows = append(allFollows, follow) |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Update the policy follows cache |
|
s.policyManager.UpdatePolicyFollows(allFollows) |
|
|
|
log.I.F("policy follows initialized with %d pubkeys from %d admin(s)", |
|
len(allFollows), len(admins)) |
|
|
|
return nil |
|
}
|
|
|