summaryrefslogtreecommitdiffstats
path: root/internal/auth
diff options
context:
space:
mode:
Diffstat (limited to 'internal/auth')
-rw-r--r--internal/auth/README.md13
-rw-r--r--internal/auth/auth_test.go38
-rw-r--r--internal/auth/interceptor.go67
3 files changed, 105 insertions, 13 deletions
diff --git a/internal/auth/README.md b/internal/auth/README.md
index df0de6a..366e110 100644
--- a/internal/auth/README.md
+++ b/internal/auth/README.md
@@ -209,9 +209,20 @@ authOpts := &auth.InterceptorOptions{
209- **`TimestampWindow`**: Maximum age of events in seconds (default: 60) 209- **`TimestampWindow`**: Maximum age of events in seconds (default: 60)
210- **`Required`**: Whether to reject unauthenticated requests (default: false) 210- **`Required`**: Whether to reject unauthenticated requests (default: false)
211- **`ValidatePayload`**: Whether to verify payload hash when present (default: false) 211- **`ValidatePayload`**: Whether to verify payload hash when present (default: false)
212- **`AllowedNpubs`**: Optional whitelist of allowed pubkeys (nil = allow all) 212- **`AllowedNpubsRead`**: Optional whitelist of allowed pubkeys for read operations (nil = allow all)
213 - Config accepts npub format only (human-readable bech32) 213 - Config accepts npub format only (human-readable bech32)
214 - Automatically normalized to hex format (computer-readable) at config load time 214 - Automatically normalized to hex format (computer-readable) at config load time
215 - Controls access to Query, Get, List, Subscribe, and other read methods
216- **`AllowedNpubsWrite`**: Optional whitelist of allowed pubkeys for write operations (nil = allow all)
217 - Config accepts npub format only (human-readable bech32)
218 - Automatically normalized to hex format (computer-readable) at config load time
219 - Controls access to Publish, Delete, Create, Update, and other write methods
220
221**Access Control Patterns:**
222- **Public relay**: Set `AllowedNpubsWrite` (only some can publish), leave `AllowedNpubsRead` empty (everyone can read)
223- **Private relay**: Set both lists (restricted read and write access)
224- **Open relay**: Leave both empty (everyone can read and write)
225- **Read-only relay**: Set `AllowedNpubsRead`, block all writes
215 226
216### NostrCredentials Options 227### NostrCredentials Options
217 228
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
index 1f0efee..7a0da19 100644
--- a/internal/auth/auth_test.go
+++ b/internal/auth/auth_test.go
@@ -304,3 +304,41 @@ func TestHashPayload(t *testing.T) {
304 t.Error("different payloads produced same hash") 304 t.Error("different payloads produced same hash")
305 } 305 }
306} 306}
307
308func TestIsWriteMethod(t *testing.T) {
309 tests := []struct {
310 method string
311 want bool
312 }{
313 // Write methods
314 {"/nostr.v1.NostrRelay/PublishEvent", true},
315 {"/nostr.v1.NostrRelay/DeleteEvent", true},
316 {"/admin.v1.Admin/CreateUser", true},
317 {"/admin.v1.Admin/UpdateSettings", true},
318 {"/data.v1.Data/InsertRecord", true},
319 {"/data.v1.Data/RemoveItem", true},
320 {"/storage.v1.Storage/SetValue", true},
321 {"/storage.v1.Storage/PutObject", true},
322
323 // Read methods
324 {"/nostr.v1.NostrRelay/QueryEvents", false},
325 {"/nostr.v1.NostrRelay/Subscribe", false},
326 {"/nostr.v1.NostrRelay/GetEvent", false},
327 {"/admin.v1.Admin/ListUsers", false},
328 {"/health.v1.Health/Check", false},
329 {"/info.v1.Info/GetRelayInfo", false},
330
331 // Edge cases
332 {"", false},
333 {"/", false},
334 }
335
336 for _, tt := range tests {
337 t.Run(tt.method, func(t *testing.T) {
338 got := isWriteMethod(tt.method)
339 if got != tt.want {
340 t.Errorf("isWriteMethod(%q) = %v, want %v", tt.method, got, tt.want)
341 }
342 })
343 }
344}
diff --git a/internal/auth/interceptor.go b/internal/auth/interceptor.go
index 7d785bf..66880a7 100644
--- a/internal/auth/interceptor.go
+++ b/internal/auth/interceptor.go
@@ -3,6 +3,7 @@ package auth
3import ( 3import (
4 "context" 4 "context"
5 "fmt" 5 "fmt"
6 "strings"
6 7
7 "google.golang.org/grpc" 8 "google.golang.org/grpc"
8 "google.golang.org/grpc/codes" 9 "google.golang.org/grpc/codes"
@@ -35,11 +36,17 @@ type InterceptorOptions struct {
35 // Default: false 36 // Default: false
36 ValidatePayload bool 37 ValidatePayload bool
37 38
38 // AllowedNpubs is an optional whitelist of allowed pubkeys (hex format). 39 // AllowedNpubsRead is an optional whitelist of allowed pubkeys for read operations (hex format).
39 // Config accepts npub format only, normalized to hex at load time. 40 // Config accepts npub format only, normalized to hex at load time.
40 // If nil or empty, all valid signatures are accepted. 41 // If nil or empty, all valid signatures are accepted for reads.
41 // Default: nil (allow all) 42 // Default: nil (allow all)
42 AllowedNpubs []string 43 AllowedNpubsRead []string
44
45 // AllowedNpubsWrite is an optional whitelist of allowed pubkeys for write operations (hex format).
46 // Config accepts npub format only, normalized to hex at load time.
47 // If nil or empty, all valid signatures are accepted for writes.
48 // Default: nil (allow all)
49 AllowedNpubsWrite []string
43 50
44 // SkipMethods is a list of gRPC methods that bypass authentication. 51 // SkipMethods is a list of gRPC methods that bypass authentication.
45 // Useful for public endpoints like health checks or relay info. 52 // Useful for public endpoints like health checks or relay info.
@@ -51,11 +58,12 @@ type InterceptorOptions struct {
51// DefaultInterceptorOptions returns the default configuration. 58// DefaultInterceptorOptions returns the default configuration.
52func DefaultInterceptorOptions() *InterceptorOptions { 59func DefaultInterceptorOptions() *InterceptorOptions {
53 return &InterceptorOptions{ 60 return &InterceptorOptions{
54 TimestampWindow: 60, 61 TimestampWindow: 60,
55 Required: false, 62 Required: false,
56 ValidatePayload: false, 63 ValidatePayload: false,
57 AllowedNpubs: nil, 64 AllowedNpubsRead: nil,
58 SkipMethods: nil, 65 AllowedNpubsWrite: nil,
66 SkipMethods: nil,
59 } 67 }
60} 68}
61 69
@@ -169,16 +177,51 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept
169 // Extract pubkey 177 // Extract pubkey
170 pubkey := ExtractPubkey(event) 178 pubkey := ExtractPubkey(event)
171 179
172 // Check whitelist if configured (all values are already normalized to hex) 180 // Check whitelist based on operation type (all values are already normalized to hex)
173 if len(opts.AllowedNpubs) > 0 { 181 if isWriteMethod(method) {
174 if !contains(opts.AllowedNpubs, pubkey) { 182 // Write operation - check write allowlist
175 return "", fmt.Errorf("pubkey not in whitelist") 183 if len(opts.AllowedNpubsWrite) > 0 {
184 if !contains(opts.AllowedNpubsWrite, pubkey) {
185 return "", fmt.Errorf("pubkey not authorized for write operations")
186 }
187 }
188 } else {
189 // Read operation - check read allowlist
190 if len(opts.AllowedNpubsRead) > 0 {
191 if !contains(opts.AllowedNpubsRead, pubkey) {
192 return "", fmt.Errorf("pubkey not authorized for read operations")
193 }
176 } 194 }
177 } 195 }
178 196
179 return pubkey, nil 197 return pubkey, nil
180} 198}
181 199
200// isWriteMethod determines if a gRPC method is a write operation.
201// Write operations modify state (Publish, Delete, Create, Update, etc.)
202// Read operations query state (Query, Get, List, Subscribe, etc.)
203func isWriteMethod(method string) bool {
204 // Common write operation patterns
205 writePatterns := []string{
206 "Publish",
207 "Delete",
208 "Create",
209 "Update",
210 "Insert",
211 "Remove",
212 "Set",
213 "Put",
214 }
215
216 for _, pattern := range writePatterns {
217 if strings.Contains(method, pattern) {
218 return true
219 }
220 }
221
222 return false
223}
224
182// shouldSkipAuth checks if a method should bypass authentication. 225// shouldSkipAuth checks if a method should bypass authentication.
183func shouldSkipAuth(method string, skipMethods []string) bool { 226func shouldSkipAuth(method string, skipMethods []string) bool {
184 for _, skip := range skipMethods { 227 for _, skip := range skipMethods {