Skip to content

5219/fine grained settings permissions #5222

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions config/roles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ _meta:
type: "roles"
config_version: 2

# Allows users to update cluster routing and blocks settings
settings_admin:
cluster_permissions:
- "cluster:admin/settings/*"
allowed_settings:
cluster:
- "cluster.routing.*"
- "cluster.blocks.*"
index:
- "index.number_of_replicas"
- "index.refresh_interval"


# Allows users to update index routing and blocks settings
index_manager:
index_permissions:
- index_patterns:
- "*"
allowed_actions:
- "indices:admin/settings/*"
allowed_settings:
- "index.number_of_replicas"
- "index.refresh_interval"

# Restrict users so they can only view visualization and dashboard on OpenSearchDashboards
kibana_read_only:
reserved: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,7 @@
import org.opensearch.security.auth.BackendRegistry;
import org.opensearch.security.compliance.ComplianceIndexingOperationListener;
import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl;
import org.opensearch.security.configuration.AdminDNs;
import org.opensearch.security.configuration.ClusterInfoHolder;
import org.opensearch.security.configuration.CompatConfig;
import org.opensearch.security.configuration.ConfigurationRepository;
import org.opensearch.security.configuration.DlsFlsRequestValve;
import org.opensearch.security.configuration.DlsFlsValveImpl;
import org.opensearch.security.configuration.PrivilegesInterceptorImpl;
import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper;
import org.opensearch.security.configuration.*;
import org.opensearch.security.dlic.rest.api.Endpoint;
import org.opensearch.security.dlic.rest.api.SecurityRestApiActions;
import org.opensearch.security.dlic.rest.api.ssl.CertificatesActionType;
Expand Down Expand Up @@ -272,6 +265,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
private volatile IndexResolverReplacer irr;
private final AtomicReference<NamedXContentRegistry> namedXContentRegistry = new AtomicReference<>(NamedXContentRegistry.EMPTY);;
private volatile DlsFlsRequestValve dlsFlsValve = null;
private volatile SettingsPermissionValve settingsPermissionValve = null;
private volatile OpensearchDynamicSetting<Boolean> transportPassiveAuthSetting;
private volatile PasswordHasher passwordHasher;
private volatile DlsFlsBaseContext dlsFlsBaseContext;
Expand Down Expand Up @@ -1127,6 +1121,7 @@ public Collection<Object> createComponents(

if (SSLConfig.isSslOnlyMode()) {
dlsFlsValve = new DlsFlsRequestValve.NoopDlsFlsRequestValve();
settingsPermissionValve = new SettingsPermissionValve.NoopSettingsPermissionValve();
} else {
dlsFlsValve = new DlsFlsValveImpl(
settings,
Expand All @@ -1137,10 +1132,12 @@ public Collection<Object> createComponents(
threadPool,
dlsFlsBaseContext
);
settingsPermissionValve = new SettingsPermissionValveImpl(clusterService, threadPool, adminDns, auditLog);
cr.subscribeOnChange(configMap -> { ((SettingsPermissionValveImpl) settingsPermissionValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); });
cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); });
}

sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver);
sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver, settingsPermissionValve);

final String principalExtractorClass = settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PRINCIPAL_EXTRACTOR_CLASS, null);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.configuration;

import org.opensearch.core.action.ActionListener;
import org.opensearch.security.privileges.PrivilegesEvaluationContext;

public interface SettingsPermissionValve {
boolean invoke(PrivilegesEvaluationContext context, ActionListener<?> listener);

class NoopSettingsPermissionValve implements SettingsPermissionValve {
@Override
public boolean invoke(PrivilegesEvaluationContext context, ActionListener<?> listener) {
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.configuration;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.opensearch.OpenSearchSecurityException;
import org.opensearch.action.ActionRequest;
import org.opensearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest;
import org.opensearch.action.admin.indices.create.CreateIndexRequest;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.action.ActionListener;
import org.opensearch.core.rest.RestStatus;
import org.opensearch.security.auditlog.AuditLog;
import org.opensearch.security.privileges.PrivilegesEvaluationContext;
import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration;
import org.opensearch.security.securityconf.impl.v7.RoleV7;
import org.opensearch.security.support.WildcardMatcher;
import org.opensearch.threadpool.ThreadPool;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

public class SettingsPermissionValveImpl implements SettingsPermissionValve {
private static final Logger log = LogManager.getLogger(SettingsPermissionValveImpl.class);

private final AtomicReference<SecurityDynamicConfiguration<RoleV7>> rolesConfiguration = new AtomicReference<>();
private final ClusterService clusterService;
private final AdminDNs adminDNs;
private final AuditLog auditLog;

public SettingsPermissionValveImpl(
ClusterService clusterService,
ThreadPool threadPool,
AdminDNs adminDNs,
AuditLog auditLog
) {
this.clusterService = clusterService;
this.adminDNs = adminDNs;
this.auditLog = auditLog;

// Add listener for configuration updates
clusterService.addListener(event -> {
SecurityDynamicConfiguration<RoleV7> config = rolesConfiguration.get();
if (config != null) {
// Handle any cluster state related updates if needed
// TODO: is this needed here? It's used in DlsFlsValveImpl but seems to be working well without it here on cluster updates
}
});
}

@Override
public boolean invoke(PrivilegesEvaluationContext context, ActionListener<?> listener) {
final ActionRequest request = context.getRequest();

// Skip validation for admin users
if (adminDNs.isAdmin(context.getUser())) {
return true;
}

try {
if (request instanceof ClusterUpdateSettingsRequest) {
return validateClusterSettings(context, (ClusterUpdateSettingsRequest) request, listener);
} else if (request instanceof UpdateSettingsRequest updateSettingsRequest) {
return validateIndexSettings(context, updateSettingsRequest.settings(), updateSettingsRequest, listener);
} else if (request instanceof CreateIndexRequest createIndexRequest) {
return validateIndexSettings(context, createIndexRequest.settings(), createIndexRequest, listener);
}
return true;
} catch (Exception e) {
log.error("Error while evaluating settings permissions", e);
listener.onFailure(new OpenSearchSecurityException("Error while evaluating settings permissions: " + e.getMessage(), RestStatus.FORBIDDEN));
return false;
}
}

private boolean validateClusterSettings(
PrivilegesEvaluationContext context,
ClusterUpdateSettingsRequest request,
ActionListener<?> listener
) {
// Get allowed settings patterns from user's roles
Set<String> allowedSettings = getAllowedSettingsFromRoles(context);

// For backwards compatibility we will allow all settings if no allowed settings are defined
if (allowedSettings.isEmpty()) {
return true;
}
log.debug("Allowed settings: {} for user: {}", allowedSettings, context.getUser().getName());

// Validate persistent settings
if (!validateSettingsMap(request.persistentSettings(), allowedSettings)) {
auditLog.logMissingPrivileges(context.getAction(), request, context.getTask());
listener.onFailure(
new OpenSearchSecurityException("User not authorized to modify these cluster settings: " + request.persistentSettings().keySet(), RestStatus.FORBIDDEN)
);
return false;
}

// Validate transient settings
if (!validateSettingsMap(request.transientSettings(), allowedSettings)) {
auditLog.logMissingPrivileges(context.getAction(), request, context.getTask());
listener.onFailure(
new OpenSearchSecurityException("User not authorized to modify these cluster settings: " + request.transientSettings().keySet(), RestStatus.FORBIDDEN)
);
return false;
}

return true;
}

private boolean validateIndexSettings(
PrivilegesEvaluationContext context,
Settings requestSettings,
ActionRequest request,
ActionListener<?> listener
) {
// Get allowed settings patterns from user's roles
Set<String> allowedSettings = getAllowedSettingsFromRoles(context);
// For backwards compatibility we will allow all settings if no allowed settings are defined
if (allowedSettings.isEmpty()) {
return true;
}
log.debug("Allowed settings: {} for user: {}", allowedSettings, context.getUser().getName());
if (!validateSettingsMap(requestSettings, allowedSettings)) {
auditLog.logMissingPrivileges(context.getAction(), request, context.getTask());
listener.onFailure(
new OpenSearchSecurityException("User not authorized to use these settings during index creation/update: " + requestSettings.keySet(), RestStatus.FORBIDDEN)
);
return false;
}

return true;
}

private Set<String> getAllowedSettingsFromRoles(PrivilegesEvaluationContext context) {
Set<String> allowedSettings = new HashSet<>();
SecurityDynamicConfiguration<RoleV7> roles = rolesConfiguration.get();

if (roles != null && roles.getCEntries() != null) {
for (String role : context.getUser().getRoles()) {
RoleV7 roleConfig = roles.getCEntries().get(role);
if (roleConfig != null) {
// Get cluster-level settings permissions
Set<String> clusterSettings = roleConfig.getAllowed_cluster_settings();
if (clusterSettings != null) {
allowedSettings.addAll(clusterSettings);
}

// Get index-level settings from index permissions
List<RoleV7.Index> indexPermissions = roleConfig.getIndex_permissions();
if (indexPermissions != null) {
for (RoleV7.Index indexPermission : indexPermissions) {
Set<String> indexSettings = indexPermission.getAllowed_settings();
if (indexSettings != null) {
allowedSettings.addAll(indexSettings);
}
}
}
}
}
}

return allowedSettings;
}

private boolean validateSettingsMap(Settings settingsToValidate, Set<String> allowedPatterns) {
if (settingsToValidate.isEmpty() || allowedPatterns.isEmpty()) {
return true;
}

for (String key : settingsToValidate.keySet()) {
boolean matched = false;
for (String pattern : allowedPatterns) {
if (WildcardMatcher.from(pattern).test(key)) {
matched = true;
break;
}
}
if (!matched) {
log.debug("Setting {} not allowed for current user", key);
return false;
}
}
return true;
}

public void updateConfiguration(SecurityDynamicConfiguration<RoleV7> rolesConfig) {
if (rolesConfig != null) {
rolesConfiguration.set(rolesConfig.clone());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ public Map<String, DataType> allowedKeys() {
return allowedKeys.put("cluster_permissions", DataType.ARRAY)
.put("tenant_permissions", DataType.ARRAY)
.put("index_permissions", DataType.ARRAY)
.put("allowed_cluster_settings", DataType.ARRAY)
.put("description", DataType.STRING)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import org.opensearch.security.configuration.AdminDNs;
import org.opensearch.security.configuration.CompatConfig;
import org.opensearch.security.configuration.DlsFlsRequestValve;
import org.opensearch.security.configuration.SettingsPermissionValve;
import org.opensearch.security.http.XFFResolver;
import org.opensearch.security.privileges.PrivilegesEvaluationContext;
import org.opensearch.security.privileges.PrivilegesEvaluator;
Expand Down Expand Up @@ -113,6 +114,7 @@ public class SecurityFilter implements ActionFilter {
private final WildcardMatcher immutableIndicesMatcher;
private final RolesInjector rolesInjector;
private final UserInjector userInjector;
private final SettingsPermissionValve settingsPermissionValve;

public SecurityFilter(
final Settings settings,
Expand All @@ -124,7 +126,8 @@ public SecurityFilter(
ClusterService cs,
final CompatConfig compatConfig,
final IndexResolverReplacer indexResolverReplacer,
final XFFResolver xffResolver
final XFFResolver xffResolver,
final SettingsPermissionValve settingsPermissionValve
) {
this.evalp = evalp;
this.adminDns = adminDns;
Expand All @@ -140,6 +143,7 @@ public SecurityFilter(
);
this.rolesInjector = new RolesInjector(auditLog);
this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver);
this.settingsPermissionValve = settingsPermissionValve;
log.info("{} indices are made immutable.", immutableIndicesMatcher);
}

Expand Down Expand Up @@ -383,9 +387,16 @@ private <Request extends ActionRequest, Response extends ActionResponse> void ap
if (pres.isAllowed()) {
auditLog.logGrantedPrivileges(action, request, task);
auditLog.logIndexEvent(action, request, task);

// Add settings permission check
if (!settingsPermissionValve.invoke(context, listener)) {
return;
}

if (!dlsFlsValve.invoke(context, listener)) {
return;
}

final CreateIndexRequestBuilder createIndexRequestBuilder = pres.getCreateIndexRequestBuilder();
if (createIndexRequestBuilder == null) {
chain.proceed(task, action, request, listener);
Expand Down
Loading
Loading