change: use RFC 9535 syntax for jwt-role-claim-key config#4984
change: use RFC 9535 syntax for jwt-role-claim-key config#4984taimoorzaeem wants to merge 1 commit into
jwt-role-claim-key config#4984Conversation
0af8877 to
6746de2
Compare
jwt-role-claim-key configjwt-role-claim-key config
6746de2 to
6079929
Compare
|
@taimoorzaeem One sec, so #4603 was never released so this is not really a This gives us a chance to just have an
Does it cover the same use case as #4599? If so, I agree with removing #4603. Would like to see some docs to better understand the feature. |
Yeah not because of that, it is breaking because we lose #3813, which was released in
It covers it but is not part of the standard and I implemented the slice operator downstream. I was talking about removing the string slice because to me it seems like a bad practice to further slice a selected role value in the JWT claims. Just wanted to know any opinion on it. I will write a detailed comment on this, if that is still not clear.
Yeah, will do, that would make it clear. |
6079929 to
a871fcf
Compare
|
The new syntax for $.postgrest.roles[0] | [1:0]
# ^^^^^^^^^^^^^^^^^^
# The first part here is the standard JSON Path implemented in aeson-jsonpath library$.postgrest.roles[0] | [1:0]
# ^^^^^^^
# The second part is the optional slice part which is not part of the standard. Technically,
# JSON Path is a selector so it can't modify a selected value. We implemented the string slicing
# part in #4603. So to keep it, I have designed it to optionally specify that after
# a pipe '|' symbol in this PR.Now, I was talking a bit against keeping the slice operator in a previous comment because I think our goal is to follow the standard so we don't frequently change it and break things. That way, it is also easier to document and maintain it. I am not sure about how useful string slicing feature is. The use case in #4599 seems very niche to me. 😕 |
I agree on removing the slicing again. |
That looks good to me.
But the standard doesn't cover the use case in #4599 right? The workaround on #4594 (comment) pollutes the db 😿 I don't think there is a problem if later on we break this config, it's after all server-side so the change is easy and in one place; unlike new functionality at the API level on which a breaking change then requires updating thousands of clients.
IIRC it was based on another standard (ref) so maybe not that niche. If anything we should make more clear that WLCG is supported in the docs. |
👍
It is supported already if we go by the workaround on #4594 (comment).
IMO, the standard (ref) should be responsible for providing clean JWT profiles. |
|
Just a thought: Let's not pretend to support that WLCG thing if all we're supporting is extracting that first top-level group. The example in #4594 (comment) has multiple groups, including subgroups - and we're not anywhere close to properly supporting that. |
The above is not true as mentioned on #4594 (reply in thread), that spec defines the name to have a
Agree now, no objection from my side. |
This reverts commit fe0386e. As discussed in #4984 (comment).
a871fcf to
e0af2c9
Compare
| jwt-role-claim-key = "$$.postgrest.roles[?(@ == 'author')]" | ||
| jwt-role-claim-key = "$$.postgrest.roles[?@.search(@, '^au')]" |
There was a problem hiding this comment.
When specifying this in the config file, it requires double $ chars to get parsed correctly. Works fine with single $ char in both db and env configs. Maybe something related to configurator-ng? Hm, will look into it.
There was a problem hiding this comment.
configurator-ng allows to reference environment variables, right? I think that happens via $, so this must be escaped, yes.
We even have a hint at this behavior in the docs for jwt-secret: https://docs.postgrest.org/en/v14/references/configuration.html#jwt-secret
There was a problem hiding this comment.
configurator-ngallows to reference environment variables, right?
Yeah, ref: https://github.com/PostgREST/configurator-pg#file-format.
We even have a hint at this behavior in the docs for
jwt-secret: https://docs.postgrest.org/en/v14/references/configuration.html#jwt-secret
Cool, will document the same for this config as well.
efc2b04 to
6cfdc13
Compare
| - ``^==`` selects the first array element that starts with the right operand | ||
| - ``==^`` selects the first array element that ends with the right operand | ||
| - ``*==`` selects the first array element that contains the right operand | ||
| You can quickly try out JSON Path by visiting https://serdejsonpath.live/. If result has multiple values, first one gets selected. |
There was a problem hiding this comment.
| You can quickly try out JSON Path by visiting https://serdejsonpath.live/. If result has multiple values, first one gets selected. | |
| You can quickly try out JSON Path by visiting https://serdejsonpath.live/. If the result has multiple values, the first one gets selected. |
I'm confused by that second sentence - is it really related to the serdejsonpath.live website?
If not, it should probably be a separate paragraph. Maybe one of them as some kind of hint or so?
There was a problem hiding this comment.
Why should we promote that website? It's one more link that can break and postgres already provides jsonpath, if anything we should promote the latter.
There was a problem hiding this comment.
is it really related to the serdejsonpath.live website? If not, it should probably be a separate paragraph.
Yeah it is not related, will move it.
Why should we promote that website? It's one more link that can break and postgres already provides jsonpath, if anything we should promote the latter.
No specific reason to promote it, any website that can help users to quickly try it out could work here. postgres jsonpath isn't fully RFC 9535 compliant, for example: the following doesn't work when it should:
SELECT jsonb_path_query('{"names": ["alice", "bob"]}'::jsonb, '$.names[?search(@, "alice|bob")]');| escapeDoubleQuotes jsPathDump | ||
| where | ||
| -- When dumping, an extra $ is prefixed to escape the other $ char | ||
| jsPathDump = "$" <> JSP.dumpQuery query |
There was a problem hiding this comment.
I don't think this is enough. $ can occur multiple times in such a query. I'm not sure whether the json path syntax allows multiple references to the root document... but I'm sure there is a way to put a $ into a string / regular identifier somehow or have it in a regex for search() or elsewhere?
There was a problem hiding this comment.
Great catch! Yes, JSON Path syntax allows referencing the root path again. We should just escape the $ char all over.
6cfdc13 to
e5b61ca
Compare
| dumpJSPath (JSPIdx i) = "[" <> show i <> "]" | ||
| dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]" | ||
| where | ||
| expr = | ||
| case cond of | ||
| EqualsCond text -> " == " <> show text | ||
| NotEqualsCond text -> " != " <> show text | ||
| StartsWithCond text -> " ^== " <> show text | ||
| EndsWithCond text -> " ==^ " <> show text | ||
| ContainsCond text -> " *== " <> show text | ||
| -- | Dump JSPath | ||
| -- e.g. "$.property[0].attr.detail[?(@ == "role1")]" | ||
| dumpJSPath :: JSPath -> Text | ||
| dumpJSPath (JSPath query) = (escapeDollarChar . escapeDoubleQuotes) jsPathDump | ||
| where | ||
| jsPathDump = JSP.dumpQuery query | ||
| escapeDoubleQuotes = T.replace "\"" "\\\"" | ||
| -- When dumping, $ must be escaped | ||
| escapeDollarChar = T.replace "$" "$$" | ||
|
|
||
| -- | Evaluate JSPath on a JSON | ||
| walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value | ||
| walkJSPath x [] = x | ||
| walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest | ||
| walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest | ||
| walkJSPath (Just (JSON.Array ar)) [JSPFilter jspFilter] = case jspFilter of | ||
| EqualsCond txt -> findFirstMatch (==) txt ar | ||
| NotEqualsCond txt -> findFirstMatch (/=) txt ar | ||
| StartsWithCond txt -> findFirstMatch T.isPrefixOf txt ar | ||
| EndsWithCond txt -> findFirstMatch T.isSuffixOf txt ar | ||
| ContainsCond txt -> findFirstMatch T.isInfixOf txt ar | ||
| where | ||
| findFirstMatch matchWith pattern = find (\case | ||
| JSON.String txt -> pattern `matchWith` txt | ||
| _ -> False) | ||
| walkJSPath _ _ = Nothing | ||
| evaluateJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value | ||
| evaluateJSPath Nothing _ = Nothing | ||
| evaluateJSPath (Just json) (JSPath query) = role |
There was a problem hiding this comment.
Q: Are we covering in loadtests any performance improvements or regressions when using the JSPath? If not, could we add a new loadtest?
There was a problem hiding this comment.
Looks like for loadtest we only cover the default scenario of $.role:
postgrest/nix/tools/generate_targets.py
Lines 28 to 38 in fae6253
But yeah that's not good enough. We should add a loadtest I think. Will look into it.
There was a problem hiding this comment.
Let's make sure we're not adding yet another entirely separate loadtest for this.
It should be enough to just change some/all of the JWT tests to use at least some, possibly all, tokens with a more complex jwt-role-claim-key. It's entirely OK to test multiple things at once.
| .. note:: | ||
|
|
||
| The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_. | ||
| - If the result of JSON Path query contains multiple values, the first one gets selected. |
There was a problem hiding this comment.
| - If the result of JSON Path query contains multiple values, the first one gets selected. | |
| - If the result of the JSON Path query contains multiple values, the first one gets selected. |
There was a problem hiding this comment.
Maybe even better: "If the JSON Path query returns multiple values, ..."
| The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_. | ||
| - If the result of JSON Path query contains multiple values, the first one gets selected. | ||
| - Only when using the :ref:`file_config`, all ``$`` characters in the value must be escaped with an additional ``$`` char. For :ref:`env_variables_config` and :ref:`in_db_config`, only use a single ``$`` char. | ||
| - In our implementation, only the `search()` function from JSON Path is available for filtering. |
There was a problem hiding this comment.
| - In our implementation, only the `search()` function from JSON Path is available for filtering. | |
| - Currently, `search()` is the only JSON Path filter implemented. |
Can we maybe link "JSON Path filter" to an authoritative source on what other filters there should be?
There was a problem hiding this comment.
Sure, will link https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions here.
| - Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984 | ||
| + Removes the string comparison operators implemented in #3813 in favor of using JSON Path `search()` function |
There was a problem hiding this comment.
I think we need a vastly expanded changelog entry. "migration guide" style, with examples what people where using previously and should be using now etc.
WDYT?
There was a problem hiding this comment.
Yes, agreed. It would be much easier for users.
|
I have run loadtest with: Array IndexPerformance loss is little. # payload = {
# ... ,
# "postgrest" : { "roles" : ["other", "postgrest_test_author"] }
# }
# .postgrest.roles[1] (main)
Requests [total, rate, throughput] 200001, 6272.43, 6272.40
Duration [total, attack, wait] 31.886s, 31.886s, 420ns
Latencies [min, mean, 50, 90, 95, 99, max] 420ns, 158.564µs, 143.674µs, 245.273µs, 281.492µs, 344.152µs, 3.764ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:200000
# $.postgrest.roles[1] (HEAD)
Requests [total, rate, throughput] 200001, 5864.22, 5864.19
Duration [total, attack, wait] 34.105s, 34.105s, 551ns
Latencies [min, mean, 50, 90, 95, 99, max] 551ns, 169.711µs, 157.525µs, 273.823µs, 294.209µs, 360.819µs, 20.214ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:200000Regex searchPerformance loss is significant because regex search is expensive as compared to a prefix check. # payload = {
# ... ,
# "postgrest" : { "roles" : ["other", "postgrest_test_author"] }
# }
# .postgrest.roles[?(@ ^== "postgrest_")] (main)
Requests [total, rate, throughput] 200001, 6096.12, 6096.09
Duration [total, attack, wait] 32.808s, 32.808s, 401ns
Latencies [min, mean, 50, 90, 95, 99, max] 401ns, 163.184µs, 155.913µs, 250.21µs, 285.162µs, 349.578µs, 3.733ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:200000
# $.postgrest.roles[?search(@, '^postgrest_')] (HEAD)
Requests [total, rate, throughput] 200001, 3356.67, 3356.65
Duration [total, attack, wait] 59.583s, 59.583s, 912ns
Latencies [min, mean, 50, 90, 95, 99, max] 912ns, 296.792µs, 273.828µs, 432.092µs, 467.252µs, 556.505µs, 3.84ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:200000 |
I think we should investigate this, because it affects everyone. I didn't look at the code again, but are we making sure that we're only parsing or string config value for the jspath once? Where else could we lose that performance?
I wouldn't care about this. We're just comparing apples and oranges here, so not really a meaningful number. Also very much a special case anyway. |
e5b61ca to
d9e8170
Compare
Yes, only once. I found the culprit, I was doing an extra Updated loadtest result: Array IndexSlight improvement now: # payload = {
# ... ,
# "postgrest" : { "roles" : ["other", "postgrest_test_author"] }
# }
# .postgrest.roles[1]
Requests [total, rate, throughput] 200001, 6126.28, 6126.25
Duration [total, attack, wait] 32.646s, 32.646s, 501ns
Latencies [min, mean, 50, 90, 95, 99, max] 501ns, 162.392µs, 154.355µs, 268.83µs, 289.308µs, 354.268µs, 18.913ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:200000
# $.postgrest.roles[1]
Requests [total, rate, throughput] 200001, 6281.94, 6281.91
Duration [total, attack, wait] 31.837s, 31.837s, 501ns
Latencies [min, mean, 50, 90, 95, 99, max] 501ns, 158.278µs, 146.285µs, 256.881µs, 288.409µs, 354.087µs, 3.472ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:200000
|
b57d5f4 to
f3ea248
Compare
BREAKING CHANGE Breaks the string comparison operators implemented in PostgREST#3813. Those can be replaced with regex searches using JSON Path `search()` function. Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
f3ea248 to
90c3bee
Compare
BREAKING CHANGE
Breaks the string comparison operators implemented in #3813. Those can
be replaced with regex searches using JSON Path
search()function.Closes #4914.