summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--internal/auth/README.md37
-rw-r--r--internal/auth/auth_test.go9
-rw-r--r--internal/auth/interceptor.go118
-rw-r--r--internal/config/README.md70
-rw-r--r--internal/config/config.go57
-rw-r--r--internal/config/config_test.go102
6 files changed, 242 insertions, 151 deletions
diff --git a/internal/auth/README.md b/internal/auth/README.md
index 366e110..de37010 100644
--- a/internal/auth/README.md
+++ b/internal/auth/README.md
@@ -142,8 +142,15 @@ import (
142 142
143// Create auth options 143// Create auth options
144authOpts := &auth.InterceptorOptions{ 144authOpts := &auth.InterceptorOptions{
145 Read: auth.OperationAuthConfig{
146 Enabled: true, // Require auth for reads
147 AllowedNpubs: nil, // Accept any valid signature
148 },
149 Write: auth.OperationAuthConfig{
150 Enabled: true,
151 AllowedNpubs: []string{"hex-pubkey-1", "hex-pubkey-2"}, // Whitelist
152 },
145 TimestampWindow: 60, // Accept events within 60 seconds 153 TimestampWindow: 60, // Accept events within 60 seconds
146 Required: true, // Reject unauthenticated requests
147} 154}
148 155
149// Create gRPC server with interceptors 156// Create gRPC server with interceptors
@@ -206,23 +213,25 @@ authOpts := &auth.InterceptorOptions{
206 213
207### InterceptorOptions 214### InterceptorOptions
208 215
216- **`Read`**: Authentication config for read operations (Subscribe, QueryEvents, CountEvents)
217 - **`Enabled`**: false = no auth (allow all), true = auth required
218 - **`AllowedNpubs`**: Optional whitelist (hex format, normalized from npub in config)
219 - If `Enabled=false`: no auth required
220 - If `Enabled=true && AllowedNpubs=[]`: auth required, any valid signature accepted
221 - If `Enabled=true && AllowedNpubs=[...]`: auth required, only whitelisted npubs accepted
222
223- **`Write`**: Authentication config for write operations (PublishEvent, PublishBatch)
224 - Same structure as `Read`
225
209- **`TimestampWindow`**: Maximum age of events in seconds (default: 60) 226- **`TimestampWindow`**: Maximum age of events in seconds (default: 60)
210- **`Required`**: Whether to reject unauthenticated requests (default: false)
211- **`ValidatePayload`**: Whether to verify payload hash when present (default: false) 227- **`ValidatePayload`**: Whether to verify payload hash when present (default: false)
212- **`AllowedNpubsRead`**: Optional whitelist of allowed pubkeys for read operations (nil = allow all) 228- **`SkipMethods`**: List of methods that bypass auth (e.g., health checks)
213 - Config accepts npub format only (human-readable bech32)
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 229
221**Access Control Patterns:** 230**Access Control Patterns:**
222- **Public relay**: Set `AllowedNpubsWrite` (only some can publish), leave `AllowedNpubsRead` empty (everyone can read) 231- **Public relay**: `Read.Enabled=false`, `Write.Enabled=true` with whitelist
223- **Private relay**: Set both lists (restricted read and write access) 232- **Private relay**: Both `Enabled=true` with whitelists
224- **Open relay**: Leave both empty (everyone can read and write) 233- **Open relay**: Both `Enabled=false`
225- **Read-only relay**: Set `AllowedNpubsRead`, block all writes 234- **Authenticated reads, open writes**: `Read.Enabled=true`, `Write.Enabled=false`
226 235
227### NostrCredentials Options 236### NostrCredentials Options
228 237
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
index d5f3257..bcbb4a3 100644
--- a/internal/auth/auth_test.go
+++ b/internal/auth/auth_test.go
@@ -243,8 +243,15 @@ func TestValidateAuthFromContext(t *testing.T) {
243 ctx := metadata.NewIncomingContext(context.Background(), md) 243 ctx := metadata.NewIncomingContext(context.Background(), md)
244 244
245 opts := &InterceptorOptions{ 245 opts := &InterceptorOptions{
246 Read: OperationAuthConfig{
247 Enabled: true,
248 AllowedNpubs: nil,
249 },
250 Write: OperationAuthConfig{
251 Enabled: true,
252 AllowedNpubs: nil,
253 },
246 TimestampWindow: 60, 254 TimestampWindow: 60,
247 Required: true,
248 } 255 }
249 256
250 pubkey, err := validateAuthFromContext(ctx, "/test.Service/Method", opts) 257 pubkey, err := validateAuthFromContext(ctx, "/test.Service/Method", opts)
diff --git a/internal/auth/interceptor.go b/internal/auth/interceptor.go
index 149cc01..d394102 100644
--- a/internal/auth/interceptor.go
+++ b/internal/auth/interceptor.go
@@ -21,49 +21,55 @@ const (
21 21
22// InterceptorOptions configures the authentication interceptor behavior. 22// InterceptorOptions configures the authentication interceptor behavior.
23type InterceptorOptions struct { 23type InterceptorOptions struct {
24 // Read configures authentication for read operations (Subscribe, QueryEvents, etc.)
25 Read OperationAuthConfig
26
27 // Write configures authentication for write operations (PublishEvent, PublishBatch)
28 Write OperationAuthConfig
29
24 // TimestampWindow is the maximum age of auth events in seconds. 30 // TimestampWindow is the maximum age of auth events in seconds.
25 // Events older than this are rejected to prevent replay attacks. 31 // Events older than this are rejected to prevent replay attacks.
26 // Default: 60 seconds 32 // Default: 60 seconds
27 TimestampWindow int64 33 TimestampWindow int64
28 34
29 // Required determines whether authentication is mandatory.
30 // If true, requests without valid auth are rejected.
31 // If false, unauthenticated requests are allowed (pubkey will be empty).
32 // Default: false
33 Required bool
34
35 // ValidatePayload checks the payload hash tag if present. 35 // ValidatePayload checks the payload hash tag if present.
36 // Default: false 36 // Default: false
37 ValidatePayload bool 37 ValidatePayload bool
38 38
39 // AllowedNpubsRead is an optional whitelist of allowed pubkeys for read operations (hex format).
40 // Config accepts npub format only, normalized to hex at load time.
41 // If nil or empty, all valid signatures are accepted for reads.
42 // Default: nil (allow all)
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
50
51 // SkipMethods is a list of gRPC methods that bypass authentication. 39 // SkipMethods is a list of gRPC methods that bypass authentication.
52 // Useful for public endpoints like health checks or relay info. 40 // Useful for public endpoints like health checks or relay info.
53 // Example: []string{"/nostr.v1.NostrRelay/QueryEvents"} 41 // Example: []string{"/grpc.health.v1.Health/Check"}
54 // Default: nil (authenticate all methods) 42 // Default: nil (authenticate all methods)
55 SkipMethods []string 43 SkipMethods []string
56} 44}
57 45
46// OperationAuthConfig configures auth for a specific operation type.
47type OperationAuthConfig struct {
48 // Enabled determines if auth is required.
49 // false = no auth, allow all
50 // true = auth required
51 Enabled bool
52
53 // AllowedNpubs is an optional whitelist (hex format, normalized from npub at config load).
54 // If Enabled=true && AllowedNpubs=[]: any valid signature accepted
55 // If Enabled=true && AllowedNpubs=[...]: only whitelisted npubs accepted
56 AllowedNpubs []string
57}
58
58// DefaultInterceptorOptions returns the default configuration. 59// DefaultInterceptorOptions returns the default configuration.
59func DefaultInterceptorOptions() *InterceptorOptions { 60func DefaultInterceptorOptions() *InterceptorOptions {
60 return &InterceptorOptions{ 61 return &InterceptorOptions{
61 TimestampWindow: 60, 62 Read: OperationAuthConfig{
62 Required: false, 63 Enabled: false,
63 ValidatePayload: false, 64 AllowedNpubs: nil,
64 AllowedNpubsRead: nil, 65 },
65 AllowedNpubsWrite: nil, 66 Write: OperationAuthConfig{
66 SkipMethods: nil, 67 Enabled: false,
68 AllowedNpubs: nil,
69 },
70 TimestampWindow: 60,
71 ValidatePayload: false,
72 SkipMethods: nil,
67 } 73 }
68} 74}
69 75
@@ -79,14 +85,23 @@ func NostrUnaryInterceptor(opts *InterceptorOptions) grpc.UnaryServerInterceptor
79 return handler(ctx, req) 85 return handler(ctx, req)
80 } 86 }
81 87
88 // Check if auth is required for this operation type
89 var authRequired bool
90 if isWriteMethod(info.FullMethod) {
91 authRequired = opts.Write.Enabled
92 } else {
93 authRequired = opts.Read.Enabled
94 }
95
96 // If auth not required, skip validation
97 if !authRequired {
98 return handler(ctx, req)
99 }
100
82 // Extract and validate auth 101 // Extract and validate auth
83 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) 102 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts)
84 if err != nil { 103 if err != nil {
85 if opts.Required { 104 return nil, status.Error(codes.Unauthenticated, err.Error())
86 return nil, status.Error(codes.Unauthenticated, err.Error())
87 }
88 // Auth not required, continue without pubkey
89 return handler(ctx, req)
90 } 105 }
91 106
92 // Add pubkey to context for handlers 107 // Add pubkey to context for handlers
@@ -108,15 +123,24 @@ func NostrStreamInterceptor(opts *InterceptorOptions) grpc.StreamServerIntercept
108 return handler(srv, ss) 123 return handler(srv, ss)
109 } 124 }
110 125
126 // Check if auth is required for this operation type
127 var authRequired bool
128 if isWriteMethod(info.FullMethod) {
129 authRequired = opts.Write.Enabled
130 } else {
131 authRequired = opts.Read.Enabled
132 }
133
134 // If auth not required, skip validation
135 if !authRequired {
136 return handler(srv, ss)
137 }
138
111 // Extract and validate auth 139 // Extract and validate auth
112 ctx := ss.Context() 140 ctx := ss.Context()
113 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts) 141 pubkey, err := validateAuthFromContext(ctx, info.FullMethod, opts)
114 if err != nil { 142 if err != nil {
115 if opts.Required { 143 return status.Error(codes.Unauthenticated, err.Error())
116 return status.Error(codes.Unauthenticated, err.Error())
117 }
118 // Auth not required, continue without pubkey
119 return handler(srv, ss)
120 } 144 }
121 145
122 // Wrap stream with authenticated context 146 // Wrap stream with authenticated context
@@ -177,23 +201,25 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept
177 // Extract pubkey 201 // Extract pubkey
178 pubkey := ExtractPubkey(event) 202 pubkey := ExtractPubkey(event)
179 203
180 // Check whitelist based on operation type (all values are already normalized to hex) 204 // Get the operation config based on method type
205 var opConfig OperationAuthConfig
181 if isWriteMethod(method) { 206 if isWriteMethod(method) {
182 // Write operation - check write allowlist 207 opConfig = opts.Write
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 { 208 } else {
189 // Read operation - check read allowlist 209 opConfig = opts.Read
190 if len(opts.AllowedNpubsRead) > 0 { 210 }
191 if !contains(opts.AllowedNpubsRead, pubkey) { 211
192 return "", fmt.Errorf("pubkey not authorized for read operations") 212 // Check whitelist if configured
213 if len(opConfig.AllowedNpubs) > 0 {
214 if !contains(opConfig.AllowedNpubs, pubkey) {
215 if isWriteMethod(method) {
216 return "", fmt.Errorf("pubkey not authorized for write operations")
193 } 217 }
218 return "", fmt.Errorf("pubkey not authorized for read operations")
194 } 219 }
195 } 220 }
196 221
222 // No whitelist or pubkey in whitelist - allow
197 return pubkey, nil 223 return pubkey, nil
198} 224}
199 225
diff --git a/internal/config/README.md b/internal/config/README.md
index 7deb38f..3dcf215 100644
--- a/internal/config/README.md
+++ b/internal/config/README.md
@@ -80,41 +80,51 @@ database:
80 80
81# Authentication configuration 81# Authentication configuration
82auth: 82auth:
83 # Enable authentication 83 # Read authentication (Subscribe, QueryEvents, CountEvents)
84 enabled: false 84 read:
85 85 enabled: false # false = no auth, allow all
86 # Require authentication for all requests 86 allowed_npubs: [] # npub format only (e.g., npub1...)
87 # If false, authentication is optional (pubkey available if provided) 87 # If enabled=false: no auth, allow all reads
88 required: false 88 # If enabled=true && allowed_npubs=[]: auth required, any valid signature accepted
89 # If enabled=true && allowed_npubs=[...]: auth required, only whitelisted npubs
90
91 # Write authentication (PublishEvent, PublishBatch)
92 write:
93 enabled: false
94 allowed_npubs: []
89 95
90 # Timestamp window in seconds for replay protection 96 # Timestamp window in seconds for replay protection
91 timestamp_window: 60 97 timestamp_window: 60
92 98
93 # Allowed npubs for read operations (optional, whitelist)
94 # If empty, all valid signatures are accepted for reads
95 # Use npub format only (e.g., npub1...)
96 allowed_npubs_read: []
97
98 # Allowed npubs for write operations (optional, whitelist)
99 # If empty, all valid signatures are accepted for writes
100 # Use npub format only (e.g., npub1...)
101 allowed_npubs_write: []
102
103 # Example use cases:
104 # - Public relay: allowed_npubs_write (only some can publish), empty read (everyone can read)
105 # - Private relay: both lists populated (restricted read and write)
106 # - Open relay: both lists empty (everyone can read and write)
107 #
108 # Example:
109 # allowed_npubs_read:
110 # - npub1a2b3c4d5e6f...
111 # allowed_npubs_write:
112 # - npub1a2b3c4d5e6f...
113
114 # Skip authentication for these methods 99 # Skip authentication for these methods
115 skip_methods: 100 skip_methods:
116 - "/grpc.health.v1.Health/Check" 101 - "/grpc.health.v1.Health/Check"
117 102
103# Common patterns:
104# Public relay (anyone can read, only whitelisted can write):
105# read:
106# enabled: false
107# write:
108# enabled: true
109# allowed_npubs:
110# - npub1...
111#
112# Private relay (whitelisted read and write):
113# read:
114# enabled: true
115# allowed_npubs:
116# - npub1...
117# write:
118# enabled: true
119# allowed_npubs:
120# - npub1...
121#
122# Open relay (everyone can read and write):
123# read:
124# enabled: false
125# write:
126# enabled: false
127
118# Rate limiting configuration 128# Rate limiting configuration
119rate_limit: 129rate_limit:
120 # Enable rate limiting 130 # Enable rate limiting
@@ -235,8 +245,10 @@ Complex types:
235 245
236```bash 246```bash
237# Lists (comma-separated, npub format) 247# Lists (comma-separated, npub format)
238export MUXSTR_AUTH_ALLOWED_NPUBS_READ="npub1...,npub1..." 248export MUXSTR_AUTH_READ_ENABLED=true
239export MUXSTR_AUTH_ALLOWED_NPUBS_WRITE="npub1..." 249export MUXSTR_AUTH_READ_ALLOWED_NPUBS="npub1...,npub1..."
250export MUXSTR_AUTH_WRITE_ENABLED=true
251export MUXSTR_AUTH_WRITE_ALLOWED_NPUBS="npub1..."
240 252
241# Durations 253# Durations
242export MUXSTR_SERVER_READ_TIMEOUT="30s" 254export MUXSTR_SERVER_READ_TIMEOUT="30s"
diff --git a/internal/config/config.go b/internal/config/config.go
index 3e52272..294510d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -39,12 +39,19 @@ type DatabaseConfig struct {
39 39
40// AuthConfig holds authentication configuration. 40// AuthConfig holds authentication configuration.
41type AuthConfig struct { 41type AuthConfig struct {
42 Enabled bool `yaml:"enabled"` 42 Read AuthOperationConfig `yaml:"read"`
43 Required bool `yaml:"required"` 43 Write AuthOperationConfig `yaml:"write"`
44 TimestampWindow int64 `yaml:"timestamp_window"` 44 TimestampWindow int64 `yaml:"timestamp_window"`
45 AllowedNpubsRead []string `yaml:"allowed_npubs_read"` // npub format only (bech32) - normalized to hex internally 45 SkipMethods []string `yaml:"skip_methods"`
46 AllowedNpubsWrite []string `yaml:"allowed_npubs_write"` // npub format only (bech32) - normalized to hex internally 46}
47 SkipMethods []string `yaml:"skip_methods"` 47
48// AuthOperationConfig configures auth for a specific operation type (read or write).
49type AuthOperationConfig struct {
50 Enabled bool `yaml:"enabled"` // false = no auth required, true = auth required
51 AllowedNpubs []string `yaml:"allowed_npubs"` // npub format only - normalized to hex internally
52 // If enabled=false: no auth, allow all
53 // If enabled=true && allowed_npubs=[]: auth required, any valid signature accepted
54 // If enabled=true && allowed_npubs=[...]: auth required, only whitelisted npubs
48} 55}
49 56
50// RateLimitConfig holds rate limiting configuration. 57// RateLimitConfig holds rate limiting configuration.
@@ -111,8 +118,14 @@ func Default() *Config {
111 Path: "relay.db", 118 Path: "relay.db",
112 }, 119 },
113 Auth: AuthConfig{ 120 Auth: AuthConfig{
114 Enabled: false, 121 Read: AuthOperationConfig{
115 Required: false, 122 Enabled: false,
123 AllowedNpubs: nil,
124 },
125 Write: AuthOperationConfig{
126 Enabled: false,
127 AllowedNpubs: nil,
128 },
116 TimestampWindow: 60, 129 TimestampWindow: 60,
117 }, 130 },
118 RateLimit: RateLimitConfig{ 131 RateLimit: RateLimitConfig{
@@ -184,15 +197,15 @@ func normalizeNpubs(cfg *Config) error {
184 var err error 197 var err error
185 198
186 // Normalize read allowlist 199 // Normalize read allowlist
187 cfg.Auth.AllowedNpubsRead, err = normalizeNpubList(cfg.Auth.AllowedNpubsRead) 200 cfg.Auth.Read.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Read.AllowedNpubs)
188 if err != nil { 201 if err != nil {
189 return fmt.Errorf("allowed_npubs_read: %w", err) 202 return fmt.Errorf("auth.read.allowed_npubs: %w", err)
190 } 203 }
191 204
192 // Normalize write allowlist 205 // Normalize write allowlist
193 cfg.Auth.AllowedNpubsWrite, err = normalizeNpubList(cfg.Auth.AllowedNpubsWrite) 206 cfg.Auth.Write.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Write.AllowedNpubs)
194 if err != nil { 207 if err != nil {
195 return fmt.Errorf("allowed_npubs_write: %w", err) 208 return fmt.Errorf("auth.write.allowed_npubs: %w", err)
196 } 209 }
197 210
198 return nil 211 return nil
@@ -299,11 +312,17 @@ func applyEnvOverrides(cfg *Config) {
299 } 312 }
300 313
301 // Auth 314 // Auth
302 if val := os.Getenv("MUXSTR_AUTH_ENABLED"); val != "" { 315 if val := os.Getenv("MUXSTR_AUTH_READ_ENABLED"); val != "" {
303 cfg.Auth.Enabled = parseBool(val) 316 cfg.Auth.Read.Enabled = parseBool(val)
317 }
318 if val := os.Getenv("MUXSTR_AUTH_READ_ALLOWED_NPUBS"); val != "" {
319 cfg.Auth.Read.AllowedNpubs = strings.Split(val, ",")
304 } 320 }
305 if val := os.Getenv("MUXSTR_AUTH_REQUIRED"); val != "" { 321 if val := os.Getenv("MUXSTR_AUTH_WRITE_ENABLED"); val != "" {
306 cfg.Auth.Required = parseBool(val) 322 cfg.Auth.Write.Enabled = parseBool(val)
323 }
324 if val := os.Getenv("MUXSTR_AUTH_WRITE_ALLOWED_NPUBS"); val != "" {
325 cfg.Auth.Write.AllowedNpubs = strings.Split(val, ",")
307 } 326 }
308 if val := os.Getenv("MUXSTR_AUTH_TIMESTAMP_WINDOW"); val != "" { 327 if val := os.Getenv("MUXSTR_AUTH_TIMESTAMP_WINDOW"); val != "" {
309 var n int64 328 var n int64
@@ -311,12 +330,6 @@ func applyEnvOverrides(cfg *Config) {
311 cfg.Auth.TimestampWindow = n 330 cfg.Auth.TimestampWindow = n
312 } 331 }
313 } 332 }
314 if val := os.Getenv("MUXSTR_AUTH_ALLOWED_NPUBS_READ"); val != "" {
315 cfg.Auth.AllowedNpubsRead = strings.Split(val, ",")
316 }
317 if val := os.Getenv("MUXSTR_AUTH_ALLOWED_NPUBS_WRITE"); val != "" {
318 cfg.Auth.AllowedNpubsWrite = strings.Split(val, ",")
319 }
320 333
321 // Rate limit 334 // Rate limit
322 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" { 335 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" {
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index c0d4555..65a742a 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -40,8 +40,10 @@ database:
40 path: "test.db" 40 path: "test.db"
41 41
42auth: 42auth:
43 enabled: true 43 read:
44 required: true 44 enabled: true
45 write:
46 enabled: true
45 timestamp_window: 120 47 timestamp_window: 120
46 48
47rate_limit: 49rate_limit:
@@ -75,12 +77,12 @@ metrics:
75 t.Errorf("expected db path test.db, got %s", cfg.Database.Path) 77 t.Errorf("expected db path test.db, got %s", cfg.Database.Path)
76 } 78 }
77 79
78 if !cfg.Auth.Enabled { 80 if !cfg.Auth.Read.Enabled {
79 t.Error("expected auth enabled") 81 t.Error("expected auth read enabled")
80 } 82 }
81 83
82 if !cfg.Auth.Required { 84 if !cfg.Auth.Write.Enabled {
83 t.Error("expected auth required") 85 t.Error("expected auth write enabled")
84 } 86 }
85 87
86 if cfg.Auth.TimestampWindow != 120 { 88 if cfg.Auth.TimestampWindow != 120 {
@@ -99,11 +101,13 @@ metrics:
99func TestEnvOverrides(t *testing.T) { 101func TestEnvOverrides(t *testing.T) {
100 // Set environment variables 102 // Set environment variables
101 os.Setenv("MUXSTR_SERVER_GRPC_ADDR", ":7777") 103 os.Setenv("MUXSTR_SERVER_GRPC_ADDR", ":7777")
102 os.Setenv("MUXSTR_AUTH_ENABLED", "true") 104 os.Setenv("MUXSTR_AUTH_READ_ENABLED", "true")
105 os.Setenv("MUXSTR_AUTH_WRITE_ENABLED", "true")
103 os.Setenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS", "200") 106 os.Setenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS", "200")
104 defer func() { 107 defer func() {
105 os.Unsetenv("MUXSTR_SERVER_GRPC_ADDR") 108 os.Unsetenv("MUXSTR_SERVER_GRPC_ADDR")
106 os.Unsetenv("MUXSTR_AUTH_ENABLED") 109 os.Unsetenv("MUXSTR_AUTH_READ_ENABLED")
110 os.Unsetenv("MUXSTR_AUTH_WRITE_ENABLED")
107 os.Unsetenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS") 111 os.Unsetenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS")
108 }() 112 }()
109 113
@@ -118,8 +122,12 @@ func TestEnvOverrides(t *testing.T) {
118 t.Errorf("expected env override :7777, got %s", cfg.Server.GrpcAddr) 122 t.Errorf("expected env override :7777, got %s", cfg.Server.GrpcAddr)
119 } 123 }
120 124
121 if !cfg.Auth.Enabled { 125 if !cfg.Auth.Read.Enabled {
122 t.Error("expected auth enabled from env") 126 t.Error("expected auth read enabled from env")
127 }
128
129 if !cfg.Auth.Write.Enabled {
130 t.Error("expected auth write enabled from env")
123 } 131 }
124 132
125 if cfg.RateLimit.DefaultRPS != 200 { 133 if cfg.RateLimit.DefaultRPS != 200 {
@@ -206,7 +214,8 @@ func TestSaveAndLoad(t *testing.T) {
206 // Create config 214 // Create config
207 cfg := Default() 215 cfg := Default()
208 cfg.Server.GrpcAddr = ":9999" 216 cfg.Server.GrpcAddr = ":9999"
209 cfg.Auth.Enabled = true 217 cfg.Auth.Read.Enabled = true
218 cfg.Auth.Write.Enabled = true
210 cfg.RateLimit.DefaultRPS = 100 219 cfg.RateLimit.DefaultRPS = 100
211 220
212 // Save to temp file 221 // Save to temp file
@@ -232,8 +241,12 @@ func TestSaveAndLoad(t *testing.T) {
232 t.Errorf("expected grpc_addr :9999, got %s", loaded.Server.GrpcAddr) 241 t.Errorf("expected grpc_addr :9999, got %s", loaded.Server.GrpcAddr)
233 } 242 }
234 243
235 if !loaded.Auth.Enabled { 244 if !loaded.Auth.Read.Enabled {
236 t.Error("expected auth enabled") 245 t.Error("expected auth read enabled")
246 }
247
248 if !loaded.Auth.Write.Enabled {
249 t.Error("expected auth write enabled")
237 } 250 }
238 251
239 if loaded.RateLimit.DefaultRPS != 100 { 252 if loaded.RateLimit.DefaultRPS != 100 {
@@ -259,12 +272,15 @@ database:
259 path: "test.db" 272 path: "test.db"
260 273
261auth: 274auth:
262 enabled: true 275 read:
263 allowed_npubs_read: 276 enabled: true
264 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 277 allowed_npubs:
265 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft 278 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
266 allowed_npubs_write: 279 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft
267 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 280 write:
281 enabled: true
282 allowed_npubs:
283 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
268` 284`
269 285
270 if _, err := tmpfile.Write([]byte(configData)); err != nil { 286 if _, err := tmpfile.Write([]byte(configData)); err != nil {
@@ -278,17 +294,17 @@ auth:
278 } 294 }
279 295
280 // Verify read npubs were normalized to hex 296 // Verify read npubs were normalized to hex
281 if len(cfg.Auth.AllowedNpubsRead) != 2 { 297 if len(cfg.Auth.Read.AllowedNpubs) != 2 {
282 t.Errorf("expected 2 allowed npubs for read, got %d", len(cfg.Auth.AllowedNpubsRead)) 298 t.Errorf("expected 2 allowed npubs for read, got %d", len(cfg.Auth.Read.AllowedNpubs))
283 } 299 }
284 300
285 // Verify write npubs were normalized to hex 301 // Verify write npubs were normalized to hex
286 if len(cfg.Auth.AllowedNpubsWrite) != 1 { 302 if len(cfg.Auth.Write.AllowedNpubs) != 1 {
287 t.Errorf("expected 1 allowed npub for write, got %d", len(cfg.Auth.AllowedNpubsWrite)) 303 t.Errorf("expected 1 allowed npub for write, got %d", len(cfg.Auth.Write.AllowedNpubs))
288 } 304 }
289 305
290 // Check that they're hex format (64 chars, not npub1...) 306 // Check that they're hex format (64 chars, not npub1...)
291 for i, pubkey := range cfg.Auth.AllowedNpubsRead { 307 for i, pubkey := range cfg.Auth.Read.AllowedNpubs {
292 if len(pubkey) != 64 { 308 if len(pubkey) != 64 {
293 t.Errorf("read npub %d: expected 64 hex chars, got %d", i, len(pubkey)) 309 t.Errorf("read npub %d: expected 64 hex chars, got %d", i, len(pubkey))
294 } 310 }
@@ -297,7 +313,7 @@ auth:
297 } 313 }
298 } 314 }
299 315
300 for i, pubkey := range cfg.Auth.AllowedNpubsWrite { 316 for i, pubkey := range cfg.Auth.Write.AllowedNpubs {
301 if len(pubkey) != 64 { 317 if len(pubkey) != 64 {
302 t.Errorf("write npub %d: expected 64 hex chars, got %d", i, len(pubkey)) 318 t.Errorf("write npub %d: expected 64 hex chars, got %d", i, len(pubkey))
303 } 319 }
@@ -310,14 +326,14 @@ auth:
310 expectedHex1 := "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" 326 expectedHex1 := "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
311 expectedHex2 := "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" 327 expectedHex2 := "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"
312 328
313 if cfg.Auth.AllowedNpubsRead[0] != expectedHex1 { 329 if cfg.Auth.Read.AllowedNpubs[0] != expectedHex1 {
314 t.Errorf("read npub 0: expected %s, got %s", expectedHex1, cfg.Auth.AllowedNpubsRead[0]) 330 t.Errorf("read npub 0: expected %s, got %s", expectedHex1, cfg.Auth.Read.AllowedNpubs[0])
315 } 331 }
316 if cfg.Auth.AllowedNpubsRead[1] != expectedHex2 { 332 if cfg.Auth.Read.AllowedNpubs[1] != expectedHex2 {
317 t.Errorf("read npub 1: expected %s, got %s", expectedHex2, cfg.Auth.AllowedNpubsRead[1]) 333 t.Errorf("read npub 1: expected %s, got %s", expectedHex2, cfg.Auth.Read.AllowedNpubs[1])
318 } 334 }
319 if cfg.Auth.AllowedNpubsWrite[0] != expectedHex1 { 335 if cfg.Auth.Write.AllowedNpubs[0] != expectedHex1 {
320 t.Errorf("write npub 0: expected %s, got %s", expectedHex1, cfg.Auth.AllowedNpubsWrite[0]) 336 t.Errorf("write npub 0: expected %s, got %s", expectedHex1, cfg.Auth.Write.AllowedNpubs[0])
321 } 337 }
322} 338}
323 339
@@ -337,8 +353,10 @@ server:
337database: 353database:
338 path: "test.db" 354 path: "test.db"
339auth: 355auth:
340 allowed_npubs_read: 356 read:
341 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d 357 enabled: true
358 allowed_npubs:
359 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
342`, 360`,
343 expectError: true, 361 expectError: true,
344 errorMsg: "must start with 'npub1'", 362 errorMsg: "must start with 'npub1'",
@@ -352,8 +370,10 @@ server:
352database: 370database:
353 path: "test.db" 371 path: "test.db"
354auth: 372auth:
355 allowed_npubs_write: 373 write:
356 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d 374 enabled: true
375 allowed_npubs:
376 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
357`, 377`,
358 expectError: true, 378 expectError: true,
359 errorMsg: "must start with 'npub1'", 379 errorMsg: "must start with 'npub1'",
@@ -367,10 +387,14 @@ server:
367database: 387database:
368 path: "test.db" 388 path: "test.db"
369auth: 389auth:
370 allowed_npubs_read: 390 read:
371 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 391 enabled: true
372 allowed_npubs_write: 392 allowed_npubs:
373 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft 393 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
394 write:
395 enabled: true
396 allowed_npubs:
397 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft
374`, 398`,
375 expectError: false, 399 expectError: false,
376 }, 400 },