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

Make reserved built-in roles queryable #117581

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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 @@ -50,6 +50,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

import static org.elasticsearch.common.xcontent.XContentHelper.createParserNotCompressed;
import static org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions.ROLE_REMOTE_CLUSTER_PRIVS;
Expand Down Expand Up @@ -189,9 +190,9 @@ public RoleDescriptor(
this.indicesPrivileges = indicesPrivileges != null ? indicesPrivileges : IndicesPrivileges.NONE;
this.applicationPrivileges = applicationPrivileges != null ? applicationPrivileges : ApplicationResourcePrivileges.NONE;
this.runAs = runAs != null ? runAs : Strings.EMPTY_ARRAY;
this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap();
this.metadata = metadata != null ? Collections.unmodifiableMap(new TreeMap<>(metadata)) : Collections.emptyMap();
this.transientMetadata = transientMetadata != null
? Collections.unmodifiableMap(transientMetadata)
? Collections.unmodifiableMap(new TreeMap<>(transientMetadata))
: Collections.singletonMap("enabled", true);
this.remoteIndicesPrivileges = remoteIndicesPrivileges != null ? remoteIndicesPrivileges : RemoteIndicesPrivileges.NONE;
this.remoteClusterPermissions = remoteClusterPermissions != null && remoteClusterPermissions.hasAnyPrivileges()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ public final class QueryRoleIT extends SecurityInBasicRestTestCase {

private static final String READ_SECURITY_USER_AUTH_HEADER = "Basic cmVhZF9zZWN1cml0eV91c2VyOnJlYWQtc2VjdXJpdHktcGFzc3dvcmQ=";

public void testSimpleQueryAllRoles() throws IOException {
assertQuery("", 0, roles -> assertThat(roles, emptyIterable()));
RoleDescriptor createdRole = createRandomRole();
assertQuery("", 1, roles -> {
assertThat(roles, iterableWithSize(1));
assertRoleMap(roles.get(0), createdRole);
public void testSimpleQueryAllRoles() throws Exception {
createRandomRole();

// 31 built-in reserved roles + 1 random role
assertQuery("", 1 + 31, roles -> {
// default size is 10
assertThat(roles, iterableWithSize(10));
});
assertQuery("""
{"query":{"match_all":{}},"from":1}""", 1, roles -> assertThat(roles, emptyIterable()));
{"query":{"match_all":{}},"from":32}""", 1 + 31, roles -> assertThat(roles, emptyIterable()));
}

public void testDisallowedFields() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public abstract class SecurityInBasicRestTestCase extends ESRestTestCase {
.user(API_KEY_USER, API_KEY_USER_PASSWORD.toString(), "api_key_user_role", false)
.user(API_KEY_ADMIN_USER, API_KEY_ADMIN_USER_PASSWORD.toString(), "api_key_admin_role", false)
.user(READ_SECURITY_USER, READ_SECURITY_PASSWORD.toString(), "read_security_user_role", false)
.systemProperty("es.queryable_built_in_roles_enabled", "true")
.build();

@Override
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/security/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
exports org.elasticsearch.xpack.security.slowlog to org.elasticsearch.server;
exports org.elasticsearch.xpack.security.authc.support to org.elasticsearch.internal.security;
exports org.elasticsearch.xpack.security.rest.action.apikey to org.elasticsearch.internal.security;
exports org.elasticsearch.xpack.security.support to org.elasticsearch.internal.security;

provides org.elasticsearch.index.SlowLogFieldProvider with org.elasticsearch.xpack.security.slowlog.SecuritySlowLogFieldProvider;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore;
import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
import org.elasticsearch.xpack.security.authz.store.QueryableBuiltInRolesStore;
import org.elasticsearch.xpack.security.authz.store.RoleProviders;
import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor;
import org.elasticsearch.xpack.security.operator.DefaultOperatorOnlyRegistry;
Expand Down Expand Up @@ -411,6 +412,9 @@
import org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction;
import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
import org.elasticsearch.xpack.security.support.ExtensionComponents;
import org.elasticsearch.xpack.security.support.QueryableBuiltInRoles;
import org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer;
import org.elasticsearch.xpack.security.support.QueryableReservedRolesProvider;
import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
import org.elasticsearch.xpack.security.support.SecurityMigrationExecutor;
Expand Down Expand Up @@ -461,6 +465,7 @@
import static org.elasticsearch.xpack.core.security.SecurityField.FIELD_LEVEL_SECURITY_FEATURE;
import static org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore.INCLUDED_RESERVED_ROLES_SETTING;
import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED;
import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_ENABLED;
import static org.elasticsearch.xpack.security.transport.SSLEngineUtils.extractClientCertificates;

public class Security extends Plugin
Expand Down Expand Up @@ -631,7 +636,7 @@ public class Security extends Plugin
private final SetOnce<ReservedRoleNameChecker.Factory> reservedRoleNameCheckerFactory = new SetOnce<>();
private final SetOnce<FileRoleValidator> fileRoleValidator = new SetOnce<>();
private final SetOnce<SecondaryAuthActions> secondaryAuthActions = new SetOnce<>();

private final SetOnce<QueryableBuiltInRoles.Provider> queryableRolesProvider = new SetOnce<>();
private final SetOnce<SecurityMigrationExecutor> securityMigrationExecutor = new SetOnce<>();

// Node local retry count for migration jobs that's checked only on the master node to make sure
Expand Down Expand Up @@ -1202,6 +1207,22 @@ Collection<Object> createComponents(

reservedRoleMappingAction.set(new ReservedRoleMappingAction());

if (QUERYABLE_BUILT_IN_ROLES_ENABLED) {
if (queryableRolesProvider.get() == null) {
queryableRolesProvider.set(new QueryableReservedRolesProvider());
}
components.add(
new QueryableBuiltInRolesSynchronizer(
clusterService,
featureService,
queryableRolesProvider.get(),
new QueryableBuiltInRolesStore(nativeRolesStore),
systemIndices.getMainIndexManager(),
threadPool
)
);
}

cacheInvalidatorRegistry.validate();

final List<ReloadableSecurityComponent> reloadableComponents = new ArrayList<>();
Expand Down Expand Up @@ -2317,6 +2338,7 @@ public void loadExtensions(ExtensionLoader loader) {
loadSingletonExtensionAndSetOnce(loader, grantApiKeyRequestTranslator, RestGrantApiKeyAction.RequestTranslator.class);
loadSingletonExtensionAndSetOnce(loader, fileRoleValidator, FileRoleValidator.class);
loadSingletonExtensionAndSetOnce(loader, secondaryAuthActions, SecondaryAuthActions.class);
loadSingletonExtensionAndSetOnce(loader, queryableRolesProvider, QueryableBuiltInRoles.Provider.class);
}

private <T> void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce<T> setOnce, Class<T> clazz) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.util.Set;

import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_FEATURE;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MIGRATION_FRAMEWORK;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_ROLES_METADATA_FLATTENED;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_ROLE_MAPPING_CLEANUP;
Expand All @@ -20,6 +21,11 @@ public class SecurityFeatures implements FeatureSpecification {

@Override
public Set<NodeFeature> getFeatures() {
return Set.of(SECURITY_ROLE_MAPPING_CLEANUP, SECURITY_ROLES_METADATA_FLATTENED, SECURITY_MIGRATION_FRAMEWORK);
return Set.of(
SECURITY_ROLE_MAPPING_CLEANUP,
SECURITY_ROLES_METADATA_FLATTENED,
SECURITY_MIGRATION_FRAMEWORK,
QUERYABLE_BUILT_IN_ROLES_FEATURE
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges;
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator;
import org.elasticsearch.xpack.core.security.support.NativeRealmValidationUtil;
import org.elasticsearch.xpack.security.authz.ReservedRoleNameChecker;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -169,6 +169,10 @@ public NativeRolesStore(
this.enabled = settings.getAsBoolean(NATIVE_ROLES_ENABLED, true);
}

public boolean isEnabled() {
return enabled;
}

@Override
public void accept(Set<String> names, ActionListener<RoleRetrievalResult> listener) {
getRoleDescriptors(names, listener);
Expand Down Expand Up @@ -263,6 +267,10 @@ public boolean isMetadataSearchable() {
}

public void queryRoleDescriptors(SearchSourceBuilder searchSourceBuilder, ActionListener<QueryRoleResult> listener) {
if (enabled == false) {
listener.onFailure(new IllegalStateException("Native role management is disabled"));
return;
}
SearchRequest searchRequest = new SearchRequest(new String[] { SECURITY_MAIN_ALIAS }, searchSourceBuilder);
SecurityIndexManager frozenSecurityIndex = securityIndex.defensiveCopy();
if (frozenSecurityIndex.indexExists() == false) {
Expand Down Expand Up @@ -345,6 +353,16 @@ public void deleteRoles(
final List<String> roleNames,
WriteRequest.RefreshPolicy refreshPolicy,
final ActionListener<BulkRolesResponse> listener
) {
deleteRoles(null, roleNames, refreshPolicy, true, listener);
}

void deleteRoles(
SecurityIndexManager securityIndexManager,
final Collection<String> roleNames,
WriteRequest.RefreshPolicy refreshPolicy,
boolean validateRoles,
final ActionListener<BulkRolesResponse> listener
) {
if (enabled == false) {
listener.onFailure(new IllegalStateException("Native role management is disabled"));
Expand All @@ -355,7 +373,7 @@ public void deleteRoles(
Map<String, Exception> validationErrorByRoleName = new HashMap<>();

for (String roleName : roleNames) {
if (reservedRoleNameChecker.isReserved(roleName)) {
if (validateRoles && reservedRoleNameChecker.isReserved(roleName)) {
validationErrorByRoleName.put(
roleName,
new IllegalArgumentException("role [" + roleName + "] is reserved and cannot be deleted")
Expand All @@ -370,7 +388,9 @@ public void deleteRoles(
return;
}

final SecurityIndexManager frozenSecurityIndex = securityIndex.defensiveCopy();
final SecurityIndexManager frozenSecurityIndex = securityIndexManager != null
? securityIndexManager
: securityIndex.defensiveCopy();
if (frozenSecurityIndex.indexExists() == false) {
logger.debug("security index does not exist");
listener.onResponse(new BulkRolesResponse(List.of()));
Expand Down Expand Up @@ -402,7 +422,7 @@ public void onFailure(Exception e) {
}

private void bulkResponseAndRefreshRolesCache(
List<String> roleNames,
Collection<String> roleNames,
BulkResponse bulkResponse,
Map<String, Exception> validationErrorByRoleName,
ActionListener<BulkRolesResponse> listener
Expand Down Expand Up @@ -430,7 +450,7 @@ private void bulkResponseAndRefreshRolesCache(
}

private void bulkResponseWithOnlyValidationErrors(
List<String> roleNames,
Collection<String> roleNames,
Map<String, Exception> validationErrorByRoleName,
ActionListener<BulkRolesResponse> listener
) {
Expand Down Expand Up @@ -542,7 +562,17 @@ public void onFailure(Exception e) {

public void putRoles(
final WriteRequest.RefreshPolicy refreshPolicy,
final List<RoleDescriptor> roles,
final Collection<RoleDescriptor> roles,
final ActionListener<BulkRolesResponse> listener
) {
putRoles(securityIndex, refreshPolicy, roles, true, listener);
}

void putRoles(
SecurityIndexManager securityIndexManager,
final WriteRequest.RefreshPolicy refreshPolicy,
final Collection<RoleDescriptor> roles,
boolean validateRoles,
final ActionListener<BulkRolesResponse> listener
) {
if (enabled == false) {
Expand All @@ -555,7 +585,7 @@ public void putRoles(
for (RoleDescriptor role : roles) {
Exception validationException;
try {
validationException = validateRoleDescriptor(role);
validationException = validateRoles ? validateRoleDescriptor(role) : null;
} catch (Exception e) {
validationException = e;
}
Expand All @@ -578,7 +608,7 @@ public void putRoles(
return;
}

securityIndex.prepareIndexIfNeededThenExecute(
securityIndexManager.prepareIndexIfNeededThenExecute(
listener::onFailure,
() -> executeAsyncWithOrigin(
client.threadPool().getThreadContext(),
Expand Down Expand Up @@ -621,8 +651,6 @@ private DeleteRequest createRoleDeleteRequest(final String roleName) {

// Package private for testing
XContentBuilder createRoleXContentBuilder(RoleDescriptor role) throws IOException {
assert NativeRealmValidationUtil.validateRoleName(role.getName(), false) == null
: "Role name was invalid or reserved: " + role.getName();
assert false == role.hasRestriction() : "restriction is not supported for native roles";

XContentBuilder builder = jsonBuilder().startObject();
Expand Down Expand Up @@ -671,7 +699,11 @@ public void usageStats(ActionListener<Map<String, Object>> listener) {
client.prepareMultiSearch()
.add(
client.prepareSearch(SECURITY_MAIN_ALIAS)
.setQuery(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.setQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true))
)
.setTrackTotalHits(true)
.setSize(0)
)
Expand All @@ -680,6 +712,7 @@ public void usageStats(ActionListener<Map<String, Object>> listener) {
.setQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true))
.must(
QueryBuilders.boolQuery()
.should(existsQuery("indices.field_security.grant"))
Expand All @@ -697,6 +730,7 @@ public void usageStats(ActionListener<Map<String, Object>> listener) {
.setQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true))
.filter(existsQuery("indices.query"))
)
.setTrackTotalHits(true)
Expand All @@ -708,6 +742,7 @@ public void usageStats(ActionListener<Map<String, Object>> listener) {
.setQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true))
.filter(existsQuery("remote_indices"))
)
.setTrackTotalHits(true)
Expand All @@ -718,6 +753,7 @@ public void usageStats(ActionListener<Map<String, Object>> listener) {
.setQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true))
.filter(existsQuery("remote_cluster"))
)
.setTrackTotalHits(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.authz.store;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.xpack.core.security.action.role.BulkRolesResponse;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;

import java.util.Collection;

/**
* Wrapper around the {@link NativeRolesStore} that provides a way to create, update and delete built-in roles.
*/
public final class QueryableBuiltInRolesStore {

private final NativeRolesStore nativeRolesStore;

public QueryableBuiltInRolesStore(NativeRolesStore nativeRolesStore) {
this.nativeRolesStore = nativeRolesStore;
}

public void putRoles(
final SecurityIndexManager securityIndexManager,
final Collection<RoleDescriptor> roles,
final ActionListener<BulkRolesResponse> listener
) {
assert roles.stream().allMatch(role -> (Boolean) role.getMetadata().get(MetadataUtils.RESERVED_METADATA_KEY));
nativeRolesStore.putRoles(securityIndexManager, WriteRequest.RefreshPolicy.IMMEDIATE, roles, false, listener);
}

public void deleteRoles(
final SecurityIndexManager securityIndexManager,
final Collection<String> roleNames,
final ActionListener<BulkRolesResponse> listener
) {
nativeRolesStore.deleteRoles(securityIndexManager, roleNames, WriteRequest.RefreshPolicy.IMMEDIATE, false, listener);
}

public boolean isEnabled() {
return nativeRolesStore.isEnabled();
}

}
Loading