Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 7 additions & 2 deletions limacharlie/commands/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)


Expand Down
29 changes: 29 additions & 0 deletions tests/unit/test_cli_extension_impersonation.py
Original file line number Diff line number Diff line change
@@ -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()
Loading