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

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
73f0307
Make reserved built-in roles queryable
slobodanadamovic Nov 26, 2024
784a922
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Nov 26, 2024
3c78126
export queryable classes
slobodanadamovic Nov 27, 2024
5a1c233
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Nov 28, 2024
252e140
suppress 'this-escape' warning
slobodanadamovic Nov 28, 2024
9bba1aa
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Nov 28, 2024
c3b41ab
introduce a factory interface in order to be able to inject different…
slobodanadamovic Nov 28, 2024
b83410c
export org.elasticsearch.xpack.security.authz.store
slobodanadamovic Nov 28, 2024
cb70eff
allow returning all file role definitions
slobodanadamovic Nov 28, 2024
31b4fb2
mark query roles API as public in serverless
slobodanadamovic Nov 29, 2024
4d6bd57
imports are important
slobodanadamovic Nov 29, 2024
8ca36ad
remove assertion as it's not true in all use cases
slobodanadamovic Nov 29, 2024
7b278e6
remove unused import
slobodanadamovic Nov 29, 2024
8c71c73
test reserved roles are all indexed and cannot be modified via API
slobodanadamovic Dec 2, 2024
58f132e
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 2, 2024
c679261
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 2, 2024
9cae158
document feature flag
slobodanadamovic Dec 2, 2024
57cac14
code cleanup
slobodanadamovic Dec 3, 2024
1330265
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 3, 2024
e821710
test that hash calculation produces consistent hash digest
slobodanadamovic Dec 3, 2024
d27c91b
update log message
slobodanadamovic Dec 3, 2024
9e7076d
simple unit test for reserved roles provider
slobodanadamovic Dec 3, 2024
0e85b77
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 3, 2024
1b56b95
make collections immutable
slobodanadamovic Dec 3, 2024
f35ba07
fix failing test
slobodanadamovic Dec 3, 2024
71ed79b
remove unused import
slobodanadamovic Dec 3, 2024
f4e0292
Update docs/changelog/117581.yaml
slobodanadamovic Dec 3, 2024
d94da64
mark query API as internal for now
slobodanadamovic Dec 3, 2024
ada74e1
mark correct API as internal
slobodanadamovic Dec 3, 2024
2defd9b
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 3, 2024
32d4494
test get reserved roles
slobodanadamovic Dec 3, 2024
ba7541c
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 3, 2024
56d49b3
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Dec 4, 2024
21646f5
remove QueryableBuiltInRolesStore and depend on NativeRolesStore
slobodanadamovic Dec 9, 2024
a229e82
move concrete index name resolution after index gets created
slobodanadamovic Dec 9, 2024
8e551b7
revert changes to QueryRoleIT
slobodanadamovic Dec 9, 2024
c633037
test that bulk delete of built-in roles fails
slobodanadamovic Dec 9, 2024
d4f99f2
log 'expected' errors at info level
slobodanadamovic Dec 9, 2024
8cc05ab
use delegateFailureAndWrap
slobodanadamovic Dec 10, 2024
085755d
sanity check rolesToDelete and rolesToUpsert
slobodanadamovic Dec 10, 2024
c3ee0b5
return empty query result if native roles are disabled
slobodanadamovic Dec 10, 2024
a6bc077
naming nit validateRoles -> allowReservedRoleNames
slobodanadamovic Dec 10, 2024
e12c3e8
trace -> debug
slobodanadamovic Dec 10, 2024
70181d6
change role hashing implementation to hash ordered and flattened JSON…
slobodanadamovic Dec 10, 2024
aa64c4d
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 10, 2024
7dce761
avoid magic numbers in assertion
slobodanadamovic Dec 11, 2024
d400669
introduce a test plugin in order to test reserved roles change
slobodanadamovic Dec 11, 2024
78ea695
ignore javadoc in test plugin
slobodanadamovic Dec 11, 2024
cde5382
test closing and deleting .security index
slobodanadamovic Dec 12, 2024
1b54b5c
imports
slobodanadamovic Dec 12, 2024
f0d4810
wait a bit longer after cluster restart
slobodanadamovic Dec 12, 2024
1b88a5d
move static methods to utility class
slobodanadamovic Dec 12, 2024
fec25ae
better exception handling and code cleanup
slobodanadamovic Dec 13, 2024
a771b52
Merge branch 'main' of github.com:elastic/elasticsearch into sa-query…
slobodanadamovic Dec 13, 2024
1fa03d7
fix logger usage
slobodanadamovic Dec 13, 2024
36f1085
unit test rolesToUpsert and rolesToDelete
slobodanadamovic Dec 13, 2024
5aacc30
wait for the index to be deleted
slobodanadamovic Dec 13, 2024
ac5c837
handle cases when .security index gets deleted in the mean time
slobodanadamovic Dec 13, 2024
9d5982b
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Dec 16, 2024
123c890
switch to LinkedHashSet to keep ordering same as before (when List wa…
slobodanadamovic Dec 16, 2024
ee484b3
validateRoleDescriptors
slobodanadamovic Dec 16, 2024
5cd4e81
deduplicate log messages
slobodanadamovic Dec 16, 2024
7d09bbf
validateRoleNames
slobodanadamovic Dec 16, 2024
ca6485a
test with randomized metadata order
slobodanadamovic Dec 16, 2024
d9f69f7
test no updates with different digests instances
slobodanadamovic Dec 16, 2024
d691466
test no updates needed with randomized role and its copy
slobodanadamovic Dec 16, 2024
8343448
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Dec 16, 2024
a253252
Merge branch 'main' into sa-queryable-built-in-roles
slobodanadamovic Dec 16, 2024
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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ordered TreeMap here to produce consistent hash.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Just in case you've considered the same thing: instead of imposing this constraint here, I wonder if it makes sense to confine it to QueryableBuiltInRolesUtils by copying the role descriptor. That would avoid data copying on each role descriptor creation. Just a thought though, good to leave as is.

Copy link
Contributor Author

@slobodanadamovic slobodanadamovic Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of a similar(ish) approach: serialize to JSON, flatten all properties, sort them and then hash strings. This way I would avoid imposing any order in the constructor or during parsing of roles, but rather during hashing. Also, we would avoid copying role descriptors (not really true as we would copy a json map) and sorting would be confined in QueryableBuiltInRolesUtils. LMWYT

Copy link
Contributor Author

@slobodanadamovic slobodanadamovic Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could look something like this:

    public static String calculateHash(final RoleDescriptor roleDescriptor) {
        final MessageDigest hash = MessageDigests.sha256();
        try (XContentBuilder jsonBuilder = XContentFactory.jsonBuilder()) {
            roleDescriptor.toXContent(jsonBuilder, EMPTY_PARAMS);
            final Map<String, Object> flattenMap = Maps.flatten(
                XContentHelper.convertToMap(BytesReference.bytes(jsonBuilder), /*ordered*/ true, XContentType.JSON).v2(),
                true, // flattenArrays
                true // ordered
            );
            hash.update(flattenMap.toString().getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            throw new IllegalStateException("failed to compute role digest of [" + roleDescriptor.getName() +"] role", e);
        }

        // HEX vs Base64 encoding is a trade-off between readability and space efficiency
        // opting for Base64 here to reduce the size of the cluster state
        return Base64.getEncoder().encodeToString(hash.digest());
    }

Edit: I pushed the change 70181d6. I think this is a better approach as it should be future proof in case we add new map properties.

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 -> {
slobodanadamovic marked this conversation as resolved.
Show resolved Hide resolved
// 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 Expand Up @@ -496,7 +497,7 @@ private RoleDescriptor createRole(
);
}

private void assertQuery(String body, int total, Consumer<List<Map<String, Object>>> roleVerifier) throws IOException {
static void assertQuery(String body, int total, Consumer<List<Map<String, Object>>> roleVerifier) throws IOException {
assertQuery(client(), body, total, roleVerifier);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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;

import org.elasticsearch.client.Request;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
import org.elasticsearch.xpack.security.support.SecurityMigrations;
import org.junit.BeforeClass;

import static org.elasticsearch.xpack.security.QueryRoleIT.assertQuery;
import static org.elasticsearch.xpack.security.QueryRoleIT.waitForMigrationCompletion;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.oneOf;

public class QueryableReservedRolesIT extends SecurityInBasicRestTestCase {

@BeforeClass
public static void setup() {
new ReservedRolesStore();
}

public void testQueryDeleteOrUpdateReservedRoles() throws Exception {
waitForMigrationCompletion(adminClient(), SecurityMigrations.ROLE_METADATA_FLATTENED_MIGRATION_VERSION);

final String[] allReservedRoles = ReservedRolesStore.names().toArray(new String[0]);
assertQuery("""
{ "query": { "bool": { "must": { "term": { "metadata._reserved": true } } } }, "size": 100 }
""", 31, roles -> {
assertThat(roles, iterableWithSize(31));
for (var role : roles) {
assertThat((String) role.get("name"), is(oneOf(allReservedRoles)));
}
});

final String roleName = randomFrom(allReservedRoles);
assertQuery(String.format("""
{ "query": { "bool": { "must": { "term": { "name": "%s" } } } } }
""", roleName), 1, roles -> {
assertThat(roles, iterableWithSize(1));
assertThat((String) roles.get(0).get("name"), equalTo(roleName));
});

assertDeleteReservedRole(roleName);
assertCreateOrUpdateReservedRole(roleName);
}

private void assertDeleteReservedRole(String roleName) throws Exception {
slobodanadamovic marked this conversation as resolved.
Show resolved Hide resolved
Request request = new Request("DELETE", "/_security/role/" + roleName);
slobodanadamovic marked this conversation as resolved.
Show resolved Hide resolved
var e = expectThrows(ResponseException.class, () -> adminClient().performRequest(request));
assertThat(e.getMessage(), containsString("role [" + roleName + "] is reserved and cannot be deleted"));
}

private void assertCreateOrUpdateReservedRole(String roleName) throws Exception {
Request request = new Request(randomBoolean() ? "PUT" : "POST", "/_security/role/" + roleName);
request.setJsonEntity("""
{
"cluster": ["all"],
"indices": [
{
"names": ["*"],
"privileges": ["all"]
}
]
}
""");
var e = expectThrows(ResponseException.class, () -> adminClient().performRequest(request));
assertThat(e.getMessage(), containsString("Role [" + roleName + "] is reserved and may not be used."));
}

}
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
2 changes: 2 additions & 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,8 @@
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;
exports org.elasticsearch.xpack.security.authz.store 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,8 @@
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.QueryableBuiltInRolesProviderFactory;
import org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer;
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 +464,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 +635,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<QueryableBuiltInRolesProviderFactory> queryableRolesProviderFactory = 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 +1206,24 @@ Collection<Object> createComponents(

reservedRoleMappingAction.set(new ReservedRoleMappingAction());

if (QUERYABLE_BUILT_IN_ROLES_ENABLED) {
if (queryableRolesProviderFactory.get() == null) {
queryableRolesProviderFactory.set(new QueryableBuiltInRolesProviderFactory.Default());
}
components.add(
new QueryableBuiltInRolesSynchronizer(
clusterService,
featureService,
queryableRolesProviderFactory.get(),
new QueryableBuiltInRolesStore(nativeRolesStore),
reservedRolesStore,
fileRolesStore.get(),
systemIndices.getMainIndexManager(),
threadPool
)
);
}

cacheInvalidatorRegistry.validate();

final List<ReloadableSecurityComponent> reloadableComponents = new ArrayList<>();
Expand Down Expand Up @@ -2317,6 +2339,7 @@ public void loadExtensions(ExtensionLoader loader) {
loadSingletonExtensionAndSetOnce(loader, grantApiKeyRequestTranslator, RestGrantApiKeyAction.RequestTranslator.class);
loadSingletonExtensionAndSetOnce(loader, fileRoleValidator, FileRoleValidator.class);
loadSingletonExtensionAndSetOnce(loader, secondaryAuthActions, SecondaryAuthActions.class);
loadSingletonExtensionAndSetOnce(loader, queryableRolesProviderFactory, QueryableBuiltInRolesProviderFactory.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 @@ -44,6 +44,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -173,6 +174,14 @@ public Path getFile() {
return file;
}

/**
* @return a map of all file role definitions. The returned map is unmodifiable.
*/
public Map<String, RoleDescriptor> getAllRoleDescriptors() {
final Map<String, RoleDescriptor> localPermissions = permissions;
return Collections.unmodifiableMap(localPermissions);
}

// package private for testing
Set<String> getAllRoleNames() {
return permissions.keySet();
Expand Down
Loading