diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index cb257d12ec..a98767501c 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -156,6 +156,7 @@ import org.opensearch.security.configuration.DlsFlsValveImpl; import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper; +import org.opensearch.security.configuration.SuperAdminAuthority; 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; @@ -287,6 +288,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile AtomicReference localNode = new AtomicReference<>(); private volatile AuditLog auditLog; private volatile BackendRegistry backendRegistry; + private volatile SuperAdminAuthority superAdminAuthority; private volatile SslExceptionHandler sslExceptionHandler; private volatile Client localClient; private final boolean disabled; @@ -683,7 +685,7 @@ public List getRestHandlers( Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(threadPool), Objects.requireNonNull(cs), - Objects.requireNonNull(adminDns), + Objects.requireNonNull(superAdminAuthority), Objects.requireNonNull(cr) ) ); @@ -692,7 +694,7 @@ public List getRestHandlers( settings, restController, Objects.requireNonNull(threadPool), - adminDns, + Objects.requireNonNull(superAdminAuthority), configPath, principalExtractor ) @@ -702,7 +704,7 @@ public List getRestHandlers( settings, restController, Objects.requireNonNull(threadPool), - adminDns, + Objects.requireNonNull(superAdminAuthority), configPath, principalExtractor ) @@ -714,7 +716,7 @@ public List getRestHandlers( configPath, restController, localClient, - adminDns, + Objects.requireNonNull(superAdminAuthority), cr, cs, principalExtractor, @@ -759,7 +761,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre return (rh) -> rh; } - return (rh) -> securityRestHandler.wrap(rh, adminDns, headersToCopy); + return (rh) -> securityRestHandler.wrap(rh, superAdminAuthority, headersToCopy); } @Override @@ -793,7 +795,7 @@ public void onIndexModule(IndexModule indexModule) { indexService -> new SecurityFlsDlsIndexSearcherWrapper( indexService, settings, - adminDns, + superAdminAuthority, cs, auditLog, ciol, @@ -1222,7 +1224,8 @@ public Collection createComponents( userService = new UserService(cs, cr, passwordHasher, settings, localClient); final XFFResolver xffResolver = new XFFResolver(threadPool); - backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, cih); + superAdminAuthority = new SuperAdminAuthority(adminDns, settings, threadPool); + backendRegistry = new BackendRegistry(settings, superAdminAuthority, xffResolver, auditLog, threadPool, cih); backendRegistry.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); cr.subscribeOnChange(configMap -> { backendRegistry.invalidateCache(); }); @@ -1255,7 +1258,7 @@ public Collection createComponents( ); this.privilegesConfiguration = privilegesConfiguration; - dlsFlsBaseContext = new DlsFlsBaseContext(privilegesConfiguration, threadPool.getThreadContext(), adminDns); + dlsFlsBaseContext = new DlsFlsBaseContext(privilegesConfiguration, threadPool.getThreadContext(), superAdminAuthority); if (SSLConfig.isSslOnlyMode()) { dlsFlsValve = new DlsFlsRequestValve.NoopDlsFlsRequestValve(); @@ -1267,13 +1270,13 @@ public Collection createComponents( xContentRegistry, threadPool, dlsFlsBaseContext, - adminDns, + superAdminAuthority, resourcePluginInfo, resourceSharingEnabledSetting ); } - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, resourcePluginInfo); + resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, superAdminAuthority, resourcePluginInfo); // Assign resource sharing client to each extension // Using the non-gated client (i.e. no additional permissions required) @@ -1299,7 +1302,7 @@ public Collection createComponents( sf = new SecurityFilter( settings, privilegesConfiguration, - adminDns, + superAdminAuthority, dlsFlsValve, auditLog, threadPool, @@ -1372,6 +1375,7 @@ public Collection createComponents( components.add(cr); components.add(xffResolver); components.add(backendRegistry); + components.add(superAdminAuthority); components.add(auditLog); components.add(privilegesConfiguration); components.add(restLayerEvaluator); @@ -2333,6 +2337,8 @@ public List> getSettings() { settings.add(SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING); settings.add(SecuritySettings.DLS_WRITE_BLOCKED); + settings.add(SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING); + settings.add(SecuritySettings.SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING); } return settings; diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java index 2c9c32f451..7134d9539b 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -165,7 +165,8 @@ public void logFailedLogin(String effectiveUser, boolean securityadmin, String i msg.addRestRequestInfo(request, auditConfigFilter); msg.addInitiatingUser(initiatingUser); msg.addEffectiveUser(effectiveUser); - msg.addIsAdminDn(securityadmin); + msg.addIsAdminDn(isDnAdmin(securityadmin, request)); + msg.addIsSuperadminSecret(isSecretAdmin(securityadmin, request)); save(msg); } @@ -183,7 +184,9 @@ public void logSucceededLogin(String effectiveUser, boolean securityadmin, Strin msg.addRestRequestInfo(request, auditConfigFilter); msg.addInitiatingUser(initiatingUser); msg.addEffectiveUser(effectiveUser); - msg.addIsAdminDn(securityadmin); + msg.addIsAdminDn(isDnAdmin(securityadmin, request)); + msg.addIsSuperadminSecret(isSecretAdmin(securityadmin, request)); + save(msg); } @@ -1031,4 +1034,12 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { ignoredUrlParams.addAll(authDomain.getHttpAuthenticator().getSensitiveUrlParams()); } } + + private boolean isDnAdmin(boolean securityadmin, SecurityRequest request) { + return (securityadmin && request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER) == null); + } + + private boolean isSecretAdmin(boolean securityadmin, SecurityRequest request) { + return (securityadmin && request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER) != null); + } } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java index 41d0228e74..9495504264 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java @@ -119,6 +119,7 @@ public final class AuditMessage { public static final String EXCEPTION = "audit_request_exception_stacktrace"; public static final String IS_ADMIN_DN = "audit_request_effective_user_is_admin"; + public static final String IS_SUPERADMIN_SECRET = "audit_request_effective_user_is_superadmin_secret"; public static final String PRIVILEGE = "audit_request_privilege"; public static final String TASK_ID = "audit_trace_task_id"; @@ -174,6 +175,10 @@ public void addIsAdminDn(boolean isAdminDn) { auditInfo.put(IS_ADMIN_DN, isAdminDn); } + public void addIsSuperadminSecret(boolean isSuperadminSecret) { + auditInfo.put(IS_SUPERADMIN_SECRET, isSuperadminSecret); + } + public void addException(Throwable t) { if (t != null) { auditInfo.put(EXCEPTION, ExceptionsHelper.stackTrace(t)); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 53baa1034e..59c8bc6e65 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -62,6 +62,7 @@ import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.GrpcRequestChannel; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestChannel; @@ -101,6 +102,7 @@ public class BackendRegistry { private volatile boolean initialized; private volatile boolean injectedUserEnabled = false; private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final XFFResolver xffResolver; private volatile boolean anonymousAuthEnabled = false; private final Settings opensearchSettings; @@ -156,13 +158,14 @@ public void registerClusterSettingsChangeListener(final ClusterSettings clusterS public BackendRegistry( final Settings settings, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, final XFFResolver xffResolver, final AuditLog auditLog, final ThreadPool threadPool, final ClusterInfoHolder clusterInfoHolder ) { - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; + this.adminDns = superAdminAuthority.getAdminDns(); this.opensearchSettings = settings; this.xffResolver = xffResolver; this.auditLog = auditLog; @@ -262,20 +265,32 @@ public boolean authenticate(final SecurityRequestChannel request) { } /* - Authenticates superuser based on client certificate auth. The certificate DN is read from thread context and - compared against adminDNs. If superuser is authenticated here we skip the remaining authentication flow. - Note that non superuser client/cert authentication is handled separately by the HTTPClientCertAuthenticator - auth backend. + Authenticates superadmin based on either certificate or superadmin secret. + Uses SuperAdminAuthority to coordinate authentication methods. + If superadmin is authenticated here, skip the remaining auth flow. */ - ThreadContext threadContext = this.threadPool.getThreadContext(); - final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); - if (adminDns.isAdminDN(sslPrincipal)) { - // PKI authenticated REST call - User superuser = new User(sslPrincipal); - UserSubject subject = new UserSubjectImpl(threadPool, superuser); - threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); - return true; + if (!gRPC && superAdminAuthority.isRequestFromSuperAdmin(request)) { + // Determine which type of superadmin authentication succeeded + ThreadContext threadContext = this.threadPool.getThreadContext(); + final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + if (adminDns.isAdminDN(sslPrincipal)) { + // Certificate-based admin + User superuser = new User(sslPrincipal); + UserSubject subject = new UserSubjectImpl(threadPool, superuser); + threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); + return true; + } else { + // Secret-based superadmin + User superuser = new User(superAdminAuthority.getSuperadminSecretUserName()); + UserSubject subject = new UserSubjectImpl(threadPool, superuser); + threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); + return true; + } + } else if (!gRPC && superAdminAuthority.hasSecretHeader(request)) { + // Failed superadmin secret authentication attempt + auditLog.logFailedLogin("superadmin", true, null, request); } /* diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 9b3edaefc3..5af3a4f9c1 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -106,7 +106,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final DlsFlsBaseContext dlsFlsBaseContext; private final FieldMasking.Config fieldMaskingConfig; private final Settings settings; - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final ResourcePluginInfo resourcePluginInfo; private volatile boolean dlsWriteBlockedEnabled; @@ -118,7 +118,7 @@ public DlsFlsValveImpl( NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool, DlsFlsBaseContext dlsFlsBaseContext, - AdminDNs adminDNs, + SuperAdminAuthority superAdminAuthority, ResourcePluginInfo resourcePluginInfo, OpensearchDynamicSetting resourceSharingEnabledSetting ) { @@ -131,7 +131,7 @@ public DlsFlsValveImpl( this.fieldMaskingConfig = FieldMasking.Config.fromSettings(settings); this.dlsFlsBaseContext = dlsFlsBaseContext; this.settings = settings; - this.adminDNs = adminDNs; + this.superAdminAuthority = superAdminAuthority; this.resourcePluginInfo = resourcePluginInfo; clusterService.addListener(event -> { @@ -171,7 +171,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< } UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); - if (userSubject != null && adminDNs.isAdmin(userSubject.getUser())) { + if (userSubject != null && superAdminAuthority.isSuperAdmin(userSubject.getUser())) { return true; } OptionallyResolvedIndices resolved = context.getResolvedIndices(); diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 96c1616183..03924362aa 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -66,7 +66,7 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapp public SecurityFlsDlsIndexSearcherWrapper( final IndexService indexService, final Settings settings, - final AdminDNs adminDNs, + final SuperAdminAuthority superAdminAuthority, final ClusterService clusterService, final AuditLog auditlog, final ComplianceIndexingOperationListener ciol, @@ -75,7 +75,7 @@ public SecurityFlsDlsIndexSearcherWrapper( final Supplier dlsFlsProcessedConfigSupplier, final DlsFlsBaseContext dlsFlsBaseContext ) { - super(indexService, settings, adminDNs, privilegesConfiguration, roleMapper); + super(indexService, settings, superAdminAuthority, privilegesConfiguration, roleMapper); Set metadataFieldsCopy; if (indexService.getMetadata().getState() == IndexMetadata.State.CLOSE) { if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java new file mode 100644 index 0000000000..78eabf7174 --- /dev/null +++ b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java @@ -0,0 +1,95 @@ +/* + * 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 java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.settings.SecureString; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SecuritySettings; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import static java.util.Arrays.fill; + +public class SuperAdminAuthority { + private static final Logger log = LogManager.getLogger(SuperAdminAuthority.class); + + private final AdminDNs adminDns; + private final ThreadContext threadContext; + private final SecureString superadminSecret; + + public SuperAdminAuthority(final AdminDNs adminDns, final Settings settings, final ThreadPool threadPool) { + this.adminDns = adminDns; + this.threadContext = threadPool.getThreadContext(); + this.superadminSecret = SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING.get(settings); + } + + public boolean isRequestFromSuperAdmin(final SecurityRequest request) { + return isAdminViaDn(request) || isAdminViaSecret(request); + } + + public AdminDNs getAdminDns() { + return adminDns; + } + + public boolean isAdminViaDn(final SecurityRequest request) { + final String sslPrincipal = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + if (adminDns.isAdminDN(sslPrincipal)) { + return true; + } + return false; + } + + public boolean isAdminViaSecret(final SecurityRequest request) { + return isSuperadminSecretValid(request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER)); + } + + public boolean hasSecretHeader(final SecurityRequest request) { + return !ObjectUtils.isEmpty(request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER)); + } + + public boolean isSuperAdmin(final User user) { + return user != null && (adminDns.isAdmin(user) || ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER.equals(user.getName())); + } + + public String getSuperadminSecretUserName() { + return ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER; + } + + private boolean isSuperadminSecretValid(final String providedSecret) { + if ((superadminSecret == null || superadminSecret.isEmpty()) || ObjectUtils.isEmpty(providedSecret)) { + return false; + } + + byte[] expectedAsBytes = new String(superadminSecret.getChars()).getBytes(StandardCharsets.UTF_8); + byte[] providedAsBytes = providedSecret.getBytes(StandardCharsets.UTF_8); + + try { + return MessageDigest.isEqual(expectedAsBytes, providedAsBytes); + } catch (Exception e) { + log.error("Failed to validate superadmin secret", e); + return false; + } finally { + fill(providedAsBytes, (byte) 0); + fill(expectedAsBytes, (byte) 0); + } + } +} diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 8277774a3c..37b94af9b3 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -56,7 +56,7 @@ public class SystemIndexSearcherWrapper implements CheckedFunction getHandler( final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, final ConfigurationRepository configurationRepository, final ClusterService clusterService, final PrincipalExtractor principalExtractor, @@ -62,14 +62,14 @@ public static Collection getHandler( final ResourcePluginInfo resourcePluginInfo ) { final var securityApiDependencies = new SecurityApiDependencies( - adminDns, + superAdminAuthority.getAdminDns(), configurationRepository, privilegesConfiguration, - new RestApiPrivilegesEvaluator(settings, adminDns, roleMapper, principalExtractor, configPath, threadPool), + new RestApiPrivilegesEvaluator(settings, superAdminAuthority, roleMapper, principalExtractor, configPath, threadPool), new RestApiAdminPrivilegesEvaluator( threadPool.getThreadContext(), privilegesConfiguration, - adminDns, + superAdminAuthority, settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) ), auditLog, @@ -89,7 +89,7 @@ public static Collection getHandler( configPath, controller, client, - adminDns, + superAdminAuthority, configurationRepository, clusterService, principalExtractor, diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 15f45b15e4..2f7efcf662 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -86,9 +86,9 @@ import org.opensearch.security.auth.UserInjector; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.compliance.ComplianceConfig; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; @@ -113,7 +113,7 @@ public class SecurityFilter implements ActionFilter { protected final Logger log = LogManager.getLogger(this.getClass()); private final PrivilegesConfiguration privilegesConfiguration; - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final DlsFlsRequestValve dlsFlsValve; private final AuditLog auditLog; private final ThreadPool threadPool; @@ -129,7 +129,7 @@ public class SecurityFilter implements ActionFilter { public SecurityFilter( final Settings settings, PrivilegesConfiguration privilegesConfiguration, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, ThreadPool threadPool, @@ -139,7 +139,7 @@ public SecurityFilter( ResourceAccessEvaluator resourceAccessEvaluator ) { this.privilegesConfiguration = privilegesConfiguration; - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.dlsFlsValve = dlsFlsValve; this.auditLog = auditLog; this.threadPool = threadPool; @@ -221,7 +221,7 @@ private void ap if (user != null && threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER) == null) { threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, new UserSubjectImpl(threadPool, user)); } - final boolean userIsAdmin = isUserAdmin(user, adminDns); + final boolean userIsAdmin = isUserAdmin(user, superAdminAuthority); final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadContext); final boolean trustedClusterRequest = HeaderHelper.isTrustedClusterRequest(threadContext); final boolean confRequest = "true".equals( @@ -555,8 +555,8 @@ private boolean return false; } - private static boolean isUserAdmin(User user, final AdminDNs adminDns) { - return user != null && adminDns.isAdmin(user); + private static boolean isUserAdmin(User user, final SuperAdminAuthority superAdminAuthority) { + return superAdminAuthority.isSuperAdmin(user); } private void attachSourceFieldContext(ActionRequest request) { diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 210d3f20c5..cd95604c39 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -57,8 +57,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.dlic.rest.api.AllowlistApiAction; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -125,12 +125,12 @@ public SecurityRestFilter( } class AuthczRestHandler extends DelegatingRestHandler { - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; private final Set headersToCopy; - public AuthczRestHandler(RestHandler original, AdminDNs adminDNs, Set headersToCopy) { + public AuthczRestHandler(RestHandler original, SuperAdminAuthority superAdminAuthority, Set headersToCopy) { super(original); - this.adminDNs = adminDNs; + this.superAdminAuthority = superAdminAuthority; this.headersToCopy = headersToCopy; } @@ -199,7 +199,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c // Authorize Request final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); String intiatingUser = threadContext.getTransient(OPENDISTRO_SECURITY_INITIATING_USER); - if (userIsSuperAdmin(user, adminDNs)) { + if (superAdminAuthority.isSuperAdmin(user)) { // Super admins are always authorized auditLog.logSucceededLogin(user.getName(), true, intiatingUser, filteredRequestChannel); if (performPermissionCheck) { @@ -256,22 +256,15 @@ RestRequest maybeFilterRestRequest(RestRequest request) throws IOException { * The allowlisting check works as follows: * If allowlisting is not enabled, then requests are handled normally. * If allowlisting is enabled, then SuperAdmin is allowed access to all APIs, regardless of what is currently allowlisted. - * If allowlisting is enabled, then Non-SuperAdmin is allowed to access only those APIs that are allowlisted in {@link #requests} + * If allowlisting is enabled, then Non-SuperAdmin is allowed to access only those APIs that are allowlisted in * For example: if allowlisting is enabled and requests = ["/_cat/nodes"], then SuperAdmin can access all APIs, but non SuperAdmin * can only access "/_cat/nodes" * Further note: Some APIs are only accessible by SuperAdmin, regardless of allowlisting. For example: /_opendistro/_security/api/allowlist is only accessible by SuperAdmin. * See {@link AllowlistApiAction} for the implementation of this API. * SuperAdmin is identified by credentials, which can be passed in the curl request. */ - public RestHandler wrap(RestHandler original, AdminDNs adminDNs, Set headersToCopy) { - return new AuthczRestHandler(original, adminDNs, headersToCopy); - } - - /** - * Checks if a given user is a SuperAdmin - */ - boolean userIsSuperAdmin(User user, AdminDNs adminDNs) { - return user != null && adminDNs.isAdmin(user); + public RestHandler wrap(RestHandler original, SuperAdminAuthority superAdminAuthority, Set headersToCopy) { + return new AuthczRestHandler(original, superAdminAuthority, headersToCopy); } /** diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java index dc3636df38..c26e1dd4ce 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java @@ -11,7 +11,7 @@ package org.opensearch.security.privileges.dlsfls; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.support.ConfigConstants; @@ -24,12 +24,16 @@ public class DlsFlsBaseContext { private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; - public DlsFlsBaseContext(PrivilegesConfiguration privilegesConfiguration, ThreadContext threadContext, AdminDNs adminDNs) { + public DlsFlsBaseContext( + PrivilegesConfiguration privilegesConfiguration, + ThreadContext threadContext, + SuperAdminAuthority superAdminAuthority + ) { this.privilegesConfiguration = privilegesConfiguration; this.threadContext = threadContext; - this.adminDNs = adminDNs; + this.superAdminAuthority = superAdminAuthority; } /** @@ -42,7 +46,7 @@ public PrivilegesEvaluationContext getPrivilegesEvaluationContext() { } User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if (HeaderHelper.isInternalOrPluginRequest(threadContext) || adminDNs.isAdmin(user)) { + if (HeaderHelper.isInternalOrPluginRequest(threadContext) || superAdminAuthority.isSuperAdmin(user)) { return null; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 23193e0726..ea915d7dd9 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -26,7 +26,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.UserSubjectImpl; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -49,19 +49,19 @@ public class ResourceAccessHandler { private final ThreadContext threadContext; private final ResourceSharingIndexHandler resourceSharingIndexHandler; - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; private final ResourcePluginInfo resourcePluginInfo; @Inject public ResourceAccessHandler( final ThreadPool threadPool, final ResourceSharingIndexHandler resourceSharingIndexHandler, - AdminDNs adminDns, + SuperAdminAuthority superAdminAuthority, ResourcePluginInfo resourcePluginInfo ) { this.threadContext = threadPool.getThreadContext(); this.resourceSharingIndexHandler = resourceSharingIndexHandler; - this.adminDNs = adminDns; + this.superAdminAuthority = superAdminAuthority; this.resourcePluginInfo = resourcePluginInfo; } @@ -83,7 +83,7 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceTyp String resourceIndex = resourcePluginInfo.indexByType(resourceType); - if (adminDNs.isAdmin(user)) { + if (superAdminAuthority.isSuperAdmin(user)) { loadAllResourceIds(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; } @@ -109,7 +109,7 @@ public void getResourceSharingInfoForCurrentUser(@NonNull String resourceType, A return; } - if (adminDNs.isAdmin(user)) { + if (superAdminAuthority.isSuperAdmin(user)) { loadAllResourceSharingRecords(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; } @@ -149,7 +149,7 @@ public void hasPermission( LOGGER.info("Checking if user '{}' has permission to resource '{}'", user.getName(), resourceId); - if (adminDNs.isAdmin(user)) { + if (superAdminAuthority.isSuperAdmin(user)) { LOGGER.debug("User '{}' is admin, automatically granted permission on '{}'", user.getName(), resourceId); listener.onResponse(true); return; diff --git a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java index d6d3843566..f8465ce452 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java @@ -27,7 +27,7 @@ import org.opensearch.rest.action.RestActions.NodesResponseRestListener; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -51,7 +51,7 @@ public class SecurityConfigUpdateAction extends BaseRestHandler { ); private final ThreadContext threadContext; - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final Settings settings; private final Path configPath; private final PrincipalExtractor principalExtractor; @@ -60,13 +60,13 @@ public SecurityConfigUpdateAction( final Settings settings, final RestController controller, final ThreadPool threadPool, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, Path configPath, PrincipalExtractor principalExtractor ) { super(); this.threadContext = threadPool.getThreadContext(); - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.settings = settings; this.configPath = configPath; this.principalExtractor = principalExtractor; @@ -92,15 +92,16 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli SecurityRequestFactory.from(request), principalExtractor ); + final boolean isSecretAdmin = superAdminAuthority.isAdminViaSecret(SecurityRequestFactory.from(request)); - if (sslInfo == null) { + if (sslInfo == null && !isSecretAdmin) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); } final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); // only allowed for admins - if (user == null || !adminDns.isAdmin(user)) { + if (!isSecretAdmin && (user == null || !superAdminAuthority.isSuperAdmin(user))) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); } else { ConfigUpdateRequest configUpdateRequest = new ConfigUpdateRequest(configTypes); diff --git a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java index 30a0506a9e..0a8a4d5734 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java @@ -30,7 +30,7 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -60,7 +60,7 @@ public class SecurityWhoAmIAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final Settings settings; private final Path configPath; private final PrincipalExtractor principalExtractor; @@ -70,12 +70,12 @@ public SecurityWhoAmIAction( final Settings settings, final RestController controller, final ThreadPool threadPool, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, Path configPath, PrincipalExtractor principalExtractor ) { super(); - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.settings = settings; this.configPath = configPath; this.principalExtractor = principalExtractor; @@ -96,8 +96,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli public void accept(RestChannel channel) throws Exception { XContentBuilder builder = channel.newBuilder(); BytesRestResponse response = null; - try { + final boolean isSecretAdmin = superAdminAuthority.isAdminViaSecret(SecurityRequestFactory.from(request)); SSLInfo sslInfo = SSLRequestHelper.getSSLInfo( settings, configPath, @@ -105,12 +105,11 @@ public void accept(RestChannel channel) throws Exception { principalExtractor ); - if (sslInfo == null) { + if (sslInfo == null && !isSecretAdmin) { response = new BytesRestResponse(RestStatus.FORBIDDEN, "No security data"); } else { - - final String dn = sslInfo.getPrincipal(); - final boolean isAdmin = adminDns.isAdminDN(dn); + final String dn = sslInfo == null ? null : sslInfo.getPrincipal(); + final boolean isAdmin = isSecretAdmin || superAdminAuthority.getAdminDns().isAdminDN(dn); final boolean isNodeCertificateRequest = dn != null && WildcardMatcher.from(nodesDn).ignoreCase().matchAny(dn); builder.startObject(); @@ -120,7 +119,6 @@ public void accept(RestChannel channel) throws Exception { builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); - } } catch (final Exception e1) { log.error(e1.toString(), e1); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index ecf1561992..1dcd7fde95 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -47,8 +47,8 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.TenantPrivileges; @@ -87,7 +87,7 @@ public class TenantInfoAction extends BaseRestHandler { private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; private final ClusterService clusterService; - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final ConfigurationRepository configurationRepository; public TenantInfoAction( @@ -96,14 +96,14 @@ public TenantInfoAction( final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool, final ClusterService clusterService, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, final ConfigurationRepository configurationRepository ) { super(); this.threadContext = threadPool.getThreadContext(); this.privilegesConfiguration = privilegesConfiguration; this.clusterService = clusterService; - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.configurationRepository = configurationRepository; } @@ -179,7 +179,7 @@ private boolean isAuthorized() { DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); // check if the user is a kibanauser or super admin - if (user.getName().equals(multiTenancyConfiguration.dashboardsServerUsername()) || adminDns.isAdmin(user)) { + if (user.getName().equals(multiTenancyConfiguration.dashboardsServerUsername()) || superAdminAuthority.isSuperAdmin(user)) { return true; } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index ae703642f3..d9b352920d 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -429,6 +429,12 @@ public class ConfigConstants { public static final List OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT = List.of(); // defaults to no registered types as // protected + // Super admin secret + public static final String SECURITY_SUPERADMIN_SECRET_USER = "_opendistro_security_superadmin_secret_user_"; + public static final String SECURITY_SUPERADMIN_SECRET = SECURITY_SETTINGS_PREFIX + "superadmin.secret"; + public static final String SECURITY_SUPERADMIN_SECRET_SECURE = SECURITY_SUPERADMIN_SECRET + "_secure"; + public static final String SECURITY_SUPERADMIN_SECRET_HEADER = "X-OpenSearch-Superadmin-Secret"; + public static Set getSettingAsSet( final Settings settings, final String key, diff --git a/src/main/java/org/opensearch/security/support/SecuritySettings.java b/src/main/java/org/opensearch/security/support/SecuritySettings.java index 61a17a953e..458d32dda4 100644 --- a/src/main/java/org/opensearch/security/support/SecuritySettings.java +++ b/src/main/java/org/opensearch/security/support/SecuritySettings.java @@ -11,7 +11,9 @@ package org.opensearch.security.support; +import org.opensearch.common.settings.SecureSetting; import org.opensearch.common.settings.Setting; +import org.opensearch.core.common.settings.SecureString; public class SecuritySettings { public static final Setting LEGACY_OPENDISTRO_SSL_DUAL_MODE_SETTING = Setting.boolSetting( @@ -57,4 +59,13 @@ public class SecuritySettings { Setting.Property.Dynamic, Setting.Property.Sensitive ); + + public static final Setting SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING = SecureSetting.insecureString( + ConfigConstants.SECURITY_SUPERADMIN_SECRET + ); + + public static final Setting SECURITY_SUPERADMIN_SECRET_SETTING = SecureSetting.secureString( + ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE, + SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING + ); } diff --git a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java index 7cd7d5fab7..6545529bdb 100644 --- a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java +++ b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java @@ -67,13 +67,16 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.ObjectUtils; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.reactor.ssl.TlsDetails; import org.apache.hc.core5.ssl.SSLContextBuilder; @@ -300,6 +303,15 @@ public static int execute(final String[] args) throws Exception { Option.builder("keypass").hasArg().argName("password").desc("Password of the key of admin certificate (optional)").build() ); + options.addOption( + Option.builder("sas") + .longOpt("superadmin-secret") + .hasArg() + .argName("secret") + .desc("Superadmin secret for HTTP header authentication") + .build() + ); + options.addOption(Option.builder("si").longOpt("show-info").desc("Show system and license info").build()); options.addOption(Option.builder("w").longOpt("whoami").desc("Show information about the used admin certificate").build()); @@ -374,6 +386,7 @@ public static int execute(final String[] args) throws Exception { final boolean promptForPassword; String explicitReplicas = null; String backup = null; + String superadminSecret = null; final boolean resolveEnvVars; Integer validateConfig = null; @@ -461,6 +474,7 @@ public static int execute(final String[] args) throws Exception { explicitReplicas = line.getOptionValue("er", explicitReplicas); backup = line.getOptionValue("backup"); + superadminSecret = line.getOptionValue("sas", superadminSecret); resolveEnvVars = line.hasOption("rev"); @@ -533,7 +547,8 @@ public static int execute(final String[] args) throws Exception { enabledProtocols, enabledCiphers, hostname, - port + port, + superadminSecret ) ) { @@ -1465,7 +1480,8 @@ private static RestHighLevelClient getRestHighLevelClient( String[] enabledProtocols, String[] enabledCiphers, String hostname, - int port + int port, + String superadminSecret ) { final HostnameVerifier hnv = !nhnv ? new DefaultHostnameVerifier() : NoopHostnameVerifier.INSTANCE; @@ -1475,7 +1491,13 @@ private static RestHighLevelClient getRestHighLevelClient( HttpHost httpHost = new HttpHost("https", hostname, port); - RestClientBuilder restClientBuilder = RestClient.builder(httpHost).setHttpClientConfigCallback(builder -> { + RestClientBuilder restClientBuilder = RestClient.builder(httpHost); + if (!ObjectUtils.isEmpty(superadminSecret)) { + restClientBuilder.setDefaultHeaders( + new Header[] { new BasicHeader(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER, superadminSecret) } + ); + } + restClientBuilder = restClientBuilder.setHttpClientConfigCallback(builder -> { TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() .setSslContext(sslContext) .setTlsVersions(supportedProtocols) diff --git a/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java b/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java index 97f9372693..8b6ddfd736 100644 --- a/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java +++ b/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java @@ -11,10 +11,12 @@ package org.opensearch.security; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper; @@ -149,4 +151,30 @@ public void testEndpoints() throws Exception { assertThat(rh.executePutRequest("_plugins/_security/configupdate", "").getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); assertThat(HttpStatus.SC_OK, is(rh.executePutRequest("_plugins/_security/configupdate?config_types=roles", "").getStatusCode())); } + + @Test + public void testEndpointsWithSuperAdminSecret() throws Exception { + final Settings settings = Settings.builder() + .put("plugins.security.ssl.http.enabled", true) + .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .put(ConfigConstants.SECURITY_SUPERADMIN_SECRET, "top-secret") + .build(); + setup(settings); + final RestHelper rh = restHelper(); + rh.enableHTTPClientSSL = true; + rh.trustHTTPServerCertificate = true; + rh.sendAdminCertificate = false; + + final var secretHeader = new BasicHeader(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER, "top-secret"); + + RestHelper.HttpResponse res = rh.executeGetRequest("_plugins/_security/whoami", secretHeader); + assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); + assertContains(res, "*\"is_admin\":true*"); + + assertThat( + rh.executePutRequest("_plugins/_security/configupdate?config_types=roles", "{}", secretHeader).getStatusCode(), + is(HttpStatus.SC_OK) + ); + } } diff --git a/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java b/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java index 67d65be9bf..64e380abfd 100644 --- a/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java +++ b/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java @@ -31,6 +31,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.GrpcRequestChannel; import org.opensearch.security.http.HTTPBasicAuthenticator; import org.opensearch.security.http.XFFResolver; @@ -70,6 +71,9 @@ public class BackendRegistryGrpcAuthTest { @Mock private ClusterInfoHolder clusterInfoHolder; + @Mock + private SuperAdminAuthority superAdminAuthority; + private BackendRegistry backendRegistry; @Before @@ -79,6 +83,7 @@ public void setUp() { // no admin user configured - ensure these checks are false when(adminDns.isAdmin(any())).thenReturn(false); when(adminDns.isAdminDN(any())).thenReturn(false); + when(superAdminAuthority.getAdminDns()).thenReturn(adminDns); when(threadPool.getThreadContext()).thenReturn(threadContext); when(clusterInfoHolder.hasClusterManager()).thenReturn(true); when(xffResolver.resolve(any())).thenReturn(new TransportAddress(new InetSocketAddress("127.0.0.1", 9200))); @@ -86,7 +91,7 @@ public void setUp() { // backend registry requires at least one auth path is available to initialize. // here we enable user injection to allow us to mock/test other failure cases. Settings settings = Settings.builder().put("plugins.security.unsupported.inject_user.enabled", true).build(); - backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, clusterInfoHolder); + backendRegistry = new BackendRegistry(settings, superAdminAuthority, xffResolver, auditLog, threadPool, clusterInfoHolder); } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java index e8172d7723..0a4959d4c6 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java @@ -19,7 +19,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; @@ -35,7 +35,7 @@ public class RestApiPrivilegesEvaluatorTest { public void setUp() { this.privilegesEvaluator = new RestApiPrivilegesEvaluator( Settings.EMPTY, - mock(AdminDNs.class), + mock(SuperAdminAuthority.class), (user, caller) -> user.getSecurityRoles(), mock(PrincipalExtractor.class), mock(Path.class), diff --git a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index fa461b1fea..836f78ea6a 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -26,9 +26,9 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.action.ActionResponse; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.ResourceAccessEvaluator; @@ -80,7 +80,7 @@ public void testImmutableIndicesWildcardMatcher() { final SecurityFilter filter = new SecurityFilter( settings, mock(PrivilegesConfiguration.class), - mock(AdminDNs.class), + mock(SuperAdminAuthority.class), mock(DlsFlsRequestValve.class), mock(AuditLog.class), mock(ThreadPool.class), @@ -103,7 +103,7 @@ public void testUnexepectedCausesAreNotSendToCallers() { final SecurityFilter filter = new SecurityFilter( settings, mock(PrivilegesConfiguration.class), - mock(AdminDNs.class), + mock(SuperAdminAuthority.class), mock(DlsFlsRequestValve.class), auditLog, new ThreadPool(Settings.builder().put("node.name", "mock").build()), diff --git a/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java b/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java index 1cbb4eea1a..af6e7caf36 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java @@ -27,8 +27,8 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; @@ -79,9 +79,8 @@ public void setUp() throws NoSuchMethodException { */ @Test public void testSecurityRestFilterWrap() throws Exception { - AdminDNs adminDNs = mock(AdminDNs.class); - - RestHandler wrappedRestHandler = sf.wrap(testRestHandler, adminDNs, new HashSet<>()); + SuperAdminAuthority superAdminAuthority = mock(SuperAdminAuthority.class); + RestHandler wrappedRestHandler = sf.wrap(testRestHandler, superAdminAuthority, new HashSet<>()); assertTrue(wrappedRestHandler instanceof SecurityRestFilter.AuthczRestHandler); assertFalse(wrappedRestHandler instanceof TestRestHandler); @@ -90,12 +89,9 @@ public void testSecurityRestFilterWrap() throws Exception { @Test public void testDoesCallDelegateOnSuccessfulAuthorization() throws Exception { SecurityRestFilter filterSpy = spy(sf); - AdminDNs adminDNs = mock(AdminDNs.class); - + SuperAdminAuthority superAdminAuthority = mock(SuperAdminAuthority.class); RestHandler testRestHandlerSpy = spy(testRestHandler); - RestHandler wrappedRestHandler = filterSpy.wrap(testRestHandlerSpy, adminDNs, new HashSet<>()); - - doReturn(false).when(filterSpy).userIsSuperAdmin(any(), any()); + RestHandler wrappedRestHandler = filterSpy.wrap(testRestHandlerSpy, superAdminAuthority, new HashSet<>()); wrappedRestHandler.handleRequest(mock(RestRequest.class), mock(RestChannel.class), mock(NodeClient.class)); diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java index b6b42e1f5a..c7924134f7 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java @@ -22,7 +22,7 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -49,7 +49,7 @@ public class ResourceAccessHandlerTests { @Mock private ResourceSharingIndexHandler sharingIndexHandler; @Mock - private AdminDNs adminDNs; + private SuperAdminAuthority superAdminAuthority; @Mock private ResourcePluginInfo resourcePluginInfo; @@ -66,7 +66,7 @@ public class ResourceAccessHandlerTests { public void setup() { threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); - handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, resourcePluginInfo); + handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, superAdminAuthority, resourcePluginInfo); // For tests that verify permission with action-group when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); @@ -83,7 +83,7 @@ private void injectUser(User user) { public void testHasPermission_adminUserAllowed() { User user = new User("admin", ImmutableSet.of("admin"), ImmutableSet.of(), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(true); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(true); ActionListener listener = mock(ActionListener.class); handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); @@ -95,7 +95,7 @@ public void testHasPermission_adminUserAllowed() { public void testHasPermission_ownerAllowed() { User user = new User("alice", ImmutableSet.of("r1"), ImmutableSet.of("b1"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); ResourceSharing doc = mock(ResourceSharing.class); when(doc.isCreatedBy("alice")).thenReturn(true); @@ -116,7 +116,7 @@ public void testHasPermission_ownerAllowed() { public void testHasPermission_sharedWithUserAllowed() { User user = new User("bob", ImmutableSet.of("role1"), ImmutableSet.of("backend1"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); // Document setup: shared with the user at access-level "read" ResourceSharing doc = mock(ResourceSharing.class); @@ -144,7 +144,7 @@ public void testHasPermission_sharedWithUserAllowed() { public void testHasPermission_noAccessLevelsDenied() { User user = new User("charlie", ImmutableSet.of("roleA"), ImmutableSet.of("backendA"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); ResourceSharing doc = mock(ResourceSharing.class); when(doc.getAccessLevelsForUser(user)).thenReturn(Collections.emptySet()); @@ -165,7 +165,7 @@ public void testHasPermission_noAccessLevelsDenied() { public void testHasPermission_nullDocumentDenied() { User user = new User("dave", ImmutableSet.of("x"), ImmutableSet.of("y"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); doAnswer(inv -> { ActionListener l = inv.getArgument(2); @@ -183,7 +183,7 @@ public void testHasPermission_nullDocumentDenied() { public void testGetOwnAndSharedResources_asAdmin() { User admin = new User("admin", ImmutableSet.of(), ImmutableSet.of(), null, ImmutableMap.of(), false); injectUser(admin); - when(adminDNs.isAdmin(admin)).thenReturn(true); + when(superAdminAuthority.isSuperAdmin(admin)).thenReturn(true); ActionListener> listener = mock(ActionListener.class); @@ -201,7 +201,7 @@ public void testGetOwnAndSharedResources_asAdmin() { public void testGetOwnAndSharedResources_asNormalUser() { User user = new User("alice", ImmutableSet.of("r1"), ImmutableSet.of("b1"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); ActionListener> listener = mock(ActionListener.class);