Skip to content

Commit 769f2bd

Browse files
authored
Support secrets and signing (#186)
2 parents 727f451 + 833e531 commit 769f2bd

File tree

7 files changed

+92
-98
lines changed

7 files changed

+92
-98
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
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_SIGN_SECRET` to set the secret key.
15+
* Use the new `sign_secret` parameter in the `DispatchClient` constructor to set the secret key.
16+
* Added `auth_key` parameter to the `dispatch-cli` and thew env variable `DISPATCH_API_AUTH_KEY` to set the authentication key for the Dispatch API.
17+
1418

1519
## Bug Fixes
1620

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: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,26 @@ def format_line(key: str, value: str, color: str = "cyan") -> str:
171171
show_envvar=True,
172172
)
173173
@click.option(
174-
"--key",
175-
help="API key for authentication",
174+
"--api-key",
175+
help="API key for authentication (deprecated, use --auth-key or DISPATCH_API_AUTH_KEY)",
176176
envvar="DISPATCH_API_KEY",
177177
show_envvar=True,
178-
required=True,
178+
required=False,
179+
)
180+
@click.option(
181+
"--auth-key",
182+
help="API auth key for authentication",
183+
envvar="DISPATCH_API_AUTH_KEY",
184+
show_envvar=True,
185+
required=False,
186+
)
187+
@click.option(
188+
"--sign-secret",
189+
help="API signing secret for authentication",
190+
envvar="DISPATCH_API_SIGN_SECRET",
191+
show_envvar=True,
192+
required=False,
193+
default=None,
179194
)
180195
@click.option(
181196
"--raw",
@@ -185,29 +200,67 @@ def format_line(key: str, value: str, color: str = "cyan") -> str:
185200
default=False,
186201
)
187202
@click.pass_context
188-
async def cli(ctx: click.Context, url: str, key: str, raw: bool) -> None:
203+
async def cli( # pylint: disable=too-many-arguments, too-many-positional-arguments
204+
ctx: click.Context,
205+
url: str,
206+
api_key: str | None,
207+
auth_key: str | None,
208+
sign_secret: str | None,
209+
raw: bool,
210+
) -> None:
189211
"""Dispatch Service CLI."""
190212
if ctx.obj is None:
191213
ctx.obj = {}
192214

215+
key = auth_key or api_key
216+
217+
if not key:
218+
raise click.BadParameter(
219+
"You must provide an API auth key using --auth-key or "
220+
"the DISPATCH_API_AUTH_KEY environment variable."
221+
)
222+
193223
click.echo(f"Using API URL: {url}", err=True)
224+
click.echo(f"Using API Auth Key: {key[:4]}{'*' * 8}", err=True)
225+
226+
if sign_secret:
227+
if len(sign_secret) > 8:
228+
click.echo(
229+
f"Using API Signing Secret: {sign_secret[:4]}{'*' * 8}", err=True
230+
)
231+
else:
232+
click.echo("Using API Signing Secret (not shown).", err=True)
233+
234+
if api_key and auth_key is None:
235+
click.echo(
236+
click.style(
237+
"Deprecation Notice: The --api-key option and the DISPATCH_API_KEY environment "
238+
"variable are deprecated. "
239+
"Please use --auth-key or set the DISPATCH_API_AUTH_KEY environment variable.",
240+
fg="red",
241+
bold=True,
242+
),
243+
err=True,
244+
)
194245

195246
ctx.obj["client"] = DispatchApiClient(
196247
server_url=url,
197-
key=key,
248+
auth_key=key,
249+
sign_secret=sign_secret,
198250
connect=True,
199251
)
200252

201253
ctx.obj["params"] = {
202254
"url": url,
203-
"key": key,
255+
"auth_key": key,
256+
"sign_secret": sign_secret,
204257
}
205258

206259
ctx.obj["raw"] = raw
207260

208261
# Check if a subcommand was given
209262
if ctx.invoked_subcommand is None:
210-
await interactive_mode(url, key)
263+
await interactive_mode(url, key, sign_secret)
211264

212265

213266
@cli.command("list")
@@ -533,8 +586,9 @@ async def repl(
533586
obj: dict[str, Any],
534587
) -> None:
535588
"""Start an interactive interface."""
536-
click.echo(f"Parameters: {obj}")
537-
await interactive_mode(obj["params"]["url"], obj["params"]["key"])
589+
await interactive_mode(
590+
obj["params"]["url"], obj["params"]["auth_key"], obj["params"]["sign_secret"]
591+
)
538592

539593

540594
@cli.command()
@@ -574,7 +628,7 @@ async def delete(
574628
raise click.ClickException("Some deletions failed.")
575629

576630

577-
async def interactive_mode(url: str, key: str) -> None:
631+
async def interactive_mode(url: str, auth_key: str, sign_secret: str | None) -> None:
578632
"""Interactive mode for the CLI."""
579633
hist_file = os.path.expanduser("~/.dispatch_cli_history.txt")
580634
session: PromptSession[str] = PromptSession(history=FileHistory(filename=hist_file))
@@ -614,12 +668,11 @@ async def display_help() -> None:
614668
break
615669
else:
616670
# Split, but keep quoted strings together
617-
params = [
618-
"--url",
619-
url,
620-
"--key",
621-
key,
622-
] + click.parser.split_arg_string(user_input)
671+
params = (
672+
["--url", url, "--auth-key", auth_key]
673+
+ (["--sign-secret", sign_secret] if sign_secret else [])
674+
+ click.parser.split_arg_string(user_input)
675+
)
623676

624677
try:
625678
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
)

0 commit comments

Comments
 (0)