Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
import org.elasticsearch.xpack.security.support.FeatureNotEnabledException;
Expand Down Expand Up @@ -588,11 +590,19 @@ public void getRoleForApiKey(Authentication authentication, ActionListener<ApiKe
if (roleDescriptors == null && authnRoleDescriptors == null) {
listener.onFailure(new ElasticsearchSecurityException("no role descriptors found for API key"));
} else if (roleDescriptors == null || roleDescriptors.isEmpty()) {
final List<RoleDescriptor> authnRoleDescriptorsList = parseRoleDescriptors(apiKeyId, authnRoleDescriptors);
final List<RoleDescriptor> authnRoleDescriptorsList = parseRoleDescriptors(
apiKeyId,
authnRoleDescriptors,
ApiKeyRoleType.LIMITED_BY
);
listener.onResponse(new ApiKeyRoleDescriptors(apiKeyId, authnRoleDescriptorsList, null));
} else {
final List<RoleDescriptor> roleDescriptorList = parseRoleDescriptors(apiKeyId, roleDescriptors);
final List<RoleDescriptor> authnRoleDescriptorsList = parseRoleDescriptors(apiKeyId, authnRoleDescriptors);
final List<RoleDescriptor> roleDescriptorList = parseRoleDescriptors(apiKeyId, roleDescriptors, ApiKeyRoleType.ASSIGNED);
final List<RoleDescriptor> authnRoleDescriptorsList = parseRoleDescriptors(
apiKeyId,
authnRoleDescriptors,
ApiKeyRoleType.LIMITED_BY
);
listener.onResponse(new ApiKeyRoleDescriptors(apiKeyId, roleDescriptorList, authnRoleDescriptorsList));
}
}
Expand Down Expand Up @@ -642,11 +652,15 @@ public List<RoleDescriptor> getLimitedByRoleDescriptors() {
}
}

private List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, final Map<String, Object> roleDescriptors) {
if (roleDescriptors == null) {
private List<RoleDescriptor> parseRoleDescriptors(
final String apiKeyId,
final Map<String, Object> roleDescriptorsMap,
ApiKeyRoleType roleType
) {
if (roleDescriptorsMap == null) {
return null;
}
return roleDescriptors.entrySet().stream().map(entry -> {
final List<RoleDescriptor> roleDescriptors = roleDescriptorsMap.entrySet().stream().map(entry -> {
final String name = entry.getKey();
@SuppressWarnings("unchecked")
final Map<String, Object> rdMap = (Map<String, Object>) entry.getValue();
Expand All @@ -666,9 +680,10 @@ private List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, final M
throw new UncheckedIOException(e);
}
}).collect(Collectors.toList());
return roleType == ApiKeyRoleType.LIMITED_BY ? maybeReplaceSuperuserRoleDescriptor(apiKeyId, roleDescriptors) : roleDescriptors;
}

public List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, BytesReference bytesReference) {
public List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, BytesReference bytesReference, ApiKeyRoleType roleType) {
if (bytesReference == null) {
return Collections.emptyList();
}
Expand All @@ -691,7 +706,40 @@ public List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, BytesRef
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return Collections.unmodifiableList(roleDescriptors);
return roleType == ApiKeyRoleType.LIMITED_BY ? maybeReplaceSuperuserRoleDescriptor(apiKeyId, roleDescriptors) : roleDescriptors;
}

// package private for tests
static final RoleDescriptor LEGACY_SUPERUSER_ROLE_DESCRIPTOR = new RoleDescriptor(
"superuser",
new String[] { "all" },
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(true).build() },
new RoleDescriptor.ApplicationResourcePrivileges[] {
RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build() },
null,
new String[] { "*" },
MetadataUtils.DEFAULT_RESERVED_METADATA,
Collections.emptyMap()
);

// This method should only be called to replace the superuser role descriptor for the limited-by roles of an API Key.
// We do not replace assigned roles because they are created explicitly by users.
// Before #82049, it is possible to specify a role descriptor for API keys that is identical to the builtin superuser role
// (including the _reserved metadata field).
private List<RoleDescriptor> maybeReplaceSuperuserRoleDescriptor(String apiKeyId, List<RoleDescriptor> roleDescriptors) {
// Scan through all the roles because superuser can be one of the roles that a user has. Unlike building the Role object,
// capturing role descriptors does not preempt for superuser.
return roleDescriptors.stream().map(rd -> {
// Since we are only replacing limited-by roles and all limited-by roles are looked up with role providers,
// it is technically possible to just check the name of superuser and the _reserved metadata field.
// But the gain is not much since role resolving is cached and comparing the whole role descriptor is still safer.
if (rd.equals(LEGACY_SUPERUSER_ROLE_DESCRIPTOR)) {
logger.debug("replacing superuser role for API key [{}]", apiKeyId);
return ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR;
}
return rd;
}).toList();
}

/**
Expand Down Expand Up @@ -1709,4 +1757,18 @@ public void invalidateAll() {
roleDescriptorsBytesCache.invalidateAll();
}
}

/**
* The type of one set of API key roles.
*/
public enum ApiKeyRoleType {
/**
* Roles directly specified by the creator user on API key creation
*/
ASSIGNED,
/**
* Roles captured for the owner user as the upper bound of the assigned roles
*/
LIMITED_BY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,11 @@ private void buildAndCacheRoleForApiKey(Authentication authentication, boolean l
final Role existing = roleCache.get(roleKey);
if (existing == null) {
final long invalidationCounter = numInvalidation.get();
final List<RoleDescriptor> roleDescriptors = apiKeyService.parseRoleDescriptors(apiKeyIdAndBytes.v1(), apiKeyIdAndBytes.v2());
final List<RoleDescriptor> roleDescriptors = apiKeyService.parseRoleDescriptors(
apiKeyIdAndBytes.v1(),
apiKeyIdAndBytes.v2(),
limitedBy ? ApiKeyService.ApiKeyRoleType.LIMITED_BY : ApiKeyService.ApiKeyRoleType.ASSIGNED
);
buildThenMaybeCacheRole(roleKey, roleDescriptors, Collections.emptySet(), true, invalidationCounter, roleActionListener);
} else {
roleActionListener.onResponse(existing);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS;
import static org.elasticsearch.xpack.security.Security.SECURITY_CRYPTO_THREAD_POOL_NAME;
import static org.elasticsearch.xpack.security.authc.ApiKeyService.API_KEY_METADATA_KEY;
import static org.elasticsearch.xpack.security.authc.ApiKeyService.LEGACY_SUPERUSER_ROLE_DESCRIPTOR;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
Expand Down Expand Up @@ -561,21 +562,22 @@ public void testValidateApiKey() throws Exception {
}

public void testGetRolesForApiKeyNotInContext() throws Exception {
final boolean useLegacySuperuserRole = randomBoolean();
final RoleDescriptor superuserRoleDescriptor = useLegacySuperuserRole
? LEGACY_SUPERUSER_ROLE_DESCRIPTOR
: SUPERUSER_ROLE_DESCRIPTOR;
Map<String, Object> superUserRdMap;
try (XContentBuilder builder = JsonXContent.contentBuilder()) {
superUserRdMap = XContentHelper.convertToMap(
XContentType.JSON.xContent(),
BytesReference.bytes(SUPERUSER_ROLE_DESCRIPTOR.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(),
BytesReference.bytes(superuserRoleDescriptor.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(),
false
);
}
Map<String, Object> authMetadata = new HashMap<>();
authMetadata.put(ApiKeyService.API_KEY_ID_KEY, randomAlphaOfLength(12));
authMetadata.put(API_KEY_ROLE_DESCRIPTORS_KEY, Collections.singletonMap(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap));
authMetadata.put(
API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
Collections.singletonMap(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)
);
authMetadata.put(API_KEY_ROLE_DESCRIPTORS_KEY, Collections.singletonMap(superuserRoleDescriptor.getName(), superUserRdMap));
authMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, Collections.singletonMap(superuserRoleDescriptor.getName(), superUserRdMap));

final Authentication authentication = new Authentication(
new User("joe"),
Expand All @@ -591,7 +593,12 @@ public void testGetRolesForApiKeyNotInContext() throws Exception {
service.getRoleForApiKey(authentication, roleFuture);
ApiKeyRoleDescriptors result = roleFuture.get();
assertThat(result.getRoleDescriptors().size(), is(1));
assertThat(result.getRoleDescriptors().get(0).getName(), is("superuser"));
// Assigned role descriptor should not change
assertThat(result.getRoleDescriptors().get(0), equalTo(superuserRoleDescriptor));

assertThat(result.getLimitedByRoleDescriptors().size(), is(1));
// Limited role descriptor should always be the updated superuser role descriptor
assertThat(result.getLimitedByRoleDescriptors().get(0), equalTo(SUPERUSER_ROLE_DESCRIPTOR));
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -702,11 +709,11 @@ public void testGetApiKeyIdAndRoleBytes() {
public void testParseRoleDescriptors() {
ApiKeyService service = createApiKeyService(Settings.EMPTY);
final String apiKeyId = randomAlphaOfLength(12);
List<RoleDescriptor> roleDescriptors = service.parseRoleDescriptors(apiKeyId, null);
List<RoleDescriptor> roleDescriptors = service.parseRoleDescriptors(apiKeyId, null, randomApiKeyRoleType());
assertTrue(roleDescriptors.isEmpty());

BytesReference roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}");
roleDescriptors = service.parseRoleDescriptors(apiKeyId, roleBytes);
roleDescriptors = service.parseRoleDescriptors(apiKeyId, roleBytes, randomApiKeyRoleType());
assertEquals(1, roleDescriptors.size());
assertEquals("a role", roleDescriptors.get(0).getName());
assertArrayEquals(new String[] { "all" }, roleDescriptors.get(0).getClusterPrivileges());
Expand All @@ -720,12 +727,19 @@ public void testParseRoleDescriptors() {
+ "\"privileges\":[\"*\"],\"resources\":[\"*\"]}],\"run_as\":[\"*\"],\"metadata\":{\"_reserved\":true},"
+ "\"transient_metadata\":{}}}\n"
);
roleDescriptors = service.parseRoleDescriptors(apiKeyId, roleBytes);
final ApiKeyService.ApiKeyRoleType apiKeyRoleType = randomApiKeyRoleType();
roleDescriptors = service.parseRoleDescriptors(apiKeyId, roleBytes, apiKeyRoleType);
assertEquals(2, roleDescriptors.size());
assertEquals(
Set.of("reporting_user", "superuser"),
roleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toSet())
);
assertThat(
roleDescriptors.get(1),
equalTo(
apiKeyRoleType == ApiKeyService.ApiKeyRoleType.LIMITED_BY ? SUPERUSER_ROLE_DESCRIPTOR : LEGACY_SUPERUSER_ROLE_DESCRIPTOR
)
);
}

public void testApiKeyServiceDisabled() throws Exception {
Expand Down Expand Up @@ -1122,7 +1136,11 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru
final BytesReference limitedByRoleDescriptorsBytes = service.getRoleDescriptorsBytesCache()
.get(cachedApiKeyDoc.limitedByRoleDescriptorsHash);
assertNotNull(limitedByRoleDescriptorsBytes);
final List<RoleDescriptor> limitedByRoleDescriptors = service.parseRoleDescriptors(docId, limitedByRoleDescriptorsBytes);
final List<RoleDescriptor> limitedByRoleDescriptors = service.parseRoleDescriptors(
docId,
limitedByRoleDescriptorsBytes,
ApiKeyService.ApiKeyRoleType.LIMITED_BY
);
assertEquals(1, limitedByRoleDescriptors.size());
assertEquals(SUPERUSER_ROLE_DESCRIPTOR, limitedByRoleDescriptors.get(0));
if (metadata == null) {
Expand Down Expand Up @@ -1695,4 +1713,8 @@ private void checkAuthApiKeyMetadata(Object metadata, AuthenticationResult<User>
);
}
}

private ApiKeyService.ApiKeyRoleType randomApiKeyRoleType() {
return randomFrom(ApiKeyService.ApiKeyRoleType.values());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1620,8 +1620,8 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() {
roleFuture.actionGet();
assertThat(effectiveRoleDescriptors.get(), is(nullValue()));
verify(apiKeyService, times(2)).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean());
verify(apiKeyService).parseRoleDescriptors("key-id-1", roleBytes);
verify(apiKeyService).parseRoleDescriptors("key-id-1", limitedByRoleBytes);
verify(apiKeyService).parseRoleDescriptors("key-id-1", roleBytes, ApiKeyService.ApiKeyRoleType.ASSIGNED);
verify(apiKeyService).parseRoleDescriptors("key-id-1", limitedByRoleBytes, ApiKeyService.ApiKeyRoleType.LIMITED_BY);

// Different API key with the same roles should read from cache
authentication = new Authentication(
Expand All @@ -1645,7 +1645,7 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() {
roleFuture.actionGet();
assertThat(effectiveRoleDescriptors.get(), is(nullValue()));
verify(apiKeyService, times(2)).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean());
verify(apiKeyService, never()).parseRoleDescriptors(eq("key-id-2"), any(BytesReference.class));
verify(apiKeyService, never()).parseRoleDescriptors(eq("key-id-2"), any(BytesReference.class), any());

// Different API key with the same limitedBy role should read from cache, new role should be built
final BytesArray anotherRoleBytes = new BytesArray("{\"b role\": {\"cluster\": [\"manage_security\"]}}");
Expand All @@ -1670,7 +1670,7 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() {
roleFuture.actionGet();
assertThat(effectiveRoleDescriptors.get(), is(nullValue()));
verify(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), eq(false));
verify(apiKeyService).parseRoleDescriptors("key-id-3", anotherRoleBytes);
verify(apiKeyService).parseRoleDescriptors("key-id-3", anotherRoleBytes, ApiKeyService.ApiKeyRoleType.ASSIGNED);
}

private Authentication createAuthentication() {
Expand Down
Loading