-
Notifications
You must be signed in to change notification settings - Fork 24.3k
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
Fix Security's expression resolver to not remove unavailable but authorized names #92625
Fix Security's expression resolver to not remove unavailable but authorized names #92625
Conversation
112f864
to
ce1e4d2
Compare
a8954ee
to
6c116f0
Compare
.setIndicesOptions(IndicesOptions.lenientExpandOpen())::get, | ||
GetAliasesAction.NAME, | ||
"aliases_only" | ||
); |
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 ignore_unavailable
request option does not currently work for "unavailable" aliases, see:
Line 375 in 844bb4c
private static List<String> replaceWildcardsWithAuthorizedAliases(String[] aliases, List<String> authorizedAliases) { |
It worked in this particular test by coincidence.
In this particular test the test_1
index is authorized but non-existent, so the request was previously authorized after the request was rewritten to empty. But now this is not the case anymore.
test_1
is authorized, so it is not considered "unavailable" from Security's POV,
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.
From end-user's perspective, strictly speaking, this is a slight behaviour change:
- Status code from 404 to 403
- Response content from containing found aliases (and missing aliases) to unauthorized error response
Just want to call it out explicitly. But I don't think it is a real concern because the existing response is not a 200 anyway and it feels like an edge case.
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.
Yes, it's indeed a behavior change for an edge case.
Rereading this now, after a week, I realise I haven't done a good job explaining what's going on here.
Let me try again:
The get aliases action in this test uses the "lenient expand open" request option, which means that missing explicit indices are ignored (and that wildcards can expand to empty).
The test_1
index does not exist, and it is the only index in the request, so when retrieving the non_authorized
alias for it, because of the request option, this previously returned a 200 empty result, because there were ultimately no indices to return the aliases for.
But, after the change, Security does not care if test_1
exists or not (Core does, and if Security lets the request through, given the request option, it will return the empty result). For Security, the test_1
is authorized, but the alias non_authorized
is not, hence it will then return a 403 because of the alias. For indices, the request option ignore_unavailable
silently discards unauthorized index names, but the option never discards unauthorized aliases (but maybe it should?).
Therefore, clients will notice behavior differences for this API, when all the indices are authorized but missing, the request option tells to ignore missing indices, and there are unauthorized aliases (missing or not). Before they got a 200 empty, after they get a 403.
They got
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.
Before they got a 200 empty, after they get a 403.
Does it return 200 today? At REST layer, the response code is set to 404 if there is any explicitly requested alias not found.
Lines 130 to 133 in c0e702c
if (missingAliases.isEmpty()) { | |
status = RestStatus.OK; | |
} else { | |
status = RestStatus.NOT_FOUND; |
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.
You're correct. It does return 404, in the existing code, with Security enabled, when user is only authorized for the test_1
index that does not exist, but it is not authorized for the non_authorized
alias.
}, name -> { | ||
final IndexAbstraction indexAbstraction = lookup.get(name); | ||
if (indexAbstraction == null) { | ||
return false; | ||
} | ||
if (includeDataStreams) { | ||
// missing but authorized resources should be handled downstream in the action handler, not here in the Security filter | ||
return predicate.testMissingResource(name); | ||
} else { | ||
// We check the parent data stream first if there is one. For testing requested indices, this is most likely | ||
// more efficient than checking the index name first because we recommend grant privileges over data stream | ||
// instead of backing indices. | ||
if (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream())) { | ||
return true; | ||
} else { | ||
return predicate.test(indexAbstraction); | ||
} | ||
} else { | ||
return indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM && predicate.test(indexAbstraction); | ||
return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream())) | ||
|| predicate.test(indexAbstraction); | ||
} |
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.
This is the core of the change proposed by this PR.
This predicate here is used to remove explicit names when rewriting the request if ignore_unavailable
is true
, see:
elasticsearch/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
Line 112 in 844bb4c
} else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction)) { |
After this expression rewrite, the request goes into the authz stage, where every expression item is checked again:
Line 510 in 844bb4c
private boolean isActionGranted(final String action, final Map<String, IndexResource> requestedResources) { |
The intent is that the test here, ie:
final IndexAbstraction indexAbstraction = lookup.get(name);
if (indexAbstraction == null) {
// missing but authorized resources should be handled downstream in the action handler, not here in the Security filter
return predicate.testMissingResource(name);
} else {
// We check the parent data stream first if there is one. For testing requested indices, this is most likely
// more efficient than checking the index name first because we recommend grant privileges over data stream
// instead of backing indices.
return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream()))
|| predicate.test(indexAbstraction);
}
be equivalent to the test in IndicesPermission#isActionGranted
, such that only unauthorized names be removed when rewriting the expression with ignore_unavailable=true.
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.
Can we unify the loigc of test
and testMissingResource
into a single method? It is better that callers do not have to worry about deciding on which method to use. It should be possible if the predicate takes an object that encapsulate both name
and indexAbstraction
, something similar to IndicesPermission#IndexResource
.
I think ideally it would be great to reuse the existing IndexResource
and unify the logic between this method and isActionGranted
. It will likely help understand this part of code. But it does not seem to be an easy job. So we can tackle it later gradually. But I think we can avoid having both test
and testMissingResource
as suggest earlier. It also helps the IsResourceAuthorizedPredicate
to be just a simple Predicate
and avoid having to implement its own and
method.
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.
There's a subtle problem with making the IsResourceAuthorizedPredicate
take an IndexResource
parameter.
The problem is that, when the resource exists, it matters if the "is resource authorized" predicate is called in the context of wildcard expansion, or in the context of checking explicit names.
The difference is that the predicate in the context of checking explicit names first tests on the parent datastream (if applicable) and then it tests on the resource itself. This is not the case for the predicate in the wildcard expansion context.
So, in essence, the IsResourceAuthorizedPredicate
would still have to implement two slightly different test methods.
Not exactly what you asked for, but I could be hidding this whole
final IndexAbstraction indexAbstraction = lookup.get(name);
if (indexAbstraction == null) {
// missing but authorized resources should be handled downstream in the action handler, not here in the Security filter
return predicate.testMissingResource(name);
} else {
// We check the parent data stream first if there is one. For testing requested indices, this is most likely
// more efficient than checking the index name first because we recommend grant privileges over data stream
// instead of backing indices.
return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream()))
|| predicate.test(indexAbstraction);
}
block inside a method in the IsResourceAuthorizedPredicate
class, something of the sorts of #testExplicitName
. This should be equivalent (and someday replace) the isActionGranted
method.
What do you think?
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.
My preference is to avoid having a second test method for IsResourceAuthorizedPredicate
. It is surprising that a predicate has more than one test method and you need some effort in deciding which one to use.
Based on your comment here #92625 (comment)
Can we make IsResourceAuthorizedPredicate
implement BiPredicate
if you prefer not having a IndexResource
like class?
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.
As we've discussed earlier today, I've reimplemented the IsResourceAuthorizedPredicate
as a BiPredicate<String, IndexAbstraction>
, see 878ba9e :
public static class IsResourceAuthorizedPredicate implements BiPredicate<String, IndexAbstraction> {
private final BiPredicate<String, IndexAbstraction> biPredicate;
// public for tests
public IsResourceAuthorizedPredicate(StringMatcher resourceNameMatcher, StringMatcher additionalNonDatastreamNameMatcher) {
this((String name, @Nullable IndexAbstraction indexAbstraction) -> {
assert indexAbstraction == null || name.equals(indexAbstraction.getName());
return resourceNameMatcher.test(name)
|| (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(name));
});
}
private IsResourceAuthorizedPredicate(BiPredicate<String, IndexAbstraction> biPredicate) {
this.biPredicate = biPredicate;
}
public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate other) {
return new IsResourceAuthorizedPredicate(this.biPredicate.and(other.biPredicate));
}
public final boolean test(IndexAbstraction indexAbstraction) {
return test(indexAbstraction.getName(), indexAbstraction);
}
@Override
public boolean test(String name, @Nullable IndexAbstraction indexAbstraction) {
return biPredicate.test(name, indexAbstraction);
}
private static boolean isPartOfDatastream(IndexAbstraction indexAbstraction) {
return indexAbstraction != null
&& (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM || indexAbstraction.getParentDataStream() != null);
}
}
For convenience, I also added a test(IndexAbstraction indexAbstraction)
in addition to test(String name, @Nullable IndexAbstraction indexAbstraction)
, to avoid the test(index.getName(), index)
pattern.
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.
However, I'm hesitant to change the predicate further, in order to move the logic that tests the parent datastream of a backing index. I still can't see any serious issues, but I very much prefer that such a change be in a separate PR.
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.
OK let's keep it separate. Thanks for consideration.
} | ||
|
||
@Override | ||
protected String getInexistentWildcardErrorMessage() { |
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.
Some EQL tests assert the audit log messages, and some will change because the transport request, which gets audited, is not rewritten anymore to erase authorized but unavailable indices.
Hi @albertzaharovits, I've created a changelog YAML for you. |
Pinging @elastic/es-security (Team:Security) |
@jakelandis and @ywangd Thank you for the review, I acknowledge it must've been laborious. |
@elasticmachine update branch |
@elasticmachine update branch |
@elasticmachine update branch |
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.
LGTM , but would appreciate a second set of eyes for final approval.
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.
LGTM
A few minor comments for your consideration.
.../src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java
Outdated
Show resolved
Hide resolved
}, name -> { | ||
final IndexAbstraction indexAbstraction = lookup.get(name); | ||
if (indexAbstraction == null) { | ||
return false; | ||
} | ||
if (includeDataStreams) { | ||
// missing but authorized resources should be handled downstream in the action handler, not here in the Security filter | ||
return predicate.testMissingResource(name); | ||
} else { | ||
// We check the parent data stream first if there is one. For testing requested indices, this is most likely | ||
// more efficient than checking the index name first because we recommend grant privileges over data stream | ||
// instead of backing indices. | ||
if (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream())) { | ||
return true; | ||
} else { | ||
return predicate.test(indexAbstraction); | ||
} | ||
} else { | ||
return indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM && predicate.test(indexAbstraction); | ||
return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream())) | ||
|| predicate.test(indexAbstraction); | ||
} |
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.
OK let's keep it separate. Thanks for consideration.
...ity/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java
Outdated
Show resolved
Hide resolved
...ity/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java
Show resolved
Hide resolved
...ity/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java
Outdated
Show resolved
Hide resolved
...c/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java
Outdated
Show resolved
Hide resolved
@elasticmachine run elasticsearch-ci/part-2 |
For explicit (non-wildcard) names in expressions from requests with the
ignoreUnavailable
request option set totrue
, the Security filter removes names for "unavailable" resources. This behavior is theoretically correct, but in practice the cluster state might not be recovered at the point in time when the Security filter does the rewrite (this causes problems, see #90215); In any case, the logic to remove "unavailable" names is unnecessarily duplicated from Core's expression resolver.This PR makes the expression resolver in the Security filter to NOT:
It will only remove names of unauthorized resources, and let the unavailable names go through to be handled (to be ignored or throw "not found" exception) by the expression resolver in Core.
Fixes: #90215