authorization.go raw

   1  // Package authorization provides event authorization services for the ORLY relay.
   2  // It handles ACL checks, policy evaluation, and access level decisions.
   3  package authorization
   4  
   5  import (
   6  	"next.orly.dev/pkg/nostr/encoders/event"
   7  	"next.orly.dev/pkg/nostr/encoders/hex"
   8  )
   9  
  10  // Decision carries authorization context through the event processing pipeline.
  11  type Decision struct {
  12  	Allowed      bool
  13  	AccessLevel  string // none/read/write/admin/owner/blocked/banned
  14  	IsAdmin      bool
  15  	IsOwner      bool
  16  	IsPeerRelay  bool
  17  	SkipACLCheck bool   // For admin/owner deletes
  18  	DenyReason   string // Human-readable reason for denial
  19  	RequireAuth  bool   // Should send AUTH challenge
  20  }
  21  
  22  // Allow returns an allowed decision with the given access level.
  23  func Allow(accessLevel string) Decision {
  24  	return Decision{
  25  		Allowed:     true,
  26  		AccessLevel: accessLevel,
  27  	}
  28  }
  29  
  30  // Deny returns a denied decision with the given reason.
  31  func Deny(reason string, requireAuth bool) Decision {
  32  	return Decision{
  33  		Allowed:     false,
  34  		DenyReason:  reason,
  35  		RequireAuth: requireAuth,
  36  	}
  37  }
  38  
  39  // Authorizer makes authorization decisions for events.
  40  type Authorizer interface {
  41  	// Authorize checks if event is allowed based on ACL and policy.
  42  	Authorize(ev *event.E, authedPubkey []byte, remote string, eventKind uint16) Decision
  43  }
  44  
  45  // ACLRegistry abstracts the ACL registry for authorization checks.
  46  type ACLRegistry interface {
  47  	// GetAccessLevel returns the access level for a pubkey and remote address.
  48  	GetAccessLevel(pub []byte, address string) string
  49  	// CheckPolicy checks if an event passes ACL policy.
  50  	CheckPolicy(ev *event.E) (bool, error)
  51  	// Active returns the active ACL mode name.
  52  	Active() string
  53  }
  54  
  55  // PolicyManager abstracts the policy manager for authorization checks.
  56  type PolicyManager interface {
  57  	// IsEnabled returns whether policy is enabled.
  58  	IsEnabled() bool
  59  	// CheckPolicy checks if an action is allowed by policy.
  60  	CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error)
  61  }
  62  
  63  // SyncManager abstracts the sync manager for peer relay checking.
  64  type SyncManager interface {
  65  	// GetPeers returns the list of peer relay URLs.
  66  	GetPeers() []string
  67  	// IsAuthorizedPeer checks if a pubkey is an authorized peer.
  68  	IsAuthorizedPeer(url, pubkey string) bool
  69  }
  70  
  71  // Config holds configuration for the authorization service.
  72  type Config struct {
  73  	AuthRequired    bool     // Whether auth is required for all operations
  74  	AuthToWrite     bool     // Whether auth is required for write operations
  75  	NIP46BypassAuth bool     // Allow NIP-46 events through without auth
  76  	Admins          [][]byte // Admin pubkeys
  77  	Owners          [][]byte // Owner pubkeys
  78  }
  79  
  80  // Service implements the Authorizer interface.
  81  type Service struct {
  82  	cfg         *Config
  83  	acl         ACLRegistry
  84  	policy      PolicyManager
  85  	sync        SyncManager
  86  }
  87  
  88  // New creates a new authorization service.
  89  func New(cfg *Config, acl ACLRegistry, policy PolicyManager, sync SyncManager) *Service {
  90  	return &Service{
  91  		cfg:    cfg,
  92  		acl:    acl,
  93  		policy: policy,
  94  		sync:   sync,
  95  	}
  96  }
  97  
  98  // Authorize checks if event is allowed based on ACL and policy.
  99  func (s *Service) Authorize(ev *event.E, authedPubkey []byte, remote string, eventKind uint16) Decision {
 100  	// Check if peer relay - they get special treatment
 101  	if s.isPeerRelayPubkey(authedPubkey) {
 102  		return Decision{
 103  			Allowed:     true,
 104  			AccessLevel: "admin",
 105  			IsPeerRelay: true,
 106  		}
 107  	}
 108  
 109  	// Check policy if enabled
 110  	if s.policy != nil && s.policy.IsEnabled() {
 111  		allowed, err := s.policy.CheckPolicy("write", ev, authedPubkey, remote)
 112  		if err != nil {
 113  			return Deny("policy check failed", false)
 114  		}
 115  		if !allowed {
 116  			return Deny("event blocked by policy", false)
 117  		}
 118  
 119  		// Check ACL policy for managed ACL mode
 120  		if s.acl != nil && s.acl.Active() == "managed" {
 121  			allowed, err := s.acl.CheckPolicy(ev)
 122  			if err != nil {
 123  				return Deny("ACL policy check failed", false)
 124  			}
 125  			if !allowed {
 126  				return Deny("event blocked by ACL policy", false)
 127  			}
 128  		}
 129  	}
 130  
 131  	// Determine pubkey for ACL check
 132  	pubkeyForACL := authedPubkey
 133  	if len(authedPubkey) == 0 && s.acl != nil && s.acl.Active() == "none" &&
 134  		!s.cfg.AuthRequired && !s.cfg.AuthToWrite {
 135  		pubkeyForACL = ev.Pubkey
 136  	}
 137  
 138  	// Check if auth is required but user not authenticated
 139  	// NIP-46 bunker events (kind 24133) can bypass auth if configured
 140  	const kindNIP46 = 24133
 141  	if (s.cfg.AuthRequired || s.cfg.AuthToWrite) && len(authedPubkey) == 0 {
 142  		if !(s.cfg.NIP46BypassAuth && eventKind == kindNIP46) {
 143  			return Deny("authentication required for write operations", true)
 144  		}
 145  	}
 146  
 147  	// Get access level
 148  	accessLevel := "write" // Default for none mode
 149  	if s.acl != nil {
 150  		accessLevel = s.acl.GetAccessLevel(pubkeyForACL, remote)
 151  	}
 152  
 153  	// Check if admin/owner for delete events (skip ACL check)
 154  	isAdmin := s.isAdmin(ev.Pubkey)
 155  	isOwner := s.isOwner(ev.Pubkey)
 156  	skipACL := (isAdmin || isOwner) && eventKind == 5 // kind 5 = deletion
 157  
 158  	decision := Decision{
 159  		AccessLevel:  accessLevel,
 160  		IsAdmin:      isAdmin,
 161  		IsOwner:      isOwner,
 162  		SkipACLCheck: skipACL,
 163  	}
 164  
 165  	// Handle access levels
 166  	if !skipACL {
 167  		switch accessLevel {
 168  		case "none":
 169  			decision.Allowed = false
 170  			decision.DenyReason = "auth required for write access"
 171  			decision.RequireAuth = true
 172  		case "read":
 173  			decision.Allowed = false
 174  			decision.DenyReason = "auth required for write access"
 175  			decision.RequireAuth = true
 176  		case "blocked":
 177  			decision.Allowed = false
 178  			decision.DenyReason = "IP address blocked"
 179  		case "banned":
 180  			decision.Allowed = false
 181  			decision.DenyReason = "pubkey banned"
 182  		default:
 183  			// write/admin/owner - allowed
 184  			decision.Allowed = true
 185  		}
 186  	} else {
 187  		decision.Allowed = true
 188  	}
 189  
 190  	return decision
 191  }
 192  
 193  // isPeerRelayPubkey checks if the given pubkey belongs to a peer relay.
 194  func (s *Service) isPeerRelayPubkey(pubkey []byte) bool {
 195  	if s.sync == nil || len(pubkey) == 0 {
 196  		return false
 197  	}
 198  
 199  	peerPubkeyHex := hex.Enc(pubkey)
 200  
 201  	for _, peerURL := range s.sync.GetPeers() {
 202  		if s.sync.IsAuthorizedPeer(peerURL, peerPubkeyHex) {
 203  			return true
 204  		}
 205  	}
 206  
 207  	return false
 208  }
 209  
 210  // isAdmin checks if a pubkey is an admin.
 211  func (s *Service) isAdmin(pubkey []byte) bool {
 212  	for _, admin := range s.cfg.Admins {
 213  		if fastEqual(admin, pubkey) {
 214  			return true
 215  		}
 216  	}
 217  	return false
 218  }
 219  
 220  // isOwner checks if a pubkey is an owner.
 221  func (s *Service) isOwner(pubkey []byte) bool {
 222  	for _, owner := range s.cfg.Owners {
 223  		if fastEqual(owner, pubkey) {
 224  			return true
 225  		}
 226  	}
 227  	return false
 228  }
 229  
 230  // fastEqual compares two byte slices for equality.
 231  func fastEqual(a, b []byte) bool {
 232  	if len(a) != len(b) {
 233  		return false
 234  	}
 235  	for i := range a {
 236  		if a[i] != b[i] {
 237  			return false
 238  		}
 239  	}
 240  	return true
 241  }
 242