mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-22 06:41:44 +01:00
commando: add support for parameters by array, parameter count.
Awkward to filter, but they're really practical for many commands. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
cf28cff398
commit
4ab09f7cfb
4 changed files with 128 additions and 42 deletions
|
@ -33,7 +33,9 @@ being run:
|
||||||
* time: the current UNIX time, e.g. "time<1656759180".
|
* time: the current UNIX time, e.g. "time<1656759180".
|
||||||
* id: the node_id of the peer, e.g. "id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605".
|
* id: the node_id of the peer, e.g. "id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605".
|
||||||
* method: the command being run, e.g. "method=withdraw".
|
* method: the command being run, e.g. "method=withdraw".
|
||||||
|
* pnum: the number of parameters. e.g. "pnum<2".
|
||||||
* pnameX: the parameter named X. e.g. "pnamedestination=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T".
|
* pnameX: the parameter named X. e.g. "pnamedestination=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T".
|
||||||
|
* parrN: the N'th parameter. e.g. "parr0=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T".
|
||||||
|
|
||||||
RESTRICTION FORMAT
|
RESTRICTION FORMAT
|
||||||
------------------
|
------------------
|
||||||
|
@ -56,11 +58,66 @@ a `\\`.
|
||||||
* `!`: only passes if the *name* does *not* exist. e.g. `pnamedestination!`.
|
* `!`: only passes if the *name* does *not* exist. e.g. `pnamedestination!`.
|
||||||
Every other operator except `#` fails if *name* does not exist!
|
Every other operator except `#` fails if *name* does not exist!
|
||||||
|
|
||||||
For example, the "readonly" restriction is actually two restrictions:
|
EXAMPLES
|
||||||
|
--------
|
||||||
|
|
||||||
|
This creates a fresh rune which can do anything:
|
||||||
|
|
||||||
|
$ lightning-cli commando-rune
|
||||||
|
{
|
||||||
|
"rune": "KUhZzNlECC7pYsz3QVbF1TqjIUYi3oyESTI7n60hLMs9MA==",
|
||||||
|
"unique_id": "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
We can add restrictions to that rune, like so:
|
||||||
|
|
||||||
|
$ lightning-cli commando-rune rune=KUhZzNlECC7pYsz3QVbF1TqjIUYi3oyESTI7n60hLMs9MA== restrictions=readonly
|
||||||
|
{
|
||||||
|
"rune": "NbL7KkXcPQsVseJ9TdJNjJK2KsPjnt_q4cE_wvc873I9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
|
||||||
|
"unique_id": "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
The "readonly" restriction is a short-cut for two restrictions:
|
||||||
|
|
||||||
1. `method^list|method^get|method=summary`: You may call list, get or summary.
|
1. `method^list|method^get|method=summary`: You may call list, get or summary.
|
||||||
2. `method/listdatastore`: But not listdatastore: that contains sensitive stuff!
|
2. `method/listdatastore`: But not listdatastore: that contains sensitive stuff!
|
||||||
|
|
||||||
|
We can do the same manually, like so:
|
||||||
|
|
||||||
|
$ lightning-cli commando-rune rune=KUhZzNlECC7pYsz3QVbF1TqjIUYi3oyESTI7n60hLMs9MA== restrictions='["method^list|method^get|method=summary","method/listdatastore"]'
|
||||||
|
{
|
||||||
|
"rune": "NbL7KkXcPQsVseJ9TdJNjJK2KsPjnt_q4cE_wvc873I9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
|
||||||
|
"unique_id": "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
Let's create a rune which lets a specific peer
|
||||||
|
(024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605)
|
||||||
|
run "listpeers" on themselves:
|
||||||
|
|
||||||
|
$ lightning-cli commando-rune restrictions='["id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605","method=listpeers","pnum=1","pnameid=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605|parr0=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605"]'
|
||||||
|
{
|
||||||
|
"rune": "FE8GHiGVvxcFqCQcClVRRiNE_XEeLYQzyG2jmqto4jM9MiZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDV8cGFycjA9MDI0YjlhMWZhOGUwMDZmMWUzOTM3ZjY1ZjY2YzQwOGU2ZGE4ZTFjYTcyOGVhNDMyMjJhNzM4MWRmMWNjNDQ5NjA1",
|
||||||
|
"unique_id": "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
This allows `listpeers` with 1 argument (`pnum=1`), which is either by name (`pnameid`), or position (`parr0`). We could shorten this in several ways: either allowing only positional or named parameters, or by testing the start of the parameters only. Here's an example which only checks the first 9 bytes of the `listpeers` parameter:
|
||||||
|
|
||||||
|
$ lightning-cli commando-rune restrictions='["id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605","method=listpeers","pnum=1","pnameid^024b9a1fa8e006f1e393|parr0^024b9a1fa8e006f1e393"]'
|
||||||
|
{
|
||||||
|
"rune": "fTQnfL05coEbiBO8SS0cvQwCcPLxE9c02pZCC6HRVEY9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5Mw==",
|
||||||
|
"unique_id": "3"
|
||||||
|
}
|
||||||
|
|
||||||
|
Before we give this to our peer, let's add another restriction: that
|
||||||
|
it only be usable for 24 hours from now. `date +%s` can give us the
|
||||||
|
current time in seconds:
|
||||||
|
|
||||||
|
$ lightning-cli commando-rune rune=fTQnfL05coEbiBO8SS0cvQwCcPLxE9c02pZCC6HRVEY9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5Mw== restrictions="t<$(($(date +%s) + 24*60*60))"
|
||||||
|
{
|
||||||
|
"rune": "Sh-jGdfO9UGByLvah2AHgc_VwgoNujckPNkxTx54ugg9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5MyZ0PDE2NTY4OTc5MjU=",
|
||||||
|
"unique_id": "3"
|
||||||
|
}
|
||||||
|
|
||||||
SHARING RUNES
|
SHARING RUNES
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,16 @@
|
||||||
"description": "method to invoke on peer"
|
"description": "method to invoke on peer"
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"description": "array of positional parameters"
|
||||||
|
},
|
||||||
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "parameters for method"
|
"description": "parameters for method"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"rune": {
|
"rune": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -201,14 +201,16 @@ static const char *check_condition(const tal_t *ctx,
|
||||||
return rune_alt_single_str(ctx, alt,
|
return rune_alt_single_str(ctx, alt,
|
||||||
cinfo->buf + cinfo->method->start,
|
cinfo->buf + cinfo->method->start,
|
||||||
cinfo->method->end - cinfo->method->start);
|
cinfo->method->end - cinfo->method->start);
|
||||||
|
} else if (streq(alt->fieldname, "pnum")) {
|
||||||
|
return rune_alt_single_int(ctx, alt, cinfo->params->size);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rest are params looksup: generate this once! */
|
/* Rest are params looksup: generate this once! */
|
||||||
if (cinfo->params) {
|
if (strmap_empty(&cinfo->cached_params)) {
|
||||||
/* Note: we require that params be an obj! */
|
|
||||||
const jsmntok_t *t;
|
const jsmntok_t *t;
|
||||||
size_t i;
|
size_t i;
|
||||||
|
|
||||||
|
if (cinfo->params->type == JSMN_OBJECT) {
|
||||||
json_for_each_obj(i, t, cinfo->params) {
|
json_for_each_obj(i, t, cinfo->params) {
|
||||||
char *pmemname = tal_fmt(tmpctx,
|
char *pmemname = tal_fmt(tmpctx,
|
||||||
"pname%.*s",
|
"pname%.*s",
|
||||||
|
@ -224,7 +226,12 @@ static const char *check_condition(const tal_t *ctx,
|
||||||
pmemname[off++] = '\0';
|
pmemname[off++] = '\0';
|
||||||
strmap_add(&cinfo->cached_params, pmemname, t+1);
|
strmap_add(&cinfo->cached_params, pmemname, t+1);
|
||||||
}
|
}
|
||||||
cinfo->params = NULL;
|
} else if (cinfo->params->type == JSMN_ARRAY) {
|
||||||
|
json_for_each_arr(i, t, cinfo->params) {
|
||||||
|
char *pmemname = tal_fmt(tmpctx, "parr%zu", i);
|
||||||
|
strmap_add(&cinfo->cached_params, pmemname, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ptok = strmap_get(&cinfo->cached_params, alt->fieldname);
|
ptok = strmap_get(&cinfo->cached_params, alt->fieldname);
|
||||||
|
@ -300,9 +307,9 @@ static void try_command(struct node_id *peer,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
params = json_get_member(buf, toks, "params");
|
params = json_get_member(buf, toks, "params");
|
||||||
if (params && params->type != JSMN_OBJECT) {
|
if (!params || (params->type != JSMN_OBJECT && params->type != JSMN_ARRAY)) {
|
||||||
commando_error(incoming, COMMANDO_ERROR_REMOTE,
|
commando_error(incoming, COMMANDO_ERROR_REMOTE,
|
||||||
"Params must be object");
|
"Params must be object or array");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rune = json_get_member(buf, toks, "rune");
|
rune = json_get_member(buf, toks, "rune");
|
||||||
|
@ -323,8 +330,9 @@ static void try_command(struct node_id *peer,
|
||||||
size_t i;
|
size_t i;
|
||||||
const jsmntok_t *t;
|
const jsmntok_t *t;
|
||||||
|
|
||||||
json_object_start(req->js, "params");
|
|
||||||
/* FIXME: This is ugly! */
|
/* FIXME: This is ugly! */
|
||||||
|
if (params->type == JSMN_OBJECT) {
|
||||||
|
json_object_start(req->js, "params");
|
||||||
json_for_each_obj(i, t, params) {
|
json_for_each_obj(i, t, params) {
|
||||||
json_add_jsonstr(req->js,
|
json_add_jsonstr(req->js,
|
||||||
json_strdup(tmpctx, buf, t),
|
json_strdup(tmpctx, buf, t),
|
||||||
|
@ -332,6 +340,17 @@ static void try_command(struct node_id *peer,
|
||||||
json_tok_full_len(t+1));
|
json_tok_full_len(t+1));
|
||||||
}
|
}
|
||||||
json_object_end(req->js);
|
json_object_end(req->js);
|
||||||
|
} else {
|
||||||
|
assert(params->type == JSMN_ARRAY);
|
||||||
|
json_array_start(req->js, "params");
|
||||||
|
json_for_each_arr(i, t, params) {
|
||||||
|
json_add_jsonstr(req->js,
|
||||||
|
NULL,
|
||||||
|
json_tok_full(buf, t),
|
||||||
|
json_tok_full_len(t));
|
||||||
|
}
|
||||||
|
json_array_end(req->js);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
json_object_start(req->js, "params");
|
json_object_start(req->js, "params");
|
||||||
json_object_end(req->js);
|
json_object_end(req->js);
|
||||||
|
|
|
@ -2660,6 +2660,12 @@ def test_commando_rune(node_factory):
|
||||||
rune5 = l1.rpc.commando_rune(rune4['rune'], "pnamelevel!|pnamelevel/io")
|
rune5 = l1.rpc.commando_rune(rune4['rune'], "pnamelevel!|pnamelevel/io")
|
||||||
assert rune5['rune'] == 'Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8='
|
assert rune5['rune'] == 'Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8='
|
||||||
assert rune5['unique_id'] == '3'
|
assert rune5['unique_id'] == '3'
|
||||||
|
rune6 = l1.rpc.commando_rune(rune5['rune'], "parr1!|parr1/io")
|
||||||
|
assert rune6['rune'] == '2Wh6F4R51D3esZzp-7WWG51OhzhfcYKaaI8qiIonaHE9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8mcGFycjEhfHBhcnIxL2lv'
|
||||||
|
assert rune6['unique_id'] == '3'
|
||||||
|
rune7 = l1.rpc.commando_rune(restrictions="pnum=0")
|
||||||
|
assert rune7['rune'] == 'QJonN6ySDFw-P5VnilZxlOGRs_tST1ejtd-bAYuZfjk9NCZwbnVtPTA='
|
||||||
|
assert rune7['unique_id'] == '4'
|
||||||
|
|
||||||
# Replace rune3 with a more useful timestamp!
|
# Replace rune3 with a more useful timestamp!
|
||||||
expiry = int(time.time()) + 15
|
expiry = int(time.time()) + 15
|
||||||
|
@ -2671,12 +2677,19 @@ def test_commando_rune(node_factory):
|
||||||
(rune3, "getinfo", {}),
|
(rune3, "getinfo", {}),
|
||||||
(rune4, "listpeers", {}),
|
(rune4, "listpeers", {}),
|
||||||
(rune5, "listpeers", {'id': l2.info['id']}),
|
(rune5, "listpeers", {'id': l2.info['id']}),
|
||||||
(rune5, "listpeers", {'id': l2.info['id'], 'level': 'broken'}))
|
(rune5, "listpeers", {'id': l2.info['id'], 'level': 'broken'}),
|
||||||
|
(rune6, "listpeers", [l2.info['id'], 'broken']),
|
||||||
|
(rune6, "listpeers", [l2.info['id']]),
|
||||||
|
(rune7, "listpeers", []),
|
||||||
|
(rune7, "getinfo", {}))
|
||||||
failures = ((rune2, "withdraw", {}),
|
failures = ((rune2, "withdraw", {}),
|
||||||
(rune2, "plugin", {'subcommand': 'list'}),
|
(rune2, "plugin", {'subcommand': 'list'}),
|
||||||
(rune3, "getinfo", {}),
|
(rune3, "getinfo", {}),
|
||||||
(rune4, "listnodes", {}),
|
(rune4, "listnodes", {}),
|
||||||
(rune5, "listpeers", {'id': l2.info['id'], 'level': 'io'}))
|
(rune5, "listpeers", {'id': l2.info['id'], 'level': 'io'}),
|
||||||
|
(rune6, "listpeers", [l2.info['id'], 'io']),
|
||||||
|
(rune7, "listpeers", [l2.info['id']]),
|
||||||
|
(rune7, "listpeers", {'id': l2.info['id']}))
|
||||||
|
|
||||||
for rune, cmd, params in successes:
|
for rune, cmd, params in successes:
|
||||||
l2.rpc.call(method='commando',
|
l2.rpc.call(method='commando',
|
||||||
|
@ -2708,14 +2721,3 @@ def test_commando_rune(node_factory):
|
||||||
'method': "listpeers",
|
'method': "listpeers",
|
||||||
'params': {}})
|
'params': {}})
|
||||||
assert exc_info.value.error['code'] == 0x4c51
|
assert exc_info.value.error['code'] == 0x4c51
|
||||||
|
|
||||||
# Remote doesn't allow array parameters.
|
|
||||||
l2.rpc.check_request_schemas = False
|
|
||||||
with pytest.raises(RpcError, match='Params must be object') as exc_info:
|
|
||||||
l2.rpc.call(method='commando',
|
|
||||||
payload={'peer_id': l1.info['id'],
|
|
||||||
'rune': rune5['rune'],
|
|
||||||
'method': "listpeers",
|
|
||||||
'params': [l2.info['id'], 'io']})
|
|
||||||
assert exc_info.value.error['code'] == 0x4c50
|
|
||||||
l2.rpc.check_request_schemas = True
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue