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.
236 lines
6.3 KiB
236 lines
6.3 KiB
// Package authorization provides event authorization services for the ORLY relay. |
|
// It handles ACL checks, policy evaluation, and access level decisions. |
|
package authorization |
|
|
|
import ( |
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
) |
|
|
|
// Decision carries authorization context through the event processing pipeline. |
|
type Decision struct { |
|
Allowed bool |
|
AccessLevel string // none/read/write/admin/owner/blocked/banned |
|
IsAdmin bool |
|
IsOwner bool |
|
IsPeerRelay bool |
|
SkipACLCheck bool // For admin/owner deletes |
|
DenyReason string // Human-readable reason for denial |
|
RequireAuth bool // Should send AUTH challenge |
|
} |
|
|
|
// Allow returns an allowed decision with the given access level. |
|
func Allow(accessLevel string) Decision { |
|
return Decision{ |
|
Allowed: true, |
|
AccessLevel: accessLevel, |
|
} |
|
} |
|
|
|
// Deny returns a denied decision with the given reason. |
|
func Deny(reason string, requireAuth bool) Decision { |
|
return Decision{ |
|
Allowed: false, |
|
DenyReason: reason, |
|
RequireAuth: requireAuth, |
|
} |
|
} |
|
|
|
// Authorizer makes authorization decisions for events. |
|
type Authorizer interface { |
|
// Authorize checks if event is allowed based on ACL and policy. |
|
Authorize(ev *event.E, authedPubkey []byte, remote string, eventKind uint16) Decision |
|
} |
|
|
|
// ACLRegistry abstracts the ACL registry for authorization checks. |
|
type ACLRegistry interface { |
|
// GetAccessLevel returns the access level for a pubkey and remote address. |
|
GetAccessLevel(pub []byte, address string) string |
|
// CheckPolicy checks if an event passes ACL policy. |
|
CheckPolicy(ev *event.E) (bool, error) |
|
// Active returns the active ACL mode name. |
|
Active() string |
|
} |
|
|
|
// PolicyManager abstracts the policy manager for authorization checks. |
|
type PolicyManager interface { |
|
// IsEnabled returns whether policy is enabled. |
|
IsEnabled() bool |
|
// CheckPolicy checks if an action is allowed by policy. |
|
CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error) |
|
} |
|
|
|
// SyncManager abstracts the sync manager for peer relay checking. |
|
type SyncManager interface { |
|
// GetPeers returns the list of peer relay URLs. |
|
GetPeers() []string |
|
// IsAuthorizedPeer checks if a pubkey is an authorized peer. |
|
IsAuthorizedPeer(url, pubkey string) bool |
|
} |
|
|
|
// Config holds configuration for the authorization service. |
|
type Config struct { |
|
AuthRequired bool // Whether auth is required for all operations |
|
AuthToWrite bool // Whether auth is required for write operations |
|
Admins [][]byte // Admin pubkeys |
|
Owners [][]byte // Owner pubkeys |
|
} |
|
|
|
// Service implements the Authorizer interface. |
|
type Service struct { |
|
cfg *Config |
|
acl ACLRegistry |
|
policy PolicyManager |
|
sync SyncManager |
|
} |
|
|
|
// New creates a new authorization service. |
|
func New(cfg *Config, acl ACLRegistry, policy PolicyManager, sync SyncManager) *Service { |
|
return &Service{ |
|
cfg: cfg, |
|
acl: acl, |
|
policy: policy, |
|
sync: sync, |
|
} |
|
} |
|
|
|
// Authorize checks if event is allowed based on ACL and policy. |
|
func (s *Service) Authorize(ev *event.E, authedPubkey []byte, remote string, eventKind uint16) Decision { |
|
// Check if peer relay - they get special treatment |
|
if s.isPeerRelayPubkey(authedPubkey) { |
|
return Decision{ |
|
Allowed: true, |
|
AccessLevel: "admin", |
|
IsPeerRelay: true, |
|
} |
|
} |
|
|
|
// Check policy if enabled |
|
if s.policy != nil && s.policy.IsEnabled() { |
|
allowed, err := s.policy.CheckPolicy("write", ev, authedPubkey, remote) |
|
if err != nil { |
|
return Deny("policy check failed", false) |
|
} |
|
if !allowed { |
|
return Deny("event blocked by policy", false) |
|
} |
|
|
|
// Check ACL policy for managed ACL mode |
|
if s.acl != nil && s.acl.Active() == "managed" { |
|
allowed, err := s.acl.CheckPolicy(ev) |
|
if err != nil { |
|
return Deny("ACL policy check failed", false) |
|
} |
|
if !allowed { |
|
return Deny("event blocked by ACL policy", false) |
|
} |
|
} |
|
} |
|
|
|
// Determine pubkey for ACL check |
|
pubkeyForACL := authedPubkey |
|
if len(authedPubkey) == 0 && s.acl != nil && s.acl.Active() == "none" && |
|
!s.cfg.AuthRequired && !s.cfg.AuthToWrite { |
|
pubkeyForACL = ev.Pubkey |
|
} |
|
|
|
// Check if auth is required but user not authenticated |
|
if (s.cfg.AuthRequired || s.cfg.AuthToWrite) && len(authedPubkey) == 0 { |
|
return Deny("authentication required for write operations", true) |
|
} |
|
|
|
// Get access level |
|
accessLevel := "write" // Default for none mode |
|
if s.acl != nil { |
|
accessLevel = s.acl.GetAccessLevel(pubkeyForACL, remote) |
|
} |
|
|
|
// Check if admin/owner for delete events (skip ACL check) |
|
isAdmin := s.isAdmin(ev.Pubkey) |
|
isOwner := s.isOwner(ev.Pubkey) |
|
skipACL := (isAdmin || isOwner) && eventKind == 5 // kind 5 = deletion |
|
|
|
decision := Decision{ |
|
AccessLevel: accessLevel, |
|
IsAdmin: isAdmin, |
|
IsOwner: isOwner, |
|
SkipACLCheck: skipACL, |
|
} |
|
|
|
// Handle access levels |
|
if !skipACL { |
|
switch accessLevel { |
|
case "none": |
|
decision.Allowed = false |
|
decision.DenyReason = "auth required for write access" |
|
decision.RequireAuth = true |
|
case "read": |
|
decision.Allowed = false |
|
decision.DenyReason = "auth required for write access" |
|
decision.RequireAuth = true |
|
case "blocked": |
|
decision.Allowed = false |
|
decision.DenyReason = "IP address blocked" |
|
case "banned": |
|
decision.Allowed = false |
|
decision.DenyReason = "pubkey banned" |
|
default: |
|
// write/admin/owner - allowed |
|
decision.Allowed = true |
|
} |
|
} else { |
|
decision.Allowed = true |
|
} |
|
|
|
return decision |
|
} |
|
|
|
// isPeerRelayPubkey checks if the given pubkey belongs to a peer relay. |
|
func (s *Service) isPeerRelayPubkey(pubkey []byte) bool { |
|
if s.sync == nil || len(pubkey) == 0 { |
|
return false |
|
} |
|
|
|
peerPubkeyHex := hex.Enc(pubkey) |
|
|
|
for _, peerURL := range s.sync.GetPeers() { |
|
if s.sync.IsAuthorizedPeer(peerURL, peerPubkeyHex) { |
|
return true |
|
} |
|
} |
|
|
|
return false |
|
} |
|
|
|
// isAdmin checks if a pubkey is an admin. |
|
func (s *Service) isAdmin(pubkey []byte) bool { |
|
for _, admin := range s.cfg.Admins { |
|
if fastEqual(admin, pubkey) { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
// isOwner checks if a pubkey is an owner. |
|
func (s *Service) isOwner(pubkey []byte) bool { |
|
for _, owner := range s.cfg.Owners { |
|
if fastEqual(owner, pubkey) { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
// fastEqual compares two byte slices for equality. |
|
func fastEqual(a, b []byte) bool { |
|
if len(a) != len(b) { |
|
return false |
|
} |
|
for i := range a { |
|
if a[i] != b[i] { |
|
return false |
|
} |
|
} |
|
return true |
|
}
|
|
|