Skip to content

Commit 25d1225

Browse files
n1v0lgjfreden
authored andcommitted
Fix BWC for file-settings based role mappings (elastic#113900)
* Fix BWC for file settings based role mappings --------- Co-authored-by: Johannes Freden Jansson <[email protected]>
1 parent 4abddad commit 25d1225

File tree

13 files changed

+265
-64
lines changed

13 files changed

+265
-64
lines changed

docs/changelog/113900.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 113900
2+
summary: Fix BWC for file-settings based role mappings
3+
area: Authentication
4+
type: bug
5+
issues: []

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/GetRoleMappingsResponse.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
1212

1313
import java.io.IOException;
14+
import java.util.Collection;
1415

1516
/**
1617
* Response to {@link GetRoleMappingsAction get role-mappings API}.
@@ -21,6 +22,10 @@ public class GetRoleMappingsResponse extends ActionResponse {
2122

2223
private final ExpressionRoleMapping[] mappings;
2324

25+
public GetRoleMappingsResponse(Collection<ExpressionRoleMapping> mappings) {
26+
this(mappings.toArray(new ExpressionRoleMapping[0]));
27+
}
28+
2429
public GetRoleMappingsResponse(ExpressionRoleMapping... mappings) {
2530
this.mappings = mappings;
2631
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleMappingMetadata.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.cluster.AbstractNamedDiffable;
1313
import org.elasticsearch.cluster.ClusterState;
1414
import org.elasticsearch.cluster.NamedDiff;
15+
import org.elasticsearch.cluster.block.ClusterBlockLevel;
1516
import org.elasticsearch.cluster.metadata.Metadata;
1617
import org.elasticsearch.common.collect.Iterators;
1718
import org.elasticsearch.common.io.stream.StreamInput;
@@ -57,6 +58,7 @@ public final class RoleMappingMetadata extends AbstractNamedDiffable<Metadata.Cu
5758
private static final RoleMappingMetadata EMPTY = new RoleMappingMetadata(Set.of());
5859

5960
public static RoleMappingMetadata getFromClusterState(ClusterState clusterState) {
61+
clusterState.blocks().globalBlockedRaiseException(ClusterBlockLevel.READ);
6062
return clusterState.metadata().custom(RoleMappingMetadata.TYPE, RoleMappingMetadata.EMPTY);
6163
}
6264

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/RoleMappingFileSettingsIT.java

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingAction;
3535
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest;
3636
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequestBuilder;
37+
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingResponse;
3738
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
3839
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
3940
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
@@ -58,12 +59,11 @@
5859
import static org.elasticsearch.indices.recovery.RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING;
5960
import static org.elasticsearch.xcontent.XContentType.JSON;
6061
import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7;
62+
import static org.elasticsearch.xpack.security.authc.support.mapper.ClusterStateRoleMapper.RESERVED_ROLE_MAPPING_SUFFIX;
6163
import static org.hamcrest.Matchers.allOf;
62-
import static org.hamcrest.Matchers.contains;
6364
import static org.hamcrest.Matchers.containsInAnyOrder;
6465
import static org.hamcrest.Matchers.containsString;
6566
import static org.hamcrest.Matchers.empty;
66-
import static org.hamcrest.Matchers.emptyArray;
6767
import static org.hamcrest.Matchers.equalTo;
6868
import static org.hamcrest.Matchers.hasSize;
6969
import static org.hamcrest.Matchers.notNullValue;
@@ -270,21 +270,28 @@ private void assertRoleMappingsSaveOK(CountDownLatch savedClusterState, AtomicLo
270270
assertThat(resolveRolesFuture.get(), containsInAnyOrder("kibana_user", "fleet_user"));
271271
}
272272

273-
// the role mappings are not retrievable by the role mapping action (which only accesses "native" i.e. index-based role mappings)
274-
var request = new GetRoleMappingsRequest();
275-
request.setNames("everyone_kibana", "everyone_fleet");
276-
var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
277-
assertFalse(response.hasMappings());
278-
assertThat(response.mappings(), emptyArray());
279-
280-
// role mappings (with the same names) can also be stored in the "native" store
281-
var putRoleMappingResponse = client().execute(PutRoleMappingAction.INSTANCE, sampleRestRequest("everyone_kibana")).actionGet();
282-
assertTrue(putRoleMappingResponse.isCreated());
283-
putRoleMappingResponse = client().execute(PutRoleMappingAction.INSTANCE, sampleRestRequest("everyone_fleet")).actionGet();
284-
assertTrue(putRoleMappingResponse.isCreated());
273+
// the role mappings are retrievable by the role mapping action for BWC
274+
assertGetResponseHasMappings(true, "everyone_kibana", "everyone_fleet");
275+
276+
// role mappings (with the same names) can be stored in the "native" store
277+
{
278+
PutRoleMappingResponse response = client().execute(PutRoleMappingAction.INSTANCE, sampleRestRequest("everyone_kibana"))
279+
.actionGet();
280+
assertTrue(response.isCreated());
281+
response = client().execute(PutRoleMappingAction.INSTANCE, sampleRestRequest("everyone_fleet")).actionGet();
282+
assertTrue(response.isCreated());
283+
}
284+
{
285+
// deleting role mappings that exist in the native store and in cluster-state should result in success
286+
var response = client().execute(DeleteRoleMappingAction.INSTANCE, deleteRequest("everyone_kibana")).actionGet();
287+
assertTrue(response.isFound());
288+
response = client().execute(DeleteRoleMappingAction.INSTANCE, deleteRequest("everyone_fleet")).actionGet();
289+
assertTrue(response.isFound());
290+
}
291+
285292
}
286293

287-
public void testRoleMappingsApplied() throws Exception {
294+
public void testClusterStateRoleMappingsAddedThenDeleted() throws Exception {
288295
ensureGreen();
289296

290297
var savedClusterState = setupClusterStateListener(internalCluster().getMasterName(), "everyone_kibana");
@@ -293,6 +300,12 @@ public void testRoleMappingsApplied() throws Exception {
293300
assertRoleMappingsSaveOK(savedClusterState.v1(), savedClusterState.v2());
294301
logger.info("---> cleanup cluster settings...");
295302

303+
{
304+
// Deleting non-existent native role mappings returns not found even if they exist in config file
305+
var response = client().execute(DeleteRoleMappingAction.INSTANCE, deleteRequest("everyone_kibana")).get();
306+
assertFalse(response.isFound());
307+
}
308+
296309
savedClusterState = setupClusterStateListenerForCleanup(internalCluster().getMasterName());
297310

298311
writeJSONFile(internalCluster().getMasterName(), emptyJSON, logger, versionCounter);
@@ -307,48 +320,85 @@ public void testRoleMappingsApplied() throws Exception {
307320
clusterStateResponse.getState().metadata().persistentSettings().get(INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING.getKey())
308321
);
309322

310-
// native role mappings are not affected by the removal of the cluster-state based ones
323+
// cluster-state role mapping was removed and is not returned in the API anymore
311324
{
312325
var request = new GetRoleMappingsRequest();
313326
request.setNames("everyone_kibana", "everyone_fleet");
314327
var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
315-
assertTrue(response.hasMappings());
316-
assertThat(
317-
Arrays.stream(response.mappings()).map(ExpressionRoleMapping::getName).toList(),
318-
containsInAnyOrder("everyone_kibana", "everyone_fleet")
319-
);
328+
assertFalse(response.hasMappings());
320329
}
321330

322-
// and roles are resolved based on the native role mappings
331+
// no role mappings means no roles are resolved
323332
for (UserRoleMapper userRoleMapper : internalCluster().getInstances(UserRoleMapper.class)) {
324333
PlainActionFuture<Set<String>> resolveRolesFuture = new PlainActionFuture<>();
325334
userRoleMapper.resolveRoles(
326335
new UserRoleMapper.UserData("anyUsername", null, List.of(), Map.of(), mock(RealmConfig.class)),
327336
resolveRolesFuture
328337
);
329-
assertThat(resolveRolesFuture.get(), contains("kibana_user_native"));
338+
assertThat(resolveRolesFuture.get(), empty());
330339
}
340+
}
331341

332-
{
333-
var request = new DeleteRoleMappingRequest();
334-
request.setName("everyone_kibana");
335-
var response = client().execute(DeleteRoleMappingAction.INSTANCE, request).get();
336-
assertTrue(response.isFound());
337-
request = new DeleteRoleMappingRequest();
338-
request.setName("everyone_fleet");
339-
response = client().execute(DeleteRoleMappingAction.INSTANCE, request).get();
340-
assertTrue(response.isFound());
342+
public void testGetRoleMappings() throws Exception {
343+
ensureGreen();
344+
345+
final List<String> nativeMappings = List.of("everyone_kibana", "_everyone_kibana", "zzz_mapping", "123_mapping");
346+
for (var mapping : nativeMappings) {
347+
client().execute(PutRoleMappingAction.INSTANCE, sampleRestRequest(mapping)).actionGet();
341348
}
342349

343-
// no roles are resolved now, because both native and cluster-state based stores have been cleared
344-
for (UserRoleMapper userRoleMapper : internalCluster().getInstances(UserRoleMapper.class)) {
345-
PlainActionFuture<Set<String>> resolveRolesFuture = new PlainActionFuture<>();
346-
userRoleMapper.resolveRoles(
347-
new UserRoleMapper.UserData("anyUsername", null, List.of(), Map.of(), mock(RealmConfig.class)),
348-
resolveRolesFuture
349-
);
350-
assertThat(resolveRolesFuture.get(), empty());
350+
var savedClusterState = setupClusterStateListener(internalCluster().getMasterName(), "everyone_kibana");
351+
writeJSONFile(internalCluster().getMasterName(), testJSON, logger, versionCounter);
352+
boolean awaitSuccessful = savedClusterState.v1().await(20, TimeUnit.SECONDS);
353+
assertTrue(awaitSuccessful);
354+
355+
var request = new GetRoleMappingsRequest();
356+
var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
357+
assertTrue(response.hasMappings());
358+
assertThat(
359+
Arrays.stream(response.mappings()).map(ExpressionRoleMapping::getName).toList(),
360+
containsInAnyOrder(
361+
"everyone_kibana",
362+
"everyone_kibana " + RESERVED_ROLE_MAPPING_SUFFIX,
363+
"_everyone_kibana",
364+
"everyone_fleet " + RESERVED_ROLE_MAPPING_SUFFIX,
365+
"zzz_mapping",
366+
"123_mapping"
367+
)
368+
);
369+
370+
int readOnlyCount = 0;
371+
// assert that cluster-state role mappings come last
372+
for (ExpressionRoleMapping mapping : response.mappings()) {
373+
readOnlyCount = mapping.getName().endsWith(RESERVED_ROLE_MAPPING_SUFFIX) ? readOnlyCount + 1 : readOnlyCount;
351374
}
375+
// Two sourced from cluster-state
376+
assertEquals(readOnlyCount, 2);
377+
378+
// it's possible to delete overlapping native role mapping
379+
assertTrue(client().execute(DeleteRoleMappingAction.INSTANCE, deleteRequest("everyone_kibana")).actionGet().isFound());
380+
381+
savedClusterState = setupClusterStateListenerForCleanup(internalCluster().getMasterName());
382+
writeJSONFile(internalCluster().getMasterName(), emptyJSON, logger, versionCounter);
383+
awaitSuccessful = savedClusterState.v1().await(20, TimeUnit.SECONDS);
384+
assertTrue(awaitSuccessful);
385+
386+
final ClusterStateResponse clusterStateResponse = clusterAdmin().state(
387+
new ClusterStateRequest(TEST_REQUEST_TIMEOUT).waitForMetadataVersion(savedClusterState.v2().get())
388+
).get();
389+
390+
assertNull(
391+
clusterStateResponse.getState().metadata().persistentSettings().get(INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING.getKey())
392+
);
393+
394+
// Make sure remaining native mappings can still be fetched
395+
request = new GetRoleMappingsRequest();
396+
response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
397+
assertTrue(response.hasMappings());
398+
assertThat(
399+
Arrays.stream(response.mappings()).map(ExpressionRoleMapping::getName).toList(),
400+
containsInAnyOrder("_everyone_kibana", "zzz_mapping", "123_mapping")
401+
);
352402
}
353403

354404
public static Tuple<CountDownLatch, AtomicLong> setupClusterStateListenerForError(
@@ -433,11 +483,8 @@ public void testRoleMappingApplyWithSecurityIndexClosed() throws Exception {
433483
boolean awaitSuccessful = savedClusterState.v1().await(20, TimeUnit.SECONDS);
434484
assertTrue(awaitSuccessful);
435485

436-
// no native role mappings exist
437-
var request = new GetRoleMappingsRequest();
438-
request.setNames("everyone_kibana", "everyone_fleet");
439-
var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
440-
assertFalse(response.hasMappings());
486+
// even if index is closed, cluster-state role mappings are still returned
487+
assertGetResponseHasMappings(true, "everyone_kibana", "everyone_fleet");
441488

442489
// cluster state settings are also applied
443490
var clusterStateResponse = clusterAdmin().state(
@@ -476,6 +523,12 @@ public void testRoleMappingApplyWithSecurityIndexClosed() throws Exception {
476523
}
477524
}
478525

526+
private DeleteRoleMappingRequest deleteRequest(String name) {
527+
var request = new DeleteRoleMappingRequest();
528+
request.setName(name);
529+
return request;
530+
}
531+
479532
private PutRoleMappingRequest sampleRestRequest(String name) throws Exception {
480533
var json = """
481534
{
@@ -494,4 +547,19 @@ private PutRoleMappingRequest sampleRestRequest(String name) throws Exception {
494547
return new PutRoleMappingRequestBuilder(null).source(name, parser).request();
495548
}
496549
}
550+
551+
private static void assertGetResponseHasMappings(boolean readOnly, String... mappings) throws InterruptedException, ExecutionException {
552+
var request = new GetRoleMappingsRequest();
553+
request.setNames(mappings);
554+
var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get();
555+
assertTrue(response.hasMappings());
556+
assertThat(
557+
Arrays.stream(response.mappings()).map(ExpressionRoleMapping::getName).toList(),
558+
containsInAnyOrder(
559+
Arrays.stream(mappings)
560+
.map(mapping -> mapping + (readOnly ? " " + RESERVED_ROLE_MAPPING_SUFFIX : ""))
561+
.toArray(String[]::new)
562+
)
563+
);
564+
}
497565
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,8 @@ Collection<Object> createComponents(
897897
reservedRealm
898898
);
899899
components.add(nativeUsersStore);
900-
components.add(new PluginComponentBinding<>(NativeRoleMappingStore.class, nativeRoleMappingStore));
900+
components.add(clusterStateRoleMapper);
901+
components.add(nativeRoleMappingStore);
901902
components.add(new PluginComponentBinding<>(UserRoleMapper.class, userRoleMapper));
902903
components.add(reservedRealm);
903904
components.add(realms);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/TransportDeleteRoleMappingAction.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,28 @@
99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.action.support.ActionFilters;
1111
import org.elasticsearch.action.support.HandledTransportAction;
12+
import org.elasticsearch.common.logging.HeaderWarning;
1213
import org.elasticsearch.common.util.concurrent.EsExecutors;
1314
import org.elasticsearch.injection.guice.Inject;
1415
import org.elasticsearch.tasks.Task;
1516
import org.elasticsearch.transport.TransportService;
1617
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingAction;
1718
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequest;
1819
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingResponse;
20+
import org.elasticsearch.xpack.security.authc.support.mapper.ClusterStateRoleMapper;
1921
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
2022

2123
public class TransportDeleteRoleMappingAction extends HandledTransportAction<DeleteRoleMappingRequest, DeleteRoleMappingResponse> {
2224

2325
private final NativeRoleMappingStore roleMappingStore;
26+
private final ClusterStateRoleMapper clusterStateRoleMapper;
2427

2528
@Inject
2629
public TransportDeleteRoleMappingAction(
2730
ActionFilters actionFilters,
2831
TransportService transportService,
29-
NativeRoleMappingStore roleMappingStore
32+
NativeRoleMappingStore roleMappingStore,
33+
ClusterStateRoleMapper clusterStateRoleMapper
3034
) {
3135
super(
3236
DeleteRoleMappingAction.NAME,
@@ -36,10 +40,22 @@ public TransportDeleteRoleMappingAction(
3640
EsExecutors.DIRECT_EXECUTOR_SERVICE
3741
);
3842
this.roleMappingStore = roleMappingStore;
43+
this.clusterStateRoleMapper = clusterStateRoleMapper;
3944
}
4045

4146
@Override
4247
protected void doExecute(Task task, DeleteRoleMappingRequest request, ActionListener<DeleteRoleMappingResponse> listener) {
48+
if (clusterStateRoleMapper.hasMapping(request.getName())) {
49+
// Since it's allowed to add a mapping with the same name in the native role mapping store as the file_settings namespace,
50+
// a warning header is added to signal to the caller that this could be a problem.
51+
HeaderWarning.addWarning(
52+
"A read only role mapping with the same name ["
53+
+ request.getName()
54+
+ "] has been previously been defined in a configuration file. The role mapping ["
55+
+ request.getName()
56+
+ "] defined in the configuration file is read only, will not be deleted, and will remain active."
57+
);
58+
}
4359
roleMappingStore.deleteRoleMapping(request, listener.safeMap(DeleteRoleMappingResponse::new));
4460
}
4561
}

0 commit comments

Comments
 (0)