diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9c901..ccc820a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Enhancements + +- Support optional gRPC channel options + ## v0.9.1 (2023-07-25) ### Bug fixes diff --git a/README.md b/README.md index 61783b7..40f9f6f 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,37 @@ with CerbosClient("unix:/var/cerbos.sock", tls_verify=False) as c: ... ``` +**Enabling TLS** + +`tls_verify` can either be the certificate location (string) or a boolean. If `True`, it'll look for the file at the location specified by the environment variable `SSL_CERT_FILE`, else the default OS location. + +```python +with CerbosClient("localhost:3593", tls_verify=True) as c: + ... +``` + +```python +with CerbosClient("localhost:3593", tls_verify="path/to/tls.crt") as c: + ... +``` + +**Optional channel arguments** + +You can pass additional options in the `channel_options` dict. +Available options are described [here](https://github.com/grpc/grpc/blob/7536d8a849c0096e4c968e7730306872bb5ec674/include/grpc/impl/grpc_types.h). +The argument is of type `dict[str, Any]` where the `Any` value must match the expected type defined in the previous link. + +NOTE: We provide this as a generic method to set arbitrary options for particular use cases. +For purely demonstrative purposes, our example below overrides `grpc.ssl_target_name_override`, which is certainly not recommended practice for production applications. + +```python +opts = { + "grpc.ssl_target_name_override": "localhost" +} +with CerbosClient("localhost:3593", tls_verify=True, channel_options=opts) as c: + ... +``` + ### HTTP client We maintain this for backwards compatibility. It is recommended to use the [gRPC client](#grpc-client). diff --git a/cerbos/sdk/_async/_grpc.py b/cerbos/sdk/_async/_grpc.py index df97202..fcbc7b2 100644 --- a/cerbos/sdk/_async/_grpc.py +++ b/cerbos/sdk/_async/_grpc.py @@ -88,6 +88,7 @@ def __init__( timeout_secs: float | None = None, request_retries: int = 0, wait_for_ready: bool = False, + channel_options: dict[str, Any] | None = None, ): if timeout_secs and not isinstance(timeout_secs, (int, float)): raise TypeError("timeout_secs must be a number type") @@ -100,7 +101,7 @@ def __init__( if request_retries < 2: request_retries = 0 - method_config: dict[Any, Any] = {} + method_config: dict[str, Any] = {} if methods: method_config["name"] = methods @@ -120,19 +121,22 @@ def __init__( if wait_for_ready: method_config["waitForReady"] = wait_for_ready - service_config = {"methodConfig": [method_config]} - options = [ - ("grpc.service_config", json.dumps(service_config)), - ] + options = { + "grpc.service_config": json.dumps({"methodConfig": [method_config]}), + } + + if channel_options: + options |= channel_options + opts = [(k, v) for k, v in options.items()] if tls_verify: self._channel = grpc.aio.secure_channel( host, credentials=creds, - options=options, + options=opts, ) else: - self._channel = grpc.aio.insecure_channel(host, options=options) + self._channel = grpc.aio.insecure_channel(host, options=opts) async def __aenter__(self): return self @@ -154,6 +158,7 @@ class AsyncCerbosClient(AsyncClientBase): timeout_secs (float): Optional request timeout in seconds (no timeout by default) request_retries (int): Optional maximum number of retries, including the original attempt. Anything below 2 will be treated as 0 (disabled) wait_for_ready (bool): Boolean specifying whether RPCs should wait until the connection is ready. Defaults to False + channel_options (dict[str, Any]): Optional gRPC channel options to pass on channel creation. The values need to match the expected types: https://github.com/grpc/grpc/blob/7536d8a849c0096e4c968e7730306872bb5ec674/include/grpc/impl/grpc_types.h Example: with AsyncCerbosClient("localhost:3593") as cerbos: @@ -175,6 +180,7 @@ def __init__( timeout_secs: float | None = None, request_retries: int = 0, wait_for_ready: bool = False, + channel_options: dict[str, Any] | None = None, ): creds: grpc.ChannelCredentials = None if tls_verify: @@ -203,6 +209,7 @@ def __init__( timeout_secs, request_retries, wait_for_ready, + channel_options, ) self._client = svc_pb2_grpc.CerbosServiceStub(self._channel) @@ -426,6 +433,7 @@ class AsyncCerbosAdminClient(AsyncClientBase): timeout_secs (float): Optional request timeout in seconds (no timeout by default) request_retries (int): Optional maximum number of retries, including the original attempt. Anything below 2 will be treated as 0 (disabled) wait_for_ready (bool): Boolean specifying whether RPCs should wait until the connection is ready. Defaults to False + channel_options (dict[str, Any]): Optional gRPC channel options to pass on channel creation. The values need to match the expected types: https://github.com/grpc/grpc/blob/7536d8a849c0096e4c968e7730306872bb5ec674/include/grpc/impl/grpc_types.h Example: with AsyncCerbosAdminClient("localhost:3593", admin_credentials=AdminCredentials("admin", "some_password")) as cerbos: @@ -448,6 +456,7 @@ def __init__( timeout_secs: float | None = None, request_retries: int = 0, wait_for_ready: bool = False, + channel_options: dict[str, Any] | None = None, ): admin_credentials = admin_credentials or AdminCredentials() self._creds_metadata = admin_credentials.metadata() @@ -479,6 +488,7 @@ def __init__( timeout_secs, request_retries, wait_for_ready, + channel_options, ) self._client = svc_pb2_grpc.CerbosAdminServiceStub(self._channel) diff --git a/cerbos/sdk/_sync/_grpc.py b/cerbos/sdk/_sync/_grpc.py index 92b96e6..fbfb653 100644 --- a/cerbos/sdk/_sync/_grpc.py +++ b/cerbos/sdk/_sync/_grpc.py @@ -88,6 +88,7 @@ def __init__( timeout_secs: float | None = None, request_retries: int = 0, wait_for_ready: bool = False, + channel_options: dict[str, Any] | None = None, ): if timeout_secs and not isinstance(timeout_secs, (int, float)): raise TypeError("timeout_secs must be a number type") @@ -100,7 +101,7 @@ def __init__( if request_retries < 2: request_retries = 0 - method_config: dict[Any, Any] = {} + method_config: dict[str, Any] = {} if methods: method_config["name"] = methods @@ -120,19 +121,22 @@ def __init__( if wait_for_ready: method_config["waitForReady"] = wait_for_ready - service_config = {"methodConfig": [method_config]} - options = [ - ("grpc.service_config", json.dumps(service_config)), - ] + options = { + "grpc.service_config": json.dumps({"methodConfig": [method_config]}), + } + + if channel_options: + options |= channel_options + opts = [(k, v) for k, v in options.items()] if tls_verify: self._channel = grpc.secure_channel( host, credentials=creds, - options=options, + options=opts, ) else: - self._channel = grpc.insecure_channel(host, options=options) + self._channel = grpc.insecure_channel(host, options=opts) def __enter__(self): return self @@ -154,6 +158,7 @@ class CerbosClient(SyncClientBase): timeout_secs (float): Optional request timeout in seconds (no timeout by default) request_retries (int): Optional maximum number of retries, including the original attempt. Anything below 2 will be treated as 0 (disabled) wait_for_ready (bool): Boolean specifying whether RPCs should wait until the connection is ready. Defaults to False + channel_options (dict[str, Any]): Optional gRPC channel options to pass on channel creation. The values need to match the expected types: https://github.com/grpc/grpc/blob/7536d8a849c0096e4c968e7730306872bb5ec674/include/grpc/impl/grpc_types.h Example: with AsyncCerbosClient("localhost:3593") as cerbos: @@ -175,6 +180,7 @@ def __init__( timeout_secs: float | None = None, request_retries: int = 0, wait_for_ready: bool = False, + channel_options: dict[str, Any] | None = None, ): creds: grpc.ChannelCredentials = None if tls_verify: @@ -203,6 +209,7 @@ def __init__( timeout_secs, request_retries, wait_for_ready, + channel_options, ) self._client = svc_pb2_grpc.CerbosServiceStub(self._channel) @@ -426,6 +433,7 @@ class CerbosAdminClient(SyncClientBase): timeout_secs (float): Optional request timeout in seconds (no timeout by default) request_retries (int): Optional maximum number of retries, including the original attempt. Anything below 2 will be treated as 0 (disabled) wait_for_ready (bool): Boolean specifying whether RPCs should wait until the connection is ready. Defaults to False + channel_options (dict[str, Any]): Optional gRPC channel options to pass on channel creation. The values need to match the expected types: https://github.com/grpc/grpc/blob/7536d8a849c0096e4c968e7730306872bb5ec674/include/grpc/impl/grpc_types.h Example: with AsyncCerbosAdminClient("localhost:3593", admin_credentials=AdminCredentials("admin", "some_password")) as cerbos: @@ -448,6 +456,7 @@ def __init__( timeout_secs: float | None = None, request_retries: int = 0, wait_for_ready: bool = False, + channel_options: dict[str, Any] | None = None, ): admin_credentials = admin_credentials or AdminCredentials() self._creds_metadata = admin_credentials.metadata() @@ -479,6 +488,7 @@ def __init__( timeout_secs, request_retries, wait_for_ready, + channel_options, ) self._client = svc_pb2_grpc.CerbosAdminServiceStub(self._channel)