-
Notifications
You must be signed in to change notification settings - Fork 25.2k
ES|QL: Fix queries with document level security on lookup indexes #120617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ES|QL: Fix queries with document level security on lookup indexes #120617
Conversation
Hi @luigidellaquila, I've created a changelog YAML for you. |
Pinging @elastic/es-analytical-engine (Team:Analytics) |
…to esql/fix_lookup_dls
Failures due to #120637 |
@@ -109,6 +112,16 @@ Collection<Failure> verify(LogicalPlan plan, BitSet partialMetrics) { | |||
return failures.failures(); | |||
} | |||
|
|||
private void checkLookupJoin(LogicalPlan p, Failures failures) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive by comment: LookupJoin
could implement PostAnalysisVerificationAware
and perform the check there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @bpintea, good point, doing it now
Muting a yaml bwc test called "alias" apparently is not possible: ``` task.skipTest("esql/190_lookup_join/alias", "LOOKUP JOIN does not support index aliases for now") ``` ``` * What went wrong: Execution failed for task ':x-pack:plugin:yamlRestCompatTestTransform'. > class com.fasterxml.jackson.databind.node.TextNode cannot be cast to class com.fasterxml.jackson.databind.node.ArrayNode (com.fasterxml.jackson.databind.node.TextNode and com.fasterxml.jackson.databind.node.ArrayNode are in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader$InstrumentingVisitableURLClassLoader @725f7fdd) ``` I need to mute this test in #120617
Muting a yaml bwc test called "alias" apparently is not possible: ``` task.skipTest("esql/190_lookup_join/alias", "LOOKUP JOIN does not support index aliases for now") ``` ``` * What went wrong: Execution failed for task ':x-pack:plugin:yamlRestCompatTestTransform'. > class com.fasterxml.jackson.databind.node.TextNode cannot be cast to class com.fasterxml.jackson.databind.node.ArrayNode (com.fasterxml.jackson.databind.node.TextNode and com.fasterxml.jackson.databind.node.ArrayNode are in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader$InstrumentingVisitableURLClassLoader @725f7fdd) ``` I need to mute this test in elastic#120617
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LookupService looks good to me. Thanks @luigidellaquila
Muting a yaml bwc test called "alias" apparently is not possible: ``` task.skipTest("esql/190_lookup_join/alias", "LOOKUP JOIN does not support index aliases for now") ``` ``` * What went wrong: Execution failed for task ':x-pack:plugin:yamlRestCompatTestTransform'. > class com.fasterxml.jackson.databind.node.TextNode cannot be cast to class com.fasterxml.jackson.databind.node.ArrayNode (com.fasterxml.jackson.databind.node.TextNode and com.fasterxml.jackson.databind.node.ArrayNode are in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader$InstrumentingVisitableURLClassLoader @725f7fdd) ``` I need to mute this test in #120617
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a first look, will continue tomorrow. Looking good so far, thanks @luigidellaquila !
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml
Show resolved
Hide resolved
...ugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java
Outdated
Show resolved
Hide resolved
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java
Outdated
Show resolved
Hide resolved
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java
Show resolved
Hide resolved
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java
Show resolved
Hide resolved
public void postAnalysisVerification(Failures failures) { | ||
super.postAnalysisVerification(failures); | ||
if (right() instanceof EsRelation esr) { | ||
if (esr.concreteIndices().contains(esr.indexPattern()) == false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also: Could there be weird edge cases where the name of the index contains e.g. an escaped wildcard character, so that an index pattern is being used, but coincidentally, it also matches a concrete index? There's no escaping in index names, right? Per the docs it doesn't seem so: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
I understand this is as edge case-y as it gets; but since avoiding aliases is required to ensure permissions are respected correctly, we better try to think about every possible scenario.
to make them easier to run on Serverless
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the iterations @luigidellaquila !
I have a final test suggestion, but otherwise LGTM.
assertThat(respMap.get("values"), equalTo(List.of(Arrays.asList(40.0, "sales")))); | ||
|
||
resp = runESQLCommand("dls_user2", "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN `lookup-user2` ON value | KEEP x, org"); | ||
assertOK(resp); | ||
respMap = entityAsMap(resp); | ||
assertThat( | ||
respMap.get("columns"), | ||
equalTo(List.of(Map.of("name", "x", "type", "double"), Map.of("name", "org", "type", "keyword"))) | ||
); | ||
assertThat(respMap.get("values"), equalTo(List.of(List.of(32.0, "marketing")))); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, these are very helpful. Thanks for adding!
failures.add( | ||
fail( | ||
esr, | ||
"invalid [{}] resolution in lookup mode to an index in [{}] indices", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"invalid [{}] resolution in lookup mode to an index in [{}] indices", | |
"invalid [{}] resolution in lookup mode to [{}] indices", |
Previously it was a slightly different message, not sure if it was changed intentionally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the iterations @luigidellaquila ! I'm feeling much more confident about the security of LOOKUP JOIN
now :)
TransportRequest transportRequest | ||
) { | ||
ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); | ||
try (ThreadContext.StoredContext unused = threadContext.stashWithOrigin(ClientHelper.ENRICH_ORIGIN)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, with @dnhatn 's suggestion the whole situation is more localized and more clear.
Personally, I'd still leave a comment in place because for people unfamiliar with this code, it can be unexpected that the call to ThreadContext.stashWithOrigin
will abandon the current user's authorization. (It's explained in stashWithOrigin
's javadoc, but that requires an extra hop.)
Map.of("name", "org", "type", "keyword") | ||
) | ||
) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, one last test suggestion - we can also add this in a separate PR if you're already about to make this ready to merge.
I just noticed: all of the fls test cases have permission to read the value
field, which is the join key.
I think it'd be interesting to test if forbidding this field for a user makes it so all look ups are null. And, if we wanna be extra paranoid, consider the case where the lookup index has duplicates for a given join key, so the LOOKUP JOIN
would lead to extra rows - but without permissions granted on the join key, no extra rows should be emitted. (Otherwise, users could exfiltrate statistics about fields they have no permissions on).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Alex, I still have a couple of things to push, I'll add this one as well.
I think the whole query will fail because the user doesn't see the join key in the schema, but I'll double-check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new tests confirm the above: if a role cannot see the join key in the lookup index, the query will fail with "Unknown column"
Thanks for the reviews folks! |
💔 Backport failed
You can use sqren/backport to manually backport by running |
There are some analyser tests for this, but I thought it useful to also have a yaml test for more coverage, since there was temporarily an issue elastic#120189 with this, later fixed in elastic#120617.
There are some analyser tests for this, but I thought it useful to also have a yaml test for more coverage, since there was temporarily an issue elastic#120189 with this, later fixed in elastic#120617.
There are some analyser tests for this, but I thought it useful to also have a yaml test for more coverage, since there was temporarily an issue elastic#120189 with this, later fixed in elastic#120617.
Fixes: #120509
Make ES|QL respect document level permissions on lookup indexes.
This fix exposes a problem with aliases as lookup indexes: if a user has permissions on an alias, but not on the underlying index, the query fails with an authorization error. This needs to be investigated further and fixed. For now we'll just disable support for aliases in LOOKUP JOIN command