From 5d21632ea70e1c7de7becb7ab6227b06b1535a83 Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 14 Feb 2026 10:02:52 -0800 Subject: 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" --- internal/auth/interceptor.go | 67 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 12 deletions(-) (limited to 'internal/auth/interceptor.go') 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 import ( "context" "fmt" + "strings" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -35,11 +36,17 @@ type InterceptorOptions struct { // Default: false ValidatePayload bool - // AllowedNpubs is an optional whitelist of allowed pubkeys (hex format). + // AllowedNpubsRead is an optional whitelist of allowed pubkeys for read operations (hex format). // Config accepts npub format only, normalized to hex at load time. - // If nil or empty, all valid signatures are accepted. + // If nil or empty, all valid signatures are accepted for reads. // Default: nil (allow all) - AllowedNpubs []string + AllowedNpubsRead []string + + // AllowedNpubsWrite is an optional whitelist of allowed pubkeys for write operations (hex format). + // Config accepts npub format only, normalized to hex at load time. + // If nil or empty, all valid signatures are accepted for writes. + // Default: nil (allow all) + AllowedNpubsWrite []string // SkipMethods is a list of gRPC methods that bypass authentication. // Useful for public endpoints like health checks or relay info. @@ -51,11 +58,12 @@ type InterceptorOptions struct { // DefaultInterceptorOptions returns the default configuration. func DefaultInterceptorOptions() *InterceptorOptions { return &InterceptorOptions{ - TimestampWindow: 60, - Required: false, - ValidatePayload: false, - AllowedNpubs: nil, - SkipMethods: nil, + TimestampWindow: 60, + Required: false, + ValidatePayload: false, + AllowedNpubsRead: nil, + AllowedNpubsWrite: nil, + SkipMethods: nil, } } @@ -169,16 +177,51 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept // Extract pubkey pubkey := ExtractPubkey(event) - // Check whitelist if configured (all values are already normalized to hex) - if len(opts.AllowedNpubs) > 0 { - if !contains(opts.AllowedNpubs, pubkey) { - return "", fmt.Errorf("pubkey not in whitelist") + // Check whitelist based on operation type (all values are already normalized to hex) + if isWriteMethod(method) { + // Write operation - check write allowlist + if len(opts.AllowedNpubsWrite) > 0 { + if !contains(opts.AllowedNpubsWrite, pubkey) { + return "", fmt.Errorf("pubkey not authorized for write operations") + } + } + } else { + // Read operation - check read allowlist + if len(opts.AllowedNpubsRead) > 0 { + if !contains(opts.AllowedNpubsRead, pubkey) { + return "", fmt.Errorf("pubkey not authorized for read operations") + } } } return pubkey, nil } +// isWriteMethod determines if a gRPC method is a write operation. +// Write operations modify state (Publish, Delete, Create, Update, etc.) +// Read operations query state (Query, Get, List, Subscribe, etc.) +func isWriteMethod(method string) bool { + // Common write operation patterns + writePatterns := []string{ + "Publish", + "Delete", + "Create", + "Update", + "Insert", + "Remove", + "Set", + "Put", + } + + for _, pattern := range writePatterns { + if strings.Contains(method, pattern) { + return true + } + } + + return false +} + // shouldSkipAuth checks if a method should bypass authentication. func shouldSkipAuth(method string, skipMethods []string) bool { for _, skip := range skipMethods { -- cgit v1.2.3