userver: gRPC middleware
Loading...
Searching...
No Matches
gRPC middleware

Before you start

Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.

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 and link with necessary libraries:

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

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 auth authentication. See middlewares groups for more information.
static inline const auto kDependency =
AuthMiddleware();
~AuthMiddleware() override;
void PreStartCall(ugrpc::client::MiddlewareCallContext& context) const override;
};
// This component creates Middleware. Name of component is 'Middleware::kName'.
// In this case we use a short-cut for defining a middleware-factory, but you can declare your own factory by
// inheritance from 'ugrpc::client::MiddlewareFactoryComponentBase'

PreStartCall method of Middleware does the actual work:

Lastly, add this component to the static config:

# yaml
components_manager:
components:
grpc-auth-client:

And connect it with ClientFactory:

# yaml
# Contains machinery common to all gRPC clients
grpc-client-common:
# The TaskProcessor for blocking connection initiation
blocking-task-processor: grpc-blocking-task-processor
# Creates gRPC clients
grpc-client-factory:
channel-args: {}
# Disable all middlewares. But we can force enable some
disable-all-pipeline-middlewares: true
# The object of gRPC client middleware components to use
middlewares:
grpc-auth-client:
enabled: true

The server middleware

Server middleware, in its turn, will validate 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-auth-server";
// 'Auth' is a group for auth authentication. See middlewares groups for more information.
static inline const auto kDependency =
Middleware();
void Handle(ugrpc::server::MiddlewareCallContext& context) const override;
};

Handle method of Middleware does the actual work:

Respective component:

// This component creates Middleware. Name of component is 'Middleware::kName'.
// In this case we use a short-cut for defining a middleware-factory, but you can declare your own factory by
// inheritance from 'ugrpc::server::MiddlewareFactoryComponentBase'

Lastly, add this component to the static config:

# yaml
components_manager:
components:
grpc-auth-server:

And connect it with Service:

# yaml
greeter-service:
task-processor: main-task-processor
greeting-prefix: Hello
middlewares:
grpc-server-meta-filter:
# So, the middleware of this service will get this header instead of 'global-header'
headers:
- specific-header

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<sample::grpc::auth::client::AuthComponent>()
.Append<sample::grpc::auth::client::ChaosComponent>()
.Append<sample::grpc::auth::server::AuthComponent>()
.Append<sample::grpc::auth::server::MetaFilterComponent>();
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

To mock the gRPC server provide a hook for the static config to change the endpoint:

USERVER_CONFIG_HOOKS = ['prepare_service_config']
@pytest.fixture(scope='session')
def prepare_service_config(grpc_mockserver_endpoint):
def patch_config(config, config_vars):
components = config['components_manager']['components']
components['greeter-client']['endpoint'] = grpc_mockserver_endpoint
return patch_config

Alternatively, use $grpc_mockserver substitution var in config_vars.testsuite.yaml:

greeter-client-endpoint: $grpc_mockserver

Write the mocking fixtures using grpc_mockserver:

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: