diff --git a/grpcserver/server.go b/grpcserver/server.go index 2848cae..1fba879 100644 --- a/grpcserver/server.go +++ b/grpcserver/server.go @@ -48,21 +48,45 @@ type Server struct { listener net.Listener } +// Options configures optional behavior for the gRPC server. +type Options struct { + // PreInterceptors run before the logging and auth interceptors. + // Use for lifecycle gates like seal checks that should reject + // requests before any auth validation occurs. + PreInterceptors []grpc.UnaryServerInterceptor + + // PostInterceptors run after auth but before the handler. + // Use for audit logging, rate limiting, or other cross-cutting + // concerns that need access to the authenticated identity. + PostInterceptors []grpc.UnaryServerInterceptor +} + // New creates a gRPC server with TLS (if certFile and keyFile are -// non-empty) and an interceptor chain: logging → auth → handler. +// non-empty) and an interceptor chain: +// +// [pre-interceptors] → logging → auth → [post-interceptors] → handler // // The auth interceptor uses methods to determine the access level for // each RPC. Methods not in any map are denied by default. // // If certFile and keyFile are empty, TLS is skipped (for testing). -func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger) (*Server, error) { - chain := grpc.ChainUnaryInterceptor( +// opts is optional; pass nil for the default chain (logging + auth only). +func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger, opts *Options) (*Server, error) { + var interceptors []grpc.UnaryServerInterceptor + if opts != nil { + interceptors = append(interceptors, opts.PreInterceptors...) + } + interceptors = append(interceptors, loggingInterceptor(logger), authInterceptor(authenticator, methods), ) + if opts != nil { + interceptors = append(interceptors, opts.PostInterceptors...) + } + chain := grpc.ChainUnaryInterceptor(interceptors...) - var opts []grpc.ServerOption - opts = append(opts, chain) + var serverOpts []grpc.ServerOption + serverOpts = append(serverOpts, chain) if certFile != "" && keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) @@ -73,11 +97,11 @@ func New(certFile, keyFile string, authenticator *auth.Authenticator, methods Me Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS13, } - opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg))) + serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(tlsCfg))) } return &Server{ - GRPCServer: grpc.NewServer(opts...), + GRPCServer: grpc.NewServer(serverOpts...), Logger: logger, }, nil } diff --git a/grpcserver/server_test.go b/grpcserver/server_test.go index a843045..19d6be8 100644 --- a/grpcserver/server_test.go +++ b/grpcserver/server_test.go @@ -216,7 +216,7 @@ func TestNewWithoutTLS(t *testing.T) { defer srv.Close() a := testAuth(t, srv.URL) - s, err := New("", "", a, testMethods, slog.Default()) + s, err := New("", "", a, testMethods, slog.Default(), nil) if err != nil { t.Fatalf("New: %v", err) }