Skip to content
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

A new search API for API keys - core search function #75335

Merged
merged 199 commits into from
Aug 3, 2021

Conversation

ywangd
Copy link
Member

@ywangd ywangd commented Jul 14, 2021

This PR adds a new API for searching API keys. The API supports searching API keys with a controlled list of field names and a subset of Query DSL. It also provides a translation layer between the field names used in the REST layer and those in the index layer. This is to prevent tight coupling between the user facing request and index mappings so that they can evolve separately.

Compared to the Get API key API, this new search API automatically applies calling user's security context similar to regular searches, e.g. if the user has only manage_own_api_key privilege, only keys owned by the user are returned in the search response.

Relates: #71023
Supercedes: #73705

@ywangd ywangd added >enhancement :Security/Security Security issues without another label v8.0.0 v7.15.0 labels Jul 14, 2021
@elasticmachine elasticmachine added the Team:Security Meta label for security team label Jul 14, 2021
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-security (Team:Security)

Comment on lines 79 to 82
} else if (request instanceof QueryApiKeyRequest) {
final QueryApiKeyRequest queryApiKeyRequest = (QueryApiKeyRequest) request;
queryApiKeyRequest.setFilterForCurrentUser();
return true;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, this is the simple way of applying calling user's security context. It works but is probably not a long term solution. I am open to alternatives.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to do this in an interceptor, but I don't think that's very easy to do.

Logically, it would be a case of checking whether the Role has unconditional access to some action, but that's not actually easy to do with an artibrary Authz Engine, and what action would you use?

We could take an approach like we do with the create_doc index privilege and have different logical action names for the unrestricted query vs the restricted query.

That is,

 public final class QueryApiKeyAction extends ActionType<QueryApiKeyResponse> {

    public static final String QUERY_ALL = NAME + "/all"
}

And then have special case code somewhere like:

if( role.grantsAction(QueryApiKeyAction.QUERY_ALL ) ) {
   request.setFilterForCurrentUser(false);
} else {
   request.setFilterForCurrentUser(true);
}

I don't have a great suggestion, but this approach really bothers me and I don't want to set a precedent that gets reused for other actions. This is definitely not how we want to make this work in general.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume your example is about the interceptor approach. We don't have such an interceptor for cluster actions, but I assume it would work similarly to the interceptors for index requests. The interface is

void intercept(
  RequestInfo requestInfo, 
  AuthorizationEngine authorizationEngine, 
  AuthorizationInfo authorizationInfo,
  ActionListener<Void> listener);

Are you concerning about that, inside this method, we need to only rely on the fact that both AuthorizationEngine and AuthorizationInfo are interfaces and we need do it in a way that does not make RBACEngine and our Role special for at least in this method? It's challenging even with the "separate action name" approach. None of the existing methods fit this purpose really well. We can sorta fit it into AuthorizationEngine#authorizeClusterAction. But it would be better if we can add a new method to AuthorizationEngine. I think we'll need high bandwidth conversation on this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came up with a new approach inspired by your comment. The idea is to authorize the action a second time if it fails for the first time and it is the QueryApiKey action. I think this approach is promising because:

  • It is simple. A few lines change in AuthorizationService
  • In theory works with arbitrary AuthorizationEngine and AuthorizationInfo
  • Does not need a separate action name and works kinda smoothly with the manage_own_api_key privilege.

It has the overhead of a second authorization for the QueryApiKey action. But I think it is acceptable because it authorizing on name is pretty fast and it does not affect any other actions.

Comment on lines +1128 to +1147
public void queryApiKeys(ApiKeyBoolQueryBuilder apiKeyBoolQueryBuilder, ActionListener<QueryApiKeyResponse> listener) {
ensureEnabled();
final ActionListener<Collection<ApiKey>> wrappedListener = ActionListener.wrap(apiKeyInfos -> {
if (apiKeyInfos.isEmpty()) {
logger.debug("No active api keys found for query [{}]", apiKeyBoolQueryBuilder);
listener.onResponse(QueryApiKeyResponse.emptyResponse());
} else {
listener.onResponse(new QueryApiKeyResponse(apiKeyInfos));
}
}, listener::onFailure);

final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
if (frozenSecurityIndex.indexExists() == false) {
wrappedListener.onResponse(Collections.emptyList());
} else if (frozenSecurityIndex.isAvailable() == false) {
wrappedListener.onFailure(frozenSecurityIndex.getUnavailableReason());
} else {
findApiKeys(apiKeyBoolQueryBuilder, true, true, wrappedListener);
}
}
Copy link
Member Author

@ywangd ywangd Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being, this uses the existing findApiKeys method which in turns uses scroll. It will be changed to use normal search once the API supports pagination parameters like size, from etc.

@ywangd
Copy link
Member Author

ywangd commented Jul 15, 2021

@elasticmachine update branch

Comment on lines 79 to 82
} else if (request instanceof QueryApiKeyRequest) {
final QueryApiKeyRequest queryApiKeyRequest = (QueryApiKeyRequest) request;
queryApiKeyRequest.setFilterForCurrentUser();
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to do this in an interceptor, but I don't think that's very easy to do.

Logically, it would be a case of checking whether the Role has unconditional access to some action, but that's not actually easy to do with an artibrary Authz Engine, and what action would you use?

We could take an approach like we do with the create_doc index privilege and have different logical action names for the unrestricted query vs the restricted query.

That is,

 public final class QueryApiKeyAction extends ActionType<QueryApiKeyResponse> {

    public static final String QUERY_ALL = NAME + "/all"
}

And then have special case code somewhere like:

if( role.grantsAction(QueryApiKeyAction.QUERY_ALL ) ) {
   request.setFilterForCurrentUser(false);
} else {
   request.setFilterForCurrentUser(true);
}

I don't have a great suggestion, but this approach really bothers me and I don't want to set a precedent that gets reused for other actions. This is definitely not how we want to make this work in general.

Comment on lines 45 to 53
} else {
final BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) processedQuery;
finalQuery.minimumShouldMatch(boolQueryBuilder.minimumShouldMatch());
finalQuery.adjustPureNegative(boolQueryBuilder.adjustPureNegative());
boolQueryBuilder.must().forEach(finalQuery::must);
boolQueryBuilder.should().forEach(finalQuery::should);
boolQueryBuilder.mustNot().forEach(finalQuery::mustNot);
boolQueryBuilder.filter().forEach(finalQuery::filter);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this special case necessary? Isn't it simpler to always wrap it in a new bool so we know everything works the same way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention is to always have the final query to be an instance of ApiKeyBoolQueryBuilder because it has special handling to disable certain fields on the SearchExecutionContext level. So yes this is necessary because it converts a normal boolQueryBuilder to a ApiKeyBoolQueryBuilder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can't we just have the if case used all the time? Why doesn't that do what we want?

           QueryBuilder processedQuery = doProcess(queryBuilder);
           finalQuery.must(processedQuery);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok. Sorry I see what you mean now. You are right! The must clause can simply accept any query builder including a bool query (now it's kinda obvious after you pointed it out ...) It's a better approach and I assume the end results are the same after lucene level query rewrite. Will update. Thanks!

return newQuery;
} else if (qb instanceof MatchAllQueryBuilder) {
return qb;
} else if (qb instanceof IdsQueryBuilder) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support ids? Isn't that exposing that API key docs have their doc id set to the api-key-id?

Maybe we need to chat about this one. I think getting the right behaviour for the API Key Id search is important.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth between ids and term. I understand the ids approach exposes the internal knowledge of an API Key document use the API Key ID as its document ID. And this is non-ideal. But the term approach has its own problem:

  • How do we handle case-sensitivity? Is it not allowed? It is not obvious and could be trappy either way.
  • It's not possible to support prefix, wildcard etc for the _id field, but it is again not obivious just from the Query DSL side.

(I remember I had some other reasons as well, but I cannot remember them right now.)

The bottom line is that the ID field will be an anonamly regardless of the approach. So we need to choose something that are less concerning. If "exposing the doc ID" is a big enough concern, I am happy to take the term (terms) approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation.
I assume if in the future the id for the API Key document wasn't exactly the same as the API Key id, (but the key id was a field inside the doc) then we could translate an ids query into a term query.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's my thinking as well. Translate ids to a term query (in future) is easy, but not so much the other way around.

ywangd and others added 6 commits July 30, 2021 15:57
…tic#75308)

Elasticsearch's server sometimes has to do things 
differently in order to handle searchable snapshots 
shards. In such cases it relies on index settings 
and hard coded values to know if a shard is a 
searchable snapshot one.

This commit adds a new SearchableSnapshotsSettings 
class in server that provides methods to check if 
an index is a searchable snapshot index. This class 
also contains the names of some index settings 
related to searchable snapshots that are required 
by the server.
…c#75187)

Warning related transformations missed the possibility to apply per single test only.
Also a warning changed in elastic#67158 for indices.close so this PR also applies the transformation for 7.x test

relates elastic#51816
This commit upgrades the existing SSPL licensed "ssl-config" library
to include additional features that are supported by the X-Pack SSL
library.

This commit does not make any changes to X-Pack to use these new
features - it introduces them in preparation for their future use by
X-Pack.

The reindex module is updated to reflect API changes in ssl-config
…snapshots (elastic#74977)

Today there is no relationship between the lifecycle of a 
snapshot mounted as an index and the lifecycle of index 
itself. This lack of relationship makes it possible to delete 
a snapshot in a repository while the mounted index still 
exists, causing the shards to fail in the future.

On the other hand creating a snapshot that contains a 
single index to later mount it as a searchable snapshot 
index becomes more and more natural for users and 
ILM; but it comes with the risk to forget to delete the
 snapshot when the searchable snapshot index is 
deleted from the cluster, "leaking" snapshots in 
repositories.

We'd like to improve the situation and provide a way 
to automatically delete the snapshot when the mounted 
index is deleted. To opt in for this behaviour, a user has 
to enable a specific index setting when mounting the 
snapshot (the proposed name for the setting is 
index.store.snapshot.delete_searchable_snapshot). 

Elasticsearch then verifies that the snapshot to mount 
contains only 1 snapshotted index and that the snapshot 
is not used by another mounted index with a different 
value for the opt in setting, and mounts the snapshot 
with the new private index setting. 

This is the part implemented in this commit.

In follow-up pull requests this index setting will be used 
when the last mounted index is deleted and removed 
from the cluster state in order to add the searchable 
snapshot id to a list of snapshots to delete in the 
repository metadata. Snapshots that are marked as 
"to delete" will not be able to be restored or cloned, and 
Elasticsearch will take care of deleting the snapshot as 
soon as possible. Then ILM will be changed to use this 
setting when mounting a snapshot as a cold or frozen 
index and delete_searchable_snapshot option in ILM 
will be removed. Finally, deleting a snapshot that is 
still used by mounted indices will be prevented.
@ywangd ywangd merged commit 8c09fc8 into elastic:master Aug 3, 2021
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Aug 3, 2021
This PR adds a new API for searching API keys. The API supports searching API
keys with a controlled list of field names and a subset of Query DSL. It also
provides a translation layer between the field names used in the REST layer and
those in the index layer. This is to prevent tight coupling between the user
facing request and index mappings so that they can evolve separately.

Compared to the Get API key API, this new search API automatically applies
calling user's security context similar to regular searches, e.g. if the user
has only manage_own_api_key privilege, only keys owned by the user are returned
in the search response.

Relates: elastic#71023
ywangd added a commit that referenced this pull request Aug 3, 2021
This PR adds a new API for searching API keys. The API supports searching API
keys with a controlled list of field names and a subset of Query DSL. It also
provides a translation layer between the field names used in the REST layer and
those in the index layer. This is to prevent tight coupling between the user
facing request and index mappings so that they can evolve separately.

Compared to the Get API key API, this new search API automatically applies
calling user's security context similar to regular searches, e.g. if the user
has only manage_own_api_key privilege, only keys owned by the user are returned
in the search response.

Relates: #71023
lockewritesdocs pushed a commit to lockewritesdocs/elasticsearch that referenced this pull request Aug 3, 2021
This PR adds a new API for searching API keys. The API supports searching API
keys with a controlled list of field names and a subset of Query DSL. It also
provides a translation layer between the field names used in the REST layer and
those in the index layer. This is to prevent tight coupling between the user
facing request and index mappings so that they can evolve separately.

Compared to the Get API key API, this new search API automatically applies
calling user's security context similar to regular searches, e.g. if the user
has only manage_own_api_key privilege, only keys owned by the user are returned
in the search response.

Relates: elastic#71023
ywangd added a commit that referenced this pull request Aug 9, 2021
This PR fixed an old bug and a new bug introduced #75335. Interestingly, the
two bugs somewhat cancelled each other in tests. In addition, the test setup
also contributed to the overall issue.

The old bug is about filtering out expired API keys, but the relationship was
wrong in the search query. The new bug is that _id field should be allowed in
the index level for the new API key search API. Because of the old bug, the
query always gets rewritten because the tests do not have any API keys that are
expired before the query time. The query rewriting effectively bypasses the _id
field check. Hence the new bug is not triggered.
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Aug 9, 2021
This PR fixed an old bug and a new bug introduced elastic#75335. Interestingly, the
two bugs somewhat cancelled each other in tests. In addition, the test setup
also contributed to the overall issue.

The old bug is about filtering out expired API keys, but the relationship was
wrong in the search query. The new bug is that _id field should be allowed in
the index level for the new API key search API. Because of the old bug, the
query always gets rewritten because the tests do not have any API keys that are
expired before the query time. The query rewriting effectively bypasses the _id
field check. Hence the new bug is not triggered.
ywangd added a commit that referenced this pull request Aug 9, 2021
This PR fixed an old bug and a new bug introduced #75335. Interestingly, the
two bugs somewhat cancelled each other in tests. In addition, the test setup
also contributed to the overall issue.

The old bug is about filtering out expired API keys, but the relationship was
wrong in the search query. The new bug is that _id field should be allowed in
the index level for the new API key search API. Because of the old bug, the
query always gets rewritten because the tests do not have any API keys that are
expired before the query time. The query rewriting effectively bypasses the _id
field check. Hence the new bug is not triggered.
ywangd added a commit that referenced this pull request Aug 17, 2021
This PR adds HLRC for the new Query API key API added with #75335 and #76144

Relates: #71023
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Aug 17, 2021
This PR adds HLRC for the new Query API key API added with elastic#75335 and elastic#76144

Relates: elastic#71023
ywangd added a commit that referenced this pull request Aug 18, 2021
This PR adds HLRC for the new Query API key API added with #75335 and #76144

Relates: #71023
ywangd added a commit that referenced this pull request Aug 19, 2021
This PR adds documentation for the new QueryApiKey API added in #75335 and #76144

Relates: #71023
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Aug 19, 2021
This PR adds documentation for the new QueryApiKey API added in elastic#75335 and elastic#76144

Relates: elastic#71023
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Aug 19, 2021
This PR adds documentation for the new QueryApiKey API added in elastic#75335 and elastic#76144

Relates: elastic#71023
elasticsearchmachine pushed a commit that referenced this pull request Aug 19, 2021
This PR adds documentation for the new QueryApiKey API added in #75335 and #76144

Relates: #71023
elasticsearchmachine pushed a commit that referenced this pull request Aug 19, 2021
This PR adds documentation for the new QueryApiKey API added in #75335 and #76144

Relates: #71023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>enhancement :Security/Security Security issues without another label Team:Security Meta label for security team v7.15.0 v8.0.0-alpha1
Projects
None yet
Development

Successfully merging this pull request may close these issues.