userver: gRPC middleware tutorial
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
gRPC middleware tutorial

Your opinion will help to improve our service

Leave a feedback >

Before you start

Make sure that you can compile and run core tests and read a basic example gRPC client and service.

For more information about grpc middlewares see:

  1. gRPC server middlewares
  2. gRPC client middlewares

Step by step guide

In this example, you will write an authentication middleware for both GreeterService and GreeterClient of the basic gRPC service. See gRPC client and service.

Installation

Generate wrappers for proto files and link necessary libraries:

userver_add_grpc_library(${PROJECT_NAME}-proto PROTOS samples/greeter.proto)
target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}-proto)

The server middleware

Server middleware will check metadata that comes with an rpc.

Everything is the same as it is for client middleware, except there is no factory and the component stores the middleware itself:

class Middleware final : public ugrpc::server::MiddlewareBase {
public:
// Name of a middleware-factory that creates this middleware.
static constexpr std::string_view kName = "grpc-server-auth";
// 'Auth' is a group for auth authentication. See middlewares groups for more information.
static inline const auto kDependency =
middlewares::MiddlewareDependencyBuilder().InGroup<middlewares::groups::Auth>();
Middleware();
void OnCallStart(ugrpc::server::MiddlewareCallContext& context) const override;
};
// This component creates Middleware. Name of the component is 'Middleware::kName'.
// In this case we use a short-cut for defining a middleware-factory, but you can create your own factory by
// inheritance from 'ugrpc::server::MiddlewareFactoryComponentBase'

For more information about kDependency:

See also
gRPC middlewares order.

OnCallStart method of Middleware does the actual work:

Middleware::Middleware() = default;
void Middleware::OnCallStart(ugrpc::server::MiddlewareCallContext& context) const {
const auto& metadata = context.GetServerContext().client_metadata();
auto it = metadata.find(kKey);
if (it == metadata.cend() || it->second != kCredentials) {
LOG_ERROR() << "Invalid credentials";
return context.SetError(::grpc::Status{::grpc::StatusCode::PERMISSION_DENIED, "Invalid credentials"});
}
}

Lastly, add this component to the static config and register it in the pipeline:

components_manager:
components:
grpc-server-auth:
grpc-server-middlewares-pipeline:
middlewares:
grpc-server-auth: # register the middleware in the pipeline
enabled: true

The client middleware

Client middleware will add metadata to every GreeterClient call.

Derive Middleware and MiddlewareFactory from the respective base class and declare a middleware-factory:

class AuthMiddleware final : public ugrpc::client::MiddlewareBase {
public:
// Name of a middleware-factory that creates this middleware.
static constexpr std::string_view kName = "grpc-auth-client";
// 'Auth' is a group for authentication. See middlewares groups for more information.
static inline const auto kDependency =
middlewares::MiddlewareDependencyBuilder().InGroup<middlewares::groups::Auth>();
AuthMiddleware();
~AuthMiddleware() override;
void PreStartCall(ugrpc::client::MiddlewareCallContext& context) const override;
};
// This component creates Middleware. Name of the component is 'Middleware::kName'.
// In this case we use a short-cut for defining a middleware-factory, but you can create your own factory by
// inheritance from 'ugrpc::client::MiddlewareFactoryComponentBase'

For more information about kDependency:

See also
gRPC middlewares order.

PreStartCall method of Middleware does the actual work:

void ApplyCredentials(::grpc::ClientContext& context) { context.AddMetadata(kKey, kCredentials); }
AuthMiddleware::AuthMiddleware() = default;
AuthMiddleware::~AuthMiddleware() = default;
void AuthMiddleware::PreStartCall(ugrpc::client::MiddlewareCallContext& context) const {
ApplyCredentials(context.GetContext());
}

Lastly, add this component to the static config:

grpc-auth-client:
grpc-client-middlewares-pipeline:
middlewares:
grpc-auth-client:
enabled: true # register the middleware in the pipeline

To add static config options for the middleware:

See also
gRPC middlewares configuration.

int main()

Finally, register components and start the server.

int main(int argc, char* argv[]) {
const auto component_list = components::MinimalServerComponentList()
.Append<components::TestsuiteSupport>()
.Append<samples::grpc::auth::GreeterClient>()
.Append<samples::grpc::auth::GreeterServiceComponent>()
.Append<samples::grpc::auth::GreeterHttpHandler>()
.Append<samples::grpc::auth::server::AuthComponent>()
.Append<samples::grpc::auth::server::MetaFilterComponent>()
.Append<samples::grpc::auth::client::AuthComponent>()
.Append<samples::grpc::auth::client::ChaosComponent>();
return utils::DaemonMain(argc, argv, component_list);
}

Build and Run

To build the sample, execute the following build steps at the userver root directory:

mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-grpc_middleware_service

The sample could be started by running make start-userver-samples-grpc_middleware_service. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service.

To start the service manually run ./samples/grpc_middleware_service/userver-samples-grpc_middleware_service -c </path/to/static_config.yaml>.

The service is available locally at port 8091 (as per project static_config.yaml).

Functional testing

To implement Functional tests for the service some preparational steps should be done.

Preparations

First of all, import the required modules and add the required pytest_userver.plugins.grpc pytest plugin:

import pytest
import samples.greeter_pb2_grpc as greeter_services
pytest_plugins = ['pytest_userver.plugins.grpc']

gRPC server mock

You can use $grpc_mockserver substitution var in config_vars.testsuite.yaml:

greeter-endpoint: $grpc_mockserver

And in static_config.yaml:

greeter-client:
# The service endpoint (URI). We talk to our own service,
# which is kind of pointless, but works for an example.
endpoint: $greeter-endpoint

Write the mocking fixtures using grpc_mockserver:

import samples.greeter_pb2 as greeter_protos
import samples.greeter_pb2_grpc as greeter_services
async def test_basic_mock_server(service_client, grpc_mockserver):
@grpc_mockserver(greeter_services.GreeterServiceServicer.SayHello)
async def mock_say_hello(request, context):
return greeter_protos.GreetingResponse(
greeting=f'Hello, {request.name} from mockserver!',
)
response = await service_client.post('/hello?case=say_hello', data='tests')
assert response.status == 200
assert response.text == 'Hello, tests from mockserver!'
assert mock_say_hello.times_called == 1

gRPC client

To do the gRPC requests write a client fixture using grpc_channel:

@pytest.fixture
def grpc_client(grpc_channel):
return greeter_services.GreeterServiceStub(grpc_channel)

Use it to do gRPC requests to the service:

async def test_correct_credentials(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
response = await grpc_client.SayHello(
request=request,
metadata=[
('x-key', 'secret-credentials'),
('specific-header', 'specific-value'),
],
)
assert response.greeting == 'Hello, Python!'
async def test_incorrect_credentials(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
with pytest.raises(AioRpcError) as err:
await grpc_client.SayHello(
request=request,
metadata=[('x-key', 'secretcredentials')],
)
assert err.value.code() == StatusCode.PERMISSION_DENIED
async def test_no_credentials(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
with pytest.raises(AioRpcError) as err:
await grpc_client.SayHello(request=request)
assert err.value.code() == StatusCode.PERMISSION_DENIED

Full sources

See the full example at: