commando: make rune alternatives a JSON array.

This avoids having to escape | or &, though we still allow that for
the deprecation period.

To detect deprecated usage, we insist that alternatives are *always*
an array (which could be loosened later), but that also means that
restrictions must *always* be an array for now.

Before:

```
# invoice, description either A or B
lightning-cli commando-rune '["method=invoice","pnamedescription=A|pnamedescription=B"]'
# invoice, description literally 'A|B'
lightning-cli commando-rune '["method=invoice","pnamedescription=A\\|B"]'
```

After:

```
# invoice, description either A or B
lightning-cli commando-rune '[["method=invoice"],["pnamedescription=A", "pnamedescription=B"]]'
# invoice, description literally 'A|B'
lightning-cli commando-rune '[["method=invoice"],["pnamedescription=A|B"]]'
```

Changelog-Deprecated: JSON-RPC: `commando-rune` restrictions is always an array, each element an array of alternatives.  Replaces a string with `|`-separators, so no escaping necessary except for `\\`.
This commit is contained in:
Rusty Russell 2022-09-14 15:00:26 +09:30 committed by Christian Decker
parent d57d87ea3a
commit a6d4756d08
4 changed files with 87 additions and 35 deletions

View File

@ -23,11 +23,10 @@ If *rune* is supplied, the restrictions are simple appended to that
*restrictions* can be the string "readonly" (creates a rune which
allows most *get* and *list* commands, and the *summary* command), or
an array of restrictions, or a single resriction.
an array of restrictions.
Each restriction is a set of one or more alternatives, such as "method
is listpeers", or "method is listpeers OR time is before 2023".
Alternatives use a simple language to examine the command which is
Each restriction is an array of one or more alternatives, such as "method
is listpeers", or "method is listpeers OR time is before 2023". Alternatives use a simple language to examine the command which is
being run:
* time: the current UNIX time, e.g. "time<1656759180".
@ -41,10 +40,10 @@ being run:
RESTRICTION FORMAT
------------------
Restrictions are one or more alternatives, separated by `|`. Each
Restrictions are one or more alternatives. Each
alternative is *name* *operator* *value*. The valid names are shown
above. If a value contains `|`, `&` or `\\`, it must be preceeded by
a `\\`.
above. Note that if a value contains `\\`, it must be preceeded by another `\\`
to form valid JSON:
* `=`: passes if equal ie. identical. e.g. `method=withdraw`
* `/`: not equals, e.g. `method/withdraw`
@ -80,12 +79,12 @@ We can add restrictions to that rune, like so:
The "readonly" restriction is a short-cut for two restrictions:
1. `method^list|method^get|method=summary`: You may call list, get or summary.
2. `method/listdatastore`: But not listdatastore: that contains sensitive stuff!
1. `["method^list", "method^get", "method=summary"]`: You may call list, get or summary.
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"]'
$ lightning-cli commando-rune rune=KUhZzNlECC7pYsz3QVbF1TqjIUYi3oyESTI7n60hLMs9MA== restrictions='[["method^list", "method^get", "method=summary"],["method/listdatastore"]]'
{
"rune": "NbL7KkXcPQsVseJ9TdJNjJK2KsPjnt_q4cE_wvc873I9MCZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl",
"unique_id": "0"
@ -95,7 +94,7 @@ 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"]'
$ lightning-cli commando-rune restrictions='[["id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605"],["method=listpeers"],["pnum=1"],["pnameid=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605","parr0=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605"]]'
{
"rune": "FE8GHiGVvxcFqCQcClVRRiNE_XEeLYQzyG2jmqto4jM9MiZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDV8cGFycjA9MDI0YjlhMWZhOGUwMDZmMWUzOTM3ZjY1ZjY2YzQwOGU2ZGE4ZTFjYTcyOGVhNDMyMjJhNzM4MWRmMWNjNDQ5NjA1",
"unique_id": "2"
@ -103,7 +102,7 @@ run "listpeers" on themselves:
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"]'
$ lightning-cli commando-rune restrictions='[["id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605"],["method=listpeers"],["pnum=1"],["pnameid^024b9a1fa8e006f1e393", "parr0^024b9a1fa8e006f1e393"]'
{
"rune": "fTQnfL05coEbiBO8SS0cvQwCcPLxE9c02pZCC6HRVEY9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5Mw==",
"unique_id": "3"
@ -114,7 +113,7 @@ it only be usable for 24 hours from now (`time<`), and that it can only
be used twice a minute (`rate=2`). `date +%s` can give us the current
time in seconds:
$ lightning-cli commando-rune rune=fTQnfL05coEbiBO8SS0cvQwCcPLxE9c02pZCC6HRVEY9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5Mw== restrictions='["time<'$(($(date +%s) + 24*60*60))'","rate=2"]'
$ lightning-cli commando-rune rune=fTQnfL05coEbiBO8SS0cvQwCcPLxE9c02pZCC6HRVEY9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5Mw== restrictions='[["time<'$(($(date +%s) + 24*60*60))'","rate=2"]]'
{
"rune": "tU-RLjMiDpY2U0o3W1oFowar36RFGpWloPbW9-RuZdo9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5MyZ0aW1lPDE2NTY5MjA1MzgmcmF0ZT0y",
"unique_id": "3"

View File

@ -14,12 +14,18 @@
"type": "array",
"description": "array of restrictions to add to rune",
"items": {
"type": "string"
"type": "array",
"items": {
"type": "string"
}
}
},
{
"type": "string",
"description": "single restrictions to add to rune, or readonly."
"enum": [
"readonly"
],
"description": "readonly string to indicate standard readonly restrictions."
}
]
}

View File

@ -762,11 +762,14 @@ static struct rune_restr **readonly_restrictions(const tal_t *ctx)
return restrs;
}
/* \| is not valid JSON, so they use \\|: undo it! */
static struct rune_restr *rune_restr_from_json(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok)
static struct rune_altern *rune_altern_from_json(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok)
{
struct rune_altern *alt;
size_t condoff;
/* We still need to unescape here, for \\ -> \. JSON doesn't
* allow unnecessary \ */
const char *unescape;
struct json_escape *e = json_escape_string_(tmpctx,
buffer + tok->start,
@ -774,7 +777,51 @@ static struct rune_restr *rune_restr_from_json(const tal_t *ctx,
unescape = json_escape_unescape(tmpctx, e);
if (!unescape)
return NULL;
return rune_restr_from_string(ctx, unescape, strlen(unescape));
condoff = rune_altern_fieldname_len(unescape, strlen(unescape));
if (!rune_condition_is_valid(unescape[condoff]))
return NULL;
alt = tal(ctx, struct rune_altern);
alt->fieldname = tal_strndup(alt, unescape, condoff);
alt->condition = unescape[condoff];
alt->value = tal_strdup(alt, unescape + condoff + 1);
return alt;
}
static struct rune_restr *rune_restr_from_json(const tal_t *ctx,
const char *buffer,
const jsmntok_t *tok)
{
const jsmntok_t *t;
size_t i;
struct rune_restr *restr;
/* \| is not valid JSON, so they use \\|: undo it! */
if (deprecated_apis && tok->type == JSMN_STRING) {
const char *unescape;
struct json_escape *e = json_escape_string_(tmpctx,
buffer + tok->start,
tok->end - tok->start);
unescape = json_escape_unescape(tmpctx, e);
if (!unescape)
return NULL;
return rune_restr_from_string(ctx, unescape, strlen(unescape));
}
restr = tal(ctx, struct rune_restr);
/* FIXME: after deprecation removed, allow singletons again! */
if (tok->type != JSMN_ARRAY)
return NULL;
restr->alterns = tal_arr(restr, struct rune_altern *, tok->size);
json_for_each_arr(i, t, tok) {
restr->alterns[i] = rune_altern_from_json(restr->alterns,
buffer, t);
if (!restr->alterns[i])
return tal_free(restr);
}
return restr;
}
static struct command_result *param_restrictions(struct command *cmd,
@ -794,7 +841,7 @@ static struct command_result *param_restrictions(struct command *cmd,
(*restrs)[i] = rune_restr_from_json(*restrs, buffer, t);
if (!(*restrs)[i]) {
return command_fail_badparam(cmd, name, buffer, t,
"not a valid restriction");
"not a valid restriction (should be array)");
}
}
} else {
@ -802,7 +849,7 @@ static struct command_result *param_restrictions(struct command *cmd,
(*restrs)[0] = rune_restr_from_json(*restrs, buffer, tok);
if (!(*restrs)[0])
return command_fail_badparam(cmd, name, buffer, tok,
"not a valid restriction");
"not a valid restriction (should be array)");
}
return NULL;
}

View File

@ -2649,11 +2649,11 @@ def test_commando_rune(node_factory):
# "rune": "zKc2W88jopslgUBl0UE77aEe5PNCLn5WwqSusU_Ov3A9MA=="
# $ l1-cli commando-rune restrictions=readonly
# "rune": "1PJnoR9a7u4Bhglj2s7rVOWqRQnswIwUoZrDVMKcLTY9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl"
# $ l1-cli commando-rune restrictions='time>1656675211'
# $ l1-cli commando-rune restrictions='[[time>1656675211]]'
# "rune": "RnlWC4lwBULFaObo6ZP8jfqYRyTbfWPqcMT3qW-Wmso9MiZ0aW1lPjE2NTY2NzUyMTE="
# $ l1-cli commando-rune restrictions='["id^022d223620a359a47ff7","method=listpeers"]'
# $ l1-cli commando-rune restrictions='[["id^022d223620a359a47ff7"],["method=listpeers"]]'
# "rune": "lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz"
# $ l1-cli commando-rune lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz 'pnamelevel!|pnamelevel/io'
# $ l1-cli commando-rune lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz '[pnamelevel!,pnamelevel/io]'
# "rune": "Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8="
rune1 = l1.rpc.commando_rune()
@ -2662,31 +2662,31 @@ def test_commando_rune(node_factory):
rune2 = l1.rpc.commando_rune(restrictions="readonly")
assert rune2['rune'] == '1PJnoR9a7u4Bhglj2s7rVOWqRQnswIwUoZrDVMKcLTY9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl'
assert rune2['unique_id'] == '1'
rune3 = l1.rpc.commando_rune(restrictions="time>1656675211")
rune3 = l1.rpc.commando_rune(restrictions=[["time>1656675211"]])
assert rune3['rune'] == 'RnlWC4lwBULFaObo6ZP8jfqYRyTbfWPqcMT3qW-Wmso9MiZ0aW1lPjE2NTY2NzUyMTE='
assert rune3['unique_id'] == '2'
rune4 = l1.rpc.commando_rune(restrictions=["id^022d223620a359a47ff7", "method=listpeers"])
rune4 = l1.rpc.commando_rune(restrictions=[["id^022d223620a359a47ff7"], ["method=listpeers"]])
assert rune4['rune'] == 'lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz'
assert rune4['unique_id'] == '3'
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['unique_id'] == '3'
rune6 = l1.rpc.commando_rune(rune5['rune'], "parr1!|parr1/io")
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")
rune7 = l1.rpc.commando_rune(restrictions=[["pnum=0"]])
assert rune7['rune'] == 'QJonN6ySDFw-P5VnilZxlOGRs_tST1ejtd-bAYuZfjk9NCZwbnVtPTA='
assert rune7['unique_id'] == '4'
rune8 = l1.rpc.commando_rune(rune7['rune'], "rate=3")
rune8 = l1.rpc.commando_rune(rune7['rune'], [["rate=3"]])
assert rune8['rune'] == 'kSYFx6ON9hr_ExcQLwVkm1ABnvc1TcMFBwLrAVee0EA9NCZwbnVtPTAmcmF0ZT0z'
assert rune8['unique_id'] == '4'
rune9 = l1.rpc.commando_rune(rune8['rune'], "rate=1")
rune9 = l1.rpc.commando_rune(rune8['rune'], [["rate=1"]])
assert rune9['rune'] == 'O8Zr-ULTBKO3_pKYz0QKE9xYl1vQ4Xx9PtlHuist9Rk9NCZwbnVtPTAmcmF0ZT0zJnJhdGU9MQ=='
assert rune9['unique_id'] == '4'
# Test rune with \|.
weirdrune = l1.rpc.commando_rune(restrictions=["method=invoice",
"pnamedescription=@tipjar\\|jb55@sendsats.lol"])
weirdrune = l1.rpc.commando_rune(restrictions=[["method=invoice"],
["pnamedescription=@tipjar|jb55@sendsats.lol"]])
with pytest.raises(RpcError, match='Not authorized:'):
l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
@ -2758,7 +2758,7 @@ def test_commando_rune(node_factory):
# Replace rune3 with a more useful timestamp!
expiry = int(time.time()) + 15
rune3 = l1.rpc.commando_rune(restrictions="time<{}".format(expiry))
rune3 = l1.rpc.commando_rune(restrictions=[["time<{}".format(expiry)]])
successes = ((rune1, "listpeers", {}),
(rune2, "listpeers", {}),