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 "Has Privilege" performance for boolean-only response #86685

Conversation

albertzaharovits
Copy link
Contributor

@albertzaharovits albertzaharovits commented May 11, 2022

Boolean-only privilege checks, i.e. the ones currently used in the "profile has privilege" API,
now benefit from a performance improvement, because the check will now stop upon first
encountering a privilege that is NOT granted over a resource (and return false overall).
Previously, all the privileges were always checked over all the resources in order to assemble
a comprehensive response with all the privileges that are not granted.

@albertzaharovits albertzaharovits added >enhancement :Security/Authorization Roles, Privileges, DLS/FLS, RBAC/ABAC v8.3.0 labels May 11, 2022
@albertzaharovits albertzaharovits self-assigned this May 11, 2022
@elasticsearchmachine
Copy link
Collaborator

Hi @albertzaharovits, I've created a changelog YAML for you.

Set<String> checkForIndexPatterns,
boolean allowRestrictedIndices,
Set<String> checkForPrivileges
Set<String> checkForPrivileges,
@Nullable ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would like to take some space to explain my interface decisions for: Role#checkIndicesPrivilegeges, Role#checkApplicationResourcePrivileges, IndicesPermission#checkREsourcePrivileges, and ApplicationPermission#checkResourcePrivileges.

These methods must be aware of the mode of operation, ie whether to short-circuit or not, because they must return early in the short-circuit mode (in addition, the response "type" is different between the 2 modes) . So, one of the parameters of these methods must express this operation mode.

In this case, I think there are two implementation options to go about it:

  • pass a dedicated parameter, to signal the short-circuit mode, and make the return value contain an optional field which is null when in the short-circuit mode; eg RBACEngine#checkPrivileges(..., boolean runDetailedCheck, ..., ActionListener<PrivilegesCheckResult> listener) where PrivilegesCheckResult has two fields: boolean allChecksSuccess and an optional Details details.
  • return 2 values, rather than a single one with an optional null field. The first return value is the allChecksSuccess flag. The second returned value is via an out-parameter 👻 . When the out-parameter is non-null, it is used to return the privileges details, ie it works in the regular, non-short-circuit mode; otherwise, if null, no check details are returned and the operation mode is short-circuit.

I had 2 reasons to choose the second option for these methods (unlike RBACEngine#checkPrivileges):

  • I disliked having a boolean parameter going from the REST handler all the layers through to the permission level inside a role.
  • ResourcePrivilegesMap has a boolean field allowAll. Its value is currently driven by the individual privilege check results. But In the short-circuit mode, it becomes separated from the check results, as it can be true/false when the individual privilege check results are null. With such a separated allowAll parameter, keeping it still a member of the ResourcePrivilegesMap felt janky (what does it mean to update its value in the non-short-circuit mode, for every privilege result, when its value can technically also be set externally - the encapsulation was gone).

I hope the scare of an out-parameter is not too great.
I think this is the more elegant implementation of the two. I like that these check* methods always return a simple boolean. Also, the out parameter works OK in the application check section in RBACEngine#checkPrivileges and in the LimitedRole class, I think.

Copy link
Member

Choose a reason for hiding this comment

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

I noticed the out parameters on my first reading of the PR. I would probably approach it differently. But what is proposed in this PR works as well. Plus you already considered the other option and ruled it out with reasons. Also your reasoning about the standalone allowAll field and the individual check results of ResourcePrivilegesMap is a good one.

In summary, I am OK with the out parameters.

PS: In future, we may need support caching for LimitedRole (now it is only supported for SimpleRole). For that I am thinking that the result intersecting between role and limited-by-role should be done at the overall PrivilegesCheckResult level rather than the individual checks level (and also move most RBCEngine#checkPrivileges to Role level). This has nothing to do with the current PR. Just thinking out loud in case it is something you also thought about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In summary, I am OK with the out parameters.

🙇

PS: In future, we may need support caching for LimitedRole (now it is only supported for SimpleRole). For that I am thinking that the result intersecting between role and limited-by-role should be done at the overall PrivilegesCheckResult level rather than the individual checks level (and also move most RBCEngine#checkPrivileges to Role level). This has nothing to do with the current PR. Just thinking out loud in case it is something you also thought about.

No I haven't considered something like that. I'm not sure I follow either. My first approach would be to investigate whether the LimitedRole can be cached like the Role can.

@elasticmachine elasticmachine added the Team:Security Meta label for security team label May 18, 2022
@elasticmachine
Copy link
Collaborator

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

@albertzaharovits
Copy link
Contributor Author

@ywangd this is ready for review

Copy link
Member

@ywangd ywangd left a comment

Choose a reason for hiding this comment

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

LGTM

Some comments nothing major. Up to you how to deal with them. Thanks!

Comment on lines 357 to 358
* The result of a (has) privilege check. This is not an Elasticsearch authorization result (though clients can base their authorization
* decisions on this response). The {@link #allChecksSuccess} field tells if all the privileges are granted over all the resources.
Copy link
Member

Choose a reason for hiding this comment

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

Can you clarify what do you mean by "thi is not an Elasticsearch authorization result"? Do you mean it is literally not the AuthorizationResult class or do you mean the authorization result might be different if you execute a request with the same privileged user?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean that this not to be used/interpreted as the Elasticsearch authorization result.

Comment on lines +584 to 589
boolean privilegesGranted = userRole.checkIndicesPrivileges(
Sets.newHashSet(check.getIndices()),
check.allowRestrictedIndices(),
Sets.newHashSet(check.getPrivileges())
Sets.newHashSet(check.getPrivileges()),
combineIndicesResourcePrivileges
);
Copy link
Member

Choose a reason for hiding this comment

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

There is still a split information in the return boolean and the out-parameter. So I think it's helpful to assert the returned value is consistent with the out-parameter when it is not null (here and other call sites).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to revert these the assertions on the ResourcePrivilegesMap.Builder .
I have encountered NPEs which I was unable to track down all day.
I suspect there is some stupid magic about working with streams that I was unable to untangle. I don't think there are issues with the main code.

Comment on lines +786 to +808
ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = randomBoolean() ? ResourcePrivilegesMap.builder() : null;
boolean privilegesCheck = role.checkIndicesPrivileges(
checkForIndexPatterns,
allowRestrictedIndices,
checkForPrivileges,
resourcePrivilegesMapBuilder
);
assertThat(privilegesCheck, is(expectedCheckResult));

if (resourcePrivilegesMapBuilder == null) {
resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder();
privilegesCheck = role.checkIndicesPrivileges(
checkForIndexPatterns,
allowRestrictedIndices,
checkForPrivileges,
resourcePrivilegesMapBuilder
);
assertThat(privilegesCheck, is(expectedCheckResult));
} else {
privilegesCheck = role.checkIndicesPrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges, null);
assertThat(privilegesCheck, is(expectedCheckResult));
}
assertThat(resourcePrivilegesMapBuilder.build(), equalTo(expectedAppPrivsByResource));
Copy link
Member

Choose a reason for hiding this comment

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

I think we can just remove the randomisation and just let the code call role.checkIndicesPrivileges twice with null and non-null builder? That is what it does anyway. Removing randomisation probably reads a bit easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer we keep it. I think there's a high chance the order can make a difference when we evolve the caching.

Comment on lines -31 to -33
public boolean allAllowed() {
return allAllowed;
}
Copy link
Member

Choose a reason for hiding this comment

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

I wonder whether there is value to keep this method and let it compute it on the fly from the inner map?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Part of the ethos of this change was to keep the ResourcePrivilegesMap reserved for the check details only, so not include the authorization result (because they can exist separately). The allAllowed result can be confounded with the authorization result. Overall, I think it is better we don't expose it.

@albertzaharovits albertzaharovits added the auto-merge Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) label May 23, 2022
@albertzaharovits
Copy link
Contributor Author

@elasticmachine test this please

1 similar comment
@albertzaharovits
Copy link
Contributor Author

@elasticmachine test this please

@albertzaharovits
Copy link
Contributor Author

@elasticmachine run elasticsearch-ci/bwc

@elasticsearchmachine elasticsearchmachine merged commit 346abf9 into elastic:master May 24, 2022
@albertzaharovits albertzaharovits deleted the short-circuit-has-privileges branch May 24, 2022 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-merge Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) >enhancement :Security/Authorization Roles, Privileges, DLS/FLS, RBAC/ABAC Team:Security Meta label for security team v8.3.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants