diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 136e40014c..9dd73923f9 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -30,45 +30,7 @@ package org.opensearch.security; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.security.AccessController; -import java.security.MessageDigest; -import java.security.PrivilegedAction; -import java.security.Security; -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.opensearch.security.auditlog.NullAuditLog; -import org.opensearch.security.auditlog.impl.AuditLogImpl; -import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; -import org.opensearch.security.configuration.DlsFlsValveImpl; -import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper; -import org.opensearch.security.configuration.PrivilegesInterceptorImpl; -import org.opensearch.security.configuration.Salt; -import org.opensearch.security.dlic.rest.api.SecurityRestApiActions; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.http.SecurityHttpServerTransport; -import org.opensearch.security.rest.SecurityConfigUpdateAction; -import org.opensearch.security.rest.SecurityWhoAmIAction; -import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; -import org.opensearch.security.ssl.rest.SecuritySSLReloadCertsAction; -import org.opensearch.security.ssl.rest.SecuritySSLCertsInfoAction; -import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; -import org.opensearch.security.transport.SecurityInterceptor; -import org.opensearch.security.setting.OpensearchDynamicSetting; -import org.opensearch.security.setting.TransportPassiveAuthSetting; - -import org.slf4j.LoggerFactory; -import org.slf4j.Logger; +import com.google.common.collect.Lists; import org.apache.lucene.search.QueryCachingPolicy; import org.apache.lucene.search.Weight; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -92,12 +54,8 @@ import org.opensearch.common.logging.DeprecationLogger; import org.opensearch.common.network.NetworkModule; import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.IndexScopedSettings; -import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.*; import org.opensearch.common.settings.Setting.Property; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.settings.SettingsFilter; import org.opensearch.common.util.BigArrays; import org.opensearch.common.util.PageCacheRecycler; import org.opensearch.common.util.concurrent.ThreadContext; @@ -131,54 +89,62 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auditlog.AuditLogSslExceptionHandler; +import org.opensearch.security.auditlog.NullAuditLog; +import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; +import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; +import org.opensearch.security.configuration.*; +import org.opensearch.security.dlic.rest.api.SecurityRestApiActions; import org.opensearch.security.filter.SecurityFilter; +import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.rest.DashboardsInfoAction; -import org.opensearch.security.rest.SecurityHealthAction; -import org.opensearch.security.rest.SecurityInfoAction; -import org.opensearch.security.rest.TenantInfoAction; +import org.opensearch.security.rest.*; import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.setting.OpensearchDynamicSetting; +import org.opensearch.security.setting.TransportPassiveAuthSetting; +import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; import org.opensearch.security.ssl.SslExceptionHandler; +import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; +import org.opensearch.security.ssl.rest.SecuritySSLCertsInfoAction; +import org.opensearch.security.ssl.rest.SecuritySSLReloadCertsAction; +import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.*; import org.opensearch.security.transport.DefaultInterClusterRequestEvaluator; import org.opensearch.security.transport.InterClusterRequestEvaluator; +import org.opensearch.security.transport.SecurityInterceptor; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.RemoteClusterService; -import org.opensearch.transport.Transport; +import org.opensearch.transport.*; import org.opensearch.transport.Transport.Connection; -import org.opensearch.transport.TransportChannel; -import org.opensearch.transport.TransportInterceptor; -import org.opensearch.transport.TransportRequest; -import org.opensearch.transport.TransportRequestHandler; -import org.opensearch.transport.TransportRequestOptions; -import org.opensearch.transport.TransportResponse; -import org.opensearch.transport.TransportResponseHandler; -import org.opensearch.transport.TransportService; import org.opensearch.watcher.ResourceWatcherService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -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.ssl.http.netty.ValidatingDispatcher; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.support.HeaderHelper; -import org.opensearch.security.support.ModuleInfo; -import org.opensearch.security.support.ReflectionHelper; -import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.support.SecurityUtils; -import org.opensearch.security.support.SecuritySettings; -import com.google.common.collect.Lists; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.AccessController; +import java.security.MessageDigest; +import java.security.PrivilegedAction; +import java.security.Security; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin { @@ -917,6 +883,7 @@ public List> getSettings() { settings.add(Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered)); settings.add(Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered)); settings.add(Setting.boolSetting(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, false, Property.NodeScope, Property.Filtered)); settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".", Property.NodeScope)); //not filtered here settings.add(Setting.simpleString(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, Property.NodeScope, Property.Filtered)); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index b086847433..4530f7553d 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -30,20 +30,9 @@ package org.opensearch.security.privileges; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringJoiner; -import java.util.regex.Pattern; - import com.google.common.collect.ImmutableSet; -import org.slf4j.LoggerFactory; -import org.slf4j.Logger; +import com.google.common.collect.Sets; +import org.greenrobot.eventbus.Subscribe; import org.opensearch.OpenSearchSecurityException; import org.opensearch.action.ActionRequest; import org.opensearch.action.IndicesRequest; @@ -85,23 +74,24 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.SecurityRoles; -import org.opensearch.tasks.Task; -import org.opensearch.threadpool.ThreadPool; -import org.greenrobot.eventbus.Subscribe; - -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import com.google.common.collect.Sets; +import java.util.*; +import java.util.regex.Pattern; import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; @@ -136,6 +126,7 @@ public class PrivilegesEvaluator { private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; private final TermsAggregationEvaluator termsAggregationEvaluator; private final boolean dlsFlsEnabled; + private final boolean dfmEmptyOverwritesAll; private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; @@ -164,6 +155,7 @@ public PrivilegesEvaluator(final ClusterService clusterService, final ThreadPool termsAggregationEvaluator = new TermsAggregationEvaluator(); this.namedXContentRegistry = namedXContentRegistry; this.dlsFlsEnabled = dlsFlsEnabled; + this.dfmEmptyOverwritesAll = settings.getAsBoolean(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, false); } @Subscribe @@ -292,7 +284,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin log.trace("dnfof enabled? {}", dnfofEnabled); } - presponse.evaluatedDlsFlsConfig = getSecurityRoles(mappedRoles).getDlsFls(user, resolver, clusterService, namedXContentRegistry); + presponse.evaluatedDlsFlsConfig = getSecurityRoles(mappedRoles).getDlsFls(user, dfmEmptyOverwritesAll, resolver, clusterService, namedXContentRegistry); if (isClusterPerm(action0)) { diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index cd30e83f32..84ddc6e964 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -17,26 +17,9 @@ package org.opensearch.security.securityconf; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import org.slf4j.LoggerFactory; -import org.slf4j.Logger; +import com.google.common.base.Joiner; +import com.google.common.collect.*; +import com.google.common.collect.MultimapBuilder.SetMultimapBuilder; import org.opensearch.ExceptionsHelper; import org.opensearch.action.support.IndicesOptions; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; @@ -55,13 +38,13 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; -import com.google.common.base.Joiner; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.MultimapBuilder.SetMultimapBuilder; -import com.google.common.collect.SetMultimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.*; +import java.util.stream.Collectors; import static org.opensearch.cluster.metadata.IndexAbstraction.Type.ALIAS; @@ -364,7 +347,7 @@ public SecurityRoles filter(Set keep) { @Override - public EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver resolver, ClusterService cs, + public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, IndexNameExpressionResolver resolver, ClusterService cs, NamedXContentRegistry namedXContentRegistry) { final Map> dlsQueries = new HashMap>(); diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 9481173959..a8cc92aa53 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -17,26 +17,9 @@ package org.opensearch.security.securityconf; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import org.slf4j.LoggerFactory; -import org.slf4j.Logger; +import com.google.common.base.Joiner; +import com.google.common.collect.*; +import com.google.common.collect.MultimapBuilder.SetMultimapBuilder; import org.opensearch.ExceptionsHelper; import org.opensearch.action.support.IndicesOptions; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; @@ -56,18 +39,17 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; -import com.google.common.base.Joiner; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.MultimapBuilder.SetMultimapBuilder; -import com.google.common.collect.SetMultimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.*; +import java.util.stream.Collectors; import static org.opensearch.cluster.metadata.IndexAbstraction.Type.ALIAS; public class ConfigModelV7 extends ConfigModel { - protected final Logger log = LoggerFactory.getLogger(this.getClass()); private ConfigConstants.RolesMappingResolution rolesMappingResolution; private ActionGroupResolver agr = null; @@ -351,7 +333,7 @@ public SecurityRoles filter(Set keep) { @Override - public EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver resolver, ClusterService cs, + public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, IndexNameExpressionResolver resolver, ClusterService cs, NamedXContentRegistry namedXContentRegistry) { @@ -366,11 +348,19 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver re Map> dlsQueriesByIndex = new HashMap>(); Map> flsFields = new HashMap>(); Map> maskedFieldsMap = new HashMap>(); - + + // we capture all concrete indices that do not have any + // DLS/FLS/Masked Fields restrictions. If the dmf_empty_overwrites_all + // switch is enabled, this trumps any restrictions on those indices + // that may be imposed by other roles. + Set noDlsConcreteIndices = new HashSet<>(); + Set noFlsConcreteIndices = new HashSet<>(); + Set noMaskedFieldConcreteIndices = new HashSet<>(); + for (SecurityRole role : roles) { for (IndexPattern ip : role.getIpatterns()) { Set concreteIndices; - concreteIndices = ip.getResolvedIndexPattern(user, resolver, cs); + concreteIndices = ip.getResolvedIndexPattern(user, resolver, cs, false); String dls = ip.getDlsQuery(user); if (dls != null && dls.length() > 0) { @@ -378,7 +368,9 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver re for (String concreteIndex : concreteIndices) { dlsQueriesByIndex.computeIfAbsent(concreteIndex, (key) -> new HashSet()).add(dls); } - } + } else if (dfmEmptyOverwritesAll) { + noDlsConcreteIndices.addAll(concreteIndices); + } Set fls = ip.getFls(); @@ -392,8 +384,10 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver re flsFields.get(concreteIndex).addAll(Sets.newHashSet(fls)); } } + } else if (dfmEmptyOverwritesAll) { + noFlsConcreteIndices.addAll(concreteIndices); } - + Set maskedFields = ip.getMaskedFields(); if (maskedFields != null && maskedFields.size() > 0) { @@ -406,10 +400,30 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver re maskedFieldsMap.get(concreteIndex).addAll(Sets.newHashSet(maskedFields)); } } - } + } else if (dfmEmptyOverwritesAll) { + noMaskedFieldConcreteIndices.addAll(concreteIndices); + } } } + + if (dfmEmptyOverwritesAll) { + if (log.isDebugEnabled()) { + log.debug("Index patterns with no dls queries attached: {} - They will be removed from {}", noDlsConcreteIndices, + dlsQueriesByIndex.keySet()); + log.debug("Index patterns with no fls fields attached: {} - They will be removed from {}", noFlsConcreteIndices, + flsFields.keySet()); + log.debug("Index patterns with no masked fields attached: {} - They will be removed from {}", noMaskedFieldConcreteIndices, + maskedFieldsMap.keySet()); + } + // removing the indices that do not have D/M/F restrictions + // from the keySet will also modify the underlying map + dlsQueriesByIndex.keySet().removeAll(noDlsConcreteIndices); + flsFields.keySet().removeAll(noFlsConcreteIndices); + maskedFieldsMap.keySet().removeAll(noMaskedFieldConcreteIndices); + } + + return new EvaluatedDlsFlsConfig(dlsQueriesByIndex, flsFields, maskedFieldsMap); } @@ -530,7 +544,7 @@ private Set getAllResolvedPermittedIndices(Resolved resolved, User user, // } if (patternMatch) { //resolved but can contain patterns for nonexistent indices - final WildcardMatcher permitted = WildcardMatcher.from(p.getResolvedIndexPattern(user, resolver, cs)); //maybe they do not exist + final WildcardMatcher permitted = WildcardMatcher.from(p.getResolvedIndexPattern(user, resolver, cs, true)); //maybe they do not exist final Set res = new HashSet<>(); if (!resolved.isLocalAll() && !resolved.getAllIndices().contains("*") && !resolved.getAllIndices().contains("_all")) { //resolved but can contain patterns for nonexistent indices @@ -722,7 +736,7 @@ public String getUnresolvedIndexPattern(User user) { return replaceProperties(indexPattern, user); } - public Set getResolvedIndexPattern(User user, IndexNameExpressionResolver resolver, ClusterService cs) { + public Set getResolvedIndexPattern(User user, IndexNameExpressionResolver resolver, ClusterService cs, boolean appendUnresolved) { String unresolved = getUnresolvedIndexPattern(user); WildcardMatcher matcher = WildcardMatcher.from(unresolved); String[] resolved = null; @@ -744,10 +758,12 @@ public Set getResolvedIndexPattern(User user, IndexNameExpressionResolve if (resolved == null || resolved.length == 0) { return ImmutableSet.of(unresolved); } else { - return ImmutableSet.builder() - .addAll(Arrays.asList(resolved)) - .add(unresolved) - .build(); + ImmutableSet.Builder builder = ImmutableSet.builder() + .addAll(Arrays.asList(resolved)); + if (appendUnresolved) { + builder.add(unresolved); + } + return builder.build(); } } @@ -963,12 +979,12 @@ private static boolean impliesTypePerm(Set ipatterns, Resolved res indexMatcherAndPermissions = ipatterns .stream() .filter(indexPattern -> "*".equals(indexPattern.getUnresolvedIndexPattern(user))) - .map(p -> new IndexMatcherAndPermissions(p.getResolvedIndexPattern(user, resolver, cs), p.perms)) + .map(p -> new IndexMatcherAndPermissions(p.getResolvedIndexPattern(user, resolver, cs, true), p.perms)) .toArray(IndexMatcherAndPermissions[]::new); } else { indexMatcherAndPermissions = ipatterns .stream() - .map(p -> new IndexMatcherAndPermissions(p.getResolvedIndexPattern(user, resolver, cs), p.perms)) + .map(p -> new IndexMatcherAndPermissions(p.getResolvedIndexPattern(user, resolver, cs, true), p.perms)) .toArray(IndexMatcherAndPermissions[]::new); } return resolvedRequestedIndices diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index 0ad849d51f..522e317783 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -31,14 +31,14 @@ package org.opensearch.security.securityconf; -import java.util.Set; - import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.user.User; +import java.util.Set; + public interface SecurityRoles { boolean impliesClusterPermissionPermission(String action0); @@ -51,7 +51,7 @@ public interface SecurityRoles { boolean get(Resolved requestedResolved, User user, String[] allIndexPermsRequiredA, IndexNameExpressionResolver resolver, ClusterService clusterService); - EvaluatedDlsFlsConfig getDlsFls(User user, IndexNameExpressionResolver resolver, ClusterService clusterService, NamedXContentRegistry namedXContentRegistry); + EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, IndexNameExpressionResolver resolver, ClusterService clusterService, NamedXContentRegistry namedXContentRegistry); Set getAllPermittedIndicesForDashboards(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs); diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 93e0f328be..725c98c784 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -30,18 +30,12 @@ package org.opensearch.security.support; -import org.opensearch.security.auditlog.impl.AuditCategory; - -import org.opensearch.common.settings.Settings; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.impl.AuditCategory; + +import java.util.*; public class ConfigConstants { @@ -143,7 +137,8 @@ public class ConfigConstants { public static final String SECURITY_CONFIG_INDEX_NAME = "plugins.security.config_index_name"; public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = "plugins.security.authcz.impersonation_dn"; public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS="plugins.security.authcz.rest_impersonation_user"; - + public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = "plugins.security.dfm_empty_overrides_all"; + public static final String SECURITY_AUDIT_TYPE_DEFAULT = "plugins.security.audit.type"; public static final String SECURITY_AUDIT_CONFIG_DEFAULT = "plugins.security.audit.config"; public static final String SECURITY_AUDIT_CONFIG_ROUTES = "plugins.security.audit.routes"; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java new file mode 100644 index 0000000000..44ea9c3761 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java @@ -0,0 +1,227 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.opensearch.security.dlic.dlsfls; + +import org.junit.Assert; +import org.junit.Test; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest.RefreshPolicy; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.test.DynamicSecurityConfig; +import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; + +/** + * Tests that the dfm_empty_overwrites_all flag works correctly. + * Per default, if a user has a role that adds restrictions to an index + * regarding DLS, FLS or masked fields (DFM), and another role that has no restrictions + * on the same index, the restrictions from the first role still applies. + * + * If the dfm_empty_overwrites_all flag is set to true, the logic is reversed: + * If a user has a role that places no restrictions on an index, this trumps + * all other role that eventually do place restrictions on this index. + */ +public class DfmOverwritesAllTest extends AbstractDlsFlsTest { + + protected void populateData(Client tc) { + + tc.index(new IndexRequest("index1-1").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source("{\"field1\": 1, \"field2\": \"value-2-1\", \"field3\": \"value-3-1\", \"field4\": \"value-4-1\" }", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("index1-2").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source("{\"field1\": 2, \"field2\": \"value-2-2\", \"field3\": \"value-3-2\", \"field4\": \"value-4-2\" }", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("index1-3").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source("{\"field1\": 3, \"field2\": \"value-2-3\", \"field3\": \"value-3-3\", \"field4\": \"value-4-3\" }", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("index1-4").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source("{\"field1\": 4, \"field2\": \"value-2-4\", \"field3\": \"value-3-4\", \"field4\": \"value-4-4\" }", XContentType.JSON)).actionGet(); + + } + + @Test + public void testDFMUnrestrictedUser() throws Exception { + // admin user sees all, no dfm restrictions apply + final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, true).build(); + + setup(settings, new DynamicSecurityConfig().setConfig("securityconfig_dfm_empty_overwrites_all.yml") + .setSecurityInternalUsers("internal_users_dfm_empty_overwrites_all.yml") + .setSecurityRoles("roles_dfm_empty_overwrites_all.yml") + .setSecurityRolesMapping("rolesmapping_dfm_empty_overwrites_all.yml")); + + HttpResponse response; + + response = rh.executeGetRequest("/index1-*/_search?pretty", + encodeBasicHeader("admin", "password")); + Assert.assertEquals(200, response.getStatusCode()); + + // the only document in index1-1 is filtered by DLS query, so normally no hit in index-1-1 + Assert.assertTrue(response.getBody().contains("index1-1")); + + // field3 and field4 - normally filtered out by FLS + Assert.assertTrue(response.getBody().contains("value-3-1")); + Assert.assertTrue(response.getBody().contains("value-4-1")); + Assert.assertTrue(response.getBody().contains("value-3-2")); + Assert.assertTrue(response.getBody().contains("value-4-2")); + Assert.assertTrue(response.getBody().contains("value-3-3")); + Assert.assertTrue(response.getBody().contains("value-4-3")); + Assert.assertTrue(response.getBody().contains("value-3-4")); + Assert.assertTrue(response.getBody().contains("value-4-4")); + + // field2 - normally masked + Assert.assertTrue(response.getBody().contains("value-2-1")); + Assert.assertTrue(response.getBody().contains("value-2-2")); + Assert.assertTrue(response.getBody().contains("value-2-3")); + Assert.assertTrue(response.getBody().contains("value-2-4")); + } + + + @Test + public void testDFMRestrictedUser() throws Exception { + // tests that the DFM settings are applied. User has only one role + // with D/F/M all enabled, so restrictions must kick in + final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, true).build(); + + setup(settings, new DynamicSecurityConfig().setConfig("securityconfig_dfm_empty_overwrites_all.yml") + .setSecurityInternalUsers("internal_users_dfm_empty_overwrites_all.yml") + .setSecurityRoles("roles_dfm_empty_overwrites_all.yml") + .setSecurityRolesMapping("rolesmapping_dfm_empty_overwrites_all.yml")); + + HttpResponse response; + + response = rh.executeGetRequest("/index1-*/_search?pretty", + encodeBasicHeader("dfm_restricted_role", "password")); + Assert.assertEquals(200, response.getStatusCode()); + + // the only document in index1-1 is filtered by DLS query, so no hit in index-1-1 + Assert.assertFalse(response.getBody().contains("index1-1")); + + // field3 and field4 - filtered out by FLS + Assert.assertFalse(response.getBody().contains("value-3-1")); + Assert.assertFalse(response.getBody().contains("value-4-1")); + Assert.assertFalse(response.getBody().contains("value-3-2")); + Assert.assertFalse(response.getBody().contains("value-4-2")); + Assert.assertFalse(response.getBody().contains("value-3-3")); + Assert.assertFalse(response.getBody().contains("value-4-3")); + Assert.assertFalse(response.getBody().contains("value-3-4")); + Assert.assertFalse(response.getBody().contains("value-4-4")); + + // field2 - masked + Assert.assertFalse(response.getBody().contains("value-2-1")); + Assert.assertFalse(response.getBody().contains("value-2-2")); + Assert.assertFalse(response.getBody().contains("value-2-3")); + Assert.assertFalse(response.getBody().contains("value-2-4")); + + // field2 - check also some masked vallues + Assert.assertTrue(response.getBody().contains("514b27191e2322b0f7cd6afc3a5d657ff438fd0cc8dc229bd1a589804fdffd99")); + Assert.assertTrue(response.getBody().contains("3090f7e867f390fb96b20ba30ee518b09a927b857393ebd1262f31191a385efa")); + } + + @Test + public void testDFMRestrictedAndUnrestrictedAllIndices() throws Exception { + + // user has the restricted role as in test testDFMRestrictedUser(). In addition, user has + // another role with the same index pattern as the restricted role but no DFM settings. In that + // case the unrestricted role should trump the restricted one, so basically user has + // full access again. + final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, true).build(); + + setup(settings, new DynamicSecurityConfig().setConfig("securityconfig_dfm_empty_overwrites_all.yml") + .setSecurityInternalUsers("internal_users_dfm_empty_overwrites_all.yml") + .setSecurityRoles("roles_dfm_empty_overwrites_all.yml") + .setSecurityRolesMapping("rolesmapping_dfm_empty_overwrites_all.yml")); + + HttpResponse response; + + response = rh.executeGetRequest("/index1-*/_search?pretty", + encodeBasicHeader("dfm_restricted_and_unrestricted_all_indices_role", "password")); + Assert.assertEquals(200, response.getStatusCode()); + + // the only document in index1-1 is filtered by DLS query, so normally no hit in index-1-1 + Assert.assertTrue(response.getBody().contains("index1-1")); + + // field3 and field4 - normally filtered out by FLS + Assert.assertTrue(response.getBody().contains("value-3-1")); + Assert.assertTrue(response.getBody().contains("value-4-1")); + Assert.assertTrue(response.getBody().contains("value-3-2")); + Assert.assertTrue(response.getBody().contains("value-4-2")); + Assert.assertTrue(response.getBody().contains("value-3-3")); + Assert.assertTrue(response.getBody().contains("value-4-3")); + Assert.assertTrue(response.getBody().contains("value-3-4")); + Assert.assertTrue(response.getBody().contains("value-4-4")); + + // field2 - normally masked + Assert.assertTrue(response.getBody().contains("value-2-1")); + Assert.assertTrue(response.getBody().contains("value-2-2")); + Assert.assertTrue(response.getBody().contains("value-2-3")); + Assert.assertTrue(response.getBody().contains("value-2-4")); + } + + @Test + public void testDFMRestrictedAndUnrestrictedOneIndex() throws Exception { + + // user has the restricted role as in test testDFMRestrictedUser(). In addition, user has + // another role where the index pattern matches two specific index ("index1-2", "index-1-1"), means this role has two indices + // which are more specific than the index pattern in the restricted role ("index1-*"), So the second role should + // remove the DMF restrictions from exactly two indices. Otherwise, restrictions still apply. + final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, true).build(); + setup(settings, new DynamicSecurityConfig().setConfig("securityconfig_dfm_empty_overwrites_all.yml") + .setSecurityInternalUsers("internal_users_dfm_empty_overwrites_all.yml") + .setSecurityRoles("roles_dfm_empty_overwrites_all.yml") + .setSecurityRolesMapping("rolesmapping_dfm_empty_overwrites_all.yml")); + + HttpResponse response; + + response = rh.executeGetRequest("/_plugins/_security/authinfo?pretty", + encodeBasicHeader("dfm_restricted_and_unrestricted_one_index_role", "password")); + + response = rh.executeGetRequest("/index1-*/_search?pretty", + encodeBasicHeader("dfm_restricted_and_unrestricted_one_index_role", "password")); + Assert.assertEquals(200, response.getStatusCode()); + + // we have a role that places no restrictions on index-1-1, lifting the DLS from the restricted role + Assert.assertTrue(response.getBody().contains("index1-1")); + Assert.assertTrue(response.getBody().contains("value-2-1")); + Assert.assertTrue(response.getBody().contains("value-3-1")); + Assert.assertTrue(response.getBody().contains("value-4-1")); + + // field3 and field4 - normally filtered out by FLS. The second role + // lifts restrictions for index1-1 and index1-4, so only those + // values should be visible for index1-1 and index1-4 + Assert.assertTrue(response.getBody().contains("value-3-1")); + Assert.assertTrue(response.getBody().contains("value-4-1")); + Assert.assertTrue(response.getBody().contains("value-3-4")); + Assert.assertTrue(response.getBody().contains("value-4-4")); + + // FLS restrictions still in place for index1-2 and index1-3, those + // fields must not be present + Assert.assertFalse(response.getBody().contains("value-3-2")); + Assert.assertFalse(response.getBody().contains("value-4-2")); + Assert.assertFalse(response.getBody().contains("value-3-3")); + Assert.assertFalse(response.getBody().contains("value-4-3")); + + // field2 - normally masked, but for index1-1 and index1-4 restrictions are + // lifted by second role, so we have cleartext in index1-1 and index1-4 + Assert.assertTrue(response.getBody().contains("value-2-1")); + Assert.assertTrue(response.getBody().contains("value-2-4")); + + // but we still have masked values for index1-2 and index1-3 + Assert.assertTrue(response.getBody().contains("514b27191e2322b0f7cd6afc3a5d657ff438fd0cc8dc229bd1a589804fdffd99")); + Assert.assertTrue(response.getBody().contains("3090f7e867f390fb96b20ba30ee518b09a927b857393ebd1262f31191a385efa")); + } +} diff --git a/src/test/resources/dlsfls/internal_users_dfm_empty_overwrites_all.yml b/src/test/resources/dlsfls/internal_users_dfm_empty_overwrites_all.yml new file mode 100644 index 0000000000..1030469e77 --- /dev/null +++ b/src/test/resources/dlsfls/internal_users_dfm_empty_overwrites_all.yml @@ -0,0 +1,24 @@ +--- +_meta: + type: "internalusers" + config_version: 2 + +admin: + #password + hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe + +dfm_user: + #password + hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe + +dfm_restricted_and_unrestricted_all_indices_role: + #password + hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe + +dfm_restricted_and_unrestricted_one_index_role: + #password + hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe + +dfm_restricted_role: + #password + hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe diff --git a/src/test/resources/dlsfls/roles_dfm_empty_overwrites_all.yml b/src/test/resources/dlsfls/roles_dfm_empty_overwrites_all.yml new file mode 100644 index 0000000000..89b09d3abb --- /dev/null +++ b/src/test/resources/dlsfls/roles_dfm_empty_overwrites_all.yml @@ -0,0 +1,50 @@ +--- +_meta: + type: "roles" + config_version: 2 + +os_admin: + cluster_permissions: + - '*' + index_permissions: + - index_patterns: + - '*' + allowed_actions: + - '*' + +os_dfm_restricted_all_indices: + cluster_permissions: + - "*" + index_permissions: + - index_patterns: + - "index1-*" + allowed_actions: + - "*" + dls: '{ "bool": { "must_not": { "term": { "field1": 1 }}}}' + masked_fields: + - "field2" + fls: + - "~field3" + - "~field4" + +os_dfm_not_restricted_all_indices: + cluster_permissions: + - "*" + index_permissions: + - index_patterns: + - "index1-*" + allowed_actions: + - "*" + +os_dfm_not_restricted_one_index: + cluster_permissions: + - "*" + index_permissions: + - index_patterns: + - "index1-4" + allowed_actions: + - "*" + - index_patterns: + - "index1-1" + allowed_actions: + - "*" diff --git a/src/test/resources/dlsfls/rolesmapping_dfm_empty_overwrites_all.yml b/src/test/resources/dlsfls/rolesmapping_dfm_empty_overwrites_all.yml new file mode 100644 index 0000000000..d31c4a8995 --- /dev/null +++ b/src/test/resources/dlsfls/rolesmapping_dfm_empty_overwrites_all.yml @@ -0,0 +1,22 @@ +--- +_meta: + type: "rolesmapping" + config_version: 2 + +os_admin: + users: + - admin + +os_dfm_restricted_all_indices: + users: + - dfm_restricted_role + - dfm_restricted_and_unrestricted_one_index_role + - dfm_restricted_and_unrestricted_all_indices_role + +os_dfm_not_restricted_all_indices: + users: + - dfm_restricted_and_unrestricted_all_indices_role + +os_dfm_not_restricted_one_index: + users: + - dfm_restricted_and_unrestricted_one_index_role diff --git a/src/test/resources/dlsfls/securityconfig_dfm_empty_overwrites_all.yml b/src/test/resources/dlsfls/securityconfig_dfm_empty_overwrites_all.yml new file mode 100644 index 0000000000..02c78087ce --- /dev/null +++ b/src/test/resources/dlsfls/securityconfig_dfm_empty_overwrites_all.yml @@ -0,0 +1,16 @@ +--- +_meta: + type: "config" + config_version: 2 +config: + dynamic: + do_not_fail_on_forbidden: true + authc: + authentication_domain_basic_internal: + http_enabled: true + transport_enabled: true + order: 0 + http_authenticator: + type: "basic" + authentication_backend: + type: "intern"