Skip to content

Commit 49ec839

Browse files
authored
Merge pull request #536 from digital-asset/python-connection-auth-fix
python: Fix a bug that broke specifying auth at the connection level.
2 parents f375613 + f7a8cc2 commit 49ec839

File tree

4 files changed

+9
-153
lines changed

4 files changed

+9
-153
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.4.2
1+
8.4.3

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
[tool.poetry]
55
name = "dazl"
6-
version = "8.4.2"
6+
version = "8.4.3"
77
description = "high-level Ledger API client for Daml ledgers"
88
license = "Apache-2.0"
99
authors = ["Davin K. Tanabe <davin.tanabe@digitalasset.com>"]

python/dazl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@
5757
pass
5858

5959

60-
__version__ = "8.4.2"
60+
__version__ = "8.4.3"

python/dazl/ledger/grpc/channel.py

Lines changed: 6 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,22 @@
33

44
from __future__ import annotations
55

6-
from typing import Any, AsyncIterable, Callable, Iterable, TypeVar, cast
76
from urllib.parse import urlparse
87

9-
from grpc import (
10-
AuthMetadataContext,
11-
AuthMetadataPlugin,
12-
AuthMetadataPluginCallback,
13-
ChannelCredentials,
14-
composite_channel_credentials,
15-
metadata_call_credentials,
16-
ssl_channel_credentials,
17-
)
18-
from grpc.aio import (
19-
Channel,
20-
ClientCallDetails,
21-
ClientInterceptor,
22-
StreamStreamCall,
23-
StreamStreamClientInterceptor,
24-
StreamUnaryCall,
25-
StreamUnaryClientInterceptor,
26-
UnaryStreamCall,
27-
UnaryStreamClientInterceptor,
28-
UnaryUnaryCall,
29-
UnaryUnaryClientInterceptor,
30-
insecure_channel,
31-
secure_channel,
32-
)
8+
from grpc import ssl_channel_credentials
9+
from grpc.aio import Channel, insecure_channel, secure_channel
3310

34-
from ..auth import get_token
3511
from ..config import Config
3612

3713
__all__ = ["create_channel"]
3814

39-
RequestType = TypeVar("RequestType")
40-
RequestIterableType = Iterable[Any] | AsyncIterable[Any]
41-
ResponseIterableType = AsyncIterable[Any]
42-
4315

4416
def create_channel(config: Config) -> Channel:
4517
"""
4618
Create a :class:`Channel` for the specified configuration.
19+
20+
Note that the returned channel never carries authorization; the caller is expected to supply
21+
auth tokens on every request.
4722
"""
4823
u = urlparse(config.url.url)
4924

@@ -64,125 +39,6 @@ def create_channel(config: Config) -> Channel:
6439
certificate_chain=config.ssl.cert,
6540
)
6641

67-
# The grpc Credential objects do not actually define a formal interface, and are
68-
# used interchangeably in the code.
69-
#
70-
# Additionally there are some incorrect rules in the grpc-stubs typing rules that force
71-
# us to work around the type system.
72-
credentials = cast(
73-
ChannelCredentials,
74-
composite_channel_credentials(
75-
credentials, metadata_call_credentials(GrpcAuth(config), name="auth gateway")
76-
),
77-
)
7842
return secure_channel(u.netloc, credentials, tuple(options))
79-
8043
else:
81-
# Python/C++ libraries refuse to allow "credentials" objects to be passed around on
82-
# non-TLS channels, but they don't check interceptors; use an interceptor to inject
83-
# an Authorization header instead
84-
#
85-
# There must be a distinct interceptor instance for each of
86-
# the four Request/Response arities. There is a known issue in
87-
# the underlying gRPC code that prevents reuse of a single
88-
# multiply-inherited interceptor.
89-
#
90-
# https://github.com/grpc/grpc/issues/31442
91-
return insecure_channel(
92-
u.netloc,
93-
options,
94-
interceptors=[
95-
cast(ClientInterceptor, GrpcAuthUnaryUnaryClientInterceptor(config)),
96-
cast(ClientInterceptor, GrpcAuthUnaryStreamClientInterceptor(config)),
97-
cast(ClientInterceptor, GrpcAuthStreamUnaryClientInterceptor(config)),
98-
cast(ClientInterceptor, GrpcAuthStreamStreamClientInterceptor(config)),
99-
],
100-
) # type: ignore
101-
102-
103-
class GrpcAuth(AuthMetadataPlugin):
104-
def __init__(self, config: Config):
105-
self._config = config
106-
107-
def __call__(self, context: AuthMetadataContext, callback: AuthMetadataPluginCallback):
108-
# This overly verbose type signature is here to satisfy mypy and grpc-stubs
109-
options = list[tuple[str, str | bytes]]()
110-
111-
token = get_token(self._config.access.token)
112-
if token:
113-
# note: gRPC headers MUST be lowercased
114-
options.append(("authorization", "Bearer " + token))
115-
116-
callback(tuple(options), None)
117-
118-
119-
class GrpcAuthInterceptor:
120-
"""
121-
An interceptor that injects "Authorization" metadata into a request.
122-
123-
This works around the fact that the C++ gRPC libraries (which Python is built on) highly
124-
discourage sending authorization data over the wire unless the connection is protected with TLS.
125-
"""
126-
127-
# NOTE: There are a number of typing errors in the grpc.aio classes, so we're ignoring a handful
128-
# of lines until those problems are addressed.
129-
130-
def __init__(self, config: Config):
131-
self._config = config
132-
133-
def _modify_client_call_details(self, client_call_details: ClientCallDetails):
134-
if (
135-
client_call_details.metadata is not None
136-
and "authorization" not in client_call_details.metadata
137-
):
138-
token = get_token(self._config.access.token)
139-
if token is not None:
140-
client_call_details.metadata.add("authorization", f"Bearer {token}")
141-
142-
return client_call_details
143-
144-
145-
class GrpcAuthUnaryUnaryClientInterceptor(GrpcAuthInterceptor, UnaryUnaryClientInterceptor):
146-
async def intercept_unary_unary(
147-
self,
148-
continuation: Callable[[ClientCallDetails, RequestType], UnaryUnaryCall],
149-
client_call_details: ClientCallDetails,
150-
request: RequestType,
151-
) -> UnaryUnaryCall | RequestType:
152-
return await continuation(self._modify_client_call_details(client_call_details), request)
153-
154-
155-
class GrpcAuthUnaryStreamClientInterceptor(GrpcAuthInterceptor, UnaryStreamClientInterceptor):
156-
async def intercept_unary_stream(
157-
self,
158-
continuation: Callable[[ClientCallDetails, RequestType], UnaryStreamCall],
159-
client_call_details: ClientCallDetails,
160-
request: RequestType,
161-
) -> ResponseIterableType | UnaryStreamCall:
162-
return await continuation( # type: ignore
163-
self._modify_client_call_details(client_call_details), request
164-
)
165-
166-
167-
class GrpcAuthStreamUnaryClientInterceptor(GrpcAuthInterceptor, StreamUnaryClientInterceptor):
168-
async def intercept_stream_unary( # type: ignore
169-
self,
170-
continuation: Callable[[ClientCallDetails, RequestType], StreamUnaryCall],
171-
client_call_details: ClientCallDetails,
172-
request_iterator: RequestIterableType,
173-
) -> StreamUnaryCall:
174-
return await continuation(
175-
self._modify_client_call_details(client_call_details), request_iterator # type: ignore
176-
)
177-
178-
179-
class GrpcAuthStreamStreamClientInterceptor(GrpcAuthInterceptor, StreamStreamClientInterceptor):
180-
async def intercept_stream_stream(
181-
self,
182-
continuation: Callable[[ClientCallDetails, RequestType], StreamStreamCall],
183-
client_call_details: ClientCallDetails,
184-
request_iterator: RequestIterableType,
185-
) -> ResponseIterableType | StreamStreamCall:
186-
return await continuation( # type: ignore
187-
self._modify_client_call_details(client_call_details), request_iterator # type: ignore
188-
)
44+
return insecure_channel(u.netloc, options)

0 commit comments

Comments
 (0)