Skip to content

Commit fc4c515

Browse files
committed
Support secrets & signing
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 727f451 commit fc4c515

File tree

5 files changed

+46
-31
lines changed

5 files changed

+46
-31
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-
* `Dispatch.started_at(now: datetime)` was added as alternative to the `started` property for when users want to use the same `now` for multiple calls, ensuring deterministic return values with respect to the same `now`.
13+
* Support secrets for signing and verifying messages.
14+
* Use the new env variable `DISPATCH_API_SECRET` to set the secret key.
15+
* Use the new `sign_secret` parameter in the `DispatchClient` constructor to set the secret key.
1416

1517
## Bug Fixes
1618

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ requires-python = ">= 3.11, < 4"
3838
dependencies = [
3939
"typing-extensions >= 4.13.0, < 5",
4040
"frequenz-api-dispatch == 1.0.0-rc2",
41-
"frequenz-client-base >= 0.8.0, < 0.12.0",
41+
"frequenz-client-base >= 0.11.0, < 0.12.0",
4242
"frequenz-client-common >= 0.3.2, < 0.4.0",
4343
"frequenz-core >= 1.0.2, < 2.0.0",
4444
"grpcio >= 1.70.0, < 2",

src/frequenz/client/dispatch/__main__.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,20 @@ def format_line(key: str, value: str, color: str = "cyan") -> str:
171171
show_envvar=True,
172172
)
173173
@click.option(
174-
"--key",
174+
"--api-key",
175175
help="API key for authentication",
176176
envvar="DISPATCH_API_KEY",
177177
show_envvar=True,
178178
required=True,
179179
)
180+
@click.option(
181+
"--sign-secret",
182+
help="API signing secret for authentication",
183+
envvar="DISPATCH_API_SIGN_SECRET",
184+
show_envvar=True,
185+
required=False,
186+
default=None,
187+
)
180188
@click.option(
181189
"--raw",
182190
is_flag=True,
@@ -185,29 +193,36 @@ def format_line(key: str, value: str, color: str = "cyan") -> str:
185193
default=False,
186194
)
187195
@click.pass_context
188-
async def cli(ctx: click.Context, url: str, key: str, raw: bool) -> None:
196+
async def cli(
197+
ctx: click.Context, url: str, api_key: str, sign_secret: str | None, raw: bool
198+
) -> None:
189199
"""Dispatch Service CLI."""
190200
if ctx.obj is None:
191201
ctx.obj = {}
192202

193203
click.echo(f"Using API URL: {url}", err=True)
204+
click.echo(f"Using API Key: {api_key}", err=True)
205+
if sign_secret:
206+
click.echo(f"Using API Secret: {sign_secret}", err=True)
194207

195208
ctx.obj["client"] = DispatchApiClient(
196209
server_url=url,
197-
key=key,
210+
auth_key=api_key,
211+
sign_secret=sign_secret,
198212
connect=True,
199213
)
200214

201215
ctx.obj["params"] = {
202216
"url": url,
203-
"key": key,
217+
"api_key": api_key,
218+
"sign_secret": sign_secret,
204219
}
205220

206221
ctx.obj["raw"] = raw
207222

208223
# Check if a subcommand was given
209224
if ctx.invoked_subcommand is None:
210-
await interactive_mode(url, key)
225+
await interactive_mode(url, api_key, sign_secret)
211226

212227

213228
@cli.command("list")
@@ -533,8 +548,9 @@ async def repl(
533548
obj: dict[str, Any],
534549
) -> None:
535550
"""Start an interactive interface."""
536-
click.echo(f"Parameters: {obj}")
537-
await interactive_mode(obj["params"]["url"], obj["params"]["key"])
551+
await interactive_mode(
552+
obj["params"]["url"], obj["params"]["api_key"], obj["params"]["sign_secret"]
553+
)
538554

539555

540556
@cli.command()
@@ -574,7 +590,7 @@ async def delete(
574590
raise click.ClickException("Some deletions failed.")
575591

576592

577-
async def interactive_mode(url: str, key: str) -> None:
593+
async def interactive_mode(url: str, api_key: str, sign_secret: str | None) -> None:
578594
"""Interactive mode for the CLI."""
579595
hist_file = os.path.expanduser("~/.dispatch_cli_history.txt")
580596
session: PromptSession[str] = PromptSession(history=FileHistory(filename=hist_file))
@@ -614,12 +630,11 @@ async def display_help() -> None:
614630
break
615631
else:
616632
# Split, but keep quoted strings together
617-
params = [
618-
"--url",
619-
url,
620-
"--key",
621-
key,
622-
] + click.parser.split_arg_string(user_input)
633+
params = (
634+
["--url", url, "--api-key", api_key]
635+
+ (["--sign-secret", sign_secret] if sign_secret else [])
636+
+ click.parser.split_arg_string(user_input)
637+
)
623638

624639
try:
625640
await cli.main(args=params, standalone_mode=False)

src/frequenz/client/dispatch/_client.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def __init__(
6060
self,
6161
*,
6262
server_url: str,
63-
key: str,
63+
auth_key: str,
64+
sign_secret: str | None = None,
6465
connect: bool = True,
6566
call_timeout: timedelta = timedelta(seconds=60),
6667
stream_timeout: timedelta = timedelta(minutes=5),
@@ -69,7 +70,8 @@ def __init__(
6970
7071
Args:
7172
server_url: The URL of the server to connect to.
72-
key: API key to use for authentication.
73+
auth_key: API key to use for authentication.
74+
sign_secret: Optional secret for signing requests.
7375
connect: Whether to connect to the service immediately.
7476
call_timeout: Timeout for gRPC calls, default is 60 seconds.
7577
stream_timeout: Timeout for gRPC streams, default is 5 minutes.
@@ -82,8 +84,9 @@ def __init__(
8284
port=DEFAULT_DISPATCH_PORT,
8385
ssl=SslOptions(enabled=True),
8486
),
87+
auth_key=auth_key,
88+
sign_secret=sign_secret,
8589
)
86-
self._metadata = (("key", key),)
8790
self._streams: dict[
8891
MicrogridId,
8992
GrpcStreamBroadcaster[StreamMicrogridDispatchesResponse, DispatchEvent],
@@ -138,7 +141,8 @@ async def list(
138141
139142
```python
140143
client = DispatchApiClient(
141-
key="key",
144+
auth_key="key",
145+
sign_secret="secret", # Optional so far
142146
server_url="grpc://dispatch.url.goes.here.example.com"
143147
)
144148
async for page in client.list(microgrid_id=MicrogridId(1)):
@@ -199,7 +203,7 @@ def to_interval(
199203
response = await cast(
200204
Awaitable[ListMicrogridDispatchesResponse],
201205
self.stub.ListMicrogridDispatches(
202-
request, metadata=self._metadata, timeout=self._call_timeout_seconds
206+
request, timeout=self._call_timeout_seconds
203207
),
204208
)
205209

@@ -256,7 +260,6 @@ def _get_stream(
256260
AsyncIterator[StreamMicrogridDispatchesResponse],
257261
self.stub.StreamMicrogridDispatches(
258262
request,
259-
metadata=self._metadata,
260263
timeout=self._stream_timeout_seconds,
261264
),
262265
),
@@ -327,7 +330,6 @@ async def create( # pylint: disable=too-many-positional-arguments
327330
Awaitable[CreateMicrogridDispatchResponse],
328331
self.stub.CreateMicrogridDispatch(
329332
request.to_protobuf(),
330-
metadata=self._metadata,
331333
timeout=self._call_timeout_seconds,
332334
),
333335
)
@@ -419,9 +421,7 @@ async def update(
419421

420422
response = await cast(
421423
Awaitable[UpdateMicrogridDispatchResponse],
422-
self.stub.UpdateMicrogridDispatch(
423-
msg, metadata=self._metadata, timeout=self._call_timeout_seconds
424-
),
424+
self.stub.UpdateMicrogridDispatch(msg, timeout=self._call_timeout_seconds),
425425
)
426426

427427
return Dispatch.from_protobuf(response.dispatch)
@@ -443,9 +443,7 @@ async def get(
443443
)
444444
response = await cast(
445445
Awaitable[GetMicrogridDispatchResponse],
446-
self.stub.GetMicrogridDispatch(
447-
request, metadata=self._metadata, timeout=self._call_timeout_seconds
448-
),
446+
self.stub.GetMicrogridDispatch(request, timeout=self._call_timeout_seconds),
449447
)
450448
return Dispatch.from_protobuf(response.dispatch)
451449

@@ -464,6 +462,6 @@ async def delete(
464462
await cast(
465463
Awaitable[None],
466464
self.stub.DeleteMicrogridDispatch(
467-
request, metadata=self._metadata, timeout=self._call_timeout_seconds
465+
request, timeout=self._call_timeout_seconds
468466
),
469467
)

src/frequenz/client/dispatch/test/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(
2424
self,
2525
) -> None:
2626
"""Initialize the mock client."""
27-
super().__init__(server_url="mock", key=ALL_KEY, connect=False)
27+
super().__init__(server_url="mock", auth_key=ALL_KEY, connect=False)
2828
self._stuba: FakeService = FakeService()
2929

3030
@property

0 commit comments

Comments
 (0)