From b96ace1569cd90016edca3435c60a919592357df Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 15:19:59 -0600 Subject: [PATCH 01/20] Add grpc-interceptor for insecure client setup --- poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index ece5992..da41d34 100644 --- a/poetry.lock +++ b/poetry.lock @@ -309,6 +309,23 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4 [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +[[package]] +name = "grpc-interceptor" +version = "0.15.4" +description = "Simplifies gRPC interceptors" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926"}, + {file = "grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d"}, +] + +[package.dependencies] +grpcio = ">=1.49.1,<2.0.0" + +[package.extras] +testing = ["protobuf (>=4.21.9)"] + [[package]] name = "grpc-stubs" version = "1.53.0.5" @@ -974,4 +991,4 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "0e8124f0ab131ff9bf9a3038fe0724c3e3aa40c1fff0b1b6f486961b45f62b2c" +content-hash = "3b68b5ca4acef85ad82f3e10ed58fbdba3d5c75ba1a739451b1946a5d2908d97" diff --git a/pyproject.toml b/pyproject.toml index 973c9c7..a142c64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ grpcio = "^1.63" protobuf = ">=5.26,<6" python = "^3.8" typing-extensions = ">=3.7.4,<5" +grpc-interceptor = "^0.15.4" [tool.poetry.group.dev.dependencies] black = ">=23.3,<25.0" From 8144810e8c95c8cc8e5fd382977ba62336439748 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 15:21:53 -0600 Subject: [PATCH 02/20] Add InsecureClient and reorganize code --- authzed/api/v1/__init__.py | 89 ++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/authzed/api/v1/__init__.py b/authzed/api/v1/__init__.py index ce41adb..56905ec 100644 --- a/authzed/api/v1/__init__.py +++ b/authzed/api/v1/__init__.py @@ -1,8 +1,11 @@ +from typing import Callable, Any import asyncio import grpc import grpc.aio +from grpc_interceptor import ClientCallDetails, ClientInterceptor + from authzed.api.v1.core_pb2 import ( AlgebraicSubjectSet, ContextualizedCaveat, @@ -70,47 +73,95 @@ class Client(SchemaServiceStub, PermissionsServiceStub, ExperimentalServiceStub, """ def __init__(self, target, credentials, options=None, compression=None): + channel = self.create_channel(target, credentials, options, compression) + self.init_stubs(channel) + + def init_stubs(self, channel): + SchemaServiceStub.__init__(self, channel) + PermissionsServiceStub.__init__(self, channel) + ExperimentalServiceStub.__init__(self, channel) + WatchServiceStub.__init__(self, channel) + + def create_channel(self, target, credentials, options=None, compression=None): try: asyncio.get_running_loop() channelfn = grpc.aio.secure_channel except RuntimeError: channelfn = grpc.secure_channel - channel = channelfn(target, credentials, options, compression) - SchemaServiceStub.__init__(self, channel) - PermissionsServiceStub.__init__(self, channel) - ExperimentalServiceStub.__init__(self, channel) - WatchServiceStub.__init__(self, channel) + return channelfn(target, credentials, options, compression) -class AsyncClient( - SchemaServiceStub, PermissionsServiceStub, ExperimentalServiceStub, WatchServiceStub -): +class AsyncClient(Client): """ v1 Authzed gRPC API client, for use with asyncio. """ def __init__(self, target, credentials, options=None, compression=None): channel = grpc.aio.secure_channel(target, credentials, options, compression) - SchemaServiceStub.__init__(self, channel) - PermissionsServiceStub.__init__(self, channel) - ExperimentalServiceStub.__init__(self, channel) - WatchServiceStub.__init__(self, channel) + self.init_stubs(channel) -class SyncClient( - SchemaServiceStub, PermissionsServiceStub, ExperimentalServiceStub, WatchServiceStub -): +class SyncClient(Client): """ v1 Authzed gRPC API client, running synchronously. """ def __init__(self, target, credentials, options=None, compression=None): channel = grpc.secure_channel(target, credentials, options, compression) - SchemaServiceStub.__init__(self, channel) - PermissionsServiceStub.__init__(self, channel) - ExperimentalServiceStub.__init__(self, channel) - WatchServiceStub.__init__(self, channel) + self.init_stubs(channel) + + +class TokenAuthorization(ClientInterceptor): + def __init__(self, token: str): + self._token = token + + def intercept( + self, + method: Callable, + request_or_iterator: Any, + call_details: grpc.ClientCallDetails, + ): + metadata: list[tuple[str, str | bytes]] = [("authorization", f"Bearer {self._token}")] + if call_details.metadata is not None: + metadata = [*metadata, *call_details.metadata] + + new_details = ClientCallDetails( + call_details.method, + call_details.timeout, + metadata, + call_details.credentials, + call_details.wait_for_ready, + call_details.compression, + ) + + return method(request_or_iterator, new_details) + + +class InsecureClient(Client): + """ + An insecure client variant for non-TLS contexts. + + The default behavior of the python gRPC client is to restrict non-TLS + calls to `localhost` only, which is frustrating in contexts like docker-compose, + so we provide this as a convenience. + """ + + def __init__( + self, + target: str, + token: str, + options=None, + compression=None, + ): + fake_credentials = grpc.local_channel_credentials() + channel = self.create_channel(target, fake_credentials, options, compression) + auth_interceptor = TokenAuthorization(token) + + insecure_channel = grpc.insecure_channel(target, options, compression) + channel = grpc.intercept_channel(insecure_channel, auth_interceptor) + + self.init_stubs(channel) __all__ = [ From 8f6bea49c4ade018239a5c18fa5278eaf04ab603 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:48:04 -0600 Subject: [PATCH 03/20] Add InsecureClient to exports --- authzed/api/v1/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/authzed/api/v1/__init__.py b/authzed/api/v1/__init__.py index 56905ec..c31e81e 100644 --- a/authzed/api/v1/__init__.py +++ b/authzed/api/v1/__init__.py @@ -191,6 +191,7 @@ def __init__( "DeleteRelationshipsResponse", "ExpandPermissionTreeRequest", "ExpandPermissionTreeResponse", + "InsecureClient", "LookupResourcesRequest", "LookupResourcesResponse", "LookupSubjectsRequest", From f0d633a4009bc7bff6c992ca9b72b282b6dda42d Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:49:30 -0600 Subject: [PATCH 04/20] Add conftest file that makes token fixture available --- tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4c230e2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import pytest +import uuid + +@pytest.fixture(scope="function") +def token(): + return str(uuid.uuid4()) From cd0075323ce3168004709a222ffe853c3731cac0 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:49:57 -0600 Subject: [PATCH 05/20] Add init.py file for tests --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 3436a9efbbca7f9744d6bfdfe292f6502667a3d5 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:50:11 -0600 Subject: [PATCH 06/20] Add common calls file --- tests/calls.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/calls.py diff --git a/tests/calls.py b/tests/calls.py new file mode 100644 index 0000000..c83f824 --- /dev/null +++ b/tests/calls.py @@ -0,0 +1,24 @@ +from authzed.api.v1 import ( + WriteSchemaRequest +) + +from .utils import maybe_await + +async def write_test_schema(client): + schema = """ + caveat likes_harry_potter(likes bool) { + likes == true + } + + definition post { + relation writer: user + relation reader: user + relation caveated_reader: user with likes_harry_potter + + permission write = writer + permission view = reader + writer + permission view_as_fan = caveated_reader + writer + } + definition user {} + """ + await maybe_await(client.WriteSchema(WriteSchemaRequest(schema=schema))) From 9731bc397174509d74a05fd4fd9502a8c76f7ee0 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:50:17 -0600 Subject: [PATCH 07/20] Add common utils file --- tests/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/utils.py diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..24355b0 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,24 @@ +from inspect import isawaitable +from typing import ( + TypeVar, + Union, + Iterable, + AsyncIterable, + ) + +T = TypeVar("T") + +async def maybe_async_iterable_to_list(iterable: Union[Iterable[T], AsyncIterable[T]]) -> list[T]: + items = [] + if isinstance(iterable, AsyncIterable): + async for item in iterable: + items.append(item) + else: + for item in iterable: + items.append(item) + return items + +async def maybe_await(resp: T) -> T: + if isawaitable(resp): + resp = await resp + return resp From 1546a785ec59063e48227b40b0586ba0740949a8 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:50:29 -0600 Subject: [PATCH 08/20] Add tests for new insecure client --- tests/insecure_client_test.py | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/insecure_client_test.py diff --git a/tests/insecure_client_test.py b/tests/insecure_client_test.py new file mode 100644 index 0000000..7fbe211 --- /dev/null +++ b/tests/insecure_client_test.py @@ -0,0 +1,36 @@ +import pytest +import grpc + +from authzed.api.v1 import ( + InsecureClient, + SyncClient, + AsyncClient, + ) +from grpcutil import insecure_bearer_token_credentials + +from .calls import write_test_schema + +# NOTE: this is the name of the "remote" binding of the service container +# in CI. These tests are only run in CI because otherwise setup is fiddly. +# If you want to see these tests run locally, figure out your computer's +# network-local IP address (typically 192.168.x.x) and make that the `remote_host` +# string below, and then start up a testing container bound to that interface: +# docker run --rm -p 192.168.x.x:50051:50051 authzed/spicedb serve-testing +remote_host = "remote-spicedb" + +@pytest.mark.ci_only +async def test_normal_async_client_raises_error_on_insecure_remote_call(token): + with pytest.raises(grpc.RpcError): + client = AsyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) + await write_test_schema(client) + +@pytest.mark.ci_only +async def test_normal_sync_client_raises_error_on_insecure_remote_call(token): + with pytest.raises(grpc.RpcError): + client = SyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) + await write_test_schema(client) + +@pytest.mark.ci_only +async def test_insecure_client_makes_insecure_remote_call(token): + insecure_client = InsecureClient(f"{remote_host}:50051", token) + await write_test_schema(insecure_client) From 499d415947c65f23a4756ca1ab6751b0b4c28125 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:50:46 -0600 Subject: [PATCH 09/20] Move common things out --- tests/v1_test.py | 47 ++--------------------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/tests/v1_test.py b/tests/v1_test.py index 63e502e..e622e2e 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -28,14 +28,11 @@ WriteRelationshipsRequest, WriteSchemaRequest, ) +from .calls import write_test_schema +from .utils import maybe_async_iterable_to_list, maybe_await from grpcutil import insecure_bearer_token_credentials -@pytest.fixture() -def token(): - return str(uuid.uuid4()) - - @pytest.fixture() def client_autodetect_sync(token) -> Client: with pytest.raises(RuntimeError): @@ -389,43 +386,3 @@ async def write_test_tuples(client): ) ) return beatrice, emilia, post_one, post_two - - -async def write_test_schema(client): - schema = """ - caveat likes_harry_potter(likes bool) { - likes == true - } - - definition post { - relation writer: user - relation reader: user - relation caveated_reader: user with likes_harry_potter - - permission write = writer - permission view = reader + writer - permission view_as_fan = caveated_reader + writer - } - definition user {} - """ - await maybe_await(client.WriteSchema(WriteSchemaRequest(schema=schema))) - - -T = TypeVar("T") - - -async def maybe_await(resp: T) -> T: - if isawaitable(resp): - resp = await resp - return resp - - -async def maybe_async_iterable_to_list(iterable: Union[Iterable[T], AsyncIterable[T]]) -> List[T]: - items = [] - if isinstance(iterable, AsyncIterable): - async for item in iterable: - items.append(item) - else: - for item in iterable: - items.append(item) - return items From bdf4b7fa1ee86166196ba54274c4c14db3fd4bce Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:52:11 -0600 Subject: [PATCH 10/20] Add some markers for ci_only tests and select them --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a142c64..0468a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,10 +47,13 @@ ignore_missing_imports = true module = ["google.rpc.*", "grpcutil"] [tool.pytest.ini_options] -addopts = "-x" +addopts = "-x -m \"not ci_only\"" log_level = "debug" minversion = "6.0" asyncio_mode = "auto" +markers = [ +"ci_only: marks tests that will only run in CI" +] [tool.isort] ensure_newline_before_comments = true From 6fcf09ec16f57100d87767a4362f6086711f7420 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:52:31 -0600 Subject: [PATCH 11/20] Try this configuration for tests --- .github/workflows/test.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9c887a9..b7336d1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,6 +13,10 @@ jobs: pytest: name: "Unit and Integration Tests" runs-on: "ubuntu-latest" + services: + remote-spicedb: + image: authzed/spicedb + options: "--entrypoint 'spicedb serve-testing'" strategy: matrix: python-version: @@ -39,9 +43,11 @@ jobs: with: version: "latest" - name: "Pytest" + # NOTE: the -m "" overrides the default marks, which + # selects the otherwise-unselected ci_only tests. run: | source ~/.cache/virtualenv/authzedpy/bin/activate - pytest -vv . + pytest -vv -m "" protobuf: name: "Generate & Diff Protobuf" From 3a72aefdd5131694bcd140b717a4738e66737ee4 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:53:04 -0600 Subject: [PATCH 12/20] Reformat test files --- tests/calls.py | 5 ++--- tests/conftest.py | 1 + tests/insecure_client_test.py | 11 +++++++---- tests/utils.py | 12 +++++++----- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/calls.py b/tests/calls.py index c83f824..2502db9 100644 --- a/tests/calls.py +++ b/tests/calls.py @@ -1,9 +1,8 @@ -from authzed.api.v1 import ( - WriteSchemaRequest -) +from authzed.api.v1 import WriteSchemaRequest from .utils import maybe_await + async def write_test_schema(client): schema = """ caveat likes_harry_potter(likes bool) { diff --git a/tests/conftest.py b/tests/conftest.py index 4c230e2..6825a8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import pytest import uuid + @pytest.fixture(scope="function") def token(): return str(uuid.uuid4()) diff --git a/tests/insecure_client_test.py b/tests/insecure_client_test.py index 7fbe211..0b2ee44 100644 --- a/tests/insecure_client_test.py +++ b/tests/insecure_client_test.py @@ -2,10 +2,10 @@ import grpc from authzed.api.v1 import ( - InsecureClient, - SyncClient, - AsyncClient, - ) + InsecureClient, + SyncClient, + AsyncClient, +) from grpcutil import insecure_bearer_token_credentials from .calls import write_test_schema @@ -18,18 +18,21 @@ # docker run --rm -p 192.168.x.x:50051:50051 authzed/spicedb serve-testing remote_host = "remote-spicedb" + @pytest.mark.ci_only async def test_normal_async_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): client = AsyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) await write_test_schema(client) + @pytest.mark.ci_only async def test_normal_sync_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): client = SyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) await write_test_schema(client) + @pytest.mark.ci_only async def test_insecure_client_makes_insecure_remote_call(token): insecure_client = InsecureClient(f"{remote_host}:50051", token) diff --git a/tests/utils.py b/tests/utils.py index 24355b0..39e04d1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,14 @@ from inspect import isawaitable from typing import ( - TypeVar, - Union, - Iterable, - AsyncIterable, - ) + TypeVar, + Union, + Iterable, + AsyncIterable, +) T = TypeVar("T") + async def maybe_async_iterable_to_list(iterable: Union[Iterable[T], AsyncIterable[T]]) -> list[T]: items = [] if isinstance(iterable, AsyncIterable): @@ -18,6 +19,7 @@ async def maybe_async_iterable_to_list(iterable: Union[Iterable[T], AsyncIterabl items.append(item) return items + async def maybe_await(resp: T) -> T: if isawaitable(resp): resp = await resp From 9cf89988fd93c083a99a89bd524153189ad3b12d Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:55:16 -0600 Subject: [PATCH 13/20] Add quotes --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b7336d1..991007f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,7 +15,7 @@ jobs: runs-on: "ubuntu-latest" services: remote-spicedb: - image: authzed/spicedb + image: "authzed/spicedb" options: "--entrypoint 'spicedb serve-testing'" strategy: matrix: From 709c28871b0f603b72f79b7706dae1db667463a3 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 16:59:21 -0600 Subject: [PATCH 14/20] Remove unused imports --- tests/v1_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/v1_test.py b/tests/v1_test.py index e622e2e..f85a132 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -1,7 +1,6 @@ import asyncio import uuid -from inspect import isawaitable -from typing import Any, AsyncIterable, Iterable, List, Literal, TypeVar, Union +from typing import Any, Literal import pytest from google.protobuf.struct_pb2 import Struct @@ -59,7 +58,7 @@ async def async_client(token) -> AsyncClient: # The configs array paramaterizes the tests in this file to run with different clients. # To make changes, modify both the configs array and the config fixture Config = Literal["Client_autodetect_sync", "Client_autodetect_async", "SyncClient", "AsyncClient"] -configs: List[Config] = [ +configs: list[Config] = [ "Client_autodetect_sync", "Client_autodetect_async", "SyncClient", From 560b8c0da9b305feea28bdef74b725862c961b31 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 17:05:44 -0600 Subject: [PATCH 15/20] Remove insecure client tests from ci run --- .github/workflows/test.yaml | 8 +------- pyproject.toml | 4 ++-- tests/insecure_client_test.py | 14 +++++++++----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 991007f..75ad679 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,10 +13,6 @@ jobs: pytest: name: "Unit and Integration Tests" runs-on: "ubuntu-latest" - services: - remote-spicedb: - image: "authzed/spicedb" - options: "--entrypoint 'spicedb serve-testing'" strategy: matrix: python-version: @@ -43,11 +39,9 @@ jobs: with: version: "latest" - name: "Pytest" - # NOTE: the -m "" overrides the default marks, which - # selects the otherwise-unselected ci_only tests. run: | source ~/.cache/virtualenv/authzedpy/bin/activate - pytest -vv -m "" + pytest -vv protobuf: name: "Generate & Diff Protobuf" diff --git a/pyproject.toml b/pyproject.toml index 0468a7a..750cd91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,12 +47,12 @@ ignore_missing_imports = true module = ["google.rpc.*", "grpcutil"] [tool.pytest.ini_options] -addopts = "-x -m \"not ci_only\"" +addopts = "-x -m \"not remote_calls\"" log_level = "debug" minversion = "6.0" asyncio_mode = "auto" markers = [ -"ci_only: marks tests that will only run in CI" +"remote_calls: marks tests that make remote calls, not normally run" ] [tool.isort] diff --git a/tests/insecure_client_test.py b/tests/insecure_client_test.py index 0b2ee44..60cfc0e 100644 --- a/tests/insecure_client_test.py +++ b/tests/insecure_client_test.py @@ -10,30 +10,34 @@ from .calls import write_test_schema +## NOTE: these tests aren't usually run. They theoretically could and theoretically +# should be run in CI, but getting an appropriate "remote" container is difficult with +# github actions; this will happen at some point in the future. +# To run them: `poetry run pytest -m ""` + # NOTE: this is the name of the "remote" binding of the service container # in CI. These tests are only run in CI because otherwise setup is fiddly. # If you want to see these tests run locally, figure out your computer's # network-local IP address (typically 192.168.x.x) and make that the `remote_host` # string below, and then start up a testing container bound to that interface: # docker run --rm -p 192.168.x.x:50051:50051 authzed/spicedb serve-testing -remote_host = "remote-spicedb" - +remote_host = "192.168.something.something" -@pytest.mark.ci_only +@pytest.mark.remote_calls async def test_normal_async_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): client = AsyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) await write_test_schema(client) -@pytest.mark.ci_only +@pytest.mark.remote_calls async def test_normal_sync_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): client = SyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) await write_test_schema(client) -@pytest.mark.ci_only +@pytest.mark.remote_calls async def test_insecure_client_makes_insecure_remote_call(token): insecure_client = InsecureClient(f"{remote_host}:50051", token) await write_test_schema(insecure_client) From fc0555d7fc5a2e5b99a2d3695757f36b1f256d0b Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 17:09:26 -0600 Subject: [PATCH 16/20] Add readme as well --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 35f2d1d..0346791 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,21 @@ resp = client.CheckPermission(CheckPermissionRequest( )) assert resp.permissionship == CheckPermissionResponse.PERMISSIONSHIP_HAS_PERMISSION ``` + +### Insecure Client Usage +When running in a context like `docker compose`, because of Docker's virtual networking, +the gRPC client sees the SpiceDB container as "remote." It has built-in safeguards to prevent +calling a remote client in an insecure manner, such as using client credentials without TLS. + +However, this is a pain when setting up a development or testing environment, so we provide +the `InsecureClient` as a convenience: + +```py +from authzed.api.v1 import Client +from grpcutil import bearer_token_credentials + +client = Client( + "spicedb:50051", + "my super secret token" +) +``` From ba554c99c25e56c4087127894a23d45be4d63110 Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 17:10:02 -0600 Subject: [PATCH 17/20] Blacken --- tests/insecure_client_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/insecure_client_test.py b/tests/insecure_client_test.py index 60cfc0e..4f487bc 100644 --- a/tests/insecure_client_test.py +++ b/tests/insecure_client_test.py @@ -23,6 +23,7 @@ # docker run --rm -p 192.168.x.x:50051:50051 authzed/spicedb serve-testing remote_host = "192.168.something.something" + @pytest.mark.remote_calls async def test_normal_async_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): From 4be198c9f694b5b994340f0381873c4c53c08d1e Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 17:11:49 -0600 Subject: [PATCH 18/20] Make type annotations compatible with py3.8 --- tests/utils.py | 3 ++- tests/v1_test.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 39e04d1..5e87dad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,7 @@ from inspect import isawaitable from typing import ( TypeVar, + List, Union, Iterable, AsyncIterable, @@ -9,7 +10,7 @@ T = TypeVar("T") -async def maybe_async_iterable_to_list(iterable: Union[Iterable[T], AsyncIterable[T]]) -> list[T]: +async def maybe_async_iterable_to_list(iterable: Union[Iterable[T], AsyncIterable[T]]) -> List[T]: items = [] if isinstance(iterable, AsyncIterable): async for item in iterable: diff --git a/tests/v1_test.py b/tests/v1_test.py index f85a132..c0b804d 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -1,6 +1,6 @@ import asyncio import uuid -from typing import Any, Literal +from typing import Any, Literal, List import pytest from google.protobuf.struct_pb2 import Struct @@ -58,7 +58,7 @@ async def async_client(token) -> AsyncClient: # The configs array paramaterizes the tests in this file to run with different clients. # To make changes, modify both the configs array and the config fixture Config = Literal["Client_autodetect_sync", "Client_autodetect_async", "SyncClient", "AsyncClient"] -configs: list[Config] = [ +configs: List[Config] = [ "Client_autodetect_sync", "Client_autodetect_async", "SyncClient", From e5fe5ee72fd2502e5335397515ca16058cfc88eb Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 17:20:19 -0600 Subject: [PATCH 19/20] Run isort --- authzed/api/v1/__init__.py | 3 +-- tests/conftest.py | 3 ++- tests/insecure_client_test.py | 8 ++------ tests/utils.py | 8 +------- tests/v1_test.py | 5 +++-- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/authzed/api/v1/__init__.py b/authzed/api/v1/__init__.py index c31e81e..3d05ec6 100644 --- a/authzed/api/v1/__init__.py +++ b/authzed/api/v1/__init__.py @@ -1,9 +1,8 @@ -from typing import Callable, Any import asyncio +from typing import Any, Callable import grpc import grpc.aio - from grpc_interceptor import ClientCallDetails, ClientInterceptor from authzed.api.v1.core_pb2 import ( diff --git a/tests/conftest.py b/tests/conftest.py index 6825a8d..ace4ae6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ -import pytest import uuid +import pytest + @pytest.fixture(scope="function") def token(): diff --git a/tests/insecure_client_test.py b/tests/insecure_client_test.py index 4f487bc..b654ef5 100644 --- a/tests/insecure_client_test.py +++ b/tests/insecure_client_test.py @@ -1,11 +1,7 @@ -import pytest import grpc +import pytest -from authzed.api.v1 import ( - InsecureClient, - SyncClient, - AsyncClient, -) +from authzed.api.v1 import AsyncClient, InsecureClient, SyncClient from grpcutil import insecure_bearer_token_credentials from .calls import write_test_schema diff --git a/tests/utils.py b/tests/utils.py index 5e87dad..33776e3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,11 +1,5 @@ from inspect import isawaitable -from typing import ( - TypeVar, - List, - Union, - Iterable, - AsyncIterable, -) +from typing import AsyncIterable, Iterable, List, TypeVar, Union T = TypeVar("T") diff --git a/tests/v1_test.py b/tests/v1_test.py index c0b804d..d8571b6 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -1,6 +1,6 @@ import asyncio import uuid -from typing import Any, Literal, List +from typing import Any, List, Literal import pytest from google.protobuf.struct_pb2 import Struct @@ -27,9 +27,10 @@ WriteRelationshipsRequest, WriteSchemaRequest, ) +from grpcutil import insecure_bearer_token_credentials + from .calls import write_test_schema from .utils import maybe_async_iterable_to_list, maybe_await -from grpcutil import insecure_bearer_token_credentials @pytest.fixture() From e667577e161977ec0fe1ce9729674ae84b0f1b6d Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Tue, 3 Sep 2024 17:27:08 -0600 Subject: [PATCH 20/20] Use skips instead of custom mark --- pyproject.toml | 5 +---- tests/insecure_client_test.py | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 750cd91..a142c64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,13 +47,10 @@ ignore_missing_imports = true module = ["google.rpc.*", "grpcutil"] [tool.pytest.ini_options] -addopts = "-x -m \"not remote_calls\"" +addopts = "-x" log_level = "debug" minversion = "6.0" asyncio_mode = "auto" -markers = [ -"remote_calls: marks tests that make remote calls, not normally run" -] [tool.isort] ensure_newline_before_comments = true diff --git a/tests/insecure_client_test.py b/tests/insecure_client_test.py index b654ef5..114eea5 100644 --- a/tests/insecure_client_test.py +++ b/tests/insecure_client_test.py @@ -20,21 +20,21 @@ remote_host = "192.168.something.something" -@pytest.mark.remote_calls +@pytest.mark.skip(reason="Makes a remote call that we haven't yet supported in CI") async def test_normal_async_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): client = AsyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) await write_test_schema(client) -@pytest.mark.remote_calls +@pytest.mark.skip(reason="Makes a remote call that we haven't yet supported in CI") async def test_normal_sync_client_raises_error_on_insecure_remote_call(token): with pytest.raises(grpc.RpcError): client = SyncClient(f"{remote_host}:50051", insecure_bearer_token_credentials(token)) await write_test_schema(client) -@pytest.mark.remote_calls +@pytest.mark.skip(reason="Makes a remote call that we haven't yet supported in CI") async def test_insecure_client_makes_insecure_remote_call(token): insecure_client = InsecureClient(f"{remote_host}:50051", token) await write_test_schema(insecure_client)