-
Notifications
You must be signed in to change notification settings - Fork 25k
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
Limited-by role descriptors in Get/QueryApiKey response #89273
Limited-by role descriptors in Get/QueryApiKey response #89273
Conversation
An API key's effective permission is an intersection between its assigned role descriptors and a snapshot of its owner user's role descriptors (limited-by role descriptors). In elastic#89166, the assigned role descriptors are now returned by default in Get/Query API key responses. This PR further adds support to optionally return limited-by role descriptors in the responses. Unlike assign role descriptors, an API key cannot view any limited-by role descriptors unless it has manage_api_key or higher privileges. Relates: elastic#89058
Pinging @elastic/es-security (Team:Security) |
Hi @ywangd, I've created a changelog YAML for you. |
String realm, | ||
@Nullable Map<String, Object> metadata, | ||
@Nullable List<RoleDescriptor> roleDescriptors, | ||
@Nullable RoleDescriptorsIntersection limitedBy |
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 added this new RoleDescriptorsIntersection
mainly for future proof (in case we support derived keys properly). It also mirrors what we did with RoleReferenceIntesection and LimitedRole.
It does also introduce some code level overhead. Let me know if you dislike it and prefer a simpler List<RoleDescriptor>
.
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.
Missed this comment. RoleDescriptorsIntersection
makes sense to me. We know we will need something like this and all the work is already there now 👍
public static class Builder { | ||
private String realmName = null; | ||
private String userName = null; | ||
private String apiKeyId = null; | ||
private String apiKeyName = null; | ||
private boolean ownedByAuthenticatedUser = false; | ||
private boolean withLimitedBy = false; |
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.
Added a builder class and deprecated the various convenient methods. I think this request class has come to a point that a Builder class is justified. I will have a follow-up PR to remove those deprecated methods.
# The _es_test_root role doesn't have any application privileges because that would require loading data (Application Privileges) | ||
# from the security index, which can causes problems if the index is not available | ||
applications: | ||
- application: "*" | ||
privileges: [ "*" ] | ||
resources: [ "*" ] |
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 no longer the case. Added these application privilege so this role definition is consistent with the one used for testclusters.
@@ -2488,6 +2739,7 @@ private void verifyGetResponse( | |||
List<CreateApiKeyResponse> responses, | |||
List<Map<String, Object>> metadatas, | |||
List<RoleDescriptor> expectedRoleDescriptors, | |||
List<RoleDescriptor> expectedLimitedRoleDescriptors, |
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.
Most changes in this file are due to this method signature change which allows checking the limited-by role descriptors.
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.
Nice work! Only non-blocking suggestions/questions throughout.
@@ -98,6 +107,10 @@ public void setFilterForCurrentUser() { | |||
filterForCurrentUser = true; | |||
} | |||
|
|||
public boolean isWithLimitedBy() { |
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
public boolean isWithLimitedBy() { | |
public boolean withLimitedBy() { |
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 changed it as suggested.
PS: We are not consistently on getter/setter names. In this particular class, the other methods follow JavaBean convention. So I also chose to have isWithLimitedBy
here. In GetApiKeyRequest, the existing boolean field follows Builder convention. So I had it differently over there. It is definitely not ideal. We may want to look into larger scale consistency in future.
@@ -67,6 +98,9 @@ public ApiKey( | |||
this.realm = realm; | |||
this.metadata = metadata == null ? Map.of() : metadata; | |||
this.roleDescriptors = roleDescriptors; | |||
// This assertion will need to be changed (or removed) when derived keys are properly supported | |||
assert limitedBy == null || limitedBy.roleDescriptorsList().size() == 1 : "cannot only have one set of limited-by role descriptors"; |
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 think the message should be "can only have one ..."
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.
Absolutely. I changed the wording a few times and they left to be inconsistent ...
private static List<List<RoleDescriptor>> readRoleDescriptorsListFrom(StreamInput in) throws IOException { | ||
final List<List<RoleDescriptor>> roleDescriptorsList = new ArrayList<>(); | ||
final int size = in.readVInt(); | ||
assert size >= 0; |
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: would assert false
inside the if body below with the same message as that in the exception.
...src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java
Outdated
Show resolved
Hide resolved
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public record RoleDescriptorsIntersection(List<List<RoleDescriptor>> roleDescriptorsList) implements ToXContentObject, Writeable { |
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.
Not for this PR, but I'm wondering if it makes sense to plug this class into CompositeRoleStore.getRoleDescriptorsList
-- there's a type mismatch (Collection<Set<>> vs List<List<>>), but semantically, the result of getRoleDescriptorsList
seems to be exactly a RoleDescriptorsIntersection
.
In the future, if we support non-singleton limited-by role descriptor intersections we might also want to use RoleDescriptorsIntersection
through ApiKeyService
(e.g., as an arg to createApiKey
) but that's a refactor for another day.
WDYT?
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 changed the type to be Collection<Set<>>
so it is consistent.
That said, I cannot remember why existing code uses Collection
instead of List
. It does not seem necessary. I used List
for the inner type instead of Set
in the hope to keep a predicatable order. But that was not really useful because these information is rendered as map on the user-facing output. Also we deduplicate role names when resolving role definitions. So overall Set
is a better fit for the inner type. In summary, I think the better type would be List<Set<>>
but this can wait for future PRs.
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.
Excellent and +1 on List<Set<>>
@@ -207,7 +207,8 @@ static boolean checkSameUserPermissions(String action, TransportRequest request, | |||
// if the authentication is an API key then the request must also contain same API key id | |||
String authenticatedApiKeyId = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_ID_KEY); | |||
if (Strings.hasText(getApiKeyRequest.getApiKeyId())) { | |||
return getApiKeyRequest.getApiKeyId().equals(authenticatedApiKeyId); | |||
// An API key with no manage_api_key or higher is not allowed to view its own limited-by role descriptors | |||
return getApiKeyRequest.getApiKeyId().equals(authenticatedApiKeyId) && false == getApiKeyRequest.withLimitedBy(); |
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 one was nice and hidden away 😅
final GetApiKeyResponse getApiKeyResponse = future.actionGet(); | ||
assertThat(getApiKeyResponse.getApiKeyInfos(), arrayWithSize(1)); | ||
final ApiKey apiKeyInfo = getApiKeyResponse.getApiKeyInfos()[0]; | ||
final ApiKey apiKeyInfo; |
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 like the "free" coverage this gives us but a little hesitant to sneak more implicit testing into this method since it's primarily meant for asserting on expectations for other tests. There's already a lot going on in here; adding another layer of randomization and implicit testing might make debugging test issues/maintenance cost too heavy. I'm good to keep this as is but if you had the same hesitation could be worth iterating.
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.
Alternatively, at least renaming to something like doTestApiKeyHasExpectedAttributes
would better match what's in the box
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 renamed it doTestApiKeyHasExpectedAttributes
. I'd like to keep the "free" coverge. But I also think your point of "another layer of randomization and implicit testing might make debugging test issues/maintenance cost too heavy" is valid. As a middle ground, I added a message in all relevant assertion to say which API is randomised (Get or Query). So this at least should give us some hint on which part went wrong on failure.
We can definitely still improve on the tests in future PRs. There are many things that can be cleaned up or better organised.
containsInAnyOrder(expectedLimitedRoleDescriptors.toArray(RoleDescriptor[]::new)) | ||
); | ||
} | ||
} |
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.
Could add something like to make it more explicit that we don't assert on expectedLimitedRoleDescriptors
correctly when there's more than one user passed in:
} else {
assertNull("can only assert on limited_by for single user, must pass null otherwise", expectedLimitedRoleDescriptors);
}
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.
Alternatively (this would be a bigger change - not necessary in this PR, and possibly not worth it) could pass a map of username to expected limitedBy RDs, e.g. something like:
Map.of(
ES_TEST_ROOT_USER, List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR),
"user_with_manage_api_key_role", List.of(...)
)
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 added the null assertion.
pass a map of username to expected limitedBy RDs
I considered that too but deciced not to do so because this multiple user assertion is only used in one place which does not really justify the cascading changes. There is definitely refactoring opportunities. But I'll leave it for now.
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.
On a second thought, I refactored this method to take lookup function to get expectedLimitedByRoleDescriptors for different users. This is because other changes such as randomisation (as suggested below) has made cascading changes no longer a valid concern. It is also good to get rid of the special handling inside this 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.
Doesn't have to be in this PR but would be good to add a test that covers getting API keys with legacy superuser role descriptors (i.e., that they will automatically be mapped to non-legacy ones).
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 meant to test that but somehow forgot. Thanks for catching it. Added a new test method testLegacySuperuserLimitedByWillBeReturnedAsTransformed
@@ -1171,6 +1303,92 @@ public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationBu | |||
assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", ES_TEST_ROOT_USER, responses.get(0).getId()); | |||
} | |||
|
|||
public void testApiKeyViewLimitedBy() { |
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.
Optional, but wondering if we should randomize between query and get in this method -- would give a nice single place where we test both ways to view limited by
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.
Yep, added the randomisation.
A builder class for GetApiKeyRequest is added as part of #89273. As a result, the existing convenient methods got deprecated in favour of the builder. This PR removes the deprecated methods and replaces all usages with the builder.
An API key's effective permission is an intersection between its
assigned role descriptors and a snapshot of its owner user's role
descriptors (limited-by role descriptors). In #89166, the assigned
role descriptors are now returned by default in Get/Query API key
responses.
This PR further adds support to optionally return limited-by role
descriptors in the responses. Unlike assign role descriptors,
an API key cannot view any limited-by role descriptors unless it
has manage_api_key or higher privileges.
Relates: #89058