Skip to content

Commit

Permalink
Show assigned role descriptors in Get/QueryApiKey response (#89166)
Browse files Browse the repository at this point in the history
This PR adds a new `role_descriptors` field in the API key entity
returned by both GetApiKey and QueryApiKey APIs. The field value is the
map of the role descriptors that are assigned to an API key when
creating or updating the key. If the key has no assigned role
descriptors, i.e. it inherits the owner user's privileges, an empty
object is returned in place.

Relates: #89058
  • Loading branch information
ywangd committed Aug 9, 2022
1 parent cdbd7ad commit e6cfd9c
Show file tree
Hide file tree
Showing 11 changed files with 525 additions and 113 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/89166.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 89166
summary: Show assigned role descriptors in Get/QueryApiKey response
area: Security
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;

Expand All @@ -39,6 +41,8 @@ public final class ApiKey implements ToXContentObject, Writeable {
private final String username;
private final String realm;
private final Map<String, Object> metadata;
@Nullable
private final List<RoleDescriptor> roleDescriptors;

public ApiKey(
String name,
Expand All @@ -48,7 +52,8 @@ public ApiKey(
boolean invalidated,
String username,
String realm,
@Nullable Map<String, Object> metadata
@Nullable Map<String, Object> metadata,
@Nullable List<RoleDescriptor> roleDescriptors
) {
this.name = name;
this.id = id;
Expand All @@ -61,6 +66,7 @@ public ApiKey(
this.username = username;
this.realm = realm;
this.metadata = metadata == null ? Map.of() : metadata;
this.roleDescriptors = roleDescriptors;
}

public ApiKey(StreamInput in) throws IOException {
Expand All @@ -80,6 +86,12 @@ public ApiKey(StreamInput in) throws IOException {
} else {
this.metadata = Map.of();
}
if (in.getVersion().onOrAfter(Version.V_8_5_0)) {
final List<RoleDescriptor> roleDescriptors = in.readOptionalList(RoleDescriptor::new);
this.roleDescriptors = roleDescriptors != null ? List.copyOf(roleDescriptors) : null;
} else {
this.roleDescriptors = null;
}
}

public String getId() {
Expand Down Expand Up @@ -114,6 +126,10 @@ public Map<String, Object> getMetadata() {
return metadata;
}

public List<RoleDescriptor> getRoleDescriptors() {
return roleDescriptors;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Expand All @@ -130,6 +146,13 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t
.field("username", username)
.field("realm", realm)
.field("metadata", (metadata == null ? Map.of() : metadata));
if (roleDescriptors != null) {
builder.startObject("role_descriptors");
for (var roleDescriptor : roleDescriptors) {
builder.field(roleDescriptor.getName(), roleDescriptor);
}
builder.endObject();
}
return builder;
}

Expand All @@ -149,11 +172,14 @@ public void writeTo(StreamOutput out) throws IOException {
if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
out.writeGenericMap(metadata);
}
if (out.getVersion().onOrAfter(Version.V_8_5_0)) {
out.writeOptionalCollection(roleDescriptors);
}
}

@Override
public int hashCode() {
return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata);
return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata, roleDescriptors);
}

@Override
Expand All @@ -175,7 +201,8 @@ public boolean equals(Object obj) {
&& Objects.equals(invalidated, other.invalidated)
&& Objects.equals(username, other.username)
&& Objects.equals(realm, other.realm)
&& Objects.equals(metadata, other.metadata);
&& Objects.equals(metadata, other.metadata)
&& Objects.equals(roleDescriptors, other.roleDescriptors);
}

@SuppressWarnings("unchecked")
Expand All @@ -188,7 +215,8 @@ public boolean equals(Object obj) {
(Boolean) args[4],
(String) args[5],
(String) args[6],
(args[7] == null) ? null : (Map<String, Object>) args[7]
(args[7] == null) ? null : (Map<String, Object>) args[7],
(List<RoleDescriptor>) args[8]
);
});
static {
Expand All @@ -200,6 +228,10 @@ public boolean equals(Object obj) {
PARSER.declareString(constructorArg(), new ParseField("username"));
PARSER.declareString(constructorArg(), new ParseField("realm"));
PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata"));
PARSER.declareNamedObjects(optionalConstructorArg(), (p, c, n) -> {
p.nextToken();
return RoleDescriptor.parse(n, p, false);
}, new ParseField("role_descriptors"));
}

public static ApiKey fromXContent(XContentParser parser) throws IOException {
Expand All @@ -224,6 +256,8 @@ public String toString() {
+ realm
+ ", metadata="
+ metadata
+ ", role_descriptors="
+ roleDescriptors
+ "]";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.XContentTestUtils;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.xpack.core.security.authz.RoleDescriptorTests.randomUniquelyNamedRoleDescriptors;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

public class ApiKeyTests extends ESTestCase {
Expand All @@ -38,8 +43,9 @@ public void testXContent() throws IOException {
final String username = randomAlphaOfLengthBetween(4, 10);
final String realmName = randomAlphaOfLengthBetween(3, 8);
final Map<String, Object> metadata = randomMetadata();
final List<RoleDescriptor> roleDescriptors = randomBoolean() ? null : randomUniquelyNamedRoleDescriptors(0, 3);

final ApiKey apiKey = new ApiKey(name, id, creation, expiration, invalidated, username, realmName, metadata);
final ApiKey apiKey = new ApiKey(name, id, creation, expiration, invalidated, username, realmName, metadata, roleDescriptors);
// The metadata will never be null because the constructor convert it to empty map if a null is passed in
assertThat(apiKey.getMetadata(), notNullValue());

Expand All @@ -59,6 +65,18 @@ public void testXContent() throws IOException {
assertThat(map.get("username"), equalTo(username));
assertThat(map.get("realm"), equalTo(realmName));
assertThat(map.get("metadata"), equalTo(Objects.requireNonNullElseGet(metadata, Map::of)));

if (roleDescriptors == null) {
assertThat(map, not(hasKey("role_descriptors")));
} else {
@SuppressWarnings("unchecked")
final Map<String, Object> rdMap = (Map<String, Object>) map.get("role_descriptors");
assertThat(rdMap.size(), equalTo(roleDescriptors.size()));
for (var roleDescriptor : roleDescriptors) {
assertThat(rdMap, hasKey(roleDescriptor.getName()));
assertThat(XContentTestUtils.convertToMap(roleDescriptor), equalTo(rdMap.get(roleDescriptor.getName())));
}
}
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges;

import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.elasticsearch.xpack.core.security.authz.RoleDescriptorTests.randomUniquelyNamedRoleDescriptors;
import static org.hamcrest.Matchers.equalTo;

public class GetApiKeyResponseTests extends ESTestCase {
Expand All @@ -37,19 +44,46 @@ public void testSerialization() throws IOException {
false,
randomAlphaOfLength(4),
randomAlphaOfLength(5),
randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8))
randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)),
randomBoolean() ? null : randomUniquelyNamedRoleDescriptors(0, 3)
);
GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo));

final NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(
List.of(
new NamedWriteableRegistry.Entry(
ConfigurableClusterPrivilege.class,
ConfigurableClusterPrivileges.ManageApplicationPrivileges.WRITEABLE_NAME,
ConfigurableClusterPrivileges.ManageApplicationPrivileges::createFrom
),
new NamedWriteableRegistry.Entry(
ConfigurableClusterPrivilege.class,
ConfigurableClusterPrivileges.WriteProfileDataPrivileges.WRITEABLE_NAME,
ConfigurableClusterPrivileges.WriteProfileDataPrivileges::createFrom
)
)
);

try (BytesStreamOutput output = new BytesStreamOutput()) {
response.writeTo(output);
try (StreamInput input = output.bytes().streamInput()) {
try (StreamInput input = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWriteableRegistry)) {
GetApiKeyResponse serialized = new GetApiKeyResponse(input);
assertThat(serialized.getApiKeyInfos(), equalTo(response.getApiKeyInfos()));
}
}
}

public void testToXContent() throws IOException {
final List<RoleDescriptor> roleDescriptors = List.of(
new RoleDescriptor(
"rd_42",
new String[] { "monitor" },
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder().indices("index").privileges("read").build() },
new String[] { "foo" }
)
);

ApiKey apiKeyInfo1 = createApiKeyInfo(
"name1",
"id-1",
Expand All @@ -58,6 +92,7 @@ public void testToXContent() throws IOException {
false,
"user-a",
"realm-x",
null,
null
);
ApiKey apiKeyInfo2 = createApiKeyInfo(
Expand All @@ -68,7 +103,8 @@ public void testToXContent() throws IOException {
true,
"user-b",
"realm-y",
Map.of()
Map.of(),
List.of()
);
ApiKey apiKeyInfo3 = createApiKeyInfo(
null,
Expand All @@ -78,7 +114,8 @@ public void testToXContent() throws IOException {
true,
"user-c",
"realm-z",
Map.of("foo", "bar")
Map.of("foo", "bar"),
roleDescriptors
);
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3));
XContentBuilder builder = XContentFactory.jsonBuilder();
Expand All @@ -104,7 +141,8 @@ public void testToXContent() throws IOException {
"invalidated": true,
"username": "user-b",
"realm": "realm-y",
"metadata": {}
"metadata": {},
"role_descriptors": {}
},
{
"id": "id-3",
Expand All @@ -115,6 +153,32 @@ public void testToXContent() throws IOException {
"realm": "realm-z",
"metadata": {
"foo": "bar"
},
"role_descriptors": {
"rd_42": {
"cluster": [
"monitor"
],
"indices": [
{
"names": [
"index"
],
"privileges": [
"read"
],
"allow_restricted_indices": false
}
],
"applications": [],
"run_as": [
"foo"
],
"metadata": {},
"transient_metadata": {
"enabled": true
}
}
}
}
]
Expand All @@ -129,8 +193,9 @@ private ApiKey createApiKeyInfo(
boolean invalidated,
String username,
String realm,
Map<String, Object> metadata
Map<String, Object> metadata,
List<RoleDescriptor> roleDescriptors
) {
return new ApiKey(name, id, creation, expiration, invalidated, username, realm, metadata);
return new ApiKey(name, id, creation, expiration, invalidated, username, realm, metadata, roleDescriptors);
}
}

0 comments on commit e6cfd9c

Please sign in to comment.