ext

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package ext is a starting point for building a temporal-proxy extension server.

The proxy delegates two decisions to servers the operator runs: whether an inbound caller may proceed (api.auth.v1.AuthService) and how a data encryption key is wrapped (api.kms.v1.EncryptionService). Implement Auth, KMS, or both and hand them to Serve for the listener, signal handling, and graceful shutdown. Registering the generated services by hand works as well; nothing here is privileged. Both are registered either way, and an unsupplied one answers Unimplemented rather than refusing the connection.

Two unrelated authentication decisions meet here and are easy to conflate. Auth is the proxy asking about a worker that connected to the gateway; WithServerAuth is about the caller of this server, which is the proxy. Answering the first does not imply enforcing the second, and a server that skips the second admits anyone who finds its port to everything but health.

The gRPC health service is always registered, and always exempt from WithServerAuth, since a probe has no credential to present. It reports SERVING from startup until shutdown begins, which makes it a liveness signal and not a readiness one: it says this process is up and answering, never that the Auth or KMS behind it can reach whatever it depends on. Wiring it to a readiness probe would report a server with an unreachable key store as ready.

The proxy fails closed, so any error from either service denies the request. Return a google.golang.org/grpc/status error: the code tells the proxy whether the caller can fix this or should retry, and a plain error arrives as Unknown.

Allow, Deny, BearerToken, and IsHealthCheckMethod cover the parts of an Auth implementation that are the same everywhere, and the first two are worth preferring to a hand-built response: an [api.auth.v1.AuthResponse] whose Decision is unset denies, so building one by hand can refuse a caller by omission.

Serve listens in plaintext unless WithServerOption supplies credentials, and warns once when the first call confirms it. Both are supported, but the ends must agree, since the proxy dials plaintext when the extension server's TLS block is absent.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Allow added in v0.5.0

func Allow() *auth.AuthResponse

Allow returns the response that admits a caller, and is the only response that does. Prefer it to building one by hand: a response whose Decision is unset denies, so a zero value is a refusal rather than an oversight.

func BearerToken added in v0.5.0

func BearerToken(req *auth.AuthRequest, header string) (string, error)

BearerToken returns the token the caller presented on header, with the "Bearer " scheme stripped. header is matched case-insensitively, as is the scheme.

Every failure is an google.golang.org/grpc/codes.Unauthenticated status error, ready to return from Auth.Authenticate: no credential on that header, more than one value, or a value carrying some other scheme. A repeated value is refused rather than resolved by taking the first, since choosing among credentials a caller sent is how a check gets bypassed.

An implementation that would rather answer Deny, or that accepts a credential with no scheme at all, should read req.GetCredentials() directly.

func Deny added in v0.5.0

func Deny(reason string) *auth.AuthResponse

Deny returns the response that refuses a caller. The reason is recorded by the proxy and withheld from the caller, so write it for whoever operates this server: it may name subjects and internal systems.

Use this for a caller judged and found wanting, and an error for a verdict never reached, such as an unreachable backend. Both deny, but an error keeps its status code, which is what tells a worker whether retrying could help.

func IsHealthCheckMethod added in v0.5.0

func IsHealthCheckMethod(full string) bool

IsHealthCheckMethod reports whether full, a gRPC full method name as it arrives in [api.auth.v1.Target], is one an implementation will usually admit without a credential: the gRPC health methods, and GetSystemInfo, which is the first call an SDK client makes on connect and so decides whether it can connect at all.

Whether to admit them is policy and stays with the implementation, which is why this reports rather than decides. Refusing them is a defensible choice; it makes the proxy look unhealthy to anything probing it, and makes an unauthenticated client fail at dial instead of on its first real call.

func Serve

func Serve(ctx context.Context, opts ...Option) error

Serve runs an extension server until ctx is cancelled or the process is signalled, then shuts down and returns nil. A non-nil return means the server never started, never that a caller was turned away.

Both generated services are registered whether or not WithAuth and WithKMS were given, and one left unset answers Unimplemented. Defaults are :8900 on every interface, a five second shutdown grace period, and the transport and limits WithServerOption describes. Serve blocks and installs its own handler for [signals], so a caller with no lifecycle of its own can pass context.Background.

Types

type Auth

type Auth interface {
	Authenticate(context.Context, *auth.AuthRequest) (*auth.AuthResponse, error)
}

Auth decides whether an inbound caller of the proxy may proceed. Register an implementation with WithAuth.

The request carries what the proxy knows about the call. Its credentials are what the proxy lifted from the caller's stream, one entry per configured credential header the caller actually sent, so an empty slice means it presented none; its target is what the call is addressing. The proxy's own credential to this server is not among the credentials and stays in the request metadata, alongside the caller's other metadata.

Answer with a response whose Decision is set: only DECISION_ALLOW admits, so an unset decision denies rather than admits by accident. Reason is for whoever operates this server, and the proxy keeps it out of what the rejected caller is told. Return an error only when no verdict was reached, such as an unreachable backend; the proxy denies either way, but an error keeps its status code, so google.golang.org/grpc/codes.Unavailable tells a worker to retry where a denial does not.

Implementations must be safe for concurrent use and must not block indefinitely, since a caller is waiting and the proxy denies on timeout.

type CredentialCheck

type CredentialCheck func(string) bool

CredentialCheck reports whether a credential presented to this server is valid. WithServerAuth installs it and describes the checks made first.

type KMS

type KMS interface {
	Wrap(context.Context, string, []byte) ([]byte, error)
	Unwrap(context.Context, []byte) ([]byte, error)
}

KMS wraps and unwraps the proxy's data encryption keys. Register an implementation with WithKMS.

Only key material crosses the wire. The plaintext handed to Wrap is a DEK, never a payload, so an implementation is free to make each call a round trip to an HSM; the proxy caches the DEK and does the bulk encryption itself.

Wrap receives the namespace, so an implementation may hold a distinct key per namespace. Unwrap does not: it gets only the ciphertext, so whatever identifies the key has to be inside what Wrap returned, usually an opaque header framed around it. That makes Unwrap's input a durable format worth versioning, and retiring a key destroys every payload it wrapped.

A google.golang.org/grpc/status error is passed through with its code intact, so an implementation that can tell a bad ciphertext from an unreachable backend can say which it was; any other error takes the code documented on the method that called it.

Implementations must be safe for concurrent use.

type Option

type Option func(*options)

Option configures the server built by Serve. The set is closed: options is unexported, so a caller cannot name it.

func WithAddr

func WithAddr(hostPort string) Option

WithAddr sets the address to listen on, defaulting to :8900 on every interface. Narrow it to loopback where the proxy reaches this server over one: an extension server can admit callers and unwrap key material, so it should not be published needlessly. Ignored when WithListener supplies a listener.

func WithAuth

func WithAuth(a Auth) Option

WithAuth registers an implementation of api.auth.v1.AuthService. Without it the service answers Unimplemented, and since the proxy fails closed, that denies every caller rather than admitting them.

func WithKMS

func WithKMS(kms KMS) Option

WithKMS registers an implementation of api.kms.v1.EncryptionService. Without it the service answers Unimplemented, failing every Seal and, more consequentially, every Open of a payload already sealed under it.

func WithListener

func WithListener(lis net.Listener) Option

WithListener serves on lis instead of a listener opened from WithAddr. Use it when something else owns the socket: a test that would rather not bind a port, or a process handed one by its supervisor. The server closes lis during shutdown, so a caller must not.

func WithLogger

func WithLogger(l logger.Logger) Option

WithLogger sets the logger for the server's lifecycle, defaulting to logger.Default. Handlers are not given it. A nil logger is ignored rather than installed, matching logger.SetDefault; the alternative is a panic on the first line the server logs.

func WithServerAuth

func WithServerAuth(header string, check CredentialCheck) Option

WithServerAuth guards this server's unary methods with check, which receives the single value of the header metadata. It authenticates the proxy to this server, unlike the Auth service, which is how the proxy asks about somebody else. Unary covers both generated services, since every method on them is unary; adding a streaming method to either proto means pairing this with a grpc.StreamServerInterceptor.

The health service is exempt, and deliberately: a probe has no credential to give, and guarding it would withhold nothing anyway, because Watch reports the same status over a stream that no unary interceptor sees.

A call is rejected with Unauthenticated unless the header is present exactly once and check accepts its value. Repeats are refused rather than searched, so a caller cannot spray guesses in one call. Compare in constant time (crypto/subtle.ConstantTimeCompare) for a shared secret. A nil check installs nothing and leaves the server open, while a non-nil one with an empty header would reject everything, so Serve treats it as a configuration error and declines to start.

func WithServerOption

func WithServerOption(opts ...grpc.ServerOption) Option

WithServerOption adds gRPC server options, and is the escape hatch for anything this package does not name: TLS via grpc.Creds, a stats handler, a raised limit.

Options accumulate, and Serve starts with insecure credentials, 128 concurrent streams, a 1MiB receive limit sized for key material rather than payloads, and keepalive settings that floor client ping intervals. An option given here is applied after those, so it wins for any setting gRPC resolves to one value: serving TLS is grpc.Creds(credentials.NewTLS(...)), with nothing to clear first.

Interceptor order is visible. The guard WithServerAuth installs is chained ahead of anything added here, so a grpc.ChainUnaryInterceptor sees only admitted calls. grpc.UnaryInterceptor is gRPC's own exception: prepended ahead of the whole chain, it observes calls about to be rejected.

func WithShutdownTimeout

func WithShutdownTimeout(t time.Duration) Option

WithShutdownTimeout bounds how long shutdown waits for in-flight calls before dropping connections. It defaults to five seconds and is clamped to a 50ms floor, so a zero or negative value still lets an already-answered call flush. Set it below the grace period of whatever supervises the process; overrunning that trades the graceful shutdown for a SIGKILL.

A client watching the health service holds the drain open until this expires, because a Watch ends with its stream rather than with the status going NOT_SERVING. Expect the bound to be reached, not merely available, wherever something watches.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL