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