Provide API for metadata handling and interceptors
1. Context
1.a Interceptors
Currently, SiLAServer.Builder does not allow adding ServerInterceptors. Many use cases for SiLA Client Metadata would benefit from such a mechanism.
Example: A call needs to be authorized, e.g. via the AccessToken metadata string specified in the Authorization Service core feature.
Additionally, interceptors allow attaching objects to the call context, so they are accessible to the feature implementations via Context.current().get(...).
1.b Metadata
There are two points where SiLA Client Metadata can be handled: In the actual command/property implementation, or before that in an interceptor that is added to the server.
Incoming SiLA Client Metadata is accessible to ServerInterceptors via the gRPC call headers (io.grpc.Metadata). For our purposes, this object behaves similarly to a Map<String, byte[]>.
2. Proposed APIs
2.a SiLAServer.Builder
Add method addInterceptor(ServerInterceptor)
2.b ServerMetadataContainer
This object wraps a Map<String, byte[]>, extracted from the gRPC call headers. It provides a type-safe API for retrieving the received SiLA Client Metadata.
// create from gRPC call headers (in ServerInterceptor)
ServerMetadataContainer metadata = ServerMetadataContainer.fromHeaders(headers);
// get protobuf message
AuthorizationServiceGrpc.Metadata_AccessToken accessTokenMessage = metadata.get(AuthorizationServiceGrpc.Metadata_AccessToken.class)
Implementation of <T extends Message> T get(Class<T> metadataClass)
- convert provided class name (
sila2.org.silastandard.[...].Metadata_AccessToken) to gRPC header key (sila-...-accesstoken-bin) - get bytes for this key from
Map<String, byte[]> - call
parseFrom(byte[])on the provided class
2.c MetadataExtractingInterceptor
Server-side interceptor which attaches a ServerMetadataContainer to every call context. This allows metadata handling in command/property implementations.
// add to server
SiLAServer.Builder.withConfig(...)
...
.withMetadataExtractingInterceptor()
...
.start()
// in command/property implementation
ServerMetadataContainer metadata = ServerMetadataContainer.current()
2.d MetadataUtils.newMetadataInjector
For client-side metadata injection. Attach SiLA Client Metadata objects to a stub.
// stub without metadata
stub = GreetingProviderGrpc.newBlockingStub();
stub.sayHello(request); // call without metadata
AuthorizationServiceGrpc.Metadata_AccessToken accessTokenMessage = AuthorizationServiceGrpc.Metadata_AccessToken.newBuilder()....build()
authorizedStub = stub.withInterceptors(MetadataUtils.newMetadataInjector(accessTokenMessage)) // varargs: can take multiple messages
authorizedStub(request); // call with metadata
2.e MetadataInterceptor
Abstract ServerInterceptor for simplifying metadata handling.
Benefits over plain ServerInterceptor
-
ServerMetadataContaineris extracted from header - thrown exceptions are handled properly (no messing around with custom delegates)
Structure
- Abstract method
Context intercept(ServerCall call, ServerMetadataContainer metadata). - Pass affected calls via constructor
How it works
- Incoming unaffected calls are forwarded to the next handler
-
interceptis called - if exception is thrown: abort call with appropriate
StatusRuntimeException - else: forward returned context to next call handler (usually
Context.current(), but arbitrary objects like permission tokens can be attached)
3. Example
Added module examples.metadata which shows how to use this API on the server and client side. It includes tests.