Skip to content

Commit 8de3818

Browse files
Add HMAC capabilities (frequenz-floss#181)
This adds the capability for HMAC based signing into the reporting client.
2 parents 19ea993 + d745b39 commit 8de3818

File tree

5 files changed

+44
-24
lines changed

5 files changed

+44
-24
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
## New Features
1212

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
13+
* Add HMAC generation capabilities.
14+
* The new CLI option "key" can be used to provide the server's key.
15+
* The client itself now has a "key" argument in the constructor.
1416

1517
## Bug Fixes
1618

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies = [
3232
"frequenz-client-common >= 0.3.0, < 0.4",
3333
"grpcio >=1.70.0, < 2",
3434
"protobuf >= 5.29.3, < 7",
35-
"frequenz-client-base >= 0.8.0, < 0.11.0",
35+
"frequenz-client-base >= 0.11.0, < 0.12.0",
3636
]
3737
dynamic = ["version"]
3838

@@ -156,6 +156,10 @@ disable = [
156156

157157
[tool.pytest.ini_options]
158158
addopts = "-vv"
159+
testpaths = ["tests", "src"]
160+
asyncio_mode = "auto"
161+
asyncio_default_fixture_loop_scope = "function"
162+
required_plugins = ["pytest-asyncio", "pytest-mock"]
159163
filterwarnings = [
160164
"error",
161165
"once::DeprecationWarning",
@@ -164,10 +168,6 @@ filterwarnings = [
164168
# chars as this is a regex
165169
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
166170
]
167-
testpaths = ["tests", "src"]
168-
asyncio_mode = "auto"
169-
asyncio_default_fixture_loop_scope = "function"
170-
required_plugins = ["pytest-asyncio", "pytest-mock"]
171171

172172
[tool.mypy]
173173
explicit_package_bases = true

src/frequenz/client/reporting/_client.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,22 @@
6464
class ReportingApiClient(BaseApiClient[ReportingStub]):
6565
"""A client for the Reporting service."""
6666

67+
# pylint: disable-next=too-many-arguments
6768
def __init__(
6869
self,
6970
server_url: str,
70-
key: str | None = None,
71+
*,
72+
auth_key: str | None = None,
73+
sign_secret: str | None = None,
7174
connect: bool = True,
7275
channel_defaults: ChannelOptions = ChannelOptions(), # default options
7376
) -> None:
7477
"""Create a new Reporting client.
7578
7679
Args:
7780
server_url: The URL of the Reporting service.
78-
key: The API key for the authorization.
81+
auth_key: The API key for the authorization.
82+
sign_secret: The secret to use for HMAC signing the message
7983
connect: Whether to connect to the server immediately.
8084
channel_defaults: The default channel options.
8185
"""
@@ -84,6 +88,8 @@ def __init__(
8488
ReportingStub,
8589
connect=connect,
8690
channel_defaults=channel_defaults,
91+
auth_key=auth_key,
92+
sign_secret=sign_secret,
8793
)
8894

8995
self._components_data_streams: dict[
@@ -129,8 +135,6 @@ def __init__(
129135
GrpcStreamBroadcaster[PBAggregatedStreamResponse, MetricSample],
130136
] = {}
131137

132-
self._metadata = (("key", key),) if key else ()
133-
134138
@property
135139
def stub(self) -> ReportingStub:
136140
"""The gRPC stub for the API."""
@@ -309,7 +313,7 @@ def stream_method() -> (
309313
AsyncIterable[PBReceiveMicrogridComponentsDataStreamResponse]
310314
):
311315
call_iterator = self.stub.ReceiveMicrogridComponentsDataStream(
312-
request, metadata=self._metadata
316+
request,
313317
)
314318
return cast(
315319
AsyncIterable[PBReceiveMicrogridComponentsDataStreamResponse],
@@ -493,9 +497,7 @@ def transform_response(
493497
def stream_method() -> (
494498
AsyncIterable[PBReceiveMicrogridSensorsDataStreamResponse]
495499
):
496-
call_iterator = self.stub.ReceiveMicrogridSensorsDataStream(
497-
request, metadata=self._metadata
498-
)
500+
call_iterator = self.stub.ReceiveMicrogridSensorsDataStream(request)
499501
return cast(
500502
AsyncIterable[PBReceiveMicrogridSensorsDataStreamResponse],
501503
call_iterator,
@@ -588,7 +590,7 @@ def transform_response(
588590
def stream_method() -> AsyncIterable[PBAggregatedStreamResponse]:
589591
call_iterator = (
590592
self.stub.ReceiveAggregatedMicrogridComponentsDataStream(
591-
request, metadata=self._metadata
593+
request,
592594
)
593595
)
594596

src/frequenz/client/reporting/cli/__main__.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,17 @@ def main() -> None:
7979
"--format", choices=["iter", "csv", "dict"], help="Output format", default="csv"
8080
)
8181
parser.add_argument(
82-
"--key",
82+
"--auth_key",
8383
type=str,
8484
help="API key",
8585
default=None,
8686
)
87+
parser.add_argument(
88+
"--sign_secret",
89+
type=str,
90+
help="The secret to use for generating HMAC signatures",
91+
default=None,
92+
)
8793
args = parser.parse_args()
8894
asyncio.run(
8995
run(
@@ -96,8 +102,9 @@ def main() -> None:
96102
states=args.states,
97103
bounds=args.bounds,
98104
service_address=args.url,
99-
key=args.key,
105+
auth_key=args.key,
100106
fmt=args.format,
107+
sign_secret=args.sign_secret,
101108
)
102109
)
103110

@@ -114,8 +121,9 @@ async def run( # noqa: DOC502
114121
states: bool,
115122
bounds: bool,
116123
service_address: str,
117-
key: str,
124+
auth_key: str,
118125
fmt: str,
126+
sign_secret: str | None,
119127
) -> None:
120128
"""Test the ReportingApiClient.
121129
@@ -129,13 +137,16 @@ async def run( # noqa: DOC502
129137
states: include states in the output
130138
bounds: include bounds in the output
131139
service_address: service address
132-
key: API key
140+
auth_key: API key
133141
fmt: output format
142+
sign_secret: secret used for creating HMAC signatures
134143
135144
Raises:
136145
ValueError: if output format is invalid
137146
"""
138-
client = ReportingApiClient(service_address, key)
147+
client = ReportingApiClient(
148+
service_address, auth_key=auth_key, sign_secret=sign_secret
149+
)
139150

140151
metrics = [Metric[mn] for mn in metric_names]
141152

tests/test_client_reporting.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,27 @@ async def test_client_initialization() -> None:
1919
# Parameters for the ReportingApiClient initialization
2020
server_url = "gprc://localhost:50051"
2121
key = "some-api-key"
22+
sign_secret = "hunter2"
2223
connect = True
2324
channel_defaults = ChannelOptions()
2425

2526
with patch.object(BaseApiClient, "__init__", return_value=None) as mock_base_init:
26-
client = ReportingApiClient(
27-
server_url, key=key, connect=connect, channel_defaults=channel_defaults
27+
ReportingApiClient(
28+
server_url,
29+
auth_key=key,
30+
connect=connect,
31+
channel_defaults=channel_defaults,
32+
sign_secret=sign_secret,
2833
) # noqa: F841
2934
mock_base_init.assert_called_once_with(
3035
server_url,
3136
ReportingStub,
3237
connect=connect,
3338
channel_defaults=channel_defaults,
39+
auth_key=key,
40+
sign_secret=sign_secret,
3441
)
3542

36-
assert client._metadata == (("key", key),) # pylint: disable=W0212
37-
3843

3944
def test_components_data_batch_is_empty_true() -> None:
4045
"""Test that the is_empty method returns True when the page is empty."""

0 commit comments

Comments
 (0)