Skip to content

Commit 312216f

Browse files
authored
chore: refactor complete method to pull out validation and more strictly validate TLS options (#3064)
## Description If you provided one of the GRPC TLS options (between certpath and keypath) but not the other, you'd end up with a plaintext configuration. This makes it so that that's an error state and refactors some other methods to be able to test them. ## Changes * Pull out some validation/handling methods * Test those methods * Add validation to GRPC TLS options ## Testing Review.
1 parent a813585 commit 312216f

5 files changed

Lines changed: 121 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2121
- Use cgroup-aware memory detection for cache and watch buffer sizing in containerized environments (https://github.com/authzed/spicedb/pull/3000)
2222
- Upgraded the spanner client, which changed the internal implementation to not use a session pool. This means that the `--datastore-spanner-max-sessions` and `--datastore-spanner-min-sessions` flags are now deprecated and no-op. We also strongly recommend using [Application Default Credentials](https://docs.cloud.google.com/docs/authentication/client-libraries#adc) in favor of a credentials file. (https://github.com/authzed/spicedb/pull/3038)
2323
- Query Planner: error `"ERROR: index \"pk_relation_tuple\" cannot be used for this query (SQLSTATE 42809)"` returned when using wildcards (https://github.com/authzed/spicedb/pull/3039)
24+
- Providing one of (`--grpc-tls-cert-path`, `--grpc-tls-key-path`) but not the other is now considered an error state, as both are necessary if you want to use TLS.
2425

2526
## [1.51.1] - 2026-04-14
2627
### Fixed

pkg/cmd/server/server.go

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -177,23 +177,9 @@ func (c *Config) Complete(ctx context.Context) (RunnableServer, error) {
177177
}
178178
}()
179179

180-
if len(c.PresharedSecureKey) < 1 && c.GRPCAuthFunc == nil {
181-
return nil, errors.New("a preshared key must be provided to authenticate API requests")
182-
}
183-
184-
if c.GRPCAuthFunc == nil {
185-
log.Ctx(ctx).Trace().Int("preshared-keys-count", len(c.PresharedSecureKey)).Msg("using gRPC auth with preshared key(s)")
186-
for index, presharedKey := range c.PresharedSecureKey {
187-
if len(presharedKey) == 0 {
188-
return nil, fmt.Errorf("preshared key #%d is empty", index+1)
189-
}
190-
191-
log.Ctx(ctx).Trace().Int("preshared-key-"+strconv.Itoa(index+1)+"-length", len(presharedKey)).Msg("preshared key configured")
192-
}
193-
194-
c.GRPCAuthFunc = auth.MustRequirePresharedKey(c.PresharedSecureKey)
195-
} else {
196-
log.Ctx(ctx).Trace().Msg("using preconfigured auth function")
180+
err = c.handleGrpcAuthn(ctx)
181+
if err != nil {
182+
return nil, err
197183
}
198184

199185
nscc, err := CompleteCache[cache.StringKey, schemacaching.CacheEntry](&c.NamespaceCacheConfig)
@@ -206,7 +192,7 @@ func (c *Config) Complete(ctx context.Context) (RunnableServer, error) {
206192
if ds == nil {
207193
var err error
208194
c.supportOldAndNewReadReplicaConnectionPoolFlags()
209-
ds, err = datastorecfg.NewDatastore(context.Background(), c.DatastoreConfig.ToOption(),
195+
ds, err = datastorecfg.NewDatastore(ctx, c.DatastoreConfig.ToOption(),
210196
// Datastore's filter maximum ID count is set to the max size, since the number of elements to be dispatched
211197
// are at most the number of elements returned from a datastore query
212198
datastorecfg.WithFilterMaximumIDCount(c.DispatchChunkSize),
@@ -254,6 +240,7 @@ func (c *Config) Complete(ctx context.Context) (RunnableServer, error) {
254240
closeables.AddWithoutError(cc.Close)
255241
log.Ctx(ctx).Info().EmbedObject(cc).Msg("configured dispatch cache")
256242

243+
// TODO: is there a circumstance under which this could be empty?
257244
dispatchPresharedKey := ""
258245
if len(c.PresharedSecureKey) > 0 {
259246
dispatchPresharedKey = c.PresharedSecureKey[0]
@@ -352,22 +339,9 @@ func (c *Config) Complete(ctx context.Context) (RunnableServer, error) {
352339
serverName = "spicedb"
353340
}
354341

355-
var mismatchZedTokenOption consistency.MismatchingTokenOption
356-
switch c.MismatchZedTokenBehavior {
357-
case "":
358-
fallthrough
359-
360-
case "full-consistency":
361-
mismatchZedTokenOption = consistency.TreatMismatchingTokensAsFullConsistency
362-
363-
case "min-latency":
364-
mismatchZedTokenOption = consistency.TreatMismatchingTokensAsMinLatency
365-
366-
case "error":
367-
mismatchZedTokenOption = consistency.TreatMismatchingTokensAsError
368-
369-
default:
370-
return nil, fmt.Errorf("unknown mismatched zedtoken behavior: %s", c.MismatchZedTokenBehavior)
342+
mismatchZedTokenOption, err := c.handleMismatchZedTokenOption()
343+
if err != nil {
344+
return nil, err
371345
}
372346

373347
memoryUsageProvider := c.BuildMemoryUsageProvider()
@@ -542,6 +516,48 @@ func (c *Config) Complete(ctx context.Context) (RunnableServer, error) {
542516
}, nil
543517
}
544518

519+
func (c *Config) handleGrpcAuthn(ctx context.Context) error {
520+
if len(c.PresharedSecureKey) < 1 && c.GRPCAuthFunc == nil {
521+
return errors.New("a preshared key must be provided to authenticate API requests")
522+
}
523+
524+
if c.GRPCAuthFunc == nil {
525+
log.Ctx(ctx).Trace().Int("preshared-keys-count", len(c.PresharedSecureKey)).Msg("using gRPC auth with preshared key(s)")
526+
for index, presharedKey := range c.PresharedSecureKey {
527+
if len(presharedKey) == 0 {
528+
return fmt.Errorf("preshared key #%d is empty", index+1)
529+
}
530+
531+
log.Ctx(ctx).Trace().Int("preshared-key-"+strconv.Itoa(index+1)+"-length", len(presharedKey)).Msg("preshared key configured")
532+
}
533+
534+
c.GRPCAuthFunc = auth.MustRequirePresharedKey(c.PresharedSecureKey)
535+
} else {
536+
log.Ctx(ctx).Trace().Msg("using preconfigured auth function")
537+
}
538+
return nil
539+
}
540+
541+
func (c *Config) handleMismatchZedTokenOption() (consistency.MismatchingTokenOption, error) {
542+
switch c.MismatchZedTokenBehavior {
543+
case "":
544+
fallthrough
545+
546+
case "full-consistency":
547+
return consistency.TreatMismatchingTokensAsFullConsistency, nil
548+
549+
case "min-latency":
550+
return consistency.TreatMismatchingTokensAsMinLatency, nil
551+
552+
case "error":
553+
return consistency.TreatMismatchingTokensAsError, nil
554+
555+
default:
556+
var zero consistency.MismatchingTokenOption
557+
return zero, fmt.Errorf("unknown mismatched zedtoken behavior: %s", c.MismatchZedTokenBehavior)
558+
}
559+
}
560+
545561
func (c *Config) BuildMemoryUsageProvider() memoryprotection.MemoryUsageProvider {
546562
if c.EnableMemoryProtectionMiddleware {
547563
if DefaultMemoryUsageProvider != nil {

pkg/cmd/server/server_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"reflect"
78
"slices"
89
"testing"
910
"time"
@@ -718,3 +719,30 @@ func TestDebugMapRedactsURI(t *testing.T) {
718719
out := fmt.Sprintln(c.FlatDebugMap())
719720
require.NotContains(t, out, "postgresql://root@localhost:26257/defaultdb")
720721
}
722+
723+
func TestHandleGRPCAuthn(t *testing.T) {
724+
t.Run("empty configuration errors", func(t *testing.T) {
725+
require.ErrorContains(t, (&Config{}).handleGrpcAuthn(t.Context()), "a preshared key must be provided")
726+
})
727+
t.Run("preshared key list with empty key errors", func(t *testing.T) {
728+
require.ErrorContains(t, (&Config{PresharedSecureKey: []string{"foo", ""}}).handleGrpcAuthn(t.Context()), "is empty")
729+
})
730+
t.Run("if auth fn is provided it is used", func(t *testing.T) {
731+
authFn := func(ctx context.Context) (context.Context, error) {
732+
return ctx, nil
733+
}
734+
config := &Config{GRPCAuthFunc: authFn}
735+
require.NoError(t, config.handleGrpcAuthn(t.Context()))
736+
// Assert that the function objects are the same. this hackaround is necessary
737+
// because of the way that golang treats function values.
738+
require.Equal(t,
739+
reflect.ValueOf(authFn).Pointer(),
740+
reflect.ValueOf(config.GRPCAuthFunc).Pointer(),
741+
)
742+
})
743+
t.Run("if auth fn is not provided, one is created", func(t *testing.T) {
744+
config := &Config{PresharedSecureKey: []string{"foo"}}
745+
require.NoError(t, config.handleGrpcAuthn(t.Context()))
746+
require.NotNil(t, config.GRPCAuthFunc)
747+
})
748+
}

pkg/cmd/util/util.go

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,38 @@ func (c *GRPCServerConfig) listenerAndDialer() (net.Listener, DialFunc, NetDialF
145145
}
146146

147147
func (c *GRPCServerConfig) tlsOpts() ([]grpc.ServerOption, *x509util.CertWatcher, error) {
148-
switch {
149-
case c.TLSCertPath == "" && c.TLSKeyPath == "":
150-
return nil, nil, nil
151-
case c.TLSCertPath != "" && c.TLSKeyPath != "":
152-
watcher, err := x509util.NewTLSCertWatcher(c.TLSCertPath, c.TLSKeyPath)
153-
if err != nil {
154-
return nil, nil, err
155-
}
156-
creds := credentials.NewTLS(&tls.Config{
157-
GetCertificate: watcher.GetCertificate,
158-
MinVersion: tls.VersionTLS12,
159-
})
160-
return []grpc.ServerOption{grpc.Creds(creds)}, watcher, nil
161-
default:
148+
// Ensure that we've either got both TLS config options or neither
149+
if err := c.validateTLSConfig(); err != nil {
150+
return nil, nil, err
151+
}
152+
153+
// If no TLS configuration is provided, we're in plaintext mode
154+
if c.TLSCertPath == "" {
162155
return nil, nil, nil
163156
}
157+
158+
// Else we've got TLS configuration and we'll construct the server options
159+
watcher, err := x509util.NewTLSCertWatcher(c.TLSCertPath, c.TLSKeyPath)
160+
if err != nil {
161+
return nil, nil, err
162+
}
163+
creds := credentials.NewTLS(&tls.Config{
164+
GetCertificate: watcher.GetCertificate,
165+
MinVersion: tls.VersionTLS12,
166+
})
167+
return []grpc.ServerOption{grpc.Creds(creds)}, watcher, nil
168+
}
169+
170+
func (c *GRPCServerConfig) validateTLSConfig() error {
171+
if (c.TLSCertPath == "") != (c.TLSKeyPath == "") {
172+
return fmt.Errorf(
173+
"failed to start %s gRPC server: must provide both --%s-tls-cert-path and --%s-tls-key-path",
174+
c.flagPrefix,
175+
c.flagPrefix,
176+
c.flagPrefix,
177+
)
178+
}
179+
return nil
164180
}
165181

166182
func (c *GRPCServerConfig) clientCreds() (credentials.TransportCredentials, error) {

pkg/cmd/util/util_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,15 @@ func TestDisabledHTTP(t *testing.T) {
2121
require.NoError(t, s.ListenAndServe())
2222
s.Close()
2323
}
24+
25+
func TestValidateTLSConfig(t *testing.T) {
26+
t.Run("both options present is fine", func(t *testing.T) {
27+
require.NoError(t, (&GRPCServerConfig{TLSCertPath: "some path", TLSKeyPath: "some other path"}).validateTLSConfig())
28+
})
29+
t.Run("neither option present is fine", func(t *testing.T) {
30+
require.NoError(t, (&GRPCServerConfig{}).validateTLSConfig())
31+
})
32+
t.Run("one present but not the other returns an error", func(t *testing.T) {
33+
require.ErrorContains(t, (&GRPCServerConfig{TLSCertPath: "some path"}).validateTLSConfig(), "must provide both")
34+
})
35+
}

0 commit comments

Comments
 (0)