summaryrefslogtreecommitdiffstats
path: root/internal/auth/interceptor.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 10:02:52 -0800
committerbndw <ben@bdw.to>2026-02-14 10:02:52 -0800
commit5d21632ea70e1c7de7becb7ab6227b06b1535a83 (patch)
treee63bcbe8cdf7dc888ca0e3476ad529690a0a44a8 /internal/auth/interceptor.go
parentd30459513ec44ab298fafd1bfe0edc08d6ab62e4 (diff)
feat: add separate read/write allowlists for granular access control
- Split allowed_npubs into allowed_npubs_read and allowed_npubs_write - Write operations: Publish, Delete, Create, Update, Insert, Remove, Set, Put - Read operations: everything else (Query, Subscribe, Get, List, etc.) - Auth interceptor checks appropriate list based on method type - Enables common patterns: - Public relay: only some can write, everyone can read - Private relay: restricted read and write - Open relay: everyone can read and write - Updated config, docs, and comprehensive tests Use cases: "only some can write, everyone can read"
Diffstat (limited to 'internal/auth/interceptor.go')
-rw-r--r--internal/auth/interceptor.go67
1 files changed, 55 insertions, 12 deletions
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 {