From 6141c81f194a39e6611d70f5cbce1a3bc118e3e4 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 15:50:33 +0000 Subject: [PATCH] Expose caller authorization for extension requests --- doc/cli/README.md | 11 +++++++ limacharlie/commands/extension.py | 9 ++++-- .../unit/test_cli_extension_impersonation.py | 29 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_cli_extension_impersonation.py diff --git a/doc/cli/README.md b/doc/cli/README.md index 9a82d758..70f1bd33 100644 --- a/doc/cli/README.md +++ b/doc/cli/README.md @@ -28,6 +28,17 @@ stop the command before curl runs. Header names are matched without regard to ca URLs, request bodies and other headers are not redacted. Inspect debug output before sharing it, including when combining `--debug-curl` with `--debug`. +## Extension requests + +Some extension actions require the caller's authenticated permissions. Use +`--impersonate` to forward your identity through the SDK's caller authorization +path. This grants the extension no additional permissions; requests omit that +identity unless you select the flag. + +```bash +limacharlie extension request --name my-ext --action run --impersonate --data '{"key":"value"}' +``` + ## Output Formats All commands support `--output` to control the format: diff --git a/limacharlie/commands/extension.py b/limacharlie/commands/extension.py index 5824573c..74740d76 100644 --- a/limacharlie/commands/extension.py +++ b/limacharlie/commands/extension.py @@ -229,6 +229,10 @@ def schema(ctx, name) -> None: Example: limacharlie extension request --name ext-zeek --action status limacharlie extension request --name my-ext --action run --data '{"key": "value"}' + limacharlie extension request --name my-ext --action run --impersonate + +Use --impersonate when an action requires your permissions. It forwards your +authenticated identity to the extension; it grants no additional permissions. """ register_explain("extension.request", _EXPLAIN_REQUEST) @@ -237,14 +241,15 @@ def schema(ctx, name) -> None: @click.option("--name", required=True, help="Extension name.") @click.option("--action", required=True, help="Action to invoke.") @click.option("--data", default=None, help="JSON string with request data.") +@click.option("--impersonate", is_flag=True, help="Authorize the extension action with your authenticated permissions.") @pass_context -def request(ctx, name, action, data) -> None: +def request(ctx, name, action, data, impersonate) -> None: parsed_data = None if data is not None: parsed_data = json.loads(data) org = _get_org(ctx) extensions = Extensions(org) - result = extensions.request(name, action, data=parsed_data) + result = extensions.request(name, action, data=parsed_data, is_impersonated=impersonate) _output(ctx, result) diff --git a/tests/unit/test_cli_extension_impersonation.py b/tests/unit/test_cli_extension_impersonation.py new file mode 100644 index 00000000..0d64b96d --- /dev/null +++ b/tests/unit/test_cli_extension_impersonation.py @@ -0,0 +1,29 @@ +"""Extension writes can explicitly use the caller's existing authorization.""" + +from unittest.mock import MagicMock, patch + +import pytest +from click.testing import CliRunner + +from limacharlie.cli import cli + + +@pytest.mark.parametrize("impersonate", [False, True]) +def test_request_forwards_caller_identity_only_when_selected(impersonate): + org = MagicMock() + org.oid = "11111111-2222-3333-4444-555555555555" + org.client._jwt = "synthetic-caller-jwt" + org.client.request.return_value = {"data": {"ok": True}} + args = ["--oid", org.oid, "--output", "json", "extension", "request", + "--name", "test-extension", "--action", "run", "--data", '{"value":1}'] + if impersonate: + args.append("--impersonate") + with patch("limacharlie.commands.extension._get_org", return_value=org): + result = CliRunner().invoke(cli, args) + assert result.exit_code == 0, result.output + org.client.request.assert_called_once() + params = org.client.request.call_args.kwargs["params"] + assert ("impersonator_jwt" in params) is impersonate + if impersonate: + assert params["impersonator_jwt"] == "synthetic-caller-jwt" + org.client.refresh_jwt.assert_not_called()