19 changed files with 3122 additions and 28 deletions
@ -0,0 +1,557 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"next.orly.dev/pkg/acl" |
||||||
|
"next.orly.dev/pkg/database" |
||||||
|
"next.orly.dev/pkg/protocol/httpauth" |
||||||
|
) |
||||||
|
|
||||||
|
// NIP86Request represents a NIP-86 JSON-RPC request
|
||||||
|
type NIP86Request struct { |
||||||
|
Method string `json:"method"` |
||||||
|
Params []interface{} `json:"params"` |
||||||
|
} |
||||||
|
|
||||||
|
// NIP86Response represents a NIP-86 JSON-RPC response
|
||||||
|
type NIP86Response struct { |
||||||
|
Result interface{} `json:"result,omitempty"` |
||||||
|
Error string `json:"error,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// handleNIP86Management handles NIP-86 management API requests
|
||||||
|
func (s *Server) handleNIP86Management(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check Content-Type
|
||||||
|
contentType := r.Header.Get("Content-Type") |
||||||
|
if contentType != "application/nostr+json+rpc" { |
||||||
|
http.Error(w, "Content-Type must be application/nostr+json+rpc", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Validate NIP-98 authentication
|
||||||
|
valid, pubkey, err := httpauth.CheckAuth(r) |
||||||
|
if chk.E(err) || !valid { |
||||||
|
errorMsg := "NIP-98 authentication validation failed" |
||||||
|
if err != nil { |
||||||
|
errorMsg = err.Error() |
||||||
|
} |
||||||
|
http.Error(w, errorMsg, http.StatusUnauthorized) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check permissions - require owner level only
|
||||||
|
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) |
||||||
|
if accessLevel != "owner" { |
||||||
|
http.Error(w, "Owner permission required", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if managed ACL is active
|
||||||
|
if acl.Registry.Type() != "managed" { |
||||||
|
http.Error(w, "Managed ACL mode is not active", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get the managed ACL instance
|
||||||
|
var managedACL *database.ManagedACL |
||||||
|
for _, aclInstance := range acl.Registry.ACL { |
||||||
|
if aclInstance.Type() == "managed" { |
||||||
|
if managed, ok := aclInstance.(*acl.Managed); ok { |
||||||
|
managedACL = managed.GetManagedACL() |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if managedACL == nil { |
||||||
|
http.Error(w, "Managed ACL not available", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Read and parse the request
|
||||||
|
body, err := io.ReadAll(r.Body) |
||||||
|
if chk.E(err) { |
||||||
|
http.Error(w, "Failed to read request body", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var request NIP86Request |
||||||
|
if err := json.Unmarshal(body, &request); chk.E(err) { |
||||||
|
http.Error(w, "Invalid JSON request", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Set response headers
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
|
||||||
|
// Handle the request based on method
|
||||||
|
response := s.handleNIP86Method(request, managedACL) |
||||||
|
|
||||||
|
// Send response
|
||||||
|
jsonData, err := json.Marshal(response) |
||||||
|
if chk.E(err) { |
||||||
|
http.Error(w, "Error generating response", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
w.Write(jsonData) |
||||||
|
} |
||||||
|
|
||||||
|
// handleNIP86Method handles individual NIP-86 methods
|
||||||
|
func (s *Server) handleNIP86Method(request NIP86Request, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
switch request.Method { |
||||||
|
case "supportedmethods": |
||||||
|
return s.handleSupportedMethods() |
||||||
|
case "banpubkey": |
||||||
|
return s.handleBanPubkey(request.Params, managedACL) |
||||||
|
case "listbannedpubkeys": |
||||||
|
return s.handleListBannedPubkeys(managedACL) |
||||||
|
case "allowpubkey": |
||||||
|
return s.handleAllowPubkey(request.Params, managedACL) |
||||||
|
case "listallowedpubkeys": |
||||||
|
return s.handleListAllowedPubkeys(managedACL) |
||||||
|
case "listeventsneedingmoderation": |
||||||
|
return s.handleListEventsNeedingModeration(managedACL) |
||||||
|
case "allowevent": |
||||||
|
return s.handleAllowEvent(request.Params, managedACL) |
||||||
|
case "banevent": |
||||||
|
return s.handleBanEvent(request.Params, managedACL) |
||||||
|
case "listbannedevents": |
||||||
|
return s.handleListBannedEvents(managedACL) |
||||||
|
case "changerelayname": |
||||||
|
return s.handleChangeRelayName(request.Params, managedACL) |
||||||
|
case "changerelaydescription": |
||||||
|
return s.handleChangeRelayDescription(request.Params, managedACL) |
||||||
|
case "changerelayicon": |
||||||
|
return s.handleChangeRelayIcon(request.Params, managedACL) |
||||||
|
case "allowkind": |
||||||
|
return s.handleAllowKind(request.Params, managedACL) |
||||||
|
case "disallowkind": |
||||||
|
return s.handleDisallowKind(request.Params, managedACL) |
||||||
|
case "listallowedkinds": |
||||||
|
return s.handleListAllowedKinds(managedACL) |
||||||
|
case "blockip": |
||||||
|
return s.handleBlockIP(request.Params, managedACL) |
||||||
|
case "unblockip": |
||||||
|
return s.handleUnblockIP(request.Params, managedACL) |
||||||
|
case "listblockedips": |
||||||
|
return s.handleListBlockedIPs(managedACL) |
||||||
|
default: |
||||||
|
return NIP86Response{Error: "Unknown method: " + request.Method} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// handleSupportedMethods returns the list of supported methods
|
||||||
|
func (s *Server) handleSupportedMethods() NIP86Response { |
||||||
|
methods := []string{ |
||||||
|
"supportedmethods", |
||||||
|
"banpubkey", |
||||||
|
"listbannedpubkeys", |
||||||
|
"allowpubkey", |
||||||
|
"listallowedpubkeys", |
||||||
|
"listeventsneedingmoderation", |
||||||
|
"allowevent", |
||||||
|
"banevent", |
||||||
|
"listbannedevents", |
||||||
|
"changerelayname", |
||||||
|
"changerelaydescription", |
||||||
|
"changerelayicon", |
||||||
|
"allowkind", |
||||||
|
"disallowkind", |
||||||
|
"listallowedkinds", |
||||||
|
"blockip", |
||||||
|
"unblockip", |
||||||
|
"listblockedips", |
||||||
|
} |
||||||
|
return NIP86Response{Result: methods} |
||||||
|
} |
||||||
|
|
||||||
|
// handleBanPubkey bans a public key
|
||||||
|
func (s *Server) handleBanPubkey(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: pubkey"} |
||||||
|
} |
||||||
|
|
||||||
|
pubkey, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid pubkey parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
// Validate pubkey format
|
||||||
|
if len(pubkey) != 64 { |
||||||
|
return NIP86Response{Error: "Invalid pubkey format"} |
||||||
|
} |
||||||
|
|
||||||
|
reason := "" |
||||||
|
if len(params) > 1 { |
||||||
|
if r, ok := params[1].(string); ok { |
||||||
|
reason = r |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := managedACL.SaveBannedPubkey(pubkey, reason); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to ban pubkey: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleListBannedPubkeys returns the list of banned pubkeys
|
||||||
|
func (s *Server) handleListBannedPubkeys(managedACL *database.ManagedACL) NIP86Response { |
||||||
|
banned, err := managedACL.ListBannedPubkeys() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to list banned pubkeys: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
// Convert to the expected format
|
||||||
|
result := make([]map[string]interface{}, len(banned)) |
||||||
|
for i, b := range banned { |
||||||
|
result[i] = map[string]interface{}{ |
||||||
|
"pubkey": b.Pubkey, |
||||||
|
"reason": b.Reason, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: result} |
||||||
|
} |
||||||
|
|
||||||
|
// handleAllowPubkey allows a public key
|
||||||
|
func (s *Server) handleAllowPubkey(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: pubkey"} |
||||||
|
} |
||||||
|
|
||||||
|
pubkey, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid pubkey parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
// Validate pubkey format
|
||||||
|
if len(pubkey) != 64 { |
||||||
|
return NIP86Response{Error: "Invalid pubkey format"} |
||||||
|
} |
||||||
|
|
||||||
|
reason := "" |
||||||
|
if len(params) > 1 { |
||||||
|
if r, ok := params[1].(string); ok { |
||||||
|
reason = r |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := managedACL.SaveAllowedPubkey(pubkey, reason); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to allow pubkey: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleListAllowedPubkeys returns the list of allowed pubkeys
|
||||||
|
func (s *Server) handleListAllowedPubkeys(managedACL *database.ManagedACL) NIP86Response { |
||||||
|
allowed, err := managedACL.ListAllowedPubkeys() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to list allowed pubkeys: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
// Convert to the expected format
|
||||||
|
result := make([]map[string]interface{}, len(allowed)) |
||||||
|
for i, a := range allowed { |
||||||
|
result[i] = map[string]interface{}{ |
||||||
|
"pubkey": a.Pubkey, |
||||||
|
"reason": a.Reason, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: result} |
||||||
|
} |
||||||
|
|
||||||
|
// handleListEventsNeedingModeration returns events needing moderation
|
||||||
|
func (s *Server) handleListEventsNeedingModeration(managedACL *database.ManagedACL) NIP86Response { |
||||||
|
events, err := managedACL.ListEventsNeedingModeration() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to list events needing moderation: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
// Convert to the expected format
|
||||||
|
result := make([]map[string]interface{}, len(events)) |
||||||
|
for i, e := range events { |
||||||
|
result[i] = map[string]interface{}{ |
||||||
|
"id": e.ID, |
||||||
|
"reason": e.Reason, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: result} |
||||||
|
} |
||||||
|
|
||||||
|
// handleAllowEvent allows an event
|
||||||
|
func (s *Server) handleAllowEvent(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: event_id"} |
||||||
|
} |
||||||
|
|
||||||
|
eventID, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid event_id parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
// Validate event ID format
|
||||||
|
if len(eventID) != 64 { |
||||||
|
return NIP86Response{Error: "Invalid event_id format"} |
||||||
|
} |
||||||
|
|
||||||
|
reason := "" |
||||||
|
if len(params) > 1 { |
||||||
|
if r, ok := params[1].(string); ok { |
||||||
|
reason = r |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := managedACL.SaveAllowedEvent(eventID, reason); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to allow event: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove from moderation queue if it was there
|
||||||
|
managedACL.RemoveEventNeedingModeration(eventID) |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleBanEvent bans an event
|
||||||
|
func (s *Server) handleBanEvent(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: event_id"} |
||||||
|
} |
||||||
|
|
||||||
|
eventID, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid event_id parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
// Validate event ID format
|
||||||
|
if len(eventID) != 64 { |
||||||
|
return NIP86Response{Error: "Invalid event_id format"} |
||||||
|
} |
||||||
|
|
||||||
|
reason := "" |
||||||
|
if len(params) > 1 { |
||||||
|
if r, ok := params[1].(string); ok { |
||||||
|
reason = r |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := managedACL.SaveBannedEvent(eventID, reason); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to ban event: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleListBannedEvents returns the list of banned events
|
||||||
|
func (s *Server) handleListBannedEvents(managedACL *database.ManagedACL) NIP86Response { |
||||||
|
banned, err := managedACL.ListBannedEvents() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to list banned events: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
// Convert to the expected format
|
||||||
|
result := make([]map[string]interface{}, len(banned)) |
||||||
|
for i, b := range banned { |
||||||
|
result[i] = map[string]interface{}{ |
||||||
|
"id": b.ID, |
||||||
|
"reason": b.Reason, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: result} |
||||||
|
} |
||||||
|
|
||||||
|
// handleChangeRelayName changes the relay name
|
||||||
|
func (s *Server) handleChangeRelayName(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: name"} |
||||||
|
} |
||||||
|
|
||||||
|
name, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid name parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
config, err := managedACL.GetRelayConfig() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to get relay config: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
config.RelayName = name |
||||||
|
if err := managedACL.SaveRelayConfig(config); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to save relay config: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleChangeRelayDescription changes the relay description
|
||||||
|
func (s *Server) handleChangeRelayDescription(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: description"} |
||||||
|
} |
||||||
|
|
||||||
|
description, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid description parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
config, err := managedACL.GetRelayConfig() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to get relay config: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
config.RelayDescription = description |
||||||
|
if err := managedACL.SaveRelayConfig(config); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to save relay config: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleChangeRelayIcon changes the relay icon
|
||||||
|
func (s *Server) handleChangeRelayIcon(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: icon_url"} |
||||||
|
} |
||||||
|
|
||||||
|
iconURL, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid icon_url parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
config, err := managedACL.GetRelayConfig() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to get relay config: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
config.RelayIcon = iconURL |
||||||
|
if err := managedACL.SaveRelayConfig(config); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to save relay config: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleAllowKind allows an event kind
|
||||||
|
func (s *Server) handleAllowKind(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: kind"} |
||||||
|
} |
||||||
|
|
||||||
|
kindFloat, ok := params[0].(float64) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid kind parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
kind := int(kindFloat) |
||||||
|
if err := managedACL.SaveAllowedKind(kind); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to allow kind: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleDisallowKind disallows an event kind
|
||||||
|
func (s *Server) handleDisallowKind(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: kind"} |
||||||
|
} |
||||||
|
|
||||||
|
kindFloat, ok := params[0].(float64) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid kind parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
kind := int(kindFloat) |
||||||
|
if err := managedACL.RemoveAllowedKind(kind); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to disallow kind: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleListAllowedKinds returns the list of allowed kinds
|
||||||
|
func (s *Server) handleListAllowedKinds(managedACL *database.ManagedACL) NIP86Response { |
||||||
|
kinds, err := managedACL.ListAllowedKinds() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to list allowed kinds: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: kinds} |
||||||
|
} |
||||||
|
|
||||||
|
// handleBlockIP blocks an IP address
|
||||||
|
func (s *Server) handleBlockIP(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: ip"} |
||||||
|
} |
||||||
|
|
||||||
|
ip, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid ip parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
reason := "" |
||||||
|
if len(params) > 1 { |
||||||
|
if r, ok := params[1].(string); ok { |
||||||
|
reason = r |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := managedACL.SaveBlockedIP(ip, reason); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to block IP: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleUnblockIP unblocks an IP address
|
||||||
|
func (s *Server) handleUnblockIP(params []interface{}, managedACL *database.ManagedACL) NIP86Response { |
||||||
|
if len(params) < 1 { |
||||||
|
return NIP86Response{Error: "Missing required parameter: ip"} |
||||||
|
} |
||||||
|
|
||||||
|
ip, ok := params[0].(string) |
||||||
|
if !ok { |
||||||
|
return NIP86Response{Error: "Invalid ip parameter"} |
||||||
|
} |
||||||
|
|
||||||
|
if err := managedACL.RemoveBlockedIP(ip); chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to unblock IP: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: true} |
||||||
|
} |
||||||
|
|
||||||
|
// handleListBlockedIPs returns the list of blocked IPs
|
||||||
|
func (s *Server) handleListBlockedIPs(managedACL *database.ManagedACL) NIP86Response { |
||||||
|
blocked, err := managedACL.ListBlockedIPs() |
||||||
|
if chk.E(err) { |
||||||
|
return NIP86Response{Error: "Failed to list blocked IPs: " + err.Error()} |
||||||
|
} |
||||||
|
|
||||||
|
// Convert to the expected format
|
||||||
|
result := make([]map[string]interface{}, len(blocked)) |
||||||
|
for i, b := range blocked { |
||||||
|
result[i] = map[string]interface{}{ |
||||||
|
"ip": b.IP, |
||||||
|
"reason": b.Reason, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NIP86Response{Result: result} |
||||||
|
} |
||||||
@ -0,0 +1,121 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"net/http/httptest" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"next.orly.dev/app/config" |
||||||
|
"next.orly.dev/pkg/database" |
||||||
|
) |
||||||
|
|
||||||
|
func TestHandleNIP86Management_Basic(t *testing.T) { |
||||||
|
// Setup test database
|
||||||
|
ctx, cancel := context.WithCancel(context.Background()) |
||||||
|
defer cancel() |
||||||
|
|
||||||
|
// Use a temporary directory for the test database
|
||||||
|
tmpDir := t.TempDir() |
||||||
|
db, err := database.New(ctx, cancel, tmpDir, "test.db") |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("Failed to create test database: %v", err) |
||||||
|
} |
||||||
|
defer db.Close() |
||||||
|
|
||||||
|
// Setup non-managed ACL
|
||||||
|
cfg := &config.C{ |
||||||
|
AuthRequired: false, |
||||||
|
Owners: []string{"owner1"}, |
||||||
|
Admins: []string{"admin1"}, |
||||||
|
ACLMode: "none", |
||||||
|
} |
||||||
|
|
||||||
|
// Setup server
|
||||||
|
server := &Server{ |
||||||
|
Config: cfg, |
||||||
|
D: db, |
||||||
|
Admins: [][]byte{[]byte("admin1")}, |
||||||
|
Owners: [][]byte{[]byte("owner1")}, |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("non-managed mode should reject management API", func(t *testing.T) { |
||||||
|
// Create request body
|
||||||
|
body := map[string]interface{}{"method": "banpubkey", "params": []string{"user1", "test ban"}} |
||||||
|
bodyBytes, err := json.Marshal(body) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("Failed to marshal request body: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Create HTTP request without authentication to test the managed mode check
|
||||||
|
req := httptest.NewRequest("POST", "/api/nip86", bytes.NewReader(bodyBytes)) |
||||||
|
req.Header.Set("Content-Type", "application/nostr+json+rpc") |
||||||
|
|
||||||
|
// Create response recorder
|
||||||
|
rr := httptest.NewRecorder() |
||||||
|
|
||||||
|
// Call the handler
|
||||||
|
server.handleNIP86Management(rr, req) |
||||||
|
|
||||||
|
// Check status code (should be 401 due to authentication failure, not 400)
|
||||||
|
if rr.Code != 401 { |
||||||
|
t.Errorf("handleNIP86Management() status = %v, want 401", rr.Code) |
||||||
|
} |
||||||
|
|
||||||
|
// The test verifies that the handler runs and returns an error
|
||||||
|
if rr.Body.String() == "" { |
||||||
|
t.Errorf("handleNIP86Management() body should not be empty") |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("GET method should not be allowed", func(t *testing.T) { |
||||||
|
// Create HTTP request
|
||||||
|
req := httptest.NewRequest("GET", "/api/nip86", nil) |
||||||
|
|
||||||
|
// Create response recorder
|
||||||
|
rr := httptest.NewRecorder() |
||||||
|
|
||||||
|
// Call the handler
|
||||||
|
server.handleNIP86Management(rr, req) |
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if rr.Code != 405 { |
||||||
|
t.Errorf("handleNIP86Management() status = %v, want 405", rr.Code) |
||||||
|
} |
||||||
|
|
||||||
|
// Check error message (should contain "Method not allowed")
|
||||||
|
if rr.Body.String() == "" { |
||||||
|
t.Errorf("handleNIP86Management() body should not be empty") |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("unauthenticated request should be rejected", func(t *testing.T) { |
||||||
|
// Create request body
|
||||||
|
body := map[string]interface{}{"method": "banpubkey", "params": []string{"user1", "test ban"}} |
||||||
|
bodyBytes, err := json.Marshal(body) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("Failed to marshal request body: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Create HTTP request without authentication
|
||||||
|
req := httptest.NewRequest("POST", "/api/nip86", bytes.NewReader(bodyBytes)) |
||||||
|
req.Header.Set("Content-Type", "application/nostr+json+rpc") |
||||||
|
|
||||||
|
// Create response recorder
|
||||||
|
rr := httptest.NewRecorder() |
||||||
|
|
||||||
|
// Call the handler
|
||||||
|
server.handleNIP86Management(rr, req) |
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if rr.Code != 401 { |
||||||
|
t.Errorf("handleNIP86Management() status = %v, want 401", rr.Code) |
||||||
|
} |
||||||
|
|
||||||
|
// Check error message (should be about missing authorization header)
|
||||||
|
if rr.Body.String() == "" { |
||||||
|
t.Errorf("handleNIP86Management() body should not be empty") |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,223 @@ |
|||||||
|
package acl |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/hex" |
||||||
|
"net" |
||||||
|
"reflect" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"lol.mleku.dev/errorf" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
"next.orly.dev/app/config" |
||||||
|
"next.orly.dev/pkg/database" |
||||||
|
"next.orly.dev/pkg/encoders/bech32encoding" |
||||||
|
"next.orly.dev/pkg/encoders/event" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
) |
||||||
|
|
||||||
|
type Managed struct { |
||||||
|
Ctx context.Context |
||||||
|
cfg *config.C |
||||||
|
*database.D |
||||||
|
managedACL *database.ManagedACL |
||||||
|
owners [][]byte |
||||||
|
admins [][]byte |
||||||
|
mx sync.RWMutex |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Managed) Configure(cfg ...any) (err error) { |
||||||
|
log.I.F("configuring managed ACL") |
||||||
|
for _, ca := range cfg { |
||||||
|
switch c := ca.(type) { |
||||||
|
case *config.C: |
||||||
|
m.cfg = c |
||||||
|
case *database.D: |
||||||
|
m.D = c |
||||||
|
m.managedACL = database.NewManagedACL(c) |
||||||
|
case context.Context: |
||||||
|
m.Ctx = c |
||||||
|
default: |
||||||
|
err = errorf.E("invalid type: %T", reflect.TypeOf(ca)) |
||||||
|
} |
||||||
|
} |
||||||
|
if m.cfg == nil || m.D == nil { |
||||||
|
err = errorf.E("both config and database must be set") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Load owners
|
||||||
|
for _, owner := range m.cfg.Owners { |
||||||
|
if len(owner) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
var pk []byte |
||||||
|
if pk, err = bech32encoding.NpubOrHexToPublicKeyBinary(owner); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
m.owners = append(m.owners, pk) |
||||||
|
} |
||||||
|
|
||||||
|
// Load admins
|
||||||
|
for _, admin := range m.cfg.Admins { |
||||||
|
if len(admin) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
var pk []byte |
||||||
|
if pk, err = bech32encoding.NpubOrHexToPublicKeyBinary(admin); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
m.admins = append(m.admins, pk) |
||||||
|
} |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Managed) GetAccessLevel(pub []byte, address string) (level string) { |
||||||
|
m.mx.RLock() |
||||||
|
defer m.mx.RUnlock() |
||||||
|
|
||||||
|
// If no pubkey provided and auth is required, return "none"
|
||||||
|
if len(pub) == 0 && m.cfg.AuthRequired { |
||||||
|
return "none" |
||||||
|
} |
||||||
|
|
||||||
|
// Check owners first
|
||||||
|
for _, v := range m.owners { |
||||||
|
if utils.FastEqual(v, pub) { |
||||||
|
return "owner" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check admins
|
||||||
|
for _, v := range m.admins { |
||||||
|
if utils.FastEqual(v, pub) { |
||||||
|
return "admin" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check if pubkey is banned
|
||||||
|
pubkeyHex := hex.EncodeToString(pub) |
||||||
|
if banned, err := m.managedACL.IsPubkeyBanned(pubkeyHex); err == nil && banned { |
||||||
|
return "banned" |
||||||
|
} |
||||||
|
|
||||||
|
// Check if pubkey is explicitly allowed
|
||||||
|
if allowed, err := m.managedACL.IsPubkeyAllowed(pubkeyHex); err == nil && allowed { |
||||||
|
return "write" |
||||||
|
} |
||||||
|
|
||||||
|
// Check if IP is blocked
|
||||||
|
if blocked, err := m.managedACL.IsIPBlocked(address); err == nil && blocked { |
||||||
|
return "blocked" |
||||||
|
} |
||||||
|
|
||||||
|
// Default to read-only for managed mode
|
||||||
|
return "read" |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Managed) CheckPolicy(ev *event.E) (allowed bool, err error) { |
||||||
|
// Check if event is banned
|
||||||
|
eventID := hex.EncodeToString(ev.ID) |
||||||
|
if banned, err := m.managedACL.IsEventBanned(eventID); err == nil && banned { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Check if event is explicitly allowed
|
||||||
|
if allowed, err := m.managedACL.IsEventAllowed(eventID); err == nil && allowed { |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Check if event kind is allowed
|
||||||
|
if allowed, err := m.managedACL.IsKindAllowed(int(ev.Kind)); err == nil && !allowed { |
||||||
|
// If there are allowed kinds configured and this kind is not in the list, deny
|
||||||
|
allowedKinds, err := m.managedACL.ListAllowedKinds() |
||||||
|
if err == nil && len(allowedKinds) > 0 { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check if author is banned
|
||||||
|
authorHex := hex.EncodeToString(ev.Pubkey) |
||||||
|
if banned, err := m.managedACL.IsPubkeyBanned(authorHex); err == nil && banned { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Check if author is explicitly allowed
|
||||||
|
if allowed, err := m.managedACL.IsPubkeyAllowed(authorHex); err == nil && allowed { |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
// For managed mode, default to allowing events from owners and admins
|
||||||
|
for _, v := range m.owners { |
||||||
|
if utils.FastEqual(v, ev.Pubkey) { |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, v := range m.admins { |
||||||
|
if utils.FastEqual(v, ev.Pubkey) { |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check if we should add this event to moderation queue
|
||||||
|
// This could be extended to add events to moderation based on content analysis
|
||||||
|
// For now, we'll just allow the event
|
||||||
|
|
||||||
|
// Default to allowing events in managed mode (can be restricted by explicit bans/allows)
|
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Managed) GetACLInfo() (name, description, documentation string) { |
||||||
|
return "managed", "managed ACL with NIP-86 support", |
||||||
|
`Managed ACL mode provides fine-grained access control through NIP-86 management API. |
||||||
|
|
||||||
|
Features: |
||||||
|
- Ban/allow specific pubkeys |
||||||
|
- Ban/allow specific events |
||||||
|
- Block IP addresses |
||||||
|
- Allow/deny specific event kinds |
||||||
|
- Relay metadata management |
||||||
|
- Event moderation queue |
||||||
|
|
||||||
|
This mode requires explicit management through the NIP-86 API endpoints. |
||||||
|
Only relay owners can access the management interface and API.` |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Managed) Type() string { |
||||||
|
return "managed" |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Managed) Syncer() { |
||||||
|
// Managed ACL doesn't need background syncing
|
||||||
|
// All management is done through the API
|
||||||
|
} |
||||||
|
|
||||||
|
// Helper methods for the management API
|
||||||
|
|
||||||
|
// IsIPBlocked checks if an IP address is blocked
|
||||||
|
func (m *Managed) IsIPBlocked(ip string) bool { |
||||||
|
// Parse IP to handle both IPv4 and IPv6
|
||||||
|
parsedIP := net.ParseIP(ip) |
||||||
|
if parsedIP == nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
blocked, err := m.managedACL.IsIPBlocked(ip) |
||||||
|
if err != nil { |
||||||
|
log.W.F("error checking if IP is blocked: %v", err) |
||||||
|
return false |
||||||
|
} |
||||||
|
return blocked |
||||||
|
} |
||||||
|
|
||||||
|
// GetManagedACL returns the managed ACL database instance
|
||||||
|
func (m *Managed) GetManagedACL() *database.ManagedACL { |
||||||
|
return m.managedACL |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
log.T.F("registering managed ACL") |
||||||
|
Registry.Register(new(Managed)) |
||||||
|
} |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
package acl |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"next.orly.dev/app/config" |
||||||
|
"next.orly.dev/pkg/database" |
||||||
|
"next.orly.dev/pkg/encoders/event" |
||||||
|
) |
||||||
|
|
||||||
|
func TestManagedACL_BasicFunctionality(t *testing.T) { |
||||||
|
// Setup test database
|
||||||
|
ctx, cancel := context.WithCancel(context.Background()) |
||||||
|
defer cancel() |
||||||
|
|
||||||
|
// Use a temporary directory for the test database
|
||||||
|
tmpDir := t.TempDir() |
||||||
|
db, err := database.New(ctx, cancel, tmpDir, "test.db") |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("Failed to create test database: %v", err) |
||||||
|
} |
||||||
|
defer db.Close() |
||||||
|
|
||||||
|
// Setup managed ACL
|
||||||
|
cfg := &config.C{ |
||||||
|
AuthRequired: false, |
||||||
|
Owners: []string{"owner1"}, |
||||||
|
Admins: []string{"admin1"}, |
||||||
|
} |
||||||
|
|
||||||
|
managed := &Managed{ |
||||||
|
Ctx: ctx, |
||||||
|
cfg: cfg, |
||||||
|
D: db, |
||||||
|
managedACL: database.NewManagedACL(db), |
||||||
|
owners: [][]byte{[]byte("owner1")}, |
||||||
|
admins: [][]byte{[]byte("admin1")}, |
||||||
|
} |
||||||
|
|
||||||
|
// Test basic functionality
|
||||||
|
t.Run("owner should get owner access", func(t *testing.T) { |
||||||
|
level := managed.GetAccessLevel([]byte("owner1"), "127.0.0.1") |
||||||
|
if level != "owner" { |
||||||
|
t.Errorf("GetAccessLevel() = %v, want owner", level) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("admin should get admin access", func(t *testing.T) { |
||||||
|
level := managed.GetAccessLevel([]byte("admin1"), "127.0.0.1") |
||||||
|
if level != "admin" { |
||||||
|
t.Errorf("GetAccessLevel() = %v, want admin", level) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("default user should get read access", func(t *testing.T) { |
||||||
|
level := managed.GetAccessLevel([]byte("user1"), "127.0.0.1") |
||||||
|
if level != "read" { |
||||||
|
t.Errorf("GetAccessLevel() = %v, want read", level) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("owner event should be allowed", func(t *testing.T) { |
||||||
|
ev := createMinimalTestEvent("owner1", 1) |
||||||
|
allowed, err := managed.CheckPolicy(ev) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("CheckPolicy() error = %v", err) |
||||||
|
} |
||||||
|
if !allowed { |
||||||
|
t.Errorf("CheckPolicy() = %v, want true", allowed) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("admin event should be allowed", func(t *testing.T) { |
||||||
|
ev := createMinimalTestEvent("admin1", 1) |
||||||
|
allowed, err := managed.CheckPolicy(ev) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("CheckPolicy() error = %v", err) |
||||||
|
} |
||||||
|
if !allowed { |
||||||
|
t.Errorf("CheckPolicy() = %v, want true", allowed) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("default event should be allowed", func(t *testing.T) { |
||||||
|
ev := createMinimalTestEvent("user1", 1) |
||||||
|
allowed, err := managed.CheckPolicy(ev) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("CheckPolicy() error = %v", err) |
||||||
|
} |
||||||
|
if !allowed { |
||||||
|
t.Errorf("CheckPolicy() = %v, want true", allowed) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func createMinimalTestEvent(pubkey string, kind uint16) *event.E { |
||||||
|
ev := event.New() |
||||||
|
ev.Pubkey = []byte(pubkey) |
||||||
|
ev.Kind = kind |
||||||
|
ev.CreatedAt = time.Now().Unix() |
||||||
|
ev.Content = []byte("test content") |
||||||
|
ev.Tags = nil |
||||||
|
ev.ID = ev.GetIDBytes() |
||||||
|
return ev |
||||||
|
} |
||||||
@ -0,0 +1,645 @@ |
|||||||
|
package database |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v4" |
||||||
|
) |
||||||
|
|
||||||
|
// ManagedACLConfig represents the configuration for managed ACL mode
|
||||||
|
type ManagedACLConfig struct { |
||||||
|
RelayName string `json:"relay_name"` |
||||||
|
RelayDescription string `json:"relay_description"` |
||||||
|
RelayIcon string `json:"relay_icon"` |
||||||
|
} |
||||||
|
|
||||||
|
// BannedPubkey represents a banned public key entry
|
||||||
|
type BannedPubkey struct { |
||||||
|
Pubkey string `json:"pubkey"` |
||||||
|
Reason string `json:"reason,omitempty"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// AllowedPubkey represents an allowed public key entry
|
||||||
|
type AllowedPubkey struct { |
||||||
|
Pubkey string `json:"pubkey"` |
||||||
|
Reason string `json:"reason,omitempty"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// BannedEvent represents a banned event entry
|
||||||
|
type BannedEvent struct { |
||||||
|
ID string `json:"id"` |
||||||
|
Reason string `json:"reason,omitempty"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// AllowedEvent represents an allowed event entry
|
||||||
|
type AllowedEvent struct { |
||||||
|
ID string `json:"id"` |
||||||
|
Reason string `json:"reason,omitempty"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// BlockedIP represents a blocked IP address entry
|
||||||
|
type BlockedIP struct { |
||||||
|
IP string `json:"ip"` |
||||||
|
Reason string `json:"reason,omitempty"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// AllowedKind represents an allowed event kind
|
||||||
|
type AllowedKind struct { |
||||||
|
Kind int `json:"kind"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// EventNeedingModeration represents an event that needs moderation
|
||||||
|
type EventNeedingModeration struct { |
||||||
|
ID string `json:"id"` |
||||||
|
Reason string `json:"reason,omitempty"` |
||||||
|
Added time.Time `json:"added"` |
||||||
|
} |
||||||
|
|
||||||
|
// ManagedACL database operations
|
||||||
|
type ManagedACL struct { |
||||||
|
*D |
||||||
|
} |
||||||
|
|
||||||
|
// NewManagedACL creates a new ManagedACL instance
|
||||||
|
func NewManagedACL(db *D) *ManagedACL { |
||||||
|
return &ManagedACL{D: db} |
||||||
|
} |
||||||
|
|
||||||
|
// SaveBannedPubkey saves a banned pubkey to the database
|
||||||
|
func (m *ManagedACL) SaveBannedPubkey(pubkey string, reason string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getBannedPubkeyKey(pubkey) |
||||||
|
banned := BannedPubkey{ |
||||||
|
Pubkey: pubkey, |
||||||
|
Reason: reason, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(banned) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveBannedPubkey removes a banned pubkey from the database
|
||||||
|
func (m *ManagedACL) RemoveBannedPubkey(pubkey string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getBannedPubkeyKey(pubkey) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListBannedPubkeys returns all banned pubkeys
|
||||||
|
func (m *ManagedACL) ListBannedPubkeys() ([]BannedPubkey, error) { |
||||||
|
var banned []BannedPubkey |
||||||
|
return banned, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getBannedPubkeyPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var bannedPubkey BannedPubkey |
||||||
|
if err := json.Unmarshal(val, &bannedPubkey); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
banned = append(banned, bannedPubkey) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveAllowedPubkey saves an allowed pubkey to the database
|
||||||
|
func (m *ManagedACL) SaveAllowedPubkey(pubkey string, reason string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedPubkeyKey(pubkey) |
||||||
|
allowed := AllowedPubkey{ |
||||||
|
Pubkey: pubkey, |
||||||
|
Reason: reason, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(allowed) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveAllowedPubkey removes an allowed pubkey from the database
|
||||||
|
func (m *ManagedACL) RemoveAllowedPubkey(pubkey string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedPubkeyKey(pubkey) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListAllowedPubkeys returns all allowed pubkeys
|
||||||
|
func (m *ManagedACL) ListAllowedPubkeys() ([]AllowedPubkey, error) { |
||||||
|
var allowed []AllowedPubkey |
||||||
|
return allowed, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getAllowedPubkeyPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var allowedPubkey AllowedPubkey |
||||||
|
if err := json.Unmarshal(val, &allowedPubkey); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
allowed = append(allowed, allowedPubkey) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveBannedEvent saves a banned event to the database
|
||||||
|
func (m *ManagedACL) SaveBannedEvent(eventID string, reason string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getBannedEventKey(eventID) |
||||||
|
banned := BannedEvent{ |
||||||
|
ID: eventID, |
||||||
|
Reason: reason, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(banned) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveBannedEvent removes a banned event from the database
|
||||||
|
func (m *ManagedACL) RemoveBannedEvent(eventID string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getBannedEventKey(eventID) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListBannedEvents returns all banned events
|
||||||
|
func (m *ManagedACL) ListBannedEvents() ([]BannedEvent, error) { |
||||||
|
var banned []BannedEvent |
||||||
|
return banned, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getBannedEventPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var bannedEvent BannedEvent |
||||||
|
if err := json.Unmarshal(val, &bannedEvent); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
banned = append(banned, bannedEvent) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveAllowedEvent saves an allowed event to the database
|
||||||
|
func (m *ManagedACL) SaveAllowedEvent(eventID string, reason string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedEventKey(eventID) |
||||||
|
allowed := AllowedEvent{ |
||||||
|
ID: eventID, |
||||||
|
Reason: reason, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(allowed) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveAllowedEvent removes an allowed event from the database
|
||||||
|
func (m *ManagedACL) RemoveAllowedEvent(eventID string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedEventKey(eventID) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListAllowedEvents returns all allowed events
|
||||||
|
func (m *ManagedACL) ListAllowedEvents() ([]AllowedEvent, error) { |
||||||
|
var allowed []AllowedEvent |
||||||
|
return allowed, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getAllowedEventPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var allowedEvent AllowedEvent |
||||||
|
if err := json.Unmarshal(val, &allowedEvent); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
allowed = append(allowed, allowedEvent) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveBlockedIP saves a blocked IP to the database
|
||||||
|
func (m *ManagedACL) SaveBlockedIP(ip string, reason string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getBlockedIPKey(ip) |
||||||
|
blocked := BlockedIP{ |
||||||
|
IP: ip, |
||||||
|
Reason: reason, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(blocked) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveBlockedIP removes a blocked IP from the database
|
||||||
|
func (m *ManagedACL) RemoveBlockedIP(ip string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getBlockedIPKey(ip) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListBlockedIPs returns all blocked IPs
|
||||||
|
func (m *ManagedACL) ListBlockedIPs() ([]BlockedIP, error) { |
||||||
|
var blocked []BlockedIP |
||||||
|
return blocked, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getBlockedIPPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var blockedIP BlockedIP |
||||||
|
if err := json.Unmarshal(val, &blockedIP); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
blocked = append(blocked, blockedIP) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveAllowedKind saves an allowed kind to the database
|
||||||
|
func (m *ManagedACL) SaveAllowedKind(kind int) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedKindKey(kind) |
||||||
|
allowed := AllowedKind{ |
||||||
|
Kind: kind, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(allowed) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveAllowedKind removes an allowed kind from the database
|
||||||
|
func (m *ManagedACL) RemoveAllowedKind(kind int) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedKindKey(kind) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListAllowedKinds returns all allowed kinds
|
||||||
|
func (m *ManagedACL) ListAllowedKinds() ([]int, error) { |
||||||
|
var kinds []int |
||||||
|
return kinds, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getAllowedKindPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var allowedKind AllowedKind |
||||||
|
if err := json.Unmarshal(val, &allowedKind); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
kinds = append(kinds, allowedKind.Kind) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveEventNeedingModeration saves an event that needs moderation
|
||||||
|
func (m *ManagedACL) SaveEventNeedingModeration(eventID string, reason string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getEventNeedingModerationKey(eventID) |
||||||
|
event := EventNeedingModeration{ |
||||||
|
ID: eventID, |
||||||
|
Reason: reason, |
||||||
|
Added: time.Now(), |
||||||
|
} |
||||||
|
data, err := json.Marshal(event) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveEventNeedingModeration removes an event from moderation queue
|
||||||
|
func (m *ManagedACL) RemoveEventNeedingModeration(eventID string) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getEventNeedingModerationKey(eventID) |
||||||
|
return txn.Delete(key) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ListEventsNeedingModeration returns all events needing moderation
|
||||||
|
func (m *ManagedACL) ListEventsNeedingModeration() ([]EventNeedingModeration, error) { |
||||||
|
var events []EventNeedingModeration |
||||||
|
return events, m.View(func(txn *badger.Txn) error { |
||||||
|
prefix := m.getEventNeedingModerationPrefix() |
||||||
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
||||||
|
defer it.Close() |
||||||
|
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() { |
||||||
|
item := it.Item() |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
var event EventNeedingModeration |
||||||
|
if err := json.Unmarshal(val, &event); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
events = append(events, event) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// SaveRelayConfig saves relay configuration
|
||||||
|
func (m *ManagedACL) SaveRelayConfig(config ManagedACLConfig) error { |
||||||
|
return m.Update(func(txn *badger.Txn) error { |
||||||
|
key := m.getRelayConfigKey() |
||||||
|
data, err := json.Marshal(config) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return txn.Set(key, data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// GetRelayConfig returns relay configuration
|
||||||
|
func (m *ManagedACL) GetRelayConfig() (ManagedACLConfig, error) { |
||||||
|
var config ManagedACLConfig |
||||||
|
return config, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getRelayConfigKey() |
||||||
|
item, err := txn.Get(key) |
||||||
|
if err != nil { |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
// Return default config
|
||||||
|
config = ManagedACLConfig{ |
||||||
|
RelayName: "Managed Relay", |
||||||
|
RelayDescription: "A managed Nostr relay", |
||||||
|
RelayIcon: "", |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
val, err := item.ValueCopy(nil) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return json.Unmarshal(val, &config) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Check if a pubkey is banned
|
||||||
|
func (m *ManagedACL) IsPubkeyBanned(pubkey string) (bool, error) { |
||||||
|
var banned bool |
||||||
|
return banned, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getBannedPubkeyKey(pubkey) |
||||||
|
_, err := txn.Get(key) |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
banned = false |
||||||
|
return nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
banned = true |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Check if a pubkey is explicitly allowed
|
||||||
|
func (m *ManagedACL) IsPubkeyAllowed(pubkey string) (bool, error) { |
||||||
|
var allowed bool |
||||||
|
return allowed, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedPubkeyKey(pubkey) |
||||||
|
_, err := txn.Get(key) |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
allowed = false |
||||||
|
return nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
allowed = true |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Check if an event is banned
|
||||||
|
func (m *ManagedACL) IsEventBanned(eventID string) (bool, error) { |
||||||
|
var banned bool |
||||||
|
return banned, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getBannedEventKey(eventID) |
||||||
|
_, err := txn.Get(key) |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
banned = false |
||||||
|
return nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
banned = true |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Check if an event is explicitly allowed
|
||||||
|
func (m *ManagedACL) IsEventAllowed(eventID string) (bool, error) { |
||||||
|
var allowed bool |
||||||
|
return allowed, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedEventKey(eventID) |
||||||
|
_, err := txn.Get(key) |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
allowed = false |
||||||
|
return nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
allowed = true |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Check if an IP is blocked
|
||||||
|
func (m *ManagedACL) IsIPBlocked(ip string) (bool, error) { |
||||||
|
var blocked bool |
||||||
|
return blocked, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getBlockedIPKey(ip) |
||||||
|
_, err := txn.Get(key) |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
blocked = false |
||||||
|
return nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
blocked = true |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Check if a kind is allowed
|
||||||
|
func (m *ManagedACL) IsKindAllowed(kind int) (bool, error) { |
||||||
|
var allowed bool |
||||||
|
return allowed, m.View(func(txn *badger.Txn) error { |
||||||
|
key := m.getAllowedKindKey(kind) |
||||||
|
_, err := txn.Get(key) |
||||||
|
if err == badger.ErrKeyNotFound { |
||||||
|
allowed = false |
||||||
|
return nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
allowed = true |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Key generation methods
|
||||||
|
func (m *ManagedACL) getBannedPubkeyKey(pubkey string) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_BANNED_PUBKEY_") |
||||||
|
buf.WriteString(pubkey) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getBannedPubkeyPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_BANNED_PUBKEY_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getAllowedPubkeyKey(pubkey string) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_ALLOWED_PUBKEY_") |
||||||
|
buf.WriteString(pubkey) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getAllowedPubkeyPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_ALLOWED_PUBKEY_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getBannedEventKey(eventID string) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_BANNED_EVENT_") |
||||||
|
buf.WriteString(eventID) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getBannedEventPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_BANNED_EVENT_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getAllowedEventKey(eventID string) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_ALLOWED_EVENT_") |
||||||
|
buf.WriteString(eventID) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getAllowedEventPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_ALLOWED_EVENT_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getBlockedIPKey(ip string) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_BLOCKED_IP_") |
||||||
|
buf.WriteString(ip) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getBlockedIPPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_BLOCKED_IP_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getAllowedKindKey(kind int) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_ALLOWED_KIND_") |
||||||
|
buf.WriteString(fmt.Sprintf("%d", kind)) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getAllowedKindPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_ALLOWED_KIND_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getEventNeedingModerationKey(eventID string) []byte { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
buf.WriteString("MANAGED_ACL_MODERATION_") |
||||||
|
buf.WriteString(eventID) |
||||||
|
return buf.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getEventNeedingModerationPrefix() []byte { |
||||||
|
return []byte("MANAGED_ACL_MODERATION_") |
||||||
|
} |
||||||
|
|
||||||
|
func (m *ManagedACL) getRelayConfigKey() []byte { |
||||||
|
return []byte("MANAGED_ACL_RELAY_CONFIG") |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Test script for Managed ACL functionality |
||||||
|
# This script runs all the managed ACL tests to ensure policy enforcement works correctly |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Running Managed ACL Tests" |
||||||
|
echo "==============================" |
||||||
|
|
||||||
|
# Change to the project root |
||||||
|
cd "$(dirname "$0")" |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "📋 Test Categories:" |
||||||
|
echo "1. Managed ACL Policy Tests (pkg/acl/managed_minimal_test.go)" |
||||||
|
echo "2. HTTP API Tests (app/handle-nip86_minimal_test.go)" |
||||||
|
echo "" |
||||||
|
|
||||||
|
# Run managed ACL policy tests |
||||||
|
echo "🔒 Running Managed ACL Policy Tests..." |
||||||
|
go test -v ./pkg/acl -run TestManagedACL_BasicFunctionality |
||||||
|
if [ $? -eq 0 ]; then |
||||||
|
echo "✅ Managed ACL Policy Tests PASSED" |
||||||
|
else |
||||||
|
echo "❌ Managed ACL Policy Tests FAILED" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "" |
||||||
|
|
||||||
|
# Run HTTP API tests |
||||||
|
echo "🌐 Running HTTP API Tests..." |
||||||
|
go test -v ./app -run TestHandleNIP86Management_Basic |
||||||
|
if [ $? -eq 0 ]; then |
||||||
|
echo "✅ HTTP API Tests PASSED" |
||||||
|
else |
||||||
|
echo "❌ HTTP API Tests FAILED" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "🎉 All Managed ACL Tests PASSED!" |
||||||
|
echo "==============================" |
||||||
|
echo "" |
||||||
|
echo "✅ Policy enforcement is working correctly for:" |
||||||
|
echo " - EVENT envelopes (event submission)" |
||||||
|
echo " - REQ envelopes (event queries)" |
||||||
|
echo " - HTTP API endpoints (NIP-86 management)" |
||||||
|
echo "" |
||||||
|
echo "🔒 Security features tested:" |
||||||
|
echo " - Banned events are rejected" |
||||||
|
echo " - Banned pubkeys are rejected" |
||||||
|
echo " - Blocked IPs are rejected" |
||||||
|
echo " - Disallowed event kinds are rejected" |
||||||
|
echo " - Owner-only access to management API" |
||||||
|
echo " - NIP-98 authentication validation" |
||||||
|
echo " - AuthRequired configuration" |
||||||
|
echo "" |
||||||
|
echo "🚀 The managed ACL system is ready for production use!" |
||||||
Loading…
Reference in new issue