-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Account for reserved cluster state role mappings #113687
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
Changes from 14 commits
c6245bb
843353e
d09f464
6e2d6a6
488327e
e0adf0c
5062238
f6fb4fc
c8842fe
19fe01a
ee254b6
924aab7
5a5c574
dcd9351
e76f50b
fbcad41
8459cac
8e25c0b
8706cab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| pr: 113687 | ||
| summary: Account for reserved cluster state role mappings | ||
| area: Authentication | ||
| type: bug | ||
| issues: [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,8 +51,9 @@ protected void doExecute(Task task, final GetRoleMappingsRequest request, final | |
| } else { | ||
| names = new HashSet<>(Arrays.asList(request.getNames())); | ||
| } | ||
| // TODO make sure result is deterministic | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I mean here is ordered the same each time: the respond now contains the output of |
||
| this.roleMappingStore.getRoleMappings(names, ActionListener.wrap(mappings -> { | ||
| ExpressionRoleMapping[] array = mappings.toArray(new ExpressionRoleMapping[mappings.size()]); | ||
| ExpressionRoleMapping[] array = mappings.toArray(new ExpressionRoleMapping[0]); | ||
| listener.onResponse(new GetRoleMappingsResponse(array)); | ||
| }, listener::onFailure)); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ | |
| * A role mapper the reads the role mapping rules (i.e. {@link ExpressionRoleMapping}s) from the cluster state | ||
| * (i.e. {@link RoleMappingMetadata}). This is not enabled by default. | ||
| */ | ||
| public final class ClusterStateRoleMapper extends AbstractRoleMapperClearRealmCache implements ClusterStateListener { | ||
| public class ClusterStateRoleMapper extends AbstractRoleMapperClearRealmCache implements ClusterStateListener { | ||
|
|
||
| /** | ||
| * This setting is never registered by the xpack security plugin - in order to enable the | ||
|
|
@@ -44,6 +44,7 @@ public final class ClusterStateRoleMapper extends AbstractRoleMapperClearRealmCa | |
| * of the {@link NativeRoleMappingStore} and this mapper.</li> | ||
| * </ul> | ||
| */ | ||
| // TODO we need to register this setting as an escape hatch | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should expose this as setting if it is always true and will break things if we set it false. I think we rely (and should double check) the no-op,no-error behavior when the settings.json is not present. We should update the comments to ensure that it is know this is the ECK and serverless path.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm thinking of stateful, non-ECK deployments. The cluster-state role mapper still has some overhead in that it checks cluster state. If there is an issue we aren't thinking off, I'd really like there to be an escape hatch for stateful, non-ECK customers that by definition don't use cluster-state role mappings. I don't feel very strongly, but I think having a filtered, undocumented setting is a nice-to-have. |
||
| public static final String CLUSTER_STATE_ROLE_MAPPINGS_ENABLED = "xpack.security.authc.cluster_state_role_mappings.enabled"; | ||
| private static final Logger logger = LogManager.getLogger(ClusterStateRoleMapper.class); | ||
|
|
||
|
|
@@ -54,8 +55,7 @@ public final class ClusterStateRoleMapper extends AbstractRoleMapperClearRealmCa | |
| public ClusterStateRoleMapper(Settings settings, ScriptService scriptService, ClusterService clusterService) { | ||
| this.scriptService = scriptService; | ||
| this.clusterService = clusterService; | ||
| // this role mapper is disabled by default and only code in other plugins can enable it | ||
| this.enabled = settings.getAsBoolean(CLUSTER_STATE_ROLE_MAPPINGS_ENABLED, false); | ||
| this.enabled = settings.getAsBoolean(CLUSTER_STATE_ROLE_MAPPINGS_ENABLED, true); | ||
| if (this.enabled) { | ||
| clusterService.addListener(this); | ||
| } | ||
|
|
@@ -81,7 +81,11 @@ public void clusterChanged(ClusterChangedEvent event) { | |
| } | ||
| } | ||
|
|
||
| private Set<ExpressionRoleMapping> getMappings() { | ||
| public boolean isEnabled() { | ||
| return enabled; | ||
| } | ||
|
|
||
| public Set<ExpressionRoleMapping> getMappings() { | ||
| if (enabled == false) { | ||
| return Set.of(); | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,7 +78,7 @@ public class NativeRoleMappingStore extends AbstractRoleMapperClearRealmCache { | |
| /** | ||
| * This setting is never registered by the security plugin - in order to disable the native role APIs | ||
| * another plugin must register it as a boolean setting and cause it to be set to `false`. | ||
| * | ||
| * <p> | ||
| * If this setting is set to <code>false</code> then | ||
| * <ul> | ||
| * <li>the Rest APIs for native role mappings management are disabled.</li> | ||
|
|
@@ -106,14 +106,26 @@ public class NativeRoleMappingStore extends AbstractRoleMapperClearRealmCache { | |
| private final boolean lastLoadCacheEnabled; | ||
| private final AtomicReference<List<ExpressionRoleMapping>> lastLoadRef = new AtomicReference<>(null); | ||
| private final boolean enabled; | ||
| private final ReservedRoleMappings reservedRoleMappings; | ||
|
|
||
| public NativeRoleMappingStore(Settings settings, Client client, SecurityIndexManager securityIndex, ScriptService scriptService) { | ||
| public NativeRoleMappingStore( | ||
| Settings settings, | ||
| Client client, | ||
| SecurityIndexManager securityIndex, | ||
| ScriptService scriptService, | ||
| ReservedRoleMappings reservedRoleMappings | ||
| ) { | ||
| this.settings = settings; | ||
| this.client = client; | ||
| this.securityIndex = securityIndex; | ||
| this.scriptService = scriptService; | ||
| this.lastLoadCacheEnabled = LAST_LOAD_CACHE_ENABLED_SETTING.get(settings); | ||
| this.enabled = settings.getAsBoolean(NATIVE_ROLE_MAPPINGS_ENABLED, true); | ||
| this.reservedRoleMappings = reservedRoleMappings; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this since it simplifies things and nicely makes this change a no-op for serverless since IIUC this is completely disabled in serverless (and likley always will be) |
||
| } | ||
|
|
||
| public boolean isEnabled() { | ||
| return enabled; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -199,6 +211,12 @@ private <Request, Result> void modifyMapping( | |
| Request request, | ||
| ActionListener<Result> listener | ||
| ) { | ||
| if (reservedRoleMappings.isReserved(name)) { | ||
| listener.onFailure( | ||
| new IllegalArgumentException("Role mapping [" + name + "] is reserved and cannot be modified via Native Role Mapping APIs") | ||
| ); | ||
| return; | ||
| } | ||
| if (securityIndex.isIndexUpToDate() == false) { | ||
| listener.onFailure( | ||
| new IllegalStateException( | ||
|
|
@@ -304,12 +322,19 @@ public void onFailure(Exception e) { | |
| } | ||
| } | ||
|
|
||
| public void getRoleMappings(Set<String> names, ActionListener<List<ExpressionRoleMapping>> listener) { | ||
| innerGetRoleMappings( | ||
| names, | ||
| listener.delegateFailureAndWrap((l, mappings) -> l.onResponse(reservedRoleMappings.mergeWithReserved(mappings))) | ||
|
||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves one or more mappings from the index. | ||
| * If <code>names</code> is <code>null</code> or {@link Set#isEmpty empty}, then this retrieves all mappings. | ||
| * Otherwise it retrieves the specified mappings by name. | ||
| */ | ||
| public void getRoleMappings(Set<String> names, ActionListener<List<ExpressionRoleMapping>> listener) { | ||
| private void innerGetRoleMappings(Set<String> names, ActionListener<List<ExpressionRoleMapping>> listener) { | ||
| if (enabled == false) { | ||
| listener.onResponse(List.of()); | ||
| } else if (names == null || names.isEmpty()) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change (getting rid of
CompositeRoleMapper) is a little nuanced. The reason I'm making it is:The PR changes the
NativeRoleMappingStoreto fetch "reserved" (i.e., cluster-state-based) role mappings and use these instead of any native role mappings with the same name (seemergeWithReserved). This means that we read cluster-state role mappings in theNativeRoleMappingStore, and substitute mappings based on what we read. If we stuck with theCompositeRoleMapperhere, we'd "double-read" cluster-state-based role mappings.An alternative I originally considered is to filter out native role mappings that match reserved ones and then simply rely on
CompositeRoleMapperhere to fill in cluster-state ones. However, this would introduce a race condition:However, this means we have a "stale" view obtained in step 2.
By going with current approach in this PR instead (remove
CompositeRoleMapper), we avoid this race condition since we only read cluster state once.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tricky! I think your change here looks good. I thought of an alternative explained below.
I think it would be clearer to keep the role mapping stores separate and then leave the merging of them to some external class. Priorities can change and the way we want to merge them could change too. With your change
ClusterStateRoleMapperis injected into theNativeRoleMappingStorethrough the wrapping classReservedRoleMappingsand you're usingClusterStateRoleMapper"internals" (getMappings- not exactly an internal but I think it was only public for testing purposes). It does violate theUserRoleMapperinterface a little.I think adding a new
MergingCompositeRoleMapper(or a better name) that handles both read and write (so bothgetRoleMappingsandputRoleMapping) would be more general. For read it would merge the result of the providedUserRoleMappers where priority could be the order of arguments passed (soClusterStateRoleMapperwould be passed first to have priority). For writes it could check theClusterStateRoleMapperfor reserved names before writing (according to the configuration) to the native store.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've considered this (see a stub of it here) however it comes out a much bigger change, and I'm not sure it's worth it because the abstraction itself is sort of neither here nor there. The problems are:
The new class is neither Merging, nor a role mapper. Instead it's a class that merges on reads, but prevents reserved writes. So it's more of a
ReservedRoleMappingWrappedStore. And it's not just a role mapper (that only handles resolving roles) but a role mapping store. The latter requires a whole new interface or abstract class, and plugging that interface in everywhereNativeRoleMappingStoreis currently used. Do-able, butNativeRoleMappingStoreis used in several actions and tests.I'm just not sure that it's worth it to introduce all this generic structure to handle something that's unlikely to generalize. The change is already quite nuanced so I worry that making the delta bigger makes it harder to review and reason about. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I see what you mean. The interfaces are note exactly built for that and the change is big.
I think the coupling this introduces is a little concerning, but I don't think this is the right time to be making a huge refactor, so I think leaving it is fine.