- Notifications
You must be signed in to change notification settings - Fork 25.6k
Role changes to support enforcing workflow restrictions #96744
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
Role changes to support enforcing workflow restrictions #96744
Conversation
…lows-role-restriction # Conflicts: # x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java # x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java # x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/SecurityRestFilterTests.java
...security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java Outdated Show resolved Hide resolved
...k/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java Outdated Show resolved Hide resolved
.../main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java Outdated Show resolved Hide resolved
.../main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java Outdated Show resolved Hide resolved
...n/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java Outdated Show resolved Hide resolved
| if (roleTuple.v1() == Role.EMPTY_RESTRICTED_BY_WORKFLOW || roleTuple.v2() == Role.EMPTY_RESTRICTED_BY_WORKFLOW) { | ||
| l.onResponse(DeniedAuthorizationInfo.INSTANCE); |
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.
I don't think this is an issue, but would like to write down for prosperity: v1 is the run-as user's role, v2 is the authenticating user's role. If v1 is empty and restricted while v2 is a proper role, one might argue that v2 should be able to run-as v1 (i.e. pass authorizeRunAs) then fail.
I think it can be argued both ways because:
We have a similar situation where request fails before authorization happens if run-as user is disabled.
At the same time, we also have request fails at authorization if the run-as user does not exist.
These two cases do not agree with each other. But more often we view the later one as an issue since it make an exception that realm can be null and has been causing issues in handling it. Therefore I overall feel it's OK to fail earlier here. Letting it passing through is not actually useful other than for argument sake.
The practical bottom line is that it is not possible to run-as API keys. So not a real issue anyway.
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 for raising this topic. FWIW My reasoning was that both users (authenticating and run-as) should have access to a particular workflow. Allowing the authenticating user to invoke a workflow should be prevented early on, even if run-as user can access it.
Additionally, what we could potentially do is differentiate between which role was restricted, but this felt unnecessary at the moment.
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.
Allowing the authenticating user to invoke a workflow should be prevented early on, even if run-as user can access it.
My view is the opposite. When
- The API is a workflow API
- The authenticating user has the privilege to run-as the effective user
- The effective user has a workflow role that allows it to call the workflow API
In this situation, the authorization should succeed. I believe that's what naturally would happen with the proposed change. Because Item 2 implies the authenticating user must have access to the workflow as well, either because the authenticating user has a workflow role or has a role without workflow restriction.
My original comment was about the failure case. That is, Item 1 & 2 remain unchanged, but in Item 3, the effective user does not have access to the workflow API. In this case, we could let the run-as (Item 2) to be successful (it generates an audit log entry runAsGranted), then fail due to Item 3. Or we can fail before even check Item 2 (and skip the runAsGranted audit entry), which is the behaviour of the proposed change. Both options are valid to some extend. But going with the later option seems to give us more flexibility in future (thinking moving rejection to the REST layer).
…lows-role-restriction
…lows-role-restriction # Conflicts: # server/src/main/java/org/elasticsearch/TransportVersion.java
| @elasticmachine run elasticsearch-ci/bwc |
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
I left quite a few comments. I'd appreciate if you could address them. Three of them are of some importance:
- Allow empty list of workflow names in
WorkflowRestrictionto mean nothing is permitted (workflow or not) - Call
Role.forWorkflow(...)inCompositeRolesStore.getRoleon the final role built from role reference. - Audit log or not
Please ping me if you need any clarifications. Thanks!
In addition, I think we need the >feature label instead of >non-issue for this PR since the doc PR will likely land after FF. So it is better for another PR to carry the >feature label and I think this PR is likely the best candidate.
.../main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java Show resolved Hide resolved
...ugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java Show resolved Hide resolved
| maybeAuthorizeRunAs(requestInfo, auditId, authorizationInfo, listener); | ||
| }, e -> { | ||
| if (e instanceof ElasticsearchAccessRestrictedException) { | ||
| auditTrailService.get() |
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.
I have a slight BWC concern for adding this audit event if we later move role resolution to the REST layer. We don't have transportRequest or action name at the REST layer. Therefore it will not be possible to call accessDenied as is today, i.e. we will need to modify this event, probably in a non-BWC compatible way. But that change may take a long time to come, so having this even does have short term benefit.
I slightly prefer to leave it out. What do you think?
PS: If you decided to keep it, you'll want to add auditing tests for it, probably in a separate PR.
...n/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java Outdated Show resolved Hide resolved
...e/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java Outdated Show resolved Hide resolved
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java Show resolved Hide resolved
...ugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java Outdated Show resolved Hide resolved
| } else { | ||
| roleActionListener.onResponse(existing); | ||
| String workflow = workflowService.readWorkflowFromThreadContext(threadContext); | ||
| roleActionListener.onResponse(existing.forWorkflow(workflow)); |
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.
Instead of calling Role.forWorkflow here and in buildThenMaybeCacheRole, I think we should call it once in the getRole(..) method, i.e. something like:
roleReferenceIntersection.buildRole(this::buildRoleFromRoleReference, ActionListener.wrap( role -> roleActionListener.onResponse(role.forWorkflow(workflow)), roleActionListener::onFailure));The reason is that for LimitedRole, the proposed change actually ends up not calling LimitedRole.forWorkflow on it. It calls the method on its baseRole and limitedByRole, but never calls it on the final LimitedRole. It works in practice because Role.limitedBy short-circuits and returns an "empty-restricted" if either of them is "empty-restricted". But there is a potential inconsistency: the LimitedRole constructor allows baseRole to be empty-restricted. If that ever happens, without calling LimitedRole.forWorkflows, the check in RBACEngine (role == EMPTY_RESTRICTED_BY_WORKFLOW) can misbehave. In summary, we should ensure that we call forWorkflow on the final role that is built from the role reference.
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.
Good catch. I missed this, as I was making changes bottom-up. Hence why I end-up needing a short-circuit in Role.limitedBy.
...ity/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java Show resolved Hide resolved
| * This differs from other 403 error in sense that it's additional access control that is enforced | ||
| * before permissions are checked. | ||
| */ | ||
| public class ElasticsearchAccessRestrictedException extends ElasticsearchSecurityException { |
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.
Nit: Should this be named ElasticsearchRoleRestrictionException. Or are you intentionally keeping it generic? Being access-restricted seems to suggest it to be applied in many other places. But that is not really the 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.
Initially, I wanted it to be generic, but I think now that your suggestion is better.
- Check same instance instead equalTo - Allow empty list of workflow names in WorkflowRestriction to mean nothing is permitted - Call Role.forWorkflow(...) in CompositeRolesStore.getRole on the final role - Remove audit logging - ElasticsearchRoleRestrictionException
…lows-role-restriction # Conflicts: # server/src/main/java/org/elasticsearch/TransportVersion.java
…thod fails with NPE
…lows-role-restriction
| Hi @slobodanadamovic, I've created a changelog YAML for you. |
This PR spreads the search application index shards to all nodes to show the workflow correctly works across nodes, i.e. the workflow header is recognized and role gets built on a data node the same way as on the coordinating node. Relates: elastic#96744
This PR spreads the search application index shards to all nodes to show the workflow correctly works across nodes, i.e. the workflow header is recognized and role gets built on a data node the same way as on the coordinating node. Relates: #96744
This commit adds YAML tests for Search Application Search API using API key restricted to `search_application_query` workflow. Relates to #96744
This PR adds API documentation for role restriction and workflows which are introduced in elastic#96744 and elastic#96215. --------- Co-authored-by: Abdon Pijpelink <abdon.pijpelink@elastic.co> Co-authored-by: Yang Wang <ywangd@gmail.com>
This PR implements necessary changes to
Roleclasses in order to enforceworkflow restrictions for API keys. Main change is around resolving roles from
role descriptors, where a role can either be effectively empty (or not)
depending on its workflow restriction.
Currently, the only workflow supported is
search_application_querywhichallows restricting access only to Search Application Search API.
For simplicity, when creating an API key, it's possible to specify only a single
role descriptor with workflows restriction. For example:
Relates to #96215