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

Improve role cache efficiency for API key roles #58156

Merged
merged 39 commits into from
Jul 13, 2020

Conversation

ywangd
Copy link
Member

@ywangd ywangd commented Jun 16, 2020

This PR ensure that same roles are cached only once even when they are from different API keys.

API key role descriptors and limited role descriptors are now saved in Authentication#metadata as raw bytes instead of deserialised Map<String, Object>. Hashes of these bytes are used as keys for API key roles. Only when the required role is not found in the cache, they will be deserialised to build the RoleDescriptors. The deserialisation is directly from raw bytes to RoleDescriptors without going through the current detour of "bytes -> Map -> bytes -> RoleDescriptors".

A major aspect worth attention is BWC, we need:

  1. Handles API key metadata object differently depending on version of the incoming Authentication
  2. Prepare the outgoing Authentication object differently depending on version of the target Node

Originally we planned to make v7.9.0 the shim for BWC and ultimately get rid of the BWC code for v8.0.0 and onwards. Unfortunately, this idea of shim does not work for Authentication object. This is because: though we only need to guarantee compatibility between "current major" and "last minor of last major" for wire communication, we have to cater for much wider version gaps when it comes to persisted data, e.g. at least "current major" and "last major". Authentication object can be persisted in an index as part of some background job definition, e.g. watcher, CCR etc. This means v8.0.0 may deserialise an old Authentication object from the index and it must be supported. Additionally, when it comes to reindex, it is possible that Authentication objects from even older versions are reindexed into newer ES versions without change. This basically means we have to support older Authentication objects forever. It is an issue that should be re-visited in future.

Resolves: #53939

@ywangd ywangd added >enhancement :Security/Authorization Roles, Privileges, DLS/FLS, RBAC/ABAC v8.0.0 v7.9.0 labels Jun 16, 2020
@ywangd ywangd requested a review from tvernum June 16, 2020 08:39
@@ -145,6 +145,7 @@ public void executeAsUser(User user, Consumer<StoredContext> consumer, Version v
* The original context is provided to the consumer. When this method returns, the original context is restored.
*/
public void executeAfterRewritingAuthentication(Consumer<StoredContext> consumer, Version version) {
// TODO: bwc here for API key metadata changes? i.e. deserialize the raw bytes into Map for older node.
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 think BWC code is required here so that: if target node is an old version, rewrite the roleDescriptor and limitedRoleDescriptors to be Map<String, Object>.

Comment on lines 977 to 990
InstantiatingObjectParser.Builder<ApiKeyDoc, Void> builder =
InstantiatingObjectParser.builder("api_key_doc", true, ApiKeyDoc.class);
builder.declareString(constructorArg(), new ParseField("doc_type"));
builder.declareLong(constructorArg(), new ParseField("creation_time"));
builder.declareLongOrNull(constructorArg(), -1, new ParseField("expiration_time"));
builder.declareBoolean(constructorArg(), new ParseField("api_key_invalidated"));
builder.declareString(optionalConstructorArg(), new ParseField("api_key_hash"));
builder.declareString(constructorArg(), new ParseField("name"));
builder.declareInt(constructorArg(), new ParseField("version"));
ObjectParserHelper<ApiKeyDoc, Void> parserHelper = new ObjectParserHelper<>();
parserHelper.declareRawObject(builder, optionalConstructorArg(), new ParseField("role_descriptors"));
parserHelper.declareRawObject(builder, constructorArg(), new ParseField("limited_by_role_descriptors"));
parserHelper.declareRawObject(builder, constructorArg(), new ParseField("creator"));
PARSER = builder.build();
Copy link
Member Author

Choose a reason for hiding this comment

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

Is it possible that some fields, e.g. version or name, are completely missing from the source document? This parser requires all fields to present (null value is ok but it must present). In current code, all fields do present. I wonder whether this is true for older versions.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect it's not true for all versions, or at least it might not be.
We'd probably want to generate an API key from v6.7 and check, but it might be safer to change the parser to be more accommodating of missing fields.

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 checked v6.7 and it looks fine, i.e. it has the same fields as in the master branch. But it might still be safer to have more leniency for missing fields? I'll see how it can be done.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually fairly easily to make fields optional by using

builder.declareString(ApiKeyDoc::setName, new ParseField("name"))

instead of

builder.declareString(constructorArg(), new ParseField("name")).

But I ended up didn't make any of the fields optional because:

  • All of them exist from 6.7. So not sure which one I should pick for optional. (The only thing missing in 6.7 is creator.realm_type. But it is still wrapped inside a Map<String, Object> in the new implementation so it is not an issue).
  • Once a field made optional, it cannot be final and becomes an anomaly in current implementation. It's not a strong argument, just a personal preference.

Please let me know if you have concerns.

@ywangd
Copy link
Member Author

ywangd commented Jun 16, 2020

Added bwc for outgoing requests. It rewrites the API key auth metadata when target node is on an old version.

Unfortunately, I think the idea of using v7.9.0 as a shim and make v8.0.0 free of bwc code does not work. The shim idea will only works for wire communication since we only guarantee compatibility between "current major" and "last minor of last major". But in the case of Authentication object, it can be serialised in an index as part of some background job definition. Since we guarantee compatibility for indexed data between "current major" and "last major", this means v8.0.0 may deserialise an old Authentication ojbect from the index and it must be supported.

@ywangd
Copy link
Member Author

ywangd commented Jun 16, 2020

@elasticmachine run elasticsearch-ci/2

@ywangd ywangd marked this pull request as ready for review June 17, 2020 02:08
@elasticmachine
Copy link
Collaborator

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

@elasticmachine elasticmachine added the Team:Security Meta label for security team label Jun 17, 2020
@ywangd
Copy link
Member Author

ywangd commented Jun 17, 2020

Added a yaml test in rolling upgrade test suite to ensure BWC layer is working. It is currently disabled and will be enabled once the changes are backported to v7.9.0.

This PR is now ready for review.

@@ -295,6 +318,21 @@ private void buildThenMaybeCacheRole(RoleKey roleKey, Collection<RoleDescriptor>
}, listener::onFailure));
}

private void getOrBuildRoleForApiKey(Authentication authentication, boolean limitedBy, ActionListener<Role> roleActionListener) {
final BytesReference roleDescriptorsBytes = apiKeyService.getRoleDescriptorsBytesForApiKey(authentication, limitedBy);
final String roleDescriptorsHash = new String(Hasher.SHA256.hash(new SecureString(roleDescriptorsBytes.toString().toCharArray())));
Copy link
Contributor

Choose a reason for hiding this comment

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

BytesReference.toString() is not guaranteed to be what you want here.
You want either utf8ToString or just use the bytes directly.

I'm not sure whether we really even need to hash these - we can, but it might not be necessary.
I think the options are either:

  1. Don't bother hashing, but that will mean using a separate cache for API key roles (because the cache key is different)
Suggested change
final String roleDescriptorsHash = new String(Hasher.SHA256.hash(new SecureString(roleDescriptorsBytes.toString().toCharArray())));
final BytesKey cacheKey = new BytesKey(BytesReference.toBytes(roleDescriptorsBytes));
  1. Hash using MessageDigest
Suggested change
final String roleDescriptorsHash = new String(Hasher.SHA256.hash(new SecureString(roleDescriptorsBytes.toString().toCharArray())));
MessageDigest digest = MessageDigests.sha256();
digest.update(BytesReference.toBytes(roleDescriptorsBytes));
final String roleDescriptorsHash = MessageDigests.toHexString(digest.digest());

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 really like BytesKey, unfortunately it is not compatible with the current cache and I don't want to add another cache and associated overhead just because of this incompatibility. So I'll prefer to your second suggestion, i.e. MessageDigest.sha256 where raw bytes are used.

Alternatively, we could potential change RoleKey to have either Set<byte[]> names and byte[] source, so it can accomodate both regular roles and API key roles. But I am not sure whether it really gives us much unless we have a real concern for sha256 collision.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm happy with the sha256 option. I also like BytesKey - it's simple and sensible, but I don't think it's the right fit here unless we change the cache to have aSet<?> as the key, or follow your idea of converting a Set<String> into a BytesKey

Copy link
Member Author

Choose a reason for hiding this comment

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

Cool let's keep the sha256 option then. It works and is simpler.

return (BytesReference) metadata.get(limitedBy ? API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY : API_KEY_ROLE_DESCRIPTORS_KEY);
}

public List<RoleDescriptor> getRoleDescriptorsForApiKey(Authentication authentication, boolean limitedBy) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think having getRoleForApiKey, getRoleDescriptorsBytesForApiKey and getRoleDescriptorsForApiKey is confusing and likely to get us into trouble in the future.

Is it necessary for the caller to have to know which method to use depending on the version of the Authentication object it has?
I worry about the fragility of code that calls getRoleDescriptorsForApiKey and works on 7.9+ but then fails in a mixed cluster.
Would it be possible to merge getRoleDescriptorsForApiKey and getRoleForApiKey into a single method, or at least make it save to call getRoleDescriptorsForApiKey on an old style Authentication object?

Copy link
Member Author

Choose a reason for hiding this comment

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

Short version: I removed getRoleDescriptorsForApiKey and renamed getRoleDescriptorsBytesForApiKey to getApiKeyIdAndBytes.

Longer version: I agree the names are rather confusing. Initially, I completely removed getRoleForApiKey and List<RoleDescriptor> parseRoleDescriptors(String, Map<String, Object>) because they are used only for old style Authentication. But I had to add them back when I realised there were BWC issues. Anyway, we can and should remove them in 9.0.

Unfortunately, it is not feasible to merge getRoleForApiKey and getRoleDescriptorsForApiKey even though they both return something related to role descriptors. The old getRoleForApiKey returns a ApiKeyRoleDescriptors object, which internally has two List<RoleDescriptors>, one for from-role and the other for limited-by. But this would be non-ideal for the new getRoleDescriptorsForApiKey which returns either from-role-descriptors or limited-by-role-descriptors. By returning them separately, we can cache and lookup them separately. That is, if multiple keys are created by the same user, their limited-by roles are only computed once and cached for all other keys. The reverse is also true (though maybe less likely), if multiple keys happen to have the from-role-descriptors even when they are created by different users, we only need compute the from-roles once.

So there is no shared code path between getRoleForApiKey and getRoleDescriptorsForApiKey they work like the follows:

  • Old Authentication -> getRoleForApiKey -> parseRoleDescriptors(String, Map<String, Object>)
  • New Authentication -> getRoleDescriptorsBytesForApiKey -> if not cached -> getRoleDescriptorsForApiKey -> parseRoleDescriptors(String, ByteReference)

I intentionally kept them separate so the old code path can be easily removed when the time comes.

With above being said, I do agree that the names are confusing. So I renamed getRoleDescriptorsBytesForApiKey to getApiKeyIdAndBytes and removed getRoleDescriptorsForApiKey by a direct call to parseRoleDescriptors(String, ByteReference) in the caller.

Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, getRoleForApiKey will only do the right thing for old style authentication.
If so, can we:

  1. Add that to the javadoc
  2. Add an assert authentication.getVersion().before(Version.V7_9_0) : "This method only applies to authentication objects created before v7.9.0";

Copy link
Member Author

Choose a reason for hiding this comment

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

These are both good ideas. Will update accordingly. Thanks!

Comment on lines 977 to 990
InstantiatingObjectParser.Builder<ApiKeyDoc, Void> builder =
InstantiatingObjectParser.builder("api_key_doc", true, ApiKeyDoc.class);
builder.declareString(constructorArg(), new ParseField("doc_type"));
builder.declareLong(constructorArg(), new ParseField("creation_time"));
builder.declareLongOrNull(constructorArg(), -1, new ParseField("expiration_time"));
builder.declareBoolean(constructorArg(), new ParseField("api_key_invalidated"));
builder.declareString(optionalConstructorArg(), new ParseField("api_key_hash"));
builder.declareString(constructorArg(), new ParseField("name"));
builder.declareInt(constructorArg(), new ParseField("version"));
ObjectParserHelper<ApiKeyDoc, Void> parserHelper = new ObjectParserHelper<>();
parserHelper.declareRawObject(builder, optionalConstructorArg(), new ParseField("role_descriptors"));
parserHelper.declareRawObject(builder, constructorArg(), new ParseField("limited_by_role_descriptors"));
parserHelper.declareRawObject(builder, constructorArg(), new ParseField("creator"));
PARSER = builder.build();
Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect it's not true for all versions, or at least it might not be.
We'd probably want to generate an API key from v6.7 and check, but it might be safer to change the parser to be more accommodating of missing fields.

Copy link
Contributor

@albertzaharovits albertzaharovits left a comment

Choose a reason for hiding this comment

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

I think this would work, but it feels like you're trying to force it into the existing code, and the interfaces lose consistency. I think that if you were to introduce a more moderate change, that is the cache key be based on the RoleDescriptor list, you could fit that into the CompositeRolesStore.

If we really need to get this merged before FF, I think we can address the refactoring in a follow-up.

I haven't looked at the tests too closely.

Map<String, Object> metadata = authentication.getMetadata();
if (authentication.getAuthenticationType() == AuthenticationType.API_KEY
&& authentication.getVersion().onOrAfter(Version.V_7_9_0)
&& streamVersion.before(Version.V_7_9_0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The name rewriteMetadataIfNecessary is not very inspired; I have to read the code to know what this rewriting is about.

My suggestion: rewriteApiKeyAuthenticationRoleMetadata (drop "if necessary")
I would also slightly prefer to move this to the Authentication class, and maybe have a static method there that takes an authentication and a version as parameters and returns a new authentication with that version, and any rewritings done. I would also define a new static constant on Version.V_7_9_0 inside Authentication as VERSION_API_KEY_ROLES_AS_STRING (see TokenService for examples).

Copy link
Member Author

Choose a reason for hiding this comment

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

These are all good suggestions. Will address accordingly.

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 renamed the method to rewriteMetadataForApiKeyRoleDescriptors. But after some thoughts, I decided to keep this method inside SecurityContext. Because I need to make it public if moved to Authentication and that feels a bit uneasy to me since SecurityContext is the only consumer and will probably keep it that way in future. So I feel it is better to stay within it and closer to the actual context. It is an unnecessary detail to put inside Authentication if people is not concerned of BWC. Similarly I added the new version constant to SecurityContext as well. Sorry for the back and forth.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, but the constant can sit inside Authentication to be referenced in the CompositeRolesStore too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved the new version constant to Authentication as suggested.

@@ -974,4 +1018,66 @@ private boolean verify(SecureString password) {
return hash != null && cacheHasher.verify(password, hash);
}
}

public static final class ApiKeyDoc {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why have you introduced this new class in this PR? Is it required for something?
I'd obviously vote to get rid of the Map<String, Object> source any time of day, but this doesn't look like a great improvement, and it makes this PR harder to follow than necessary.

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 is intentionally introduced and is more or less a centre piece of this PR, which is about improve the role cache efficiency for API keys, i.e. same role descriptors should only be cached once even when they are from different API keys. The current cache is basically keyed by ApiKey ID, which is unfit for the goal. The obvious choice is to use role descriptors themselves as the key, but it has a few downsides and also not compatible the existing cache. So alternatively, we could use the string representation of role descriptors as the key. To further reduce the memory cost, we actually use sha256 of the string representation as the key. More details can be found here

So this new class has roleDescriptors and limitedByRoeDescriptors as ByteReference to faciliate the calculation. By storing them as bytes instead of Map, it also helps avoid quite a few unncessary serialisation and deserialisation. So instead of four de/serialisation, it now only deserialises once. When security index is local to a node, all these de/serialisation operations are a noticable part of the total authentication time (> 20%).

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, thank you for the explanation!

I would've thought that you can get the BytesReference for a source field but haven't checked before asking and it turns out you can't, you must use a parser.

if (authentication.getVersion().onOrAfter(Version.V_7_9_0)) {
getOrBuildRoleForApiKey(authentication, false, ActionListener.wrap(
role -> {
if (role == Role.EMPTY) {
Copy link
Contributor

Choose a reason for hiding this comment

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

When can this be true?

Copy link
Member Author

@ywangd ywangd Jul 13, 2020

Choose a reason for hiding this comment

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

When a list of role descriptor is empty, the role building method buildRoleFromDescriptors returns Role.EMPTY.

Also when an API key does not have its own role descriptors, its document will have {} for the role_descriptors field, which in turn will be converted to an empty list of RoleDescriptor.

When the two work together, you can get Role.EMPTY.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it! I have to check the code every single time to tell if the role_descriptors field or the limited_by_role_descriptors can be empty, the meanings are reversed in ApiKeyService#getRoleForApiKey.

Since limitedByRole can never be null I would be building that first, for a minor branching simplification:

         final Authentication.AuthenticationType authType = authentication.getAuthenticationType();
         if (authType == Authentication.AuthenticationType.API_KEY) {
             if (authentication.getVersion().onOrAfter(Version.V_7_9_0)) {
-                buildAndCacheRoleForApiKey(authentication, false, ActionListener.wrap(
-                    role -> {
-                        if (role == Role.EMPTY) {
-                            buildAndCacheRoleForApiKey(authentication, true, roleActionListener);
-                        } else {
-                            buildAndCacheRoleForApiKey(authentication, true, ActionListener.wrap(
-                                limitedByRole -> roleActionListener.onResponse(
-                                    limitedByRole == Role.EMPTY ? role : LimitedRole.createLimitedRole(role, limitedByRole)),
-                                roleActionListener::onFailure
+                buildAndCacheRoleForApiKey(authentication, true, ActionListener.wrap(
+                        limitedByRole -> {
+                            buildAndCacheRoleForApiKey(authentication, false, ActionListener.wrap(
+                                    role -> roleActionListener.onResponse(
+                                            role == Role.EMPTY ? limitedByRole : LimitedRole.createLimitedRole(role, limitedByRole)),
+                                    roleActionListener::onFailure
                             ));
-                        }
-                    },
-                    roleActionListener::onFailure
+                        },
+                        roleActionListener::onFailure
                 ));
             } else {
                 apiKeyService.getRoleForApiKey(authentication, ActionListener.wrap(apiKeyRoleDescriptors -> {

Feel free to follow this suggestion or not.

Copy link
Member Author

Choose a reason for hiding this comment

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

You may be right. But I am not entirely sure when it comes to derived API keys. A derived keys will have limited by role descriptors from parent API key's roles. Since we now zero out API key's roles during authentication, we may end up having an empty limited by role descriptors as well. I have not tested, but I feel it is probably safer to keep the code as is for now.

final Tuple<String, BytesReference> apiKeyIdAndBytes = apiKeyService.getApiKeyIdAndRoleBytes(authentication, limitedBy);
final MessageDigest digest = MessageDigests.sha256();
digest.update(BytesReference.toBytes(apiKeyIdAndBytes.v2()));
final String roleDescriptorsHash = MessageDigests.toHexString(digest.digest());
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: you can can compute the digest in one line.

Copy link
Contributor

@albertzaharovits albertzaharovits Jul 13, 2020

Choose a reason for hiding this comment

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

Even more succinct:

final String roleDescriptorsHash = MessageDigests.toHexString(MessageDigests.sha256().digest(BytesReference.toBytes(apiKeyIdAndBytes.v2())));

MessageDigests.sha256() is thread local and it's also reset before every use, so my objection was actually about minimising the scope of the digest instance.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated as suggested. Thanks!

@@ -295,6 +318,23 @@ private void buildThenMaybeCacheRole(RoleKey roleKey, Collection<RoleDescriptor>
}, listener::onFailure));
}

private void getOrBuildRoleForApiKey(Authentication authentication, boolean limitedBy, ActionListener<Role> roleActionListener) {
final Tuple<String, BytesReference> apiKeyIdAndBytes = apiKeyService.getApiKeyIdAndRoleBytes(authentication, limitedBy);
Copy link
Contributor

Choose a reason for hiding this comment

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

Methods here don't have a clear interface, it could just as well not be methods at all; the limitedBy flag is really ruining every structure.

I would move the full logic that obtains the Role for an API Keys from the CompositeRolesStore class to the ApiKeyService class.
But I realise I'm late to the party, we can do this in a follow-up.
@tvernum would you agree merging this PR, and following up with a refactoring?

Copy link
Member Author

Choose a reason for hiding this comment

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

Role building has always been done inside CompositeRolesStore and I think it fits here since role building and caching are the main purposes of CompositeRolesStore. It only delegates retrieval of role descriptors to different roles providers, including ApiKeyService, which is the case for this method as well. Overall, I think the method name could use some help (maybe buildAndCacheRoleForApiKey to mirror the existing buildAndCacheRoleFromDescriptors?), but its content looks fine to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point! Yes, Role should stay inside the CompositeRolesStore.

Then I think we can make ApiKeyRoleDescriptors lazily parse RoleDescriptor. Not sure what to do about the cache key. Let's leave this for the future us.

Copy link
Contributor

@tvernum tvernum left a comment

Choose a reason for hiding this comment

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

LGTM.

I think what we end up with at the end is still more complex than necessary - there's a lot of scope for refactoring within these classes - but I think this change covers the right things and is the right size.
I'd be very happy to see additional PRs focused on refactoring & cleaning up the interaction of all these overlapping concerns.

@ywangd
Copy link
Member Author

ywangd commented Jul 13, 2020

I think this would work, but it feels like you're trying to force it into the existing code, and the interfaces lose consistency. I think that if you were to introduce a more moderate change, that is the cache key be based on the RoleDescriptor list, you could fit that into the CompositeRolesStore.

Thanks Albert. You are right that I did try fit the changes to the exisitng role cache, which leads to some friction. But I think it's overall a reasonable approach:

  • The RoleKeys#names is meaningless for API key anyway. So it is arguable that a singletonList of role-descriptors string is a close enough approximation. Please note that I am suggesting a singletonList of string where the string is the representation of the entire role descriptors instead of a single role descriptor. This is better in terms of de/serialisation efficiency. Basically nothing is deseirialised unless it is really needed, i.e. when building the Role.
  • I did consider using the above singletonList of role-descriptors string as the key. But decided to use its sha256 to be extra safe because the role descriptors is an user input and can be arbitrarily large.
  • Adding another cache has non-negligible overhead, I'd prefer reuse existing ones unless there are serious issues.

@albertzaharovits albertzaharovits self-requested a review July 13, 2020 09:27
Copy link
Contributor

@albertzaharovits albertzaharovits left a comment

Choose a reason for hiding this comment

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

LGTM, after I got a bit familiar with the code it become easier to follow.

@ywangd ywangd merged commit a28ce1e into elastic:master Jul 13, 2020
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Jul 13, 2020
This PR ensure that same roles are cached only once even when they are from different API keys.
API key role descriptors and limited role descriptors are now saved in Authentication#metadata
as raw bytes instead of deserialised Map<String, Object>.
Hashes of these bytes are used as keys for API key roles. Only when the required role is not found
in the cache, they will be deserialised to build the RoleDescriptors. The deserialisation is directly
from raw bytes to RoleDescriptors without going through the current detour of
"bytes -> Map -> bytes -> RoleDescriptors".
ywangd added a commit that referenced this pull request Jul 13, 2020
This PR ensure that same roles are cached only once even when they are from different API keys.
API key role descriptors and limited role descriptors are now saved in Authentication#metadata
as raw bytes instead of deserialised Map<String, Object>.
Hashes of these bytes are used as keys for API key roles. Only when the required role is not found
in the cache, they will be deserialised to build the RoleDescriptors. The deserialisation is directly
from raw bytes to RoleDescriptors without going through the current detour of
"bytes -> Map -> bytes -> RoleDescriptors".
@tylersmalley
Copy link
Contributor

This commit is resulting in a regression within Kibana which being tracked here: elastic/kibana#71555

   │ info [o.e.x.s.a.AuthenticationService] [desktop] Authentication using apikey failed - apikey authentication for id NB-BSnMBuA9FcpEJrQtR encountered a failure
   │      org.elasticsearch.common.xcontent.XContentParseException: [1:532] [api_key_doc] name doesn't support values of type: VALUE_NULL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>enhancement :Security/Authorization Roles, Privileges, DLS/FLS, RBAC/ABAC Team:Security Meta label for security team v7.9.0 v8.0.0-alpha1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support better re-use of API key role caching
6 participants