From e65b2489a88581f6701d275b1dc4e29a8138adab Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 19 Aug 2024 17:12:55 -0400 Subject: [PATCH 01/62] Implement new extension points in IdentityPlugin and add ContextProvidingPluginSubject Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 27 +++++- .../http/ExampleSystemIndexPlugin.java | 27 ------ .../IndexDocumentIntoSystemIndexAction.java | 12 +++ .../IndexDocumentIntoSystemIndexRequest.java | 30 +++++++ .../IndexDocumentIntoSystemIndexResponse.java | 36 ++++++++ .../security/plugin/PluginSubjectHolder.java | 26 ++++++ ...estIndexDocumentIntoSystemIndexAction.java | 38 ++++++++ .../security/plugin/SystemIndexPlugin1.java | 88 +++++++++++++++++++ ...ortIndexDocumentIntoSystemIndexAction.java | 62 +++++++++++++ .../security/OpenSearchSecurityPlugin.java | 19 +++- .../ContextProvidingPluginSubject.java | 36 ++++++++ .../opensearch/security/user/UserService.java | 10 +++ 12 files changed, 381 insertions(+), 30 deletions(-) delete mode 100644 src/integrationTest/java/org/opensearch/security/http/ExampleSystemIndexPlugin.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java create mode 100644 src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 599ffe9ad2..d841725843 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -13,12 +13,14 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; +import org.opensearch.common.util.FeatureFlags; import org.opensearch.core.rest.RestStatus; -import org.opensearch.security.http.ExampleSystemIndexPlugin; +import org.opensearch.security.plugin.SystemIndexPlugin1; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -26,7 +28,9 @@ import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.plugin.SystemIndexPlugin1.SYSTEM_INDEX_1; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; @@ -43,9 +47,11 @@ public class SystemIndexTests { .anonymousAuth(false) .authc(AUTHC_DOMAIN) .users(USER_ADMIN) - .plugin(ExampleSystemIndexPlugin.class) + .plugin(SystemIndexPlugin1.class) .nodeSettings( Map.of( + FeatureFlags.IDENTITY, + true, SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), SECURITY_SYSTEM_INDICES_ENABLED_KEY, @@ -54,6 +60,13 @@ public class SystemIndexTests { ) .build(); + @Before + public void wipeAllIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.delete(".system-index1"); + } + } + @Test public void adminShouldNotBeAbleToDeleteSecurityIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { @@ -80,4 +93,14 @@ public void adminShouldNotBeAbleToDeleteSecurityIndex() { assertThat(response4.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); } } + + @Test + public void testPluginShouldBeAbleToIndexDocumentIntoItsSystemIndex() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.put("_plugins/system-index/" + SYSTEM_INDEX_1); + + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + assertThat(response.getBody(), containsString(SystemIndexPlugin1.class.getCanonicalName())); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/ExampleSystemIndexPlugin.java b/src/integrationTest/java/org/opensearch/security/http/ExampleSystemIndexPlugin.java deleted file mode 100644 index b4877aae14..0000000000 --- a/src/integrationTest/java/org/opensearch/security/http/ExampleSystemIndexPlugin.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * 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. - * - */ -package org.opensearch.security.http; - -import java.util.Collection; -import java.util.Collections; - -import org.opensearch.common.settings.Settings; -import org.opensearch.indices.SystemIndexDescriptor; -import org.opensearch.plugins.Plugin; -import org.opensearch.plugins.SystemIndexPlugin; - -public class ExampleSystemIndexPlugin extends Plugin implements SystemIndexPlugin { - - @Override - public Collection getSystemIndexDescriptors(Settings settings) { - final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(".system-index1", "System index 1"); - return Collections.singletonList(systemIndexDescriptor); - } -} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java new file mode 100644 index 0000000000..40d3dd533d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java @@ -0,0 +1,12 @@ +package org.opensearch.security.plugin; + +import org.opensearch.action.ActionType; + +public class IndexDocumentIntoSystemIndexAction extends ActionType { + public static final IndexDocumentIntoSystemIndexAction INSTANCE = new IndexDocumentIntoSystemIndexAction(); + public static final String NAME = "mock:systemindex/index"; + + private IndexDocumentIntoSystemIndexAction() { + super(NAME, IndexDocumentIntoSystemIndexResponse::new); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java new file mode 100644 index 0000000000..f61f26b713 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java @@ -0,0 +1,30 @@ +package org.opensearch.security.plugin; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; + +public class IndexDocumentIntoSystemIndexRequest extends ActionRequest { + + private final String indexName; + + public IndexDocumentIntoSystemIndexRequest(String indexName) { + this.indexName = indexName; + } + + public IndexDocumentIntoSystemIndexRequest(StreamInput in) throws IOException { + super(in); + this.indexName = in.readString(); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getIndexName() { + return this.indexName; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java new file mode 100644 index 0000000000..dddd260376 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java @@ -0,0 +1,36 @@ +package org.opensearch.security.plugin; + +import java.io.IOException; + +import org.opensearch.action.support.master.AcknowledgedResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class IndexDocumentIntoSystemIndexResponse extends AcknowledgedResponse implements ToXContentObject { + + private String plugin; + + public IndexDocumentIntoSystemIndexResponse(boolean status, String plugin) { + super(status); + this.plugin = plugin; + } + + public IndexDocumentIntoSystemIndexResponse(StreamInput in) throws IOException { + super(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(plugin); + } + + @Override + public void addCustomFields(XContentBuilder builder, ToXContent.Params params) throws IOException { + super.addCustomFields(builder, params); + builder.field("plugin", plugin); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java b/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java new file mode 100644 index 0000000000..7eb7966727 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java @@ -0,0 +1,26 @@ +package org.opensearch.security.plugin; + +import org.opensearch.identity.PluginSubject; + +public class PluginSubjectHolder { + private static final PluginSubjectHolder INSTANCE = new PluginSubjectHolder(); + + private PluginSubject pluginSystemSubject; + + private PluginSubjectHolder() {} + + public void initialize(PluginSubject pluginSystemSubject) { + // if (pluginSystemSubject != null) { + // throw new OpenSearchSecurityException("pluginSystemSubjectHolder can only be initialized once"); + // } + this.pluginSystemSubject = pluginSystemSubject; + } + + public static PluginSubjectHolder getInstance() { + return PluginSubjectHolder.INSTANCE; + } + + public PluginSubject getPluginSystemSubject() { + return pluginSystemSubject; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java new file mode 100644 index 0000000000..38fa943ce1 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java @@ -0,0 +1,38 @@ +package org.opensearch.security.plugin; + +import java.util.List; + +import org.opensearch.client.Client; +import org.opensearch.client.node.NodeClient; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.PUT; + +public class RestIndexDocumentIntoSystemIndexAction extends BaseRestHandler { + + private final Client client; + + public RestIndexDocumentIntoSystemIndexAction(Client client) { + this.client = client; + } + + @Override + public List routes() { + return singletonList(new Route(PUT, "/_plugins/system-index/{index}")); + } + + @Override + public String getName() { + return "test_index_document_into_system_index_action"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String indexName = request.param("index"); + IndexDocumentIntoSystemIndexRequest indexRequest = new IndexDocumentIntoSystemIndexRequest(indexName); + return channel -> client.execute(IndexDocumentIntoSystemIndexAction.INSTANCE, indexRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java new file mode 100644 index 0000000000..b5182f3c87 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -0,0 +1,88 @@ +package org.opensearch.security.plugin; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.opensearch.action.ActionRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.node.DiscoveryNodes; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.IndexScopedSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsFilter; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.identity.PluginSubject; +import org.opensearch.indices.SystemIndexDescriptor; +import org.opensearch.plugins.IdentityAwarePlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SystemIndexPlugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestHandler; +import org.opensearch.script.ScriptService; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +public class SystemIndexPlugin1 extends Plugin implements SystemIndexPlugin, IdentityAwarePlugin { + public static final String SYSTEM_INDEX_1 = ".system-index1"; + + private Client client; + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + this.client = client; + return List.of(); + } + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(SYSTEM_INDEX_1, "System index 1"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public List getRestHandlers( + Settings settings, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster + ) { + return List.of(new RestIndexDocumentIntoSystemIndexAction(client)); + } + + @Override + public List> getActions() { + return Arrays.asList( + new ActionHandler<>(IndexDocumentIntoSystemIndexAction.INSTANCE, TransportIndexDocumentIntoSystemIndexAction.class) + ); + } + + @Override + public void assignSubject(PluginSubject pluginSystemSubject) { + PluginSubjectHolder.getInstance().initialize(pluginSystemSubject); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java new file mode 100644 index 0000000000..9118ecee11 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -0,0 +1,62 @@ +package org.opensearch.security.plugin; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.identity.PluginSubject; +import org.opensearch.security.identity.ContextProvidingPluginSubject; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class TransportIndexDocumentIntoSystemIndexAction extends HandledTransportAction< + IndexDocumentIntoSystemIndexRequest, + IndexDocumentIntoSystemIndexResponse> { + + private final Client client; + private final PluginSubject pluginSystemSubject; + + @Inject + public TransportIndexDocumentIntoSystemIndexAction( + final TransportService transportService, + final ActionFilters actionFilters, + final Client client + ) { + super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); + this.client = client; + this.pluginSystemSubject = PluginSubjectHolder.getInstance().getPluginSystemSubject(); + } + + @Override + protected void doExecute( + Task task, + IndexDocumentIntoSystemIndexRequest request, + ActionListener actionListener + ) { + String indexName = request.getIndexName(); + try { + pluginSystemSubject.runAs(() -> { + client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { + client.index( + new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON), + ActionListener.wrap(r2 -> { + String subjectHeader = client.threadPool() + .getThreadContext() + .getHeader(ContextProvidingPluginSubject.SUBJECT_HEADER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, subjectHeader)); + }, actionListener::onFailure) + ); + }, actionListener::onFailure)); + return null; + }); + } catch (Exception ex) { + throw new RuntimeException("Unexpected error: " + ex.getMessage()); + } + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 509b98f12e..5f83c16a24 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -109,6 +109,7 @@ import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.identity.PluginSubject; import org.opensearch.identity.Subject; import org.opensearch.identity.noop.NoopSubject; import org.opensearch.index.IndexModule; @@ -119,6 +120,7 @@ import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; +import org.opensearch.plugins.Plugin; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; @@ -164,6 +166,7 @@ import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.http.NonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.identity.ContextProvidingPluginSubject; import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; @@ -2102,7 +2105,7 @@ private static String handleKeyword(final String field) { } @Override - public Subject getSubject() { + public Subject getCurrentSubject() { // Not supported return new NoopSubject(); } @@ -2112,6 +2115,20 @@ public SecurityTokenManager getTokenManager() { return tokenManager; } + @Override + public PluginSubject getPluginSubject(Plugin plugin) { + return new ContextProvidingPluginSubject(threadPool, plugin); + } + + @Override + public UnaryOperator authenticate(ThreadContext threadContext) { + if (client || disabled || SSLConfig.isSslOnlyMode()) { + return (rh) -> rh; + } + + return (rh) -> securityRestHandler.wrap(rh, adminDns); + } + @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, sslExceptionHandler, securityRestHandler)); diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java new file mode 100644 index 0000000000..d706325835 --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -0,0 +1,36 @@ +package org.opensearch.security.identity; + +import java.security.Principal; +import java.util.concurrent.Callable; + +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.identity.NamedPrincipal; +import org.opensearch.identity.PluginSubject; +import org.opensearch.plugins.Plugin; +import org.opensearch.threadpool.ThreadPool; + +public class ContextProvidingPluginSubject implements PluginSubject { + public static final String SUBJECT_HEADER = "_security_subject"; + + private final ThreadPool threadPool; + private final String pluginCanonicalClassName; + + public ContextProvidingPluginSubject(ThreadPool threadPool, Plugin plugin) { + super(); + this.threadPool = threadPool; + this.pluginCanonicalClassName = plugin.getClass().getCanonicalName(); + } + + @Override + public Principal getPrincipal() { + return NamedPrincipal.UNAUTHENTICATED; + } + + @Override + public T runAs(Callable callable) throws Exception { + try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { + threadPool.getThreadContext().putHeader(SUBJECT_HEADER, pluginCanonicalClassName); + return callable.call(); + } + } +} diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index f380fdb328..2302bb818f 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -41,6 +41,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.identity.PluginSubject; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.security.DefaultObjectMapper; @@ -69,6 +70,7 @@ public class UserService { private final PasswordHasher passwordHasher; String securityIndex; Client client; + PluginSubject pluginSystemSubject; User tokenUser; final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; @@ -111,6 +113,14 @@ public UserService( this.client = client; } + void setPluginSystemSubject(PluginSubject pluginSystemSubject) { + this.pluginSystemSubject = pluginSystemSubject; + } + + public PluginSubject getPluginSystemSubject() { + return pluginSystemSubject; + } + /** * Load data for a given CType * @param config CType whose data is to be loaded in-memory From 55a7e45a78dce8901e6fd5f1e7ab8f4d37c5c522 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 09:01:51 -0400 Subject: [PATCH 02/62] Remove code from UserService Signed-off-by: Craig Perkins --- .../java/org/opensearch/security/user/UserService.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 2302bb818f..192bfc4005 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -113,14 +113,6 @@ public UserService( this.client = client; } - void setPluginSystemSubject(PluginSubject pluginSystemSubject) { - this.pluginSystemSubject = pluginSystemSubject; - } - - public PluginSubject getPluginSystemSubject() { - return pluginSystemSubject; - } - /** * Load data for a given CType * @param config CType whose data is to be loaded in-memory From 44c0d76fc5099e53c199c62185eba3264b669cdb Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 10:49:37 -0400 Subject: [PATCH 03/62] Remove commented out code Signed-off-by: Craig Perkins --- .../opensearch/security/plugin/PluginSubjectHolder.java | 3 --- .../opensearch/security/OpenSearchSecurityPlugin.java | 9 --------- .../java/org/opensearch/security/user/UserService.java | 2 -- 3 files changed, 14 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java b/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java index 7eb7966727..086efcc16c 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java @@ -10,9 +10,6 @@ public class PluginSubjectHolder { private PluginSubjectHolder() {} public void initialize(PluginSubject pluginSystemSubject) { - // if (pluginSystemSubject != null) { - // throw new OpenSearchSecurityException("pluginSystemSubjectHolder can only be initialized once"); - // } this.pluginSystemSubject = pluginSystemSubject; } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 5f83c16a24..d5eddb0af6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2120,15 +2120,6 @@ public PluginSubject getPluginSubject(Plugin plugin) { return new ContextProvidingPluginSubject(threadPool, plugin); } - @Override - public UnaryOperator authenticate(ThreadContext threadContext) { - if (client || disabled || SSLConfig.isSslOnlyMode()) { - return (rh) -> rh; - } - - return (rh) -> securityRestHandler.wrap(rh, adminDns); - } - @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, sslExceptionHandler, securityRestHandler)); diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 192bfc4005..f380fdb328 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -41,7 +41,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; -import org.opensearch.identity.PluginSubject; import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.security.DefaultObjectMapper; @@ -70,7 +69,6 @@ public class UserService { private final PasswordHasher passwordHasher; String securityIndex; Client client; - PluginSubject pluginSystemSubject; User tokenUser; final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; From 3e17b576d5733da130e64abbf26cae3ae450e128 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 11:22:58 -0400 Subject: [PATCH 04/62] Use guice dependencies Signed-off-by: Craig Perkins --- .../security/plugin/PluginSubjectHolder.java | 23 ------------------- .../security/plugin/SystemIndexPlugin1.java | 7 ++++-- .../plugin/TransportActionDependencies.java | 17 ++++++++++++++ ...ortIndexDocumentIntoSystemIndexAction.java | 5 ++-- 4 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java diff --git a/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java b/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java deleted file mode 100644 index 086efcc16c..0000000000 --- a/src/integrationTest/java/org/opensearch/security/plugin/PluginSubjectHolder.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.opensearch.security.plugin; - -import org.opensearch.identity.PluginSubject; - -public class PluginSubjectHolder { - private static final PluginSubjectHolder INSTANCE = new PluginSubjectHolder(); - - private PluginSubject pluginSystemSubject; - - private PluginSubjectHolder() {} - - public void initialize(PluginSubject pluginSystemSubject) { - this.pluginSystemSubject = pluginSystemSubject; - } - - public static PluginSubjectHolder getInstance() { - return PluginSubjectHolder.INSTANCE; - } - - public PluginSubject getPluginSystemSubject() { - return pluginSystemSubject; - } -} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index b5182f3c87..7b2e52fcee 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -35,6 +35,8 @@ public class SystemIndexPlugin1 extends Plugin implements SystemIndexPlugin, IdentityAwarePlugin { public static final String SYSTEM_INDEX_1 = ".system-index1"; + private static final TransportActionDependencies SECURITY_TRANSPORT_ACTION_DEPENDENCIES = new TransportActionDependencies(); + private Client client; @Override @@ -52,7 +54,7 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { this.client = client; - return List.of(); + return List.of(SECURITY_TRANSPORT_ACTION_DEPENDENCIES); } @Override @@ -71,6 +73,7 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { + TransportActionDependencies deps = new TransportActionDependencies(); return List.of(new RestIndexDocumentIntoSystemIndexAction(client)); } @@ -83,6 +86,6 @@ public List getRestHandlers( @Override public void assignSubject(PluginSubject pluginSystemSubject) { - PluginSubjectHolder.getInstance().initialize(pluginSystemSubject); + SECURITY_TRANSPORT_ACTION_DEPENDENCIES.setPluginSystemSubject(pluginSystemSubject); } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java new file mode 100644 index 0000000000..0d6dd8d23f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java @@ -0,0 +1,17 @@ +package org.opensearch.security.plugin; + +import org.opensearch.identity.PluginSubject; + +public class TransportActionDependencies { + private PluginSubject pluginSystemSubject; + + public TransportActionDependencies() {} + + public void setPluginSystemSubject(PluginSubject pluginSystemSubject) { + this.pluginSystemSubject = pluginSystemSubject; + } + + public PluginSubject getPluginSystemSubject() { + return pluginSystemSubject; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index 9118ecee11..0fa26698ae 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -25,11 +25,12 @@ public class TransportIndexDocumentIntoSystemIndexAction extends HandledTranspor public TransportIndexDocumentIntoSystemIndexAction( final TransportService transportService, final ActionFilters actionFilters, - final Client client + final Client client, + final TransportActionDependencies deps ) { super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); this.client = client; - this.pluginSystemSubject = PluginSubjectHolder.getInstance().getPluginSystemSubject(); + this.pluginSystemSubject = deps.getPluginSystemSubject(); } @Override From 0add3d8c7ae433c32243d2c8b4a45a25c62c8ffa Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 11:56:40 -0400 Subject: [PATCH 05/62] Move TransportActionDependencies Signed-off-by: Craig Perkins --- .../java/org/opensearch/security/plugin/SystemIndexPlugin1.java | 1 + .../plugin/TransportIndexDocumentIntoSystemIndexAction.java | 1 + .../security/identity}/TransportActionDependencies.java | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename src/{integrationTest/java/org/opensearch/security/plugin => main/java/org/opensearch/security/identity}/TransportActionDependencies.java (90%) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index 7b2e52fcee..bbfeb5fb3c 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -29,6 +29,7 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; import org.opensearch.script.ScriptService; +import org.opensearch.security.identity.TransportActionDependencies; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index 0fa26698ae..1a77f1aaff 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -11,6 +11,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.identity.PluginSubject; import org.opensearch.security.identity.ContextProvidingPluginSubject; +import org.opensearch.security.identity.TransportActionDependencies; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java b/src/main/java/org/opensearch/security/identity/TransportActionDependencies.java similarity index 90% rename from src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java rename to src/main/java/org/opensearch/security/identity/TransportActionDependencies.java index 0d6dd8d23f..4a778641c6 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportActionDependencies.java +++ b/src/main/java/org/opensearch/security/identity/TransportActionDependencies.java @@ -1,4 +1,4 @@ -package org.opensearch.security.plugin; +package org.opensearch.security.identity; import org.opensearch.identity.PluginSubject; From b76a5b8f0e7d1039a6fa762c821b9992b563bd22 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 11:59:06 -0400 Subject: [PATCH 06/62] Use NamedPrincipal Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubject.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index d706325835..98b2480840 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -13,23 +13,23 @@ public class ContextProvidingPluginSubject implements PluginSubject { public static final String SUBJECT_HEADER = "_security_subject"; private final ThreadPool threadPool; - private final String pluginCanonicalClassName; + private final NamedPrincipal pluginPrincipal; public ContextProvidingPluginSubject(ThreadPool threadPool, Plugin plugin) { super(); this.threadPool = threadPool; - this.pluginCanonicalClassName = plugin.getClass().getCanonicalName(); + this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); } @Override public Principal getPrincipal() { - return NamedPrincipal.UNAUTHENTICATED; + return pluginPrincipal; } @Override public T runAs(Callable callable) throws Exception { try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { - threadPool.getThreadContext().putHeader(SUBJECT_HEADER, pluginCanonicalClassName); + threadPool.getThreadContext().putHeader(SUBJECT_HEADER, pluginPrincipal.getName()); return callable.call(); } } From 2da6f2a801251e6e6651efd4d60dca30fb8a4927 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 14:58:59 -0400 Subject: [PATCH 07/62] Add end to end test to show stronger index protection for plugins Signed-off-by: Craig Perkins --- .../java/org/opensearch/security/plugin/SystemIndexPlugin2.java | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java new file mode 100644 index 0000000000..f65153bef6 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java @@ -0,0 +1,2 @@ +package org.opensearch.security.plugin;public class SystemIndexPlugin2 { +} From 9cf80aeb7b4965590901f57990e0670cb3f63056 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 14:59:06 -0400 Subject: [PATCH 08/62] Add end to end test to show stronger index protection for plugins Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 37 ++++++----- .../IndexDocumentIntoSystemIndexAction.java | 10 +++ .../IndexDocumentIntoSystemIndexRequest.java | 10 +++ .../IndexDocumentIntoSystemIndexResponse.java | 10 +++ ...estIndexDocumentIntoSystemIndexAction.java | 10 +++ .../security/plugin/SystemIndexPlugin1.java | 10 +++ .../security/plugin/SystemIndexPlugin2.java | 61 ++++++++++++++++++- ...ortIndexDocumentIntoSystemIndexAction.java | 23 +++++-- .../test/framework/cluster/LocalCluster.java | 11 +++- .../security/filter/SecurityFilter.java | 2 + .../ContextProvidingPluginSubject.java | 8 ++- .../privileges/PrivilegesEvaluator.java | 4 +- .../SystemIndexAccessEvaluator.java | 19 +++++- .../org/opensearch/security/user/User.java | 12 +++- 14 files changed, 199 insertions(+), 28 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index d841725843..585642f42c 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -13,7 +13,6 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -21,6 +20,7 @@ import org.opensearch.common.util.FeatureFlags; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.plugin.SystemIndexPlugin1; +import org.opensearch.security.plugin.SystemIndexPlugin2; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -31,8 +31,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.opensearch.security.plugin.SystemIndexPlugin1.SYSTEM_INDEX_1; +import static org.opensearch.security.plugin.SystemIndexPlugin2.SYSTEM_INDEX_2; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; @@ -47,11 +49,13 @@ public class SystemIndexTests { .anonymousAuth(false) .authc(AUTHC_DOMAIN) .users(USER_ADMIN) - .plugin(SystemIndexPlugin1.class) + .plugin(List.of(SystemIndexPlugin1.class, SystemIndexPlugin2.class)) .nodeSettings( Map.of( FeatureFlags.IDENTITY, true, + SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, + true, SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), SECURITY_SYSTEM_INDICES_ENABLED_KEY, @@ -60,13 +64,6 @@ public class SystemIndexTests { ) .build(); - @Before - public void wipeAllIndices() { - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.delete(".system-index1"); - } - } - @Test public void adminShouldNotBeAbleToDeleteSecurityIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { @@ -82,15 +79,10 @@ public void adminShouldNotBeAbleToDeleteSecurityIndex() { assertThat(response2.getStatusCode(), equalTo(RestStatus.OK.getStatus())); - // regular use can create system index + // regular use cannot create system index when system index protection is enforced HttpResponse response3 = client.put(".system-index1"); - assertThat(response3.getStatusCode(), equalTo(RestStatus.OK.getStatus())); - - // regular user cannot delete system index - HttpResponse response4 = client.delete(".system-index1"); - - assertThat(response4.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + assertThat(response3.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); } } @@ -103,4 +95,17 @@ public void testPluginShouldBeAbleToIndexDocumentIntoItsSystemIndex() { assertThat(response.getBody(), containsString(SystemIndexPlugin1.class.getCanonicalName())); } } + + @Test + public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByOtherPlugin() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.put("_plugins/system-index/" + SYSTEM_INDEX_2); + + assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + assertThat( + response.getBody(), + containsString("no permissions for [indices:admin/create] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1") + ); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java index 40d3dd533d..8621f2a609 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import org.opensearch.action.ActionType; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java index f61f26b713..5cf7cf90d9 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import java.io.IOException; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java index dddd260376..cc72c64887 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import java.io.IOException; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java index 38fa943ce1..614d020ced 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import java.util.List; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index bbfeb5fb3c..bd0d7cec70 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import java.util.Arrays; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java index f65153bef6..8fcf23e3db 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin2.java @@ -1,2 +1,61 @@ -package org.opensearch.security.plugin;public class SystemIndexPlugin2 { +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.plugin; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.indices.SystemIndexDescriptor; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SystemIndexPlugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.script.ScriptService; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +public class SystemIndexPlugin2 extends Plugin implements SystemIndexPlugin { + public static final String SYSTEM_INDEX_2 = ".system-index2"; + + private Client client; + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + this.client = client; + return Collections.emptyList(); + } + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(SYSTEM_INDEX_2, "System index 2"); + return Collections.singletonList(systemIndexDescriptor); + } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index 1a77f1aaff..39cbfa3656 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import org.opensearch.action.admin.indices.create.CreateIndexRequest; @@ -10,9 +20,11 @@ import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; import org.opensearch.identity.PluginSubject; -import org.opensearch.security.identity.ContextProvidingPluginSubject; import org.opensearch.security.identity.TransportActionDependencies; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; public class TransportIndexDocumentIntoSystemIndexAction extends HandledTransportAction< @@ -20,6 +32,7 @@ public class TransportIndexDocumentIntoSystemIndexAction extends HandledTranspor IndexDocumentIntoSystemIndexResponse> { private final Client client; + private final ThreadPool threadPool; private final PluginSubject pluginSystemSubject; @Inject @@ -27,10 +40,12 @@ public TransportIndexDocumentIntoSystemIndexAction( final TransportService transportService, final ActionFilters actionFilters, final Client client, + final ThreadPool threadPool, final TransportActionDependencies deps ) { super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); this.client = client; + this.threadPool = threadPool; this.pluginSystemSubject = deps.getPluginSystemSubject(); } @@ -48,10 +63,8 @@ protected void doExecute( new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .source("{\"content\":1}", XContentType.JSON), ActionListener.wrap(r2 -> { - String subjectHeader = client.threadPool() - .getThreadContext() - .getHeader(ContextProvidingPluginSubject.SUBJECT_HEADER); - actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, subjectHeader)); + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); }, actionListener::onFailure) ); }, actionListener::onFailure)); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 894bb5baa9..9d0b084655 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -379,7 +379,7 @@ public Builder nodeSettings(Map settings) { } /** - * Adds additional plugins to the cluster + * Adds additional plugin to the cluster */ public Builder plugin(Class plugin) { this.plugins.add(plugin); @@ -387,6 +387,15 @@ public Builder plugin(Class plugin) { return this; } + /** + * Adds additional plugins to the cluster + */ + public Builder plugin(List> plugins) { + this.plugins.addAll(plugins); + + return this; + } + public Builder authFailureListeners(AuthFailureListeners listener) { testSecurityConfig.authFailureListeners(listener); return this; diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 1116e70845..96b015c830 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -202,6 +202,8 @@ private void ap // However, if another plugin injected a user in the ThreadContext, we still need // to perform privileges checks. enforcePrivilegesEvaluation = true; + } else if (user != null && user.isPluginUser()) { + enforcePrivilegesEvaluation = true; } final boolean userIsAdmin = isUserAdmin(user, adminDns); final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadContext); diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 98b2480840..2517349606 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -1,12 +1,15 @@ package org.opensearch.security.identity; import java.security.Principal; +import java.util.Map; import java.util.concurrent.Callable; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.PluginSubject; import org.opensearch.plugins.Plugin; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; public class ContextProvidingPluginSubject implements PluginSubject { @@ -14,11 +17,13 @@ public class ContextProvidingPluginSubject implements PluginSubject { private final ThreadPool threadPool; private final NamedPrincipal pluginPrincipal; + private final User pluginUser; public ContextProvidingPluginSubject(ThreadPool threadPool, Plugin plugin) { super(); this.threadPool = threadPool; this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); + this.pluginUser = new User(pluginPrincipal.getName()); } @Override @@ -29,7 +34,8 @@ public Principal getPrincipal() { @Override public T runAs(Callable callable) throws Exception { try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { - threadPool.getThreadContext().putHeader(SUBJECT_HEADER, pluginPrincipal.getName()); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, pluginUser); + pluginUser.addAttributes(Map.of("attr.internal.plugin", "true")); return callable.call(); } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 199442ee03..c6ea139b3e 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -292,7 +292,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) log.debug("Mapped roles: {}", mappedRoles.toString()); } - if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { + if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant())) && !user.isPluginUser()) { // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action // indices:data/write/bulk[s]). // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default @@ -327,7 +327,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) return presponse; } - // Security index access + // System index access if (systemIndexAccessEvaluator.evaluate( request, task, diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 38825a9bf1..f2ed737959 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -255,9 +255,10 @@ private void evaluateSystemIndicesAccess( boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved); boolean serviceAccountUser = user.isServiceAccount(); + boolean pluginUser = user.isPluginUser(); if (isSystemIndexPermissionEnabled) { - if (serviceAccountUser && containsRegularIndex) { + if ((serviceAccountUser || pluginUser) && containsRegularIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (!containsSystemIndex && log.isInfoEnabled()) { log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles); @@ -289,6 +290,22 @@ private void evaluateSystemIndicesAccess( presponse.allowed = false; presponse.markComplete(); return; + } else if (containsSystemIndex && pluginUser) { + boolean requestContainsOnlyPluginsRegisteredSystemIndices = requestedResolved.getAllIndices() + .equals(SystemIndexRegistry.matchesPluginSystemIndexPattern(user.getName(), requestedResolved.getAllIndices())); + if (requestContainsOnlyPluginsRegisteredSystemIndices) { + // allow all action types, but ensure that request only contains registered system indices for this plugin + presponse.allowed = true; + } else { + auditLog.logSecurityIndexAttempt(request, action, task); + if (log.isInfoEnabled()) { + log.info("{} can only interact with its own system indices", user.getName()); + } + presponse.missingPrivileges.add(action); + presponse.allowed = false; + } + presponse.markComplete(); + return; } else if (containsSystemIndex && !securityRoles.hasExplicitIndexPermission( requestedResolved, diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index 6abba3d734..fb3ef80853 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -290,10 +290,20 @@ public final Set getSecurityRoles() { /** * Check the custom attributes associated with this user * - * @return true if it has a service account attributes. otherwise false + * @return true if it has a service account attributes, otherwise false */ public boolean isServiceAccount() { Map userAttributesMap = this.getCustomAttributesMap(); return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a plugin attribute, otherwise false + */ + public boolean isPluginUser() { + Map userAttributesMap = this.getCustomAttributesMap(); + return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.plugin")); + } } From 21fc225157e43432590408541588a270017c6dcf Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 15:02:38 -0400 Subject: [PATCH 09/62] Move to constructor Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubject.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 2517349606..0df842b707 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -24,6 +24,7 @@ public ContextProvidingPluginSubject(ThreadPool threadPool, Plugin plugin) { this.threadPool = threadPool; this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); this.pluginUser = new User(pluginPrincipal.getName()); + this.pluginUser.addAttributes(Map.of("attr.internal.plugin", "true")); } @Override @@ -35,7 +36,6 @@ public Principal getPrincipal() { public T runAs(Callable callable) throws Exception { try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, pluginUser); - pluginUser.addAttributes(Map.of("attr.internal.plugin", "true")); return callable.call(); } } From f08f311fda80967f737564c6fe78f103d4e43514 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 15:31:25 -0400 Subject: [PATCH 10/62] Remove unused constant Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubject.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 0df842b707..041a941e01 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -13,8 +13,6 @@ import org.opensearch.threadpool.ThreadPool; public class ContextProvidingPluginSubject implements PluginSubject { - public static final String SUBJECT_HEADER = "_security_subject"; - private final ThreadPool threadPool; private final NamedPrincipal pluginPrincipal; private final User pluginUser; From 2de9a5ffa24c4cc955d14a83555dc965c93c2e89 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 16:07:18 -0400 Subject: [PATCH 11/62] Add test that demonstrates cluster action forbidden when running with plugin user Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 19 ++++- ...estIndexDocumentIntoSystemIndexAction.java | 2 +- .../plugin/RestRunClusterHealthAction.java | 69 +++++++++++++++++++ .../security/plugin/SystemIndexPlugin1.java | 6 +- .../SystemIndexAccessEvaluator.java | 1 + 5 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 585642f42c..73063a1e2f 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -89,7 +89,7 @@ public void adminShouldNotBeAbleToDeleteSecurityIndex() { @Test public void testPluginShouldBeAbleToIndexDocumentIntoItsSystemIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.put("_plugins/system-index/" + SYSTEM_INDEX_1); + HttpResponse response = client.put("try-create-and-index/" + SYSTEM_INDEX_1); assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); assertThat(response.getBody(), containsString(SystemIndexPlugin1.class.getCanonicalName())); @@ -99,7 +99,7 @@ public void testPluginShouldBeAbleToIndexDocumentIntoItsSystemIndex() { @Test public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByOtherPlugin() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.put("_plugins/system-index/" + SYSTEM_INDEX_2); + HttpResponse response = client.put("try-create-and-index/" + SYSTEM_INDEX_2); assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); assertThat( @@ -108,4 +108,19 @@ public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByO ); } } + + @Test + public void testPluginShouldNotBeAbleToRunClusterActions() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get("try-cluster-health/"); + + assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + assertThat( + response.getBody(), + containsString( + "no permissions for [cluster:monitor/health] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1" + ) + ); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java index 614d020ced..a4a9fb3c6d 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java @@ -31,7 +31,7 @@ public RestIndexDocumentIntoSystemIndexAction(Client client) { @Override public List routes() { - return singletonList(new Route(PUT, "/_plugins/system-index/{index}")); + return singletonList(new Route(PUT, "/try-create-and-index/{index}")); } @Override diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java new file mode 100644 index 0000000000..eef4cd99b9 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.plugin; + +import java.util.List; + +import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.client.Client; +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.identity.PluginSubject; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; + +public class RestRunClusterHealthAction extends BaseRestHandler { + + private final Client client; + private final PluginSubject pluginSubject; + + public RestRunClusterHealthAction(Client client, PluginSubject pluginSubject) { + this.client = client; + this.pluginSubject = pluginSubject; + } + + @Override + public List routes() { + return singletonList(new Route(GET, "/try-cluster-health")); + } + + @Override + public String getName() { + return "test_run_cluster_health_action"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + return new RestChannelConsumer() { + + @Override + public void accept(RestChannel channel) throws Exception { + pluginSubject.runAs(() -> { + ActionListener chr = ActionListener.wrap(r -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + ); + }, fr -> { channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr))); }); + client.admin().cluster().health(new ClusterHealthRequest(), chr); + return null; + }); + } + }; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index bd0d7cec70..73ec6821f4 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -84,8 +84,10 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { - TransportActionDependencies deps = new TransportActionDependencies(); - return List.of(new RestIndexDocumentIntoSystemIndexAction(client)); + return List.of( + new RestIndexDocumentIntoSystemIndexAction(client), + new RestRunClusterHealthAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()) + ); } @Override diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index f2ed737959..82eeddfe01 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -273,6 +273,7 @@ private void evaluateSystemIndicesAccess( log.debug("Service account cannot access regular indices: {}", regularIndices); } presponse.allowed = false; + presponse.missingPrivileges.add(action); presponse.markComplete(); return; } From 22795df7cfe4e49fe0f53ae963457d6ea67a3188 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 16:31:14 -0400 Subject: [PATCH 12/62] Add bulk index test Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 17 +++++ ...ulkIndexDocumentIntoSystemIndexAction.java | 70 +++++++++++++++++++ .../plugin/RestRunClusterHealthAction.java | 2 +- .../security/plugin/SystemIndexPlugin1.java | 3 +- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 73063a1e2f..4ff00d0fe8 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -13,6 +13,7 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -64,6 +65,13 @@ public class SystemIndexTests { ) .build(); + @Before + public void wipeAllIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.delete(".system-index1"); + } + } + @Test public void adminShouldNotBeAbleToDeleteSecurityIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { @@ -123,4 +131,13 @@ public void testPluginShouldNotBeAbleToRunClusterActions() { ); } } + + @Test + public void testPluginShouldBeAbleToBulkIndexDocumentIntoItsSystemIndex() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.put("try-create-and-bulk-index/" + SYSTEM_INDEX_1); + + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java new file mode 100644 index 0000000000..85b89dde1c --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java @@ -0,0 +1,70 @@ +package org.opensearch.security.plugin; + +import java.util.List; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkRequestBuilder; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.identity.PluginSubject; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.PUT; + +public class RestBulkIndexDocumentIntoSystemIndexAction extends BaseRestHandler { + + private final Client client; + private final PluginSubject pluginSubject; + + public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginSubject pluginSubject) { + this.client = client; + this.pluginSubject = pluginSubject; + } + + @Override + public List routes() { + return singletonList(new Route(PUT, "/try-create-and-bulk-index/{index}")); + } + + @Override + public String getName() { + return "test_bulk_index_document_into_system_index_action"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String indexName = request.param("index"); + return new RestChannelConsumer() { + + @Override + public void accept(RestChannel channel) throws Exception { + pluginSubject.runAs(() -> { + client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { + BulkRequestBuilder builder = client.prepareBulk(); + builder.add(new IndexRequest(indexName).source("{\"content\":1}", XContentType.JSON)); + builder.add(new IndexRequest(indexName).source("{\"content\":2}", XContentType.JSON)); + builder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + BulkRequest bulkRequest = builder.request(); + client.bulk(bulkRequest, ActionListener.wrap(r2 -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + ); + }, fr -> { channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr))); })); + }, fr -> { channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr))); })); + return null; + }); + } + }; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java index eef4cd99b9..faf061f66c 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -59,7 +59,7 @@ public void accept(RestChannel channel) throws Exception { channel.sendResponse( new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) ); - }, fr -> { channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr))); }); + }, fr -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr)))); client.admin().cluster().health(new ClusterHealthRequest(), chr); return null; }); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index 73ec6821f4..0adb381908 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -86,7 +86,8 @@ public List getRestHandlers( ) { return List.of( new RestIndexDocumentIntoSystemIndexAction(client), - new RestRunClusterHealthAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()) + new RestRunClusterHealthAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()), + new RestBulkIndexDocumentIntoSystemIndexAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()) ); } From e075ae8d333bb94f1f84946ec1f4d80ffa136623 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 20 Aug 2024 16:52:42 -0400 Subject: [PATCH 13/62] Ensure bulk request with mix of actions blocks as expected Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 22 +++++- ...dexDocumentIntoMixOfSystemIndexAction.java | 68 +++++++++++++++++++ .../security/plugin/SystemIndexPlugin1.java | 3 +- 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 4ff00d0fe8..656ba297bd 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -68,7 +68,8 @@ public class SystemIndexTests { @Before public void wipeAllIndices() { try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.delete(".system-index1"); + client.delete(".system-index1"); + client.delete(".system-index2"); } } @@ -140,4 +141,23 @@ public void testPluginShouldBeAbleToBulkIndexDocumentIntoItsSystemIndex() { assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); } } + + @Test + public void testPluginShouldNotBeAbleToBulkIndexDocumentIntoMixOfSystemIndexWhereAtLeastOneDoesNotBelongToPlugin() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.put(".system-index1"); + client.put(".system-index2"); + } + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.put("try-create-and-bulk-mixed-index"); + + assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + assertThat( + response.getBody(), + containsString( + "no permissions for [indices:data/write/bulk] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1" + ) + ); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java new file mode 100644 index 0000000000..ab56363a1d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java @@ -0,0 +1,68 @@ +package org.opensearch.security.plugin; + +import java.util.List; + +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkRequestBuilder; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.identity.PluginSubject; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.security.plugin.SystemIndexPlugin1.SYSTEM_INDEX_1; +import static org.opensearch.security.plugin.SystemIndexPlugin2.SYSTEM_INDEX_2; + +public class RestBulkIndexDocumentIntoMixOfSystemIndexAction extends BaseRestHandler { + + private final Client client; + private final PluginSubject pluginSubject; + + public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginSubject pluginSubject) { + this.client = client; + this.pluginSubject = pluginSubject; + } + + @Override + public List routes() { + return singletonList(new Route(PUT, "/try-create-and-bulk-mixed-index")); + } + + @Override + public String getName() { + return "test_bulk_index_document_into_mix_of_system_index_action"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + return new RestChannelConsumer() { + + @Override + public void accept(RestChannel channel) throws Exception { + pluginSubject.runAs(() -> { + BulkRequestBuilder builder = client.prepareBulk(); + builder.add(new IndexRequest(SYSTEM_INDEX_1).source("{\"content\":1}", XContentType.JSON)); + builder.add(new IndexRequest(SYSTEM_INDEX_2).source("{\"content\":1}", XContentType.JSON)); + builder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + BulkRequest bulkRequest = builder.request(); + client.bulk(bulkRequest, ActionListener.wrap(r -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + ); + }, fr -> { channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr))); })); + return null; + }); + } + }; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index 0adb381908..ff451bf448 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -87,7 +87,8 @@ public List getRestHandlers( return List.of( new RestIndexDocumentIntoSystemIndexAction(client), new RestRunClusterHealthAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()), - new RestBulkIndexDocumentIntoSystemIndexAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()) + new RestBulkIndexDocumentIntoSystemIndexAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()), + new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()) ); } From e12f6738847e72235897ea704039eaddec2bec6b Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 21 Aug 2024 11:19:15 -0400 Subject: [PATCH 14/62] Use subclass instead of attribute Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 2 +- .../ContextProvidingPluginSubject.java | 5 ++- .../support/SafeSerializationUtils.java | 2 ++ .../opensearch/security/user/PluginUser.java | 34 +++++++++++++++++++ .../org/opensearch/security/user/User.java | 7 ++-- 5 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/opensearch/security/user/PluginUser.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 656ba297bd..7f7d608012 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -46,7 +46,7 @@ public class SystemIndexTests { public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) .anonymousAuth(false) .authc(AUTHC_DOMAIN) .users(USER_ADMIN) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 041a941e01..dd311d33e7 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -1,7 +1,6 @@ package org.opensearch.security.identity; import java.security.Principal; -import java.util.Map; import java.util.concurrent.Callable; import org.opensearch.common.util.concurrent.ThreadContext; @@ -9,6 +8,7 @@ import org.opensearch.identity.PluginSubject; import org.opensearch.plugins.Plugin; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -21,8 +21,7 @@ public ContextProvidingPluginSubject(ThreadPool threadPool, Plugin plugin) { super(); this.threadPool = threadPool; this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); - this.pluginUser = new User(pluginPrincipal.getName()); - this.pluginUser.addAttributes(Map.of("attr.internal.plugin", "true")); + this.pluginUser = new PluginUser(pluginPrincipal.getName()); } @Override diff --git a/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java b/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java index b58e4afd35..1d2289f0ae 100644 --- a/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java +++ b/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableSet; import org.opensearch.security.auth.UserInjector; +import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import com.amazon.dlic.auth.ldap.LdapUser; @@ -48,6 +49,7 @@ public final class SafeSerializationUtils { InetSocketAddress.class, Pattern.class, User.class, + PluginUser.class, UserInjector.InjectedUser.class, SourceFieldsContext.class, LdapUser.class, diff --git a/src/main/java/org/opensearch/security/user/PluginUser.java b/src/main/java/org/opensearch/security/user/PluginUser.java new file mode 100644 index 0000000000..66ed1b0149 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/PluginUser.java @@ -0,0 +1,34 @@ +/* + * 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.user; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; + +public class PluginUser extends User { + private static final long serialVersionUID = -4083322940729403322L; + + public PluginUser(StreamInput in) throws IOException { + super(in); + } + + /** + * Create a new plugin user without roles and attributes + * + * @param name The username (must not be null or empty) + * @throws IllegalArgumentException if name is null or empty + */ + public PluginUser(final String name) { + super(name, null, null); + } +} diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index fb3ef80853..67e557bf77 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -298,12 +298,9 @@ public boolean isServiceAccount() { } /** - * Check the custom attributes associated with this user - * - * @return true if it has a plugin attribute, otherwise false + * @return true if this instance is of the type PluginUser */ public boolean isPluginUser() { - Map userAttributesMap = this.getCustomAttributesMap(); - return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.plugin")); + return this instanceof PluginUser; } } From f9adcc443acf747a1bd99788bbbf3fcdf866915e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 21 Aug 2024 14:22:08 -0400 Subject: [PATCH 15/62] Rename TransportActionDependencies to PluginContextSwitcher Signed-off-by: Craig Perkins --- ...dexDocumentIntoMixOfSystemIndexAction.java | 10 +++--- ...ulkIndexDocumentIntoSystemIndexAction.java | 10 +++--- .../plugin/RestRunClusterHealthAction.java | 10 +++--- .../security/plugin/SystemIndexPlugin1.java | 17 +++++---- ...ortIndexDocumentIntoSystemIndexAction.java | 11 +++--- .../identity/PluginContextSwitcher.java | 36 +++++++++++++++++++ .../identity/TransportActionDependencies.java | 17 --------- 7 files changed, 66 insertions(+), 45 deletions(-) create mode 100644 src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java delete mode 100644 src/main/java/org/opensearch/security/identity/TransportActionDependencies.java diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java index ab56363a1d..68a3c1decf 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java @@ -12,11 +12,11 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.identity.PluginSubject; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -26,11 +26,11 @@ public class RestBulkIndexDocumentIntoMixOfSystemIndexAction extends BaseRestHandler { private final Client client; - private final PluginSubject pluginSubject; + private final PluginContextSwitcher contextSwitcher; - public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginSubject pluginSubject) { + public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginContextSwitcher contextSwitcher) { this.client = client; - this.pluginSubject = pluginSubject; + this.contextSwitcher = contextSwitcher; } @Override @@ -49,7 +49,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - pluginSubject.runAs(() -> { + contextSwitcher.runAs(() -> { BulkRequestBuilder builder = client.prepareBulk(); builder.add(new IndexRequest(SYSTEM_INDEX_1).source("{\"content\":1}", XContentType.JSON)); builder.add(new IndexRequest(SYSTEM_INDEX_2).source("{\"content\":1}", XContentType.JSON)); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java index 85b89dde1c..74c4cb9d18 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java @@ -13,11 +13,11 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.identity.PluginSubject; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -25,11 +25,11 @@ public class RestBulkIndexDocumentIntoSystemIndexAction extends BaseRestHandler { private final Client client; - private final PluginSubject pluginSubject; + private final PluginContextSwitcher contextSwitcher; - public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginSubject pluginSubject) { + public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginContextSwitcher contextSwitcher) { this.client = client; - this.pluginSubject = pluginSubject; + this.contextSwitcher = contextSwitcher; } @Override @@ -49,7 +49,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - pluginSubject.runAs(() -> { + contextSwitcher.runAs(() -> { client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { BulkRequestBuilder builder = client.prepareBulk(); builder.add(new IndexRequest(indexName).source("{\"content\":1}", XContentType.JSON)); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java index faf061f66c..d35fe2806e 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -19,11 +19,11 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.identity.PluginSubject; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.GET; @@ -31,11 +31,11 @@ public class RestRunClusterHealthAction extends BaseRestHandler { private final Client client; - private final PluginSubject pluginSubject; + private final PluginContextSwitcher contextSwitcher; - public RestRunClusterHealthAction(Client client, PluginSubject pluginSubject) { + public RestRunClusterHealthAction(Client client, PluginContextSwitcher contextSwitcher) { this.client = client; - this.pluginSubject = pluginSubject; + this.contextSwitcher = contextSwitcher; } @Override @@ -54,7 +54,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - pluginSubject.runAs(() -> { + contextSwitcher.runAs(() -> { ActionListener chr = ActionListener.wrap(r -> { channel.sendResponse( new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index ff451bf448..036c1fd6fd 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -39,14 +39,14 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; import org.opensearch.script.ScriptService; -import org.opensearch.security.identity.TransportActionDependencies; +import org.opensearch.security.identity.PluginContextSwitcher; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; public class SystemIndexPlugin1 extends Plugin implements SystemIndexPlugin, IdentityAwarePlugin { public static final String SYSTEM_INDEX_1 = ".system-index1"; - private static final TransportActionDependencies SECURITY_TRANSPORT_ACTION_DEPENDENCIES = new TransportActionDependencies(); + private PluginContextSwitcher contextSwitcher; private Client client; @@ -65,7 +65,8 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { this.client = client; - return List.of(SECURITY_TRANSPORT_ACTION_DEPENDENCIES); + this.contextSwitcher = new PluginContextSwitcher(); + return List.of(contextSwitcher); } @Override @@ -86,9 +87,9 @@ public List getRestHandlers( ) { return List.of( new RestIndexDocumentIntoSystemIndexAction(client), - new RestRunClusterHealthAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()), - new RestBulkIndexDocumentIntoSystemIndexAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()), - new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, SECURITY_TRANSPORT_ACTION_DEPENDENCIES.getPluginSystemSubject()) + new RestRunClusterHealthAction(client, contextSwitcher), + new RestBulkIndexDocumentIntoSystemIndexAction(client, contextSwitcher), + new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, contextSwitcher) ); } @@ -101,6 +102,8 @@ public List getRestHandlers( @Override public void assignSubject(PluginSubject pluginSystemSubject) { - SECURITY_TRANSPORT_ACTION_DEPENDENCIES.setPluginSystemSubject(pluginSystemSubject); + if (contextSwitcher != null) { + this.contextSwitcher.initialize(pluginSystemSubject); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index 39cbfa3656..93347a1515 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -19,8 +19,7 @@ import org.opensearch.common.inject.Inject; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; -import org.opensearch.identity.PluginSubject; -import org.opensearch.security.identity.TransportActionDependencies; +import org.opensearch.security.identity.PluginContextSwitcher; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -33,7 +32,7 @@ public class TransportIndexDocumentIntoSystemIndexAction extends HandledTranspor private final Client client; private final ThreadPool threadPool; - private final PluginSubject pluginSystemSubject; + private final PluginContextSwitcher contextSwitcher; @Inject public TransportIndexDocumentIntoSystemIndexAction( @@ -41,12 +40,12 @@ public TransportIndexDocumentIntoSystemIndexAction( final ActionFilters actionFilters, final Client client, final ThreadPool threadPool, - final TransportActionDependencies deps + final PluginContextSwitcher contextSwitcher ) { super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); this.client = client; this.threadPool = threadPool; - this.pluginSystemSubject = deps.getPluginSystemSubject(); + this.contextSwitcher = contextSwitcher; } @Override @@ -57,7 +56,7 @@ protected void doExecute( ) { String indexName = request.getIndexName(); try { - pluginSystemSubject.runAs(() -> { + contextSwitcher.runAs(() -> { client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { client.index( new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) diff --git a/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java b/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java new file mode 100644 index 0000000000..52809d4fab --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java @@ -0,0 +1,36 @@ +/* + * 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.identity; + +import java.util.concurrent.Callable; + +import org.opensearch.identity.PluginSubject; + +public class PluginContextSwitcher { + private PluginSubject pluginSubject; + + public PluginContextSwitcher() {} + + public void initialize(PluginSubject pluginSubject) { + this.pluginSubject = pluginSubject; + } + + public T runAs(Callable callable) { + if (pluginSubject == null) { + return null; + } + try { + return pluginSubject.runAs(callable); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/opensearch/security/identity/TransportActionDependencies.java b/src/main/java/org/opensearch/security/identity/TransportActionDependencies.java deleted file mode 100644 index 4a778641c6..0000000000 --- a/src/main/java/org/opensearch/security/identity/TransportActionDependencies.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opensearch.security.identity; - -import org.opensearch.identity.PluginSubject; - -public class TransportActionDependencies { - private PluginSubject pluginSystemSubject; - - public TransportActionDependencies() {} - - public void setPluginSystemSubject(PluginSubject pluginSystemSubject) { - this.pluginSystemSubject = pluginSystemSubject; - } - - public PluginSubject getPluginSystemSubject() { - return pluginSystemSubject; - } -} From 2074dab07f4fec044d80e811f82ab5a50fdd8004 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 22 Aug 2024 09:30:54 -0400 Subject: [PATCH 16/62] Prevent pluginUser from being serialized to nodes before 2.17.0 for backwards compatibility Signed-off-by: Craig Perkins --- .../security/transport/SecurityInterceptor.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index f55d9ac338..b73118fc91 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -39,6 +39,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.Version; import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsAction; import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsResponse; import org.opensearch.action.get.GetRequest; @@ -128,6 +129,15 @@ public SecurityRequestHandler getHandler(String ); } + private User determineUser(Connection connection) { + User user0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + // pluginUser did not exist prior to 2.17.0 + if (user0 != null && user0.isPluginUser() && connection.getVersion().before(Version.V_2_17_0)) { + user0 = null; + } + return user0; + } + public void sendRequestDecorate( AsyncSender sender, Connection connection, @@ -138,7 +148,7 @@ public void sendRequestDecorate( DiscoveryNode localNode ) { final Map origHeaders0 = getThreadContext().getHeaders(); - final User user0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user0 = determineUser(connection); final String injectedUserString = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER); final String injectedRolesString = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES); final String injectedRolesValidationString = getThreadContext().getTransient( @@ -310,7 +320,7 @@ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADE if (origUser != null) { // if request is going to be handled by same node, we directly put transient value as the thread context is not going to be - // stah. + // stashed. getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, origUser); } else if (StringUtils.isNotEmpty(injectedRolesString)) { getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES, injectedRolesString); From 0960ce09fe2edda7f18d6638764e5b2c9034f8c6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 22 Aug 2024 15:03:06 -0400 Subject: [PATCH 17/62] Demonstrate how a role can be created in-memory to re-use authz Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 3 +- .../security/OpenSearchSecurityPlugin.java | 18 ++++++++++-- .../ContextProvidingPluginSubject.java | 25 ++++++++++++++-- .../identity/SystemIndexRegistry.java | 21 ++++++++++++++ .../privileges/PrivilegesEvaluator.java | 29 +++++++++++++++++-- .../SystemIndexAccessEvaluator.java | 21 ++------------ .../security/securityconf/ConfigModelV6.java | 27 +++++++++++++++++ .../security/securityconf/ConfigModelV7.java | 28 ++++++++++++++++++ .../security/securityconf/SecurityRoles.java | 8 +++++ 9 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 7f7d608012..150448dab3 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -151,11 +151,10 @@ public void testPluginShouldNotBeAbleToBulkIndexDocumentIntoMixOfSystemIndexWher try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse response = client.put("try-create-and-bulk-mixed-index"); - assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); assertThat( response.getBody(), containsString( - "no permissions for [indices:data/write/bulk] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1" + "no permissions for [indices:data/write/bulk[s]] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1" ) ); } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index d5eddb0af6..63222c5c59 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -124,6 +124,7 @@ import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; +import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; @@ -168,6 +169,7 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.ContextProvidingPluginSubject; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.identity.SystemIndexRegistry; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -268,6 +270,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile Salt salt; private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; + private volatile SystemIndexRegistry systemIndexRegistry; public static boolean isActionTraceEnabled() { @@ -1117,6 +1120,8 @@ public Collection createComponents( final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); + systemIndexRegistry = new SystemIndexRegistry(); + evaluator = new PrivilegesEvaluator( clusterService, threadPool, @@ -1127,7 +1132,8 @@ public Collection createComponents( privilegesInterceptor, cih, irr, - namedXContentRegistry.get() + namedXContentRegistry.get(), + systemIndexRegistry ); sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); @@ -1206,6 +1212,7 @@ public Collection createComponents( components.add(dcf); components.add(userService); components.add(passwordHasher); + components.add(systemIndexRegistry); if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { components.add(sks); @@ -2117,7 +2124,14 @@ public SecurityTokenManager getTokenManager() { @Override public PluginSubject getPluginSubject(Plugin plugin) { - return new ContextProvidingPluginSubject(threadPool, plugin); + if (systemIndexRegistry != null) { + Collection systemIndexDescriptors = ((SystemIndexPlugin) plugin).getSystemIndexDescriptors(settings); + Set systemIndexPatterns = systemIndexDescriptors.stream() + .map(SystemIndexDescriptor::getIndexPattern) + .collect(Collectors.toSet()); + systemIndexRegistry.addSystemIntexPatterns(plugin.getClass().getCanonicalName(), systemIndexPatterns); + } + return new ContextProvidingPluginSubject(threadPool, settings, plugin); } @Override diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index dd311d33e7..0204a39898 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -1,27 +1,46 @@ package org.opensearch.security.identity; import java.security.Principal; +import java.util.Collection; +import java.util.List; import java.util.concurrent.Callable; +import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.PluginSubject; +import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SystemIndexPlugin; +import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.PluginUser; -import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; public class ContextProvidingPluginSubject implements PluginSubject { private final ThreadPool threadPool; private final NamedPrincipal pluginPrincipal; - private final User pluginUser; + private final PluginUser pluginUser; + private final RoleV7 roleV7; - public ContextProvidingPluginSubject(ThreadPool threadPool, Plugin plugin) { + public ContextProvidingPluginSubject(ThreadPool threadPool, Settings settings, Plugin plugin) { super(); this.threadPool = threadPool; this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); this.pluginUser = new PluginUser(pluginPrincipal.getName()); + if (plugin instanceof SystemIndexPlugin) { + Collection systemIndexDescriptors = ((SystemIndexPlugin) plugin).getSystemIndexDescriptors(settings); + roleV7 = new RoleV7(); + if (systemIndexDescriptors != null) { + List systemIndexPatterns = systemIndexDescriptors.stream().map(SystemIndexDescriptor::getIndexPattern).toList(); + RoleV7.Index indexPermissions = new RoleV7.Index(); + indexPermissions.setIndex_patterns(systemIndexPatterns); + indexPermissions.setAllowed_actions(List.of(ConfigConstants.SYSTEM_INDEX_PERMISSION)); + roleV7.setIndex_permissions(List.of(indexPermissions)); + } + } else { + roleV7 = null; + } } @Override diff --git a/src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java b/src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java new file mode 100644 index 0000000000..8fbb34dfc0 --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java @@ -0,0 +1,21 @@ +package org.opensearch.security.identity; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class SystemIndexRegistry { + private final Map> registeredSystemIndexPatterns; + + public SystemIndexRegistry() { + registeredSystemIndexPatterns = new HashMap<>(); + } + + public void addSystemIntexPatterns(String pluginIdentifier, Set indexPatterns) { + registeredSystemIndexPatterns.put(pluginIdentifier, indexPatterns); + } + + public Set getSystemIndexPatterns(String pluginIdentifier) { + return registeredSystemIndexPatterns.get(pluginIdentifier); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index c6ea139b3e..7739603139 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -85,6 +86,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.identity.SystemIndexRegistry; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; @@ -142,6 +144,8 @@ public class PrivilegesEvaluator { private final PitPrivilegesEvaluator pitPrivilegesEvaluator; private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; + private final SystemIndexRegistry systemIndexRegistry; + private final Map pluginRoles; public PrivilegesEvaluator( final ClusterService clusterService, @@ -153,7 +157,8 @@ public PrivilegesEvaluator( final PrivilegesInterceptor privilegesInterceptor, final ClusterInfoHolder clusterInfoHolder, final IndexResolverReplacer irr, - NamedXContentRegistry namedXContentRegistry + NamedXContentRegistry namedXContentRegistry, + final SystemIndexRegistry systemIndexRegistry ) { super(); @@ -163,6 +168,8 @@ public PrivilegesEvaluator( this.threadContext = threadPool.getThreadContext(); this.privilegesInterceptor = privilegesInterceptor; + this.systemIndexRegistry = systemIndexRegistry; + this.pluginRoles = new HashMap<>(); this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, @@ -193,6 +200,17 @@ public SecurityRoles getSecurityRoles(Set roles) { return configModel.getSecurityRoles().filter(roles); } + public SecurityRoles getSecurityRoleForPlugin(String pluginIdentifier) { + SecurityRoles pluginRole = pluginRoles.get(pluginIdentifier); + if (pluginRole == null) { + Set systemIndexPatterns = systemIndexRegistry.getSystemIndexPatterns(pluginIdentifier); + pluginRole = configModel.getSecurityRoles() + .createSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Set.of(), Set.of(), systemIndexPatterns); + pluginRoles.put(pluginIdentifier, pluginRole); + } + return pluginRole; + } + public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permissions) { final Set userRoles = mapRoles(user, remoteAddress); return hasRestAdminPermissions(userRoles, permissions); @@ -279,7 +297,12 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) context.setMappedRoles(mappedRoles); } presponse.resolvedSecurityRoles.addAll(mappedRoles); - final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); + final SecurityRoles securityRoles; + if (user.isPluginUser()) { + securityRoles = getSecurityRoleForPlugin(user.getName()); + } else { + securityRoles = getSecurityRoles(mappedRoles); + } // Add the security roles for this user so that they can be used for DLS parameter substitution. user.addSecurityRoles(mappedRoles); @@ -292,7 +315,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) log.debug("Mapped roles: {}", mappedRoles.toString()); } - if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant())) && !user.isPluginUser()) { + if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action // indices:data/write/bulk[s]). // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 82eeddfe01..affe6177b5 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -255,10 +255,9 @@ private void evaluateSystemIndicesAccess( boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved); boolean serviceAccountUser = user.isServiceAccount(); - boolean pluginUser = user.isPluginUser(); if (isSystemIndexPermissionEnabled) { - if ((serviceAccountUser || pluginUser) && containsRegularIndex) { + if (serviceAccountUser && containsRegularIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (!containsSystemIndex && log.isInfoEnabled()) { log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles); @@ -291,22 +290,6 @@ private void evaluateSystemIndicesAccess( presponse.allowed = false; presponse.markComplete(); return; - } else if (containsSystemIndex && pluginUser) { - boolean requestContainsOnlyPluginsRegisteredSystemIndices = requestedResolved.getAllIndices() - .equals(SystemIndexRegistry.matchesPluginSystemIndexPattern(user.getName(), requestedResolved.getAllIndices())); - if (requestContainsOnlyPluginsRegisteredSystemIndices) { - // allow all action types, but ensure that request only contains registered system indices for this plugin - presponse.allowed = true; - } else { - auditLog.logSecurityIndexAttempt(request, action, task); - if (log.isInfoEnabled()) { - log.info("{} can only interact with its own system indices", user.getName()); - } - presponse.missingPrivileges.add(action); - presponse.allowed = false; - } - presponse.markComplete(); - return; } else if (containsSystemIndex && !securityRoles.hasExplicitIndexPermission( requestedResolved, @@ -324,7 +307,9 @@ private void evaluateSystemIndicesAccess( String.join(", ", getAllSystemIndices(requestedResolved)) ); } + System.out.println("Not authorized"); presponse.allowed = false; + presponse.missingPrivileges.add(action); presponse.markComplete(); return; } diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index e35fb40a24..6157c9deef 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -359,6 +359,33 @@ public SecurityRoles filter(Set keep) { return retVal; } + @Override + public SecurityRoles createSecurityRole( + String roleName, + Set clusterPerms, + Set indexPatterns, + Set allowedActions, + Set systemIndexPatterns + ) { + SecurityRole role = new SecurityRole(roleName); + role.addClusterPerms(clusterPerms); + for (String ip : indexPatterns) { + IndexPattern idxPattern = new IndexPattern(ip); + TypePerm perms = new TypePerm(""); + perms.addPerms(allowedActions); + idxPattern.addTypePerms(perms); + } + for (String ip : systemIndexPatterns) { + IndexPattern idxPattern = new IndexPattern(ip); + TypePerm perms = new TypePerm(""); + perms.addPerms(Set.of("*", ConfigConstants.SYSTEM_INDEX_PERMISSION)); + idxPattern.addTypePerms(perms); + } + SecurityRoles roles = new SecurityRoles(1); + roles.addSecurityRole(role); + return roles; + } + @Override public EvaluatedDlsFlsConfig getDlsFls( User user, diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index f78c173202..82507f3815 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -273,6 +273,31 @@ public SecurityRoles filter(Set keep) { return retVal; } + @Override + public SecurityRoles createSecurityRole( + String roleName, + Set clusterPerms, + Set indexPatterns, + Set allowedActions, + Set systemIndexPatterns + ) { + Set ipatterns = new HashSet<>(); + for (String ip : indexPatterns) { + IndexPattern idxPattern = new IndexPattern(ip); + idxPattern.addPerm(allowedActions); + ipatterns.add(idxPattern); + } + for (String ip : systemIndexPatterns) { + IndexPattern idxPattern = new IndexPattern(ip); + idxPattern.addPerm(Set.of("*", ConfigConstants.SYSTEM_INDEX_PERMISSION)); + ipatterns.add(idxPattern); + } + SecurityRole role = new SecurityRole(roleName, ipatterns, WildcardMatcher.from(clusterPerms)); + SecurityRoles roles = new SecurityRoles(1); + roles.addSecurityRole(role); + return roles; + } + @Override public EvaluatedDlsFlsConfig getDlsFls( User user, @@ -439,6 +464,7 @@ public boolean hasExplicitIndexPermission( ) { final Set indicesForRequest = new HashSet<>(resolved.getAllIndicesResolved(cs, resolver)); + System.out.println("indicesForRequest: " + indicesForRequest); if (indicesForRequest.isEmpty()) { // If no indices could be found on the request there is no way to check for the explicit permissions return false; @@ -449,6 +475,8 @@ public boolean hasExplicitIndexPermission( .flatMap(Collection::stream) .collect(Collectors.toSet()); + System.out.println("explicitlyAllowedIndices: " + explicitlyAllowedIndices); + if (log.isDebugEnabled()) { log.debug( "ExplicitIndexPermission check indices for request {}, explicitly allowed indices {}", diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index fb25e1a21f..9068b37aa6 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -97,5 +97,13 @@ Set getAllPermittedIndicesForDashboards( SecurityRoles filter(Set roles); + SecurityRoles createSecurityRole( + String roleName, + Set clusterPerms, + Set indexPatterns, + Set allowedActions, + Set systemIndexPatterns + ); + boolean isPermittedOnSystemIndex(String indexName); } From 8c2b7ba06453a6ba691db089081b76ff3ffa7185 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 22 Aug 2024 15:59:46 -0400 Subject: [PATCH 18/62] Add missing license headers Signed-off-by: Craig Perkins --- ...estBulkIndexDocumentIntoMixOfSystemIndexAction.java | 10 ++++++++++ .../RestBulkIndexDocumentIntoSystemIndexAction.java | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java index 68a3c1decf..0d1dc4fe01 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import java.util.List; diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java index 74c4cb9d18..56fa3ef2a9 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java @@ -1,3 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + package org.opensearch.security.plugin; import java.util.List; From 675f20aedef4bc815452c360cd84ff07f1e8bdcc Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 22 Aug 2024 17:54:35 -0400 Subject: [PATCH 19/62] Remove unused code Signed-off-by: Craig Perkins --- .../identity/ContextProvidingPluginSubject.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 0204a39898..5cd71dfc39 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -1,17 +1,13 @@ package org.opensearch.security.identity; import java.security.Principal; -import java.util.Collection; -import java.util.List; import java.util.concurrent.Callable; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.PluginSubject; -import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.Plugin; -import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.PluginUser; @@ -28,19 +24,6 @@ public ContextProvidingPluginSubject(ThreadPool threadPool, Settings settings, P this.threadPool = threadPool; this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); this.pluginUser = new PluginUser(pluginPrincipal.getName()); - if (plugin instanceof SystemIndexPlugin) { - Collection systemIndexDescriptors = ((SystemIndexPlugin) plugin).getSystemIndexDescriptors(settings); - roleV7 = new RoleV7(); - if (systemIndexDescriptors != null) { - List systemIndexPatterns = systemIndexDescriptors.stream().map(SystemIndexDescriptor::getIndexPattern).toList(); - RoleV7.Index indexPermissions = new RoleV7.Index(); - indexPermissions.setIndex_patterns(systemIndexPatterns); - indexPermissions.setAllowed_actions(List.of(ConfigConstants.SYSTEM_INDEX_PERMISSION)); - roleV7.setIndex_permissions(List.of(indexPermissions)); - } - } else { - roleV7 = null; - } } @Override From 8d3eb1c451b151f3d55ae7ac2a0a24bf9482e402 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 23 Aug 2024 11:48:04 -0400 Subject: [PATCH 20/62] Remove unused field Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubject.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 5cd71dfc39..63ed349ca1 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -8,7 +8,6 @@ import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.PluginSubject; import org.opensearch.plugins.Plugin; -import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.PluginUser; import org.opensearch.threadpool.ThreadPool; @@ -17,7 +16,6 @@ public class ContextProvidingPluginSubject implements PluginSubject { private final ThreadPool threadPool; private final NamedPrincipal pluginPrincipal; private final PluginUser pluginUser; - private final RoleV7 roleV7; public ContextProvidingPluginSubject(ThreadPool threadPool, Settings settings, Plugin plugin) { super(); From 115fa0e01022462e83eab9bd0ebe9dc2fbda1765 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 27 Aug 2024 17:12:52 -0400 Subject: [PATCH 21/62] Implement getCurrentSubject Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 14 +++++- .../plugin/RestRunClusterHealthAction.java | 38 +++++++++++---- .../security/plugin/SystemIndexPlugin1.java | 47 +++++++++++++++++++ .../security/OpenSearchSecurityPlugin.java | 5 +- .../security/auth/BackendRegistry.java | 12 ++++- .../identity/SecurityUserSubject.java | 42 +++++++++++++++++ .../security/support/ConfigConstants.java | 1 + 7 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/opensearch/security/identity/SecurityUserSubject.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 150448dab3..647973cedb 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -45,8 +45,9 @@ public class SystemIndexTests { public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); + // TODO Change this from SINGLENODE to default to test with multiple nodes @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .anonymousAuth(false) .authc(AUTHC_DOMAIN) .users(USER_ADMIN) @@ -121,7 +122,7 @@ public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByO @Test public void testPluginShouldNotBeAbleToRunClusterActions() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.get("try-cluster-health/"); + HttpResponse response = client.get("try-cluster-health/plugin"); assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); assertThat( @@ -133,6 +134,15 @@ public void testPluginShouldNotBeAbleToRunClusterActions() { } } + @Test + public void testAdminUserShouldBeAbleToRunClusterActions() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get("try-cluster-health/user"); + + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + } + @Test public void testPluginShouldBeAbleToBulkIndexDocumentIntoItsSystemIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java index d35fe2806e..26747abc02 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -19,6 +19,8 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.identity.IdentityService; +import org.opensearch.identity.Subject; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; @@ -40,7 +42,7 @@ public RestRunClusterHealthAction(Client client, PluginContextSwitcher contextSw @Override public List routes() { - return singletonList(new Route(GET, "/try-cluster-health")); + return singletonList(new Route(GET, "/try-cluster-health/{runAs}")); } @Override @@ -50,19 +52,35 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String runAs = request.param("runAs"); + return new RestChannelConsumer() { @Override public void accept(RestChannel channel) throws Exception { - contextSwitcher.runAs(() -> { - ActionListener chr = ActionListener.wrap(r -> { - channel.sendResponse( - new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) - ); - }, fr -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr)))); - client.admin().cluster().health(new ClusterHealthRequest(), chr); - return null; - }); + if ("user".equalsIgnoreCase(runAs)) { + IdentityService identityService = SystemIndexPlugin1.GuiceHolder.getIdentityService(); + Subject user = identityService.getCurrentSubject(); + user.runAs(() -> { + ActionListener chr = ActionListener.wrap(r -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + ); + }, fr -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr)))); + client.admin().cluster().health(new ClusterHealthRequest(), chr); + return null; + }); + } else { + contextSwitcher.runAs(() -> { + ActionListener chr = ActionListener.wrap(r -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + ); + }, fr -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr)))); + client.admin().cluster().health(new ClusterHealthRequest(), chr); + return null; + }); + } } }; } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index 036c1fd6fd..cc5b801b1f 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -10,6 +10,7 @@ package org.opensearch.security.plugin; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -21,6 +22,10 @@ import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNodes; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.lifecycle.Lifecycle; +import org.opensearch.common.lifecycle.LifecycleComponent; +import org.opensearch.common.lifecycle.LifecycleListener; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.IndexScopedSettings; import org.opensearch.common.settings.Settings; @@ -30,6 +35,7 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.env.Environment; import org.opensearch.env.NodeEnvironment; +import org.opensearch.identity.IdentityService; import org.opensearch.identity.PluginSubject; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.IdentityAwarePlugin; @@ -106,4 +112,45 @@ public void assignSubject(PluginSubject pluginSystemSubject) { this.contextSwitcher.initialize(pluginSystemSubject); } } + + @Override + public Collection> getGuiceServiceClasses() { + final List> services = new ArrayList<>(1); + services.add(GuiceHolder.class); + return services; + } + + public static class GuiceHolder implements LifecycleComponent { + private static IdentityService identityService; + + @Inject + public GuiceHolder(IdentityService identityService) { + GuiceHolder.identityService = identityService; + } + + public static IdentityService getIdentityService() { + return identityService; + } + + @Override + public void close() {} + + @Override + public Lifecycle.State lifecycleState() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) {} + + @Override + public void removeLifecycleListener(LifecycleListener listener) {} + + @Override + public void start() {} + + @Override + public void stop() {} + + } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 63222c5c59..a6286a6121 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -111,7 +111,6 @@ import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.identity.PluginSubject; import org.opensearch.identity.Subject; -import org.opensearch.identity.noop.NoopSubject; import org.opensearch.index.IndexModule; import org.opensearch.index.cache.query.QueryCache; import org.opensearch.indices.IndicesService; @@ -223,6 +222,7 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_SUBJECT; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; @@ -2113,8 +2113,7 @@ private static String handleKeyword(final String field) { @Override public Subject getCurrentSubject() { - // Not supported - return new NoopSubject(); + return (Subject) threadPool.getThreadContext().getPersistent(OPENDISTRO_SECURITY_SUBJECT); } @Override diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0e39acf59e..dce8087ea3 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -55,6 +55,7 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.core.rest.RestStatus; +import org.opensearch.identity.UserSubject; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; @@ -63,6 +64,7 @@ import org.opensearch.security.filter.SecurityRequestChannel; import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.identity.SecurityUserSubject; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; @@ -222,7 +224,10 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal)); + User superuser = new User(sslPrincipal); + UserSubject subject = new SecurityUserSubject(threadPool, superuser); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, subject); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); auditLog.logSucceededLogin(sslPrincipal, true, null, request); return true; } @@ -385,6 +390,8 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); threadPool.getThreadContext() .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); + UserSubject subject = new SecurityUserSubject(threadPool, impersonatedUser == null ? authenticatedUser : impersonatedUser); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, subject); auditLog.logSucceededLogin( (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), false, @@ -417,7 +424,10 @@ public boolean authenticate(final SecurityRequestChannel request) { User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); + UserSubject subject = new SecurityUserSubject(threadPool, anonymousUser); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, subject); auditLog.logSucceededLogin(anonymousUser.getName(), false, null, request); if (isDebugEnabled) { log.debug("Anonymous User is authenticated"); diff --git a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java new file mode 100644 index 0000000000..4fe1732f86 --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java @@ -0,0 +1,42 @@ +package org.opensearch.security.identity; + +import java.security.Principal; +import java.util.concurrent.Callable; + +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.identity.NamedPrincipal; +import org.opensearch.identity.UserSubject; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +public class SecurityUserSubject implements UserSubject { + private final NamedPrincipal userPrincipal; + private final ThreadPool threadPool; + private final User user; + + public SecurityUserSubject(ThreadPool threadPool, User user) { + this.threadPool = threadPool; + this.user = user; + this.userPrincipal = new NamedPrincipal(user.getName()); + } + + @Override + public void authenticate(AuthToken authToken) { + // not implemented + } + + @Override + public Principal getPrincipal() { + return userPrincipal; + } + + @Override + public T runAs(Callable callable) throws Exception { + try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + return callable.call(); + } + } +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 11b3ac48ac..ab99275897 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -113,6 +113,7 @@ public class ConfigConstants { public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_transport_principal"; public static final String OPENDISTRO_SECURITY_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user"; + public static final String OPENDISTRO_SECURITY_SUBJECT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "subject"; public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_header"; public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; From f6650eb86071020eeda20ed6d2478992a8e30eba Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 27 Aug 2024 22:36:32 -0400 Subject: [PATCH 22/62] Use transport for cluster health tests Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 3 +- .../plugin/RestRunClusterHealthAction.java | 42 +--------- .../plugin/RunClusterHealthAction.java | 22 ++++++ .../plugin/RunClusterHealthRequest.java | 40 ++++++++++ .../plugin/RunClusterHealthResponse.java | 40 ++++++++++ .../security/plugin/SystemIndexPlugin1.java | 50 +----------- .../TransportRunClusterHealthAction.java | 78 +++++++++++++++++++ .../security/OpenSearchSecurityPlugin.java | 1 + .../security/securityconf/ConfigModelV7.java | 3 - .../transport/SecuritySSLRequestHandler.java | 2 +- .../transport/SecurityRequestHandler.java | 13 ++-- 11 files changed, 196 insertions(+), 98 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java create mode 100644 src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 647973cedb..304d48ea70 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -45,9 +45,8 @@ public class SystemIndexTests { public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); - // TODO Change this from SINGLENODE to default to test with multiple nodes @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) .anonymousAuth(false) .authc(AUTHC_DOMAIN) .users(USER_ADMIN) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java index 26747abc02..755f3278f0 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -12,19 +12,11 @@ import java.util.List; -import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; -import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; import org.opensearch.client.Client; import org.opensearch.client.node.NodeClient; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.identity.IdentityService; -import org.opensearch.identity.Subject; import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; @@ -53,35 +45,7 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { String runAs = request.param("runAs"); - - return new RestChannelConsumer() { - - @Override - public void accept(RestChannel channel) throws Exception { - if ("user".equalsIgnoreCase(runAs)) { - IdentityService identityService = SystemIndexPlugin1.GuiceHolder.getIdentityService(); - Subject user = identityService.getCurrentSubject(); - user.runAs(() -> { - ActionListener chr = ActionListener.wrap(r -> { - channel.sendResponse( - new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) - ); - }, fr -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr)))); - client.admin().cluster().health(new ClusterHealthRequest(), chr); - return null; - }); - } else { - contextSwitcher.runAs(() -> { - ActionListener chr = ActionListener.wrap(r -> { - channel.sendResponse( - new BytesRestResponse(RestStatus.OK, r.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) - ); - }, fr -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, String.valueOf(fr)))); - client.admin().cluster().health(new ClusterHealthRequest(), chr); - return null; - }); - } - } - }; + RunClusterHealthRequest runRequest = new RunClusterHealthRequest(runAs); + return channel -> client.execute(RunClusterHealthAction.INSTANCE, runRequest, new RestToXContentListener<>(channel)); } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java new file mode 100644 index 0000000000..b819465a35 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.plugin; + +import org.opensearch.action.ActionType; + +public class RunClusterHealthAction extends ActionType { + public static final RunClusterHealthAction INSTANCE = new RunClusterHealthAction(); + public static final String NAME = "mock:cluster/monitor/health"; + + private RunClusterHealthAction() { + super(NAME, RunClusterHealthResponse::new); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java new file mode 100644 index 0000000000..8ae08bd6ff --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.plugin; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; + +public class RunClusterHealthRequest extends ActionRequest { + + private final String runAs; + + public RunClusterHealthRequest(String runAs) { + this.runAs = runAs; + } + + public RunClusterHealthRequest(StreamInput in) throws IOException { + super(in); + this.runAs = in.readString(); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getRunAs() { + return this.runAs; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java new file mode 100644 index 0000000000..e171d9213a --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.plugin; + +import java.io.IOException; + +import org.opensearch.action.support.master.AcknowledgedResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class RunClusterHealthResponse extends AcknowledgedResponse implements ToXContentObject { + + public RunClusterHealthResponse(boolean status) { + super(status); + } + + public RunClusterHealthResponse(StreamInput in) throws IOException { + super(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + } + + @Override + public void addCustomFields(XContentBuilder builder, Params params) throws IOException { + super.addCustomFields(builder, params); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index cc5b801b1f..3f1112c4b6 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -10,7 +10,6 @@ package org.opensearch.security.plugin; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -22,10 +21,6 @@ import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNodes; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.lifecycle.Lifecycle; -import org.opensearch.common.lifecycle.LifecycleComponent; -import org.opensearch.common.lifecycle.LifecycleListener; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.IndexScopedSettings; import org.opensearch.common.settings.Settings; @@ -35,7 +30,6 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.env.Environment; import org.opensearch.env.NodeEnvironment; -import org.opensearch.identity.IdentityService; import org.opensearch.identity.PluginSubject; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.IdentityAwarePlugin; @@ -102,7 +96,8 @@ public List getRestHandlers( @Override public List> getActions() { return Arrays.asList( - new ActionHandler<>(IndexDocumentIntoSystemIndexAction.INSTANCE, TransportIndexDocumentIntoSystemIndexAction.class) + new ActionHandler<>(IndexDocumentIntoSystemIndexAction.INSTANCE, TransportIndexDocumentIntoSystemIndexAction.class), + new ActionHandler<>(RunClusterHealthAction.INSTANCE, TransportRunClusterHealthAction.class) ); } @@ -112,45 +107,4 @@ public void assignSubject(PluginSubject pluginSystemSubject) { this.contextSwitcher.initialize(pluginSystemSubject); } } - - @Override - public Collection> getGuiceServiceClasses() { - final List> services = new ArrayList<>(1); - services.add(GuiceHolder.class); - return services; - } - - public static class GuiceHolder implements LifecycleComponent { - private static IdentityService identityService; - - @Inject - public GuiceHolder(IdentityService identityService) { - GuiceHolder.identityService = identityService; - } - - public static IdentityService getIdentityService() { - return identityService; - } - - @Override - public void close() {} - - @Override - public Lifecycle.State lifecycleState() { - return null; - } - - @Override - public void addLifecycleListener(LifecycleListener listener) {} - - @Override - public void removeLifecycleListener(LifecycleListener listener) {} - - @Override - public void start() {} - - @Override - public void stop() {} - - } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java new file mode 100644 index 0000000000..98264d6638 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ + +package org.opensearch.security.plugin; + +import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.identity.IdentityService; +import org.opensearch.identity.Subject; +import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; + +public class TransportRunClusterHealthAction extends HandledTransportAction { + + private final Client client; + private final ThreadPool threadPool; + private final PluginContextSwitcher contextSwitcher; + private final IdentityService identityService; + + @Inject + public TransportRunClusterHealthAction( + final TransportService transportService, + final ActionFilters actionFilters, + final Client client, + final ThreadPool threadPool, + final PluginContextSwitcher contextSwitcher, + final IdentityService identityService + ) { + super(RunClusterHealthAction.NAME, transportService, actionFilters, RunClusterHealthRequest::new); + this.client = client; + this.threadPool = threadPool; + this.contextSwitcher = contextSwitcher; + this.identityService = identityService; + } + + @Override + protected void doExecute(Task task, RunClusterHealthRequest request, ActionListener actionListener) { + String runAs = request.getRunAs(); + if ("user".equalsIgnoreCase(runAs)) { + Subject user = identityService.getCurrentSubject(); + try { + user.runAs(() -> { + ActionListener chr = ActionListener.wrap( + r -> { actionListener.onResponse(new RunClusterHealthResponse(true)); }, + actionListener::onFailure + ); + client.admin().cluster().health(new ClusterHealthRequest(), chr); + return null; + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + contextSwitcher.runAs(() -> { + ActionListener chr = ActionListener.wrap( + r -> { actionListener.onResponse(new RunClusterHealthResponse(true)); }, + actionListener::onFailure + ); + client.admin().cluster().health(new ClusterHealthRequest(), chr); + return null; + }); + } + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index a6286a6121..5f9ec458bb 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1053,6 +1053,7 @@ public Collection createComponents( } this.threadPool = threadPool; + System.out.println("ThreadPool createComponents: " + threadPool); this.cs = clusterService; this.localClient = localClient; diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 82507f3815..4467260f96 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -464,7 +464,6 @@ public boolean hasExplicitIndexPermission( ) { final Set indicesForRequest = new HashSet<>(resolved.getAllIndicesResolved(cs, resolver)); - System.out.println("indicesForRequest: " + indicesForRequest); if (indicesForRequest.isEmpty()) { // If no indices could be found on the request there is no way to check for the explicit permissions return false; @@ -475,8 +474,6 @@ public boolean hasExplicitIndexPermission( .flatMap(Collection::stream) .collect(Collectors.toSet()); - System.out.println("explicitlyAllowedIndices: " + explicitlyAllowedIndices); - if (log.isDebugEnabled()) { log.debug( "ExplicitIndexPermission check indices for request {}, explicitly allowed indices {}", diff --git a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java index 7002171595..758bd12132 100644 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java +++ b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java @@ -47,7 +47,7 @@ public class SecuritySSLRequestHandler implements Tr private final String action; private final TransportRequestHandler actualHandler; - private final ThreadPool threadPool; + protected final ThreadPool threadPool; protected final Logger log = LogManager.getLogger(this.getClass()); private final PrincipalExtractor principalExtractor; private final SslExceptionHandler errorHandler; diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index 5845c63672..8cff86a2dc 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -29,7 +29,6 @@ // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions import java.net.InetSocketAddress; import java.security.cert.X509Certificate; -import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -181,10 +180,14 @@ protected void messageReceivedDecorate( getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, injectedUserHeader); } } else { - getThreadContext().putTransient( - ConfigConstants.OPENDISTRO_SECURITY_USER, - Objects.requireNonNull((User) Base64Helper.deserializeObject(userHeader, useJDKSerialization)) - ); + User deserializedUser = (User) Base64Helper.deserializeObject(userHeader, useJDKSerialization); + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, deserializedUser); + // if (!deserializedUser.isPluginUser()) { + // getThreadContext().putPersistent( + // ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, + // new SecurityUserSubject(threadPool, deserializedUser) + // ); + // } } String originalRemoteAddress = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER); From 77b728d062d5edd1154512406b2f4f4f2daebe6c Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 28 Aug 2024 09:23:11 -0400 Subject: [PATCH 23/62] Show use of default Signed-off-by: Craig Perkins --- .../java/org/opensearch/security/SystemIndexTests.java | 9 +++++++++ .../security/plugin/TransportRunClusterHealthAction.java | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 304d48ea70..39a031b227 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -142,6 +142,15 @@ public void testAdminUserShouldBeAbleToRunClusterActions() { } } + @Test + public void testAuthenticatedUserShouldBeAbleToRunClusterActions() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get("try-cluster-health/default"); + + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + } + @Test public void testPluginShouldBeAbleToBulkIndexDocumentIntoItsSystemIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java index 98264d6638..4be06933a2 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java @@ -64,7 +64,7 @@ protected void doExecute(Task task, RunClusterHealthRequest request, ActionListe } catch (Exception e) { throw new RuntimeException(e); } - } else { + } else if ("plugin".equalsIgnoreCase(runAs)) { contextSwitcher.runAs(() -> { ActionListener chr = ActionListener.wrap( r -> { actionListener.onResponse(new RunClusterHealthResponse(true)); }, @@ -73,6 +73,12 @@ protected void doExecute(Task task, RunClusterHealthRequest request, ActionListe client.admin().cluster().health(new ClusterHealthRequest(), chr); return null; }); + } else { + ActionListener chr = ActionListener.wrap( + r -> { actionListener.onResponse(new RunClusterHealthResponse(true)); }, + actionListener::onFailure + ); + client.admin().cluster().health(new ClusterHealthRequest(), chr); } } } From a64925781131ee5bfee97d20aba06561d381945c Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 28 Aug 2024 10:22:02 -0400 Subject: [PATCH 24/62] Show an example of switching subjects within a transport action Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 10 +++++ .../IndexDocumentIntoSystemIndexRequest.java | 10 ++++- ...estIndexDocumentIntoSystemIndexAction.java | 3 +- ...ortIndexDocumentIntoSystemIndexAction.java | 39 ++++++++++++++----- .../security/OpenSearchSecurityPlugin.java | 1 - .../security/filter/SecurityFilter.java | 3 +- .../identity/SecurityUserSubject.java | 1 + 7 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 39a031b227..64266b22d9 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -118,6 +118,16 @@ public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByO } } + @Test + public void testPluginShouldBeAbleToCreateSystemIndexButUserShouldNotBeAbleToIndex() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.put("try-create-and-index/" + SYSTEM_INDEX_1 + "?runAs=user"); + + assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + assertThat(response.getBody(), containsString("no permissions for [indices:data/write/index] and User [name=admin")); + } + } + @Test public void testPluginShouldNotBeAbleToRunClusterActions() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java index 5cf7cf90d9..d1b644ad11 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java @@ -20,13 +20,17 @@ public class IndexDocumentIntoSystemIndexRequest extends ActionRequest { private final String indexName; - public IndexDocumentIntoSystemIndexRequest(String indexName) { + private final String runAs; + + public IndexDocumentIntoSystemIndexRequest(String indexName, String runAs) { this.indexName = indexName; + this.runAs = runAs; } public IndexDocumentIntoSystemIndexRequest(StreamInput in) throws IOException { super(in); this.indexName = in.readString(); + this.runAs = in.readOptionalString(); } @Override @@ -37,4 +41,8 @@ public ActionRequestValidationException validate() { public String getIndexName() { return this.indexName; } + + public String getRunAs() { + return this.runAs; + } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java index a4a9fb3c6d..e668e8bccc 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestIndexDocumentIntoSystemIndexAction.java @@ -41,8 +41,9 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String runAs = request.param("runAs"); String indexName = request.param("index"); - IndexDocumentIntoSystemIndexRequest indexRequest = new IndexDocumentIntoSystemIndexRequest(indexName); + IndexDocumentIntoSystemIndexRequest indexRequest = new IndexDocumentIntoSystemIndexRequest(indexName, runAs); return channel -> client.execute(IndexDocumentIntoSystemIndexAction.INSTANCE, indexRequest, new RestToXContentListener<>(channel)); } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index 93347a1515..c549da7809 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -19,6 +19,8 @@ import org.opensearch.common.inject.Inject; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; +import org.opensearch.identity.IdentityService; +import org.opensearch.identity.Subject; import org.opensearch.security.identity.PluginContextSwitcher; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -33,6 +35,7 @@ public class TransportIndexDocumentIntoSystemIndexAction extends HandledTranspor private final Client client; private final ThreadPool threadPool; private final PluginContextSwitcher contextSwitcher; + private final IdentityService identityService; @Inject public TransportIndexDocumentIntoSystemIndexAction( @@ -40,12 +43,14 @@ public TransportIndexDocumentIntoSystemIndexAction( final ActionFilters actionFilters, final Client client, final ThreadPool threadPool, - final PluginContextSwitcher contextSwitcher + final PluginContextSwitcher contextSwitcher, + final IdentityService identityService ) { super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); this.client = client; this.threadPool = threadPool; this.contextSwitcher = contextSwitcher; + this.identityService = identityService; } @Override @@ -55,17 +60,33 @@ protected void doExecute( ActionListener actionListener ) { String indexName = request.getIndexName(); + String runAs = request.getRunAs(); + Subject userSubject = identityService.getCurrentSubject(); try { contextSwitcher.runAs(() -> { client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { - client.index( - new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source("{\"content\":1}", XContentType.JSON), - ActionListener.wrap(r2 -> { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); - }, actionListener::onFailure) - ); + if ("user".equalsIgnoreCase(runAs)) { + userSubject.runAs(() -> { + client.index( + new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON), + ActionListener.wrap(r2 -> { + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); + }, actionListener::onFailure) + ); + return null; + }); + } else { + client.index( + new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON), + ActionListener.wrap(r2 -> { + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); + }, actionListener::onFailure) + ); + } }, actionListener::onFailure)); return null; }); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 5f9ec458bb..a6286a6121 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1053,7 +1053,6 @@ public Collection createComponents( } this.threadPool = threadPool; - System.out.println("ThreadPool createComponents: " + threadPool); this.cs = clusterService; this.localClient = localClient; diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 96b015c830..ccfbc620c5 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -202,8 +202,6 @@ private void ap // However, if another plugin injected a user in the ThreadContext, we still need // to perform privileges checks. enforcePrivilegesEvaluation = true; - } else if (user != null && user.isPluginUser()) { - enforcePrivilegesEvaluation = true; } final boolean userIsAdmin = isUserAdmin(user, adminDns); final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadContext); @@ -322,6 +320,7 @@ private void ap if (Origin.LOCAL.toString().equals(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) && (interClusterRequest || HeaderHelper.isDirectRequest(threadContext)) && (injectedRoles == null) + && (user == null) && !enforcePrivilegesEvaluation) { chain.proceed(task, action, request, listener); diff --git a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java index 4fe1732f86..76eb17b7cd 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java +++ b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java @@ -35,6 +35,7 @@ public Principal getPrincipal() { @Override public T runAs(Callable callable) throws Exception { try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { + System.out.println("Switching to user: " + user); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); return callable.call(); } From 7170c82e6bf86f48b140e3cfcb5125e6aff47696 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 28 Aug 2024 11:10:31 -0400 Subject: [PATCH 25/62] Show example of attaching subject to ActionRequest Signed-off-by: Craig Perkins --- .../IndexDocumentIntoSystemIndexRequest.java | 12 ++-- ...dexDocumentIntoMixOfSystemIndexAction.java | 10 ++-- ...ulkIndexDocumentIntoSystemIndexAction.java | 10 ++-- .../plugin/RestRunClusterHealthAction.java | 8 +-- .../plugin/RunClusterHealthRequest.java | 12 ++-- .../security/plugin/SystemIndexPlugin1.java | 18 +++--- ...ortIndexDocumentIntoSystemIndexAction.java | 58 +++++++++---------- .../TransportRunClusterHealthAction.java | 12 ++-- ...Switcher.java => PluginSubjectHolder.java} | 8 ++- 9 files changed, 73 insertions(+), 75 deletions(-) rename src/main/java/org/opensearch/security/identity/{PluginContextSwitcher.java => PluginSubjectHolder.java} (84%) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java index d1b644ad11..4847ed0421 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java @@ -20,17 +20,17 @@ public class IndexDocumentIntoSystemIndexRequest extends ActionRequest { private final String indexName; - private final String runAs; + private final String runActionAs; - public IndexDocumentIntoSystemIndexRequest(String indexName, String runAs) { + public IndexDocumentIntoSystemIndexRequest(String indexName, String runActionAs) { this.indexName = indexName; - this.runAs = runAs; + this.runActionAs = runActionAs; } public IndexDocumentIntoSystemIndexRequest(StreamInput in) throws IOException { super(in); this.indexName = in.readString(); - this.runAs = in.readOptionalString(); + this.runActionAs = in.readOptionalString(); } @Override @@ -42,7 +42,7 @@ public String getIndexName() { return this.indexName; } - public String getRunAs() { - return this.runAs; + public String getRunActionAs() { + return this.runActionAs; } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java index 0d1dc4fe01..cdddea95be 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java @@ -26,7 +26,7 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.security.identity.PluginSubjectHolder; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -36,11 +36,11 @@ public class RestBulkIndexDocumentIntoMixOfSystemIndexAction extends BaseRestHandler { private final Client client; - private final PluginContextSwitcher contextSwitcher; + private final PluginSubjectHolder pluginSubjectHolder; - public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginContextSwitcher contextSwitcher) { + public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginSubjectHolder pluginSubjectHolder) { this.client = client; - this.contextSwitcher = contextSwitcher; + this.pluginSubjectHolder = pluginSubjectHolder; } @Override @@ -59,7 +59,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - contextSwitcher.runAs(() -> { + pluginSubjectHolder.runAs(() -> { BulkRequestBuilder builder = client.prepareBulk(); builder.add(new IndexRequest(SYSTEM_INDEX_1).source("{\"content\":1}", XContentType.JSON)); builder.add(new IndexRequest(SYSTEM_INDEX_2).source("{\"content\":1}", XContentType.JSON)); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java index 56fa3ef2a9..c730d79fc4 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java @@ -27,7 +27,7 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.security.identity.PluginSubjectHolder; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -35,11 +35,11 @@ public class RestBulkIndexDocumentIntoSystemIndexAction extends BaseRestHandler { private final Client client; - private final PluginContextSwitcher contextSwitcher; + private final PluginSubjectHolder pluginSubjectHolder; - public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginContextSwitcher contextSwitcher) { + public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginSubjectHolder pluginSubjectHolder) { this.client = client; - this.contextSwitcher = contextSwitcher; + this.pluginSubjectHolder = pluginSubjectHolder; } @Override @@ -59,7 +59,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - contextSwitcher.runAs(() -> { + pluginSubjectHolder.runAs(() -> { client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { BulkRequestBuilder builder = client.prepareBulk(); builder.add(new IndexRequest(indexName).source("{\"content\":1}", XContentType.JSON)); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java index 755f3278f0..3a5e1cc407 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -17,7 +17,7 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.security.identity.PluginSubjectHolder; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.GET; @@ -25,11 +25,11 @@ public class RestRunClusterHealthAction extends BaseRestHandler { private final Client client; - private final PluginContextSwitcher contextSwitcher; + private final PluginSubjectHolder pluginSubjectHolder; - public RestRunClusterHealthAction(Client client, PluginContextSwitcher contextSwitcher) { + public RestRunClusterHealthAction(Client client, PluginSubjectHolder pluginSubjectHolder) { this.client = client; - this.contextSwitcher = contextSwitcher; + this.pluginSubjectHolder = pluginSubjectHolder; } @Override diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java index 8ae08bd6ff..383847f2fa 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java @@ -18,15 +18,15 @@ public class RunClusterHealthRequest extends ActionRequest { - private final String runAs; + private final String runActionAs; - public RunClusterHealthRequest(String runAs) { - this.runAs = runAs; + public RunClusterHealthRequest(String runActionAs) { + this.runActionAs = runActionAs; } public RunClusterHealthRequest(StreamInput in) throws IOException { super(in); - this.runAs = in.readString(); + this.runActionAs = in.readString(); } @Override @@ -34,7 +34,7 @@ public ActionRequestValidationException validate() { return null; } - public String getRunAs() { - return this.runAs; + public String getRunActionAs() { + return runActionAs; } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index 3f1112c4b6..fc3b0fde93 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -39,14 +39,14 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; import org.opensearch.script.ScriptService; -import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.security.identity.PluginSubjectHolder; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; public class SystemIndexPlugin1 extends Plugin implements SystemIndexPlugin, IdentityAwarePlugin { public static final String SYSTEM_INDEX_1 = ".system-index1"; - private PluginContextSwitcher contextSwitcher; + private PluginSubjectHolder pluginSubjectHolder; private Client client; @@ -65,8 +65,8 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { this.client = client; - this.contextSwitcher = new PluginContextSwitcher(); - return List.of(contextSwitcher); + this.pluginSubjectHolder = new PluginSubjectHolder(); + return List.of(pluginSubjectHolder); } @Override @@ -87,9 +87,9 @@ public List getRestHandlers( ) { return List.of( new RestIndexDocumentIntoSystemIndexAction(client), - new RestRunClusterHealthAction(client, contextSwitcher), - new RestBulkIndexDocumentIntoSystemIndexAction(client, contextSwitcher), - new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, contextSwitcher) + new RestRunClusterHealthAction(client, pluginSubjectHolder), + new RestBulkIndexDocumentIntoSystemIndexAction(client, pluginSubjectHolder), + new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, pluginSubjectHolder) ); } @@ -103,8 +103,8 @@ public List getRestHandlers( @Override public void assignSubject(PluginSubject pluginSystemSubject) { - if (contextSwitcher != null) { - this.contextSwitcher.initialize(pluginSystemSubject); + if (pluginSubjectHolder != null) { + this.pluginSubjectHolder.initialize(pluginSystemSubject); } } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index c549da7809..9efd32a988 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -21,7 +21,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.identity.IdentityService; import org.opensearch.identity.Subject; -import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.security.identity.PluginSubjectHolder; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -34,7 +34,7 @@ public class TransportIndexDocumentIntoSystemIndexAction extends HandledTranspor private final Client client; private final ThreadPool threadPool; - private final PluginContextSwitcher contextSwitcher; + private final PluginSubjectHolder pluginSubjectHolder; private final IdentityService identityService; @Inject @@ -43,13 +43,13 @@ public TransportIndexDocumentIntoSystemIndexAction( final ActionFilters actionFilters, final Client client, final ThreadPool threadPool, - final PluginContextSwitcher contextSwitcher, + final PluginSubjectHolder pluginSubjectHolder, final IdentityService identityService ) { super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); this.client = client; this.threadPool = threadPool; - this.contextSwitcher = contextSwitcher; + this.pluginSubjectHolder = pluginSubjectHolder; this.identityService = identityService; } @@ -60,36 +60,30 @@ protected void doExecute( ActionListener actionListener ) { String indexName = request.getIndexName(); - String runAs = request.getRunAs(); + String runAs = request.getRunActionAs(); Subject userSubject = identityService.getCurrentSubject(); try { - contextSwitcher.runAs(() -> { - client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { - if ("user".equalsIgnoreCase(runAs)) { - userSubject.runAs(() -> { - client.index( - new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source("{\"content\":1}", XContentType.JSON), - ActionListener.wrap(r2 -> { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); - }, actionListener::onFailure) - ); - return null; - }); - } else { - client.index( - new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source("{\"content\":1}", XContentType.JSON), - ActionListener.wrap(r2 -> { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); - }, actionListener::onFailure) - ); - } - }, actionListener::onFailure)); - return null; - }); + CreateIndexRequest cir = new CreateIndexRequest(indexName); + cir.runAs(pluginSubjectHolder.getPluginSubject()); + client.admin().indices().create(cir, ActionListener.wrap(r -> { + if ("user".equalsIgnoreCase(runAs)) { + IndexRequest ir = new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON); + ir.runAs(userSubject); + client.index(ir, ActionListener.wrap(r2 -> { + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); + }, actionListener::onFailure)); + } else { + IndexRequest ir = new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON); + ir.runAs(pluginSubjectHolder.getPluginSubject()); + client.index(ir, ActionListener.wrap(r2 -> { + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); + }, actionListener::onFailure)); + } + }, actionListener::onFailure)); } catch (Exception ex) { throw new RuntimeException("Unexpected error: " + ex.getMessage()); } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java index 4be06933a2..89c466a8b9 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java @@ -19,7 +19,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.identity.IdentityService; import org.opensearch.identity.Subject; -import org.opensearch.security.identity.PluginContextSwitcher; +import org.opensearch.security.identity.PluginSubjectHolder; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -28,7 +28,7 @@ public class TransportRunClusterHealthAction extends HandledTransportAction actionListener) { - String runAs = request.getRunAs(); + String runAs = request.getRunActionAs(); if ("user".equalsIgnoreCase(runAs)) { Subject user = identityService.getCurrentSubject(); try { @@ -65,7 +65,7 @@ protected void doExecute(Task task, RunClusterHealthRequest request, ActionListe throw new RuntimeException(e); } } else if ("plugin".equalsIgnoreCase(runAs)) { - contextSwitcher.runAs(() -> { + pluginSubjectHolder.runAs(() -> { ActionListener chr = ActionListener.wrap( r -> { actionListener.onResponse(new RunClusterHealthResponse(true)); }, actionListener::onFailure diff --git a/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java b/src/main/java/org/opensearch/security/identity/PluginSubjectHolder.java similarity index 84% rename from src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java rename to src/main/java/org/opensearch/security/identity/PluginSubjectHolder.java index 52809d4fab..200c741eb6 100644 --- a/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java +++ b/src/main/java/org/opensearch/security/identity/PluginSubjectHolder.java @@ -14,10 +14,10 @@ import org.opensearch.identity.PluginSubject; -public class PluginContextSwitcher { +public class PluginSubjectHolder { private PluginSubject pluginSubject; - public PluginContextSwitcher() {} + public PluginSubjectHolder() {} public void initialize(PluginSubject pluginSubject) { this.pluginSubject = pluginSubject; @@ -33,4 +33,8 @@ public T runAs(Callable callable) { throw new RuntimeException(e); } } + + public PluginSubject getPluginSubject() { + return this.pluginSubject; + } } From 9c205a6d9e8413417dc29ce40cd07cfdbab4f0a6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 28 Aug 2024 11:59:09 -0400 Subject: [PATCH 26/62] Revert "Show example of attaching subject to ActionRequest" This reverts commit 7170c82e6bf86f48b140e3cfcb5125e6aff47696. Signed-off-by: Craig Perkins --- .../IndexDocumentIntoSystemIndexRequest.java | 12 ++-- ...dexDocumentIntoMixOfSystemIndexAction.java | 10 ++-- ...ulkIndexDocumentIntoSystemIndexAction.java | 10 ++-- .../plugin/RestRunClusterHealthAction.java | 8 +-- .../plugin/RunClusterHealthRequest.java | 12 ++-- .../security/plugin/SystemIndexPlugin1.java | 18 +++--- ...ortIndexDocumentIntoSystemIndexAction.java | 58 ++++++++++--------- .../TransportRunClusterHealthAction.java | 12 ++-- ...Holder.java => PluginContextSwitcher.java} | 8 +-- 9 files changed, 75 insertions(+), 73 deletions(-) rename src/main/java/org/opensearch/security/identity/{PluginSubjectHolder.java => PluginContextSwitcher.java} (84%) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java index 4847ed0421..d1b644ad11 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexRequest.java @@ -20,17 +20,17 @@ public class IndexDocumentIntoSystemIndexRequest extends ActionRequest { private final String indexName; - private final String runActionAs; + private final String runAs; - public IndexDocumentIntoSystemIndexRequest(String indexName, String runActionAs) { + public IndexDocumentIntoSystemIndexRequest(String indexName, String runAs) { this.indexName = indexName; - this.runActionAs = runActionAs; + this.runAs = runAs; } public IndexDocumentIntoSystemIndexRequest(StreamInput in) throws IOException { super(in); this.indexName = in.readString(); - this.runActionAs = in.readOptionalString(); + this.runAs = in.readOptionalString(); } @Override @@ -42,7 +42,7 @@ public String getIndexName() { return this.indexName; } - public String getRunActionAs() { - return this.runActionAs; + public String getRunAs() { + return this.runAs; } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java index cdddea95be..0d1dc4fe01 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoMixOfSystemIndexAction.java @@ -26,7 +26,7 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.security.identity.PluginSubjectHolder; +import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -36,11 +36,11 @@ public class RestBulkIndexDocumentIntoMixOfSystemIndexAction extends BaseRestHandler { private final Client client; - private final PluginSubjectHolder pluginSubjectHolder; + private final PluginContextSwitcher contextSwitcher; - public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginSubjectHolder pluginSubjectHolder) { + public RestBulkIndexDocumentIntoMixOfSystemIndexAction(Client client, PluginContextSwitcher contextSwitcher) { this.client = client; - this.pluginSubjectHolder = pluginSubjectHolder; + this.contextSwitcher = contextSwitcher; } @Override @@ -59,7 +59,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - pluginSubjectHolder.runAs(() -> { + contextSwitcher.runAs(() -> { BulkRequestBuilder builder = client.prepareBulk(); builder.add(new IndexRequest(SYSTEM_INDEX_1).source("{\"content\":1}", XContentType.JSON)); builder.add(new IndexRequest(SYSTEM_INDEX_2).source("{\"content\":1}", XContentType.JSON)); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java index c730d79fc4..56fa3ef2a9 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestBulkIndexDocumentIntoSystemIndexAction.java @@ -27,7 +27,7 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.security.identity.PluginSubjectHolder; +import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.PUT; @@ -35,11 +35,11 @@ public class RestBulkIndexDocumentIntoSystemIndexAction extends BaseRestHandler { private final Client client; - private final PluginSubjectHolder pluginSubjectHolder; + private final PluginContextSwitcher contextSwitcher; - public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginSubjectHolder pluginSubjectHolder) { + public RestBulkIndexDocumentIntoSystemIndexAction(Client client, PluginContextSwitcher contextSwitcher) { this.client = client; - this.pluginSubjectHolder = pluginSubjectHolder; + this.contextSwitcher = contextSwitcher; } @Override @@ -59,7 +59,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client @Override public void accept(RestChannel channel) throws Exception { - pluginSubjectHolder.runAs(() -> { + contextSwitcher.runAs(() -> { client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { BulkRequestBuilder builder = client.prepareBulk(); builder.add(new IndexRequest(indexName).source("{\"content\":1}", XContentType.JSON)); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java index 3a5e1cc407..755f3278f0 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RestRunClusterHealthAction.java @@ -17,7 +17,7 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.identity.PluginSubjectHolder; +import org.opensearch.security.identity.PluginContextSwitcher; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.GET; @@ -25,11 +25,11 @@ public class RestRunClusterHealthAction extends BaseRestHandler { private final Client client; - private final PluginSubjectHolder pluginSubjectHolder; + private final PluginContextSwitcher contextSwitcher; - public RestRunClusterHealthAction(Client client, PluginSubjectHolder pluginSubjectHolder) { + public RestRunClusterHealthAction(Client client, PluginContextSwitcher contextSwitcher) { this.client = client; - this.pluginSubjectHolder = pluginSubjectHolder; + this.contextSwitcher = contextSwitcher; } @Override diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java index 383847f2fa..8ae08bd6ff 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthRequest.java @@ -18,15 +18,15 @@ public class RunClusterHealthRequest extends ActionRequest { - private final String runActionAs; + private final String runAs; - public RunClusterHealthRequest(String runActionAs) { - this.runActionAs = runActionAs; + public RunClusterHealthRequest(String runAs) { + this.runAs = runAs; } public RunClusterHealthRequest(StreamInput in) throws IOException { super(in); - this.runActionAs = in.readString(); + this.runAs = in.readString(); } @Override @@ -34,7 +34,7 @@ public ActionRequestValidationException validate() { return null; } - public String getRunActionAs() { - return runActionAs; + public String getRunAs() { + return this.runAs; } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java index fc3b0fde93..3f1112c4b6 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/SystemIndexPlugin1.java @@ -39,14 +39,14 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; import org.opensearch.script.ScriptService; -import org.opensearch.security.identity.PluginSubjectHolder; +import org.opensearch.security.identity.PluginContextSwitcher; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; public class SystemIndexPlugin1 extends Plugin implements SystemIndexPlugin, IdentityAwarePlugin { public static final String SYSTEM_INDEX_1 = ".system-index1"; - private PluginSubjectHolder pluginSubjectHolder; + private PluginContextSwitcher contextSwitcher; private Client client; @@ -65,8 +65,8 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { this.client = client; - this.pluginSubjectHolder = new PluginSubjectHolder(); - return List.of(pluginSubjectHolder); + this.contextSwitcher = new PluginContextSwitcher(); + return List.of(contextSwitcher); } @Override @@ -87,9 +87,9 @@ public List getRestHandlers( ) { return List.of( new RestIndexDocumentIntoSystemIndexAction(client), - new RestRunClusterHealthAction(client, pluginSubjectHolder), - new RestBulkIndexDocumentIntoSystemIndexAction(client, pluginSubjectHolder), - new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, pluginSubjectHolder) + new RestRunClusterHealthAction(client, contextSwitcher), + new RestBulkIndexDocumentIntoSystemIndexAction(client, contextSwitcher), + new RestBulkIndexDocumentIntoMixOfSystemIndexAction(client, contextSwitcher) ); } @@ -103,8 +103,8 @@ public List getRestHandlers( @Override public void assignSubject(PluginSubject pluginSystemSubject) { - if (pluginSubjectHolder != null) { - this.pluginSubjectHolder.initialize(pluginSystemSubject); + if (contextSwitcher != null) { + this.contextSwitcher.initialize(pluginSystemSubject); } } } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index 9efd32a988..c549da7809 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -21,7 +21,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.identity.IdentityService; import org.opensearch.identity.Subject; -import org.opensearch.security.identity.PluginSubjectHolder; +import org.opensearch.security.identity.PluginContextSwitcher; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -34,7 +34,7 @@ public class TransportIndexDocumentIntoSystemIndexAction extends HandledTranspor private final Client client; private final ThreadPool threadPool; - private final PluginSubjectHolder pluginSubjectHolder; + private final PluginContextSwitcher contextSwitcher; private final IdentityService identityService; @Inject @@ -43,13 +43,13 @@ public TransportIndexDocumentIntoSystemIndexAction( final ActionFilters actionFilters, final Client client, final ThreadPool threadPool, - final PluginSubjectHolder pluginSubjectHolder, + final PluginContextSwitcher contextSwitcher, final IdentityService identityService ) { super(IndexDocumentIntoSystemIndexAction.NAME, transportService, actionFilters, IndexDocumentIntoSystemIndexRequest::new); this.client = client; this.threadPool = threadPool; - this.pluginSubjectHolder = pluginSubjectHolder; + this.contextSwitcher = contextSwitcher; this.identityService = identityService; } @@ -60,30 +60,36 @@ protected void doExecute( ActionListener actionListener ) { String indexName = request.getIndexName(); - String runAs = request.getRunActionAs(); + String runAs = request.getRunAs(); Subject userSubject = identityService.getCurrentSubject(); try { - CreateIndexRequest cir = new CreateIndexRequest(indexName); - cir.runAs(pluginSubjectHolder.getPluginSubject()); - client.admin().indices().create(cir, ActionListener.wrap(r -> { - if ("user".equalsIgnoreCase(runAs)) { - IndexRequest ir = new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source("{\"content\":1}", XContentType.JSON); - ir.runAs(userSubject); - client.index(ir, ActionListener.wrap(r2 -> { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); - }, actionListener::onFailure)); - } else { - IndexRequest ir = new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source("{\"content\":1}", XContentType.JSON); - ir.runAs(pluginSubjectHolder.getPluginSubject()); - client.index(ir, ActionListener.wrap(r2 -> { - User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); - }, actionListener::onFailure)); - } - }, actionListener::onFailure)); + contextSwitcher.runAs(() -> { + client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { + if ("user".equalsIgnoreCase(runAs)) { + userSubject.runAs(() -> { + client.index( + new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON), + ActionListener.wrap(r2 -> { + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); + }, actionListener::onFailure) + ); + return null; + }); + } else { + client.index( + new IndexRequest(indexName).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON), + ActionListener.wrap(r2 -> { + User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); + }, actionListener::onFailure) + ); + } + }, actionListener::onFailure)); + return null; + }); } catch (Exception ex) { throw new RuntimeException("Unexpected error: " + ex.getMessage()); } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java index 89c466a8b9..4be06933a2 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportRunClusterHealthAction.java @@ -19,7 +19,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.identity.IdentityService; import org.opensearch.identity.Subject; -import org.opensearch.security.identity.PluginSubjectHolder; +import org.opensearch.security.identity.PluginContextSwitcher; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -28,7 +28,7 @@ public class TransportRunClusterHealthAction extends HandledTransportAction actionListener) { - String runAs = request.getRunActionAs(); + String runAs = request.getRunAs(); if ("user".equalsIgnoreCase(runAs)) { Subject user = identityService.getCurrentSubject(); try { @@ -65,7 +65,7 @@ protected void doExecute(Task task, RunClusterHealthRequest request, ActionListe throw new RuntimeException(e); } } else if ("plugin".equalsIgnoreCase(runAs)) { - pluginSubjectHolder.runAs(() -> { + contextSwitcher.runAs(() -> { ActionListener chr = ActionListener.wrap( r -> { actionListener.onResponse(new RunClusterHealthResponse(true)); }, actionListener::onFailure diff --git a/src/main/java/org/opensearch/security/identity/PluginSubjectHolder.java b/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java similarity index 84% rename from src/main/java/org/opensearch/security/identity/PluginSubjectHolder.java rename to src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java index 200c741eb6..52809d4fab 100644 --- a/src/main/java/org/opensearch/security/identity/PluginSubjectHolder.java +++ b/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java @@ -14,10 +14,10 @@ import org.opensearch.identity.PluginSubject; -public class PluginSubjectHolder { +public class PluginContextSwitcher { private PluginSubject pluginSubject; - public PluginSubjectHolder() {} + public PluginContextSwitcher() {} public void initialize(PluginSubject pluginSubject) { this.pluginSubject = pluginSubject; @@ -33,8 +33,4 @@ public T runAs(Callable callable) { throw new RuntimeException(e); } } - - public PluginSubject getPluginSubject() { - return this.pluginSubject; - } } From 690144e75bce2f8b5064e23a277e6ed40c726a09 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 29 Aug 2024 12:11:16 -0400 Subject: [PATCH 27/62] Remove copy of SystemIndexRegistry and use exposed methods from core Signed-off-by: Craig Perkins --- .../security/OpenSearchSecurityPlugin.java | 16 +----------- .../identity/SystemIndexRegistry.java | 21 --------------- .../privileges/PrivilegesEvaluator.java | 10 ++----- .../SystemIndexAccessEvaluator.java | 26 ++++++++++++++++++- .../security/securityconf/ConfigModelV6.java | 9 +------ .../security/securityconf/ConfigModelV7.java | 8 +----- .../security/securityconf/SecurityRoles.java | 8 +----- 7 files changed, 31 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index a6286a6121..9fde77ff87 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -123,7 +123,6 @@ import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; -import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; @@ -168,7 +167,6 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.ContextProvidingPluginSubject; import org.opensearch.security.identity.SecurityTokenManager; -import org.opensearch.security.identity.SystemIndexRegistry; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -270,7 +268,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile Salt salt; private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; - private volatile SystemIndexRegistry systemIndexRegistry; public static boolean isActionTraceEnabled() { @@ -1120,8 +1117,6 @@ public Collection createComponents( final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); - systemIndexRegistry = new SystemIndexRegistry(); - evaluator = new PrivilegesEvaluator( clusterService, threadPool, @@ -1132,8 +1127,7 @@ public Collection createComponents( privilegesInterceptor, cih, irr, - namedXContentRegistry.get(), - systemIndexRegistry + namedXContentRegistry.get() ); sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); @@ -1212,7 +1206,6 @@ public Collection createComponents( components.add(dcf); components.add(userService); components.add(passwordHasher); - components.add(systemIndexRegistry); if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { components.add(sks); @@ -2123,13 +2116,6 @@ public SecurityTokenManager getTokenManager() { @Override public PluginSubject getPluginSubject(Plugin plugin) { - if (systemIndexRegistry != null) { - Collection systemIndexDescriptors = ((SystemIndexPlugin) plugin).getSystemIndexDescriptors(settings); - Set systemIndexPatterns = systemIndexDescriptors.stream() - .map(SystemIndexDescriptor::getIndexPattern) - .collect(Collectors.toSet()); - systemIndexRegistry.addSystemIntexPatterns(plugin.getClass().getCanonicalName(), systemIndexPatterns); - } return new ContextProvidingPluginSubject(threadPool, settings, plugin); } diff --git a/src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java b/src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java deleted file mode 100644 index 8fbb34dfc0..0000000000 --- a/src/main/java/org/opensearch/security/identity/SystemIndexRegistry.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.opensearch.security.identity; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class SystemIndexRegistry { - private final Map> registeredSystemIndexPatterns; - - public SystemIndexRegistry() { - registeredSystemIndexPatterns = new HashMap<>(); - } - - public void addSystemIntexPatterns(String pluginIdentifier, Set indexPatterns) { - registeredSystemIndexPatterns.put(pluginIdentifier, indexPatterns); - } - - public Set getSystemIndexPatterns(String pluginIdentifier) { - return registeredSystemIndexPatterns.get(pluginIdentifier); - } -} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 7739603139..903c5c2046 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -86,7 +86,6 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.identity.SystemIndexRegistry; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; @@ -144,7 +143,6 @@ public class PrivilegesEvaluator { private final PitPrivilegesEvaluator pitPrivilegesEvaluator; private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; - private final SystemIndexRegistry systemIndexRegistry; private final Map pluginRoles; public PrivilegesEvaluator( @@ -157,8 +155,7 @@ public PrivilegesEvaluator( final PrivilegesInterceptor privilegesInterceptor, final ClusterInfoHolder clusterInfoHolder, final IndexResolverReplacer irr, - NamedXContentRegistry namedXContentRegistry, - final SystemIndexRegistry systemIndexRegistry + NamedXContentRegistry namedXContentRegistry ) { super(); @@ -168,7 +165,6 @@ public PrivilegesEvaluator( this.threadContext = threadPool.getThreadContext(); this.privilegesInterceptor = privilegesInterceptor; - this.systemIndexRegistry = systemIndexRegistry; this.pluginRoles = new HashMap<>(); this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( @@ -203,9 +199,7 @@ public SecurityRoles getSecurityRoles(Set roles) { public SecurityRoles getSecurityRoleForPlugin(String pluginIdentifier) { SecurityRoles pluginRole = pluginRoles.get(pluginIdentifier); if (pluginRole == null) { - Set systemIndexPatterns = systemIndexRegistry.getSystemIndexPatterns(pluginIdentifier); - pluginRole = configModel.getSecurityRoles() - .createSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Set.of(), Set.of(), systemIndexPatterns); + pluginRole = configModel.getSecurityRoles().createSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Set.of(), Set.of()); pluginRoles.put(pluginIdentifier, pluginRole); } return pluginRole; diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index affe6177b5..61c487357e 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -277,6 +277,31 @@ private void evaluateSystemIndicesAccess( return; } boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); + if (user.isPluginUser()) { + Set matchingSystemIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( + user.getName(), + requestedResolved.getAllIndices() + ); + if (requestedResolved.getAllIndices().equals(matchingSystemIndices)) { + // plugin is authorized to perform any actions on its own registered system indices + presponse.allowed = true; + presponse.markComplete(); + } else { + if (log.isInfoEnabled()) { + log.info( + "Plugin {} can only perform {} on it's own registered System Indices. System indices from request that match plugin's registered system indices: {}", + user.getName(), + action, + matchingSystemIndices + ); + } + presponse.allowed = false; + presponse.missingPrivileges.add(action); + presponse.markComplete(); + } + return; + } + if (containsProtectedIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (log.isInfoEnabled()) { @@ -307,7 +332,6 @@ private void evaluateSystemIndicesAccess( String.join(", ", getAllSystemIndices(requestedResolved)) ); } - System.out.println("Not authorized"); presponse.allowed = false; presponse.missingPrivileges.add(action); presponse.markComplete(); diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index 6157c9deef..dd296931c5 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -364,8 +364,7 @@ public SecurityRoles createSecurityRole( String roleName, Set clusterPerms, Set indexPatterns, - Set allowedActions, - Set systemIndexPatterns + Set allowedActions ) { SecurityRole role = new SecurityRole(roleName); role.addClusterPerms(clusterPerms); @@ -375,12 +374,6 @@ public SecurityRoles createSecurityRole( perms.addPerms(allowedActions); idxPattern.addTypePerms(perms); } - for (String ip : systemIndexPatterns) { - IndexPattern idxPattern = new IndexPattern(ip); - TypePerm perms = new TypePerm(""); - perms.addPerms(Set.of("*", ConfigConstants.SYSTEM_INDEX_PERMISSION)); - idxPattern.addTypePerms(perms); - } SecurityRoles roles = new SecurityRoles(1); roles.addSecurityRole(role); return roles; diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 4467260f96..263ff0b459 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -278,8 +278,7 @@ public SecurityRoles createSecurityRole( String roleName, Set clusterPerms, Set indexPatterns, - Set allowedActions, - Set systemIndexPatterns + Set allowedActions ) { Set ipatterns = new HashSet<>(); for (String ip : indexPatterns) { @@ -287,11 +286,6 @@ public SecurityRoles createSecurityRole( idxPattern.addPerm(allowedActions); ipatterns.add(idxPattern); } - for (String ip : systemIndexPatterns) { - IndexPattern idxPattern = new IndexPattern(ip); - idxPattern.addPerm(Set.of("*", ConfigConstants.SYSTEM_INDEX_PERMISSION)); - ipatterns.add(idxPattern); - } SecurityRole role = new SecurityRole(roleName, ipatterns, WildcardMatcher.from(clusterPerms)); SecurityRoles roles = new SecurityRoles(1); roles.addSecurityRole(role); diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index 9068b37aa6..8c03188c8d 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -97,13 +97,7 @@ Set getAllPermittedIndicesForDashboards( SecurityRoles filter(Set roles); - SecurityRoles createSecurityRole( - String roleName, - Set clusterPerms, - Set indexPatterns, - Set allowedActions, - Set systemIndexPatterns - ); + SecurityRoles createSecurityRole(String roleName, Set clusterPerms, Set indexPatterns, Set allowedActions); boolean isPermittedOnSystemIndex(String indexName); } From e781e370a82006091446e66978a71a2d39e203f6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 29 Aug 2024 13:30:04 -0400 Subject: [PATCH 28/62] Fix failing tests Signed-off-by: Craig Perkins --- .../security/privileges/PrivilegesEvaluatorResponse.java | 4 ++++ .../security/privileges/SystemIndexAccessEvaluator.java | 2 +- .../security/privileges/SystemIndexAccessEvaluatorTest.java | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index 915514264c..6523cf94a7 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -47,6 +47,10 @@ public Set getMissingPrivileges() { return new HashSet(missingPrivileges); } + public boolean addMissingPrivileges(String action) { + return missingPrivileges.add(action); + } + public Set getMissingSecurityRoles() { return new HashSet<>(missingSecurityRoles); } diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 61c487357e..1eca349e62 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -333,7 +333,7 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; - presponse.missingPrivileges.add(action); + presponse.addMissingPrivileges(action); presponse.markComplete(); return; } diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index fa8a991db9..ee99a05704 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -46,6 +46,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.opensearch.security.support.ConfigConstants.SYSTEM_INDEX_PERMISSION; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -151,6 +152,8 @@ public void setup( when(log.isDebugEnabled()).thenReturn(true); when(log.isInfoEnabled()).thenReturn(true); + when(presponse.addMissingPrivileges(any())).thenReturn(true); + doReturn(ImmutableSet.of(index)).when(ip).getResolvedIndexPattern(user, indexNameExpressionResolver, cs, true); } @@ -288,6 +291,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); verify(log).isInfoEnabled(); verify(log).info("No {} permission for user roles {} to System Indices {}", UNPROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + verify(presponse).addMissingPrivileges(UNPROTECTED_ACTION); } @Test @@ -442,6 +446,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled ); verify(log).debug("Disable search request cache for this request"); verify(log).debug("Disable realtime for this request"); + verify(presponse, times(3)).addMissingPrivileges(UNPROTECTED_ACTION); } @Test @@ -598,6 +603,7 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withou verify(presponse).markComplete(); verify(log).isInfoEnabled(); verify(log).info("No {} permission for user roles {} to System Indices {}", PROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); } @Test From 3c608087c800d36b38dd35a0b095f072da3b88b8 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 30 Aug 2024 17:23:28 -0400 Subject: [PATCH 29/62] Fix SystemIndexPermissionEnabledTests tests Signed-off-by: Craig Perkins --- .../privileges/SnapshotRestoreEvaluator.java | 1 + .../SystemIndexAccessEvaluator.java | 8 ++- .../AbstractSystemIndicesTests.java | 3 +- .../SystemIndexPermissionEnabledTests.java | 50 ++++++++++--------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java index 23612e1a52..b206442585 100644 --- a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java @@ -109,6 +109,7 @@ public PrivilegesEvaluatorResponse evaluate( auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '{}' as source index is not allowed", action, securityIndex); presponse.allowed = false; + presponse.missingPrivileges.add(action); return presponse.markComplete(); } return presponse; diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 1eca349e62..89adab64e8 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -272,7 +272,7 @@ private void evaluateSystemIndicesAccess( log.debug("Service account cannot access regular indices: {}", regularIndices); } presponse.allowed = false; - presponse.missingPrivileges.add(action); + presponse.addMissingPrivileges(action); presponse.markComplete(); return; } @@ -296,7 +296,7 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; - presponse.missingPrivileges.add(action); + presponse.addMissingPrivileges(action); presponse.markComplete(); } return; @@ -313,6 +313,7 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; + presponse.addMissingPrivileges(action); presponse.markComplete(); return; } else if (containsSystemIndex @@ -356,6 +357,7 @@ private void evaluateSystemIndicesAccess( auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '_all' indices is not allowed for a regular user", action); presponse.allowed = false; + presponse.addMissingPrivileges(action); presponse.markComplete(); } } @@ -370,6 +372,7 @@ else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { log.debug("Filtered '{}' but resulting list is empty", securityIndex); } presponse.allowed = false; + presponse.addMissingPrivileges(action); presponse.markComplete(); return; } @@ -382,6 +385,7 @@ else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { final String foundSystemIndexes = String.join(", ", getAllSystemIndices(requestedResolved)); log.warn("{} for '{}' index is not allowed for a regular user", action, foundSystemIndexes); presponse.allowed = false; + presponse.addMissingPrivileges(action); presponse.markComplete(); } } diff --git a/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java b/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java index deb6f6f5e3..09aacec057 100644 --- a/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java +++ b/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java @@ -186,6 +186,7 @@ String permissionExceptionMessage(String action, String username) { } void validateForbiddenResponse(RestHelper.HttpResponse response, String action, String user) { + assertThat(response.getStatusCode(), is(RestStatus.FORBIDDEN.getStatus())); MatcherAssert.assertThat(response.getBody(), Matchers.containsStringIgnoringCase(permissionExceptionMessage(action, user))); } @@ -195,7 +196,7 @@ void shouldBeAllowedOnlyForAuthorizedIndices(String index, RestHelper.HttpRespon boolean isRequestingAccessToNonAuthorizedSystemIndex = (!user.equals(allAccessUser) && index.equals(SYSTEM_INDEX_WITH_NO_ASSOCIATED_ROLE_PERMISSIONS)); if (isSecurityIndexRequest || isRequestingAccessToNonAuthorizedSystemIndex) { - validateForbiddenResponse(response, isSecurityIndexRequest ? "" : action, user); + validateForbiddenResponse(response, action, user); } else { assertThat(response.getStatusCode(), is(RestStatus.OK.getStatus())); } diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java index 397b8c2286..714e5161bd 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java @@ -59,7 +59,7 @@ public void testSearchAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_search", matchAllQuery, allAccessUserHeader); // no system indices are searchable by admin - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:data/read/search", allAccessUser); } // search all indices @@ -78,7 +78,7 @@ public void testSearchAsNormalUser() throws Exception { // security index is only accessible by super-admin RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_search", "", normalUserHeader); if (index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) || index.equals(SYSTEM_INDEX_WITH_NO_ASSOCIATED_ROLE_PERMISSIONS)) { - validateForbiddenResponse(response, "", normalUser); + validateForbiddenResponse(response, "indices:data/read/search", normalUser); } else { // got 1 hits because system index permissions are enabled validateSearchResponse(response, 1); @@ -98,7 +98,7 @@ public void testSearchAsNormalUserWithoutSystemIndexAccess() { // search system indices for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_search", "", normalUserWithoutSystemIndexHeader); - validateForbiddenResponse(response, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(response, "indices:data/read/search", normalUserWithoutSystemIndex); } // search all indices @@ -151,10 +151,10 @@ public void testDeleteAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executeDeleteRequest(index + "/_doc/document1", allAccessUserHeader); - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:data/write/delete", allAccessUser); response = restHelper.executeDeleteRequest(index, allAccessUserHeader); - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:admin/delete", allAccessUser); } } @@ -166,10 +166,10 @@ public void testDeleteAsNormalUser() { // permission for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executeDeleteRequest(index + "/_doc/document1", normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:data/write/delete", normalUser); response = restHelper.executeDeleteRequest(index, normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/delete", normalUser); } } @@ -183,10 +183,10 @@ public void testDeleteAsNormalUserWithoutSystemIndexAccess() { index + "/_doc/document1", normalUserWithoutSystemIndexHeader ); - validateForbiddenResponse(response, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(response, "indices:data/write/delete", normalUserWithoutSystemIndex); response = restHelper.executeDeleteRequest(index, normalUserWithoutSystemIndexHeader); - validateForbiddenResponse(response, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(response, "indices:admin/delete", normalUserWithoutSystemIndex); } } @@ -217,11 +217,11 @@ public void testCloseOpenAsNormalUser() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/close", normalUser); // normal user cannot open or close security index response = restHelper.executePostRequest(index + "/_open", "", normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/open", normalUser); } } @@ -235,11 +235,11 @@ private void testCloseOpenWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/close", user); // admin or normal user (without system index permission) cannot open or close any system index response = restHelper.executePostRequest(index + "/_open", "", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/open", user); } } @@ -314,10 +314,10 @@ public void testUpdateAsNormalUser() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePutRequest(index + "/_settings", updateIndexSettings, normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/settings/update", normalUser); response = restHelper.executePutRequest(index + "/_mapping", newMappings, normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/mapping/put", normalUser); } } @@ -331,10 +331,10 @@ private void testUpdateWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePutRequest(index + "/_settings", updateIndexSettings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/settings/update", user); response = restHelper.executePutRequest(index + "/_mapping", newMappings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/mapping/put", user); } } @@ -393,14 +393,14 @@ public void testSnapshotSystemIndicesAsAdmin() { "", allAccessUserHeader ); - validateForbiddenResponse(res, "", allAccessUser); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", allAccessUser); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", allAccessUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); } } @@ -424,7 +424,7 @@ public void testSnapshotSystemIndicesAsNormalUser() { "", normalUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", normalUser); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", @@ -432,7 +432,9 @@ public void testSnapshotSystemIndicesAsNormalUser() { normalUserHeader ); - String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) ? "" : "indices:data/write/index, indices:admin/create"; + String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) + ? "cluster:admin/snapshot/restore" + : "indices:data/write/index, indices:admin/create"; validateForbiddenResponse(res, action, normalUser); } } @@ -457,14 +459,16 @@ public void testSnapshotSystemIndicesAsNormalUserWithoutSystemIndexAccess() { "", normalUserWithoutSystemIndexHeader ); - validateForbiddenResponse(res, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", normalUserWithoutSystemIndex); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", normalUserWithoutSystemIndexHeader ); - String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) ? "" : "indices:data/write/index, indices:admin/create"; + String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) + ? "cluster:admin/snapshot/restore" + : "indices:data/write/index, indices:admin/create"; validateForbiddenResponse(res, action, normalUserWithoutSystemIndex); } } From e42a4a0e76b47667c37bfda33161466b6ffe5fae Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 30 Aug 2024 20:41:28 -0400 Subject: [PATCH 30/62] Fix tests Signed-off-by: Craig Perkins --- .../privileges/SystemIndexAccessEvaluatorTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index ee99a05704..4dc5c16560 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -500,6 +500,7 @@ public void testProtectedActionLocalAll_systemIndexDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '_all' indices is not allowed for a regular user", "indices:data/write"); } @@ -514,6 +515,7 @@ public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); } @@ -528,6 +530,7 @@ public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); } @@ -586,6 +589,7 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, TEST_SYSTEM_INDEX); } @@ -628,6 +632,7 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); @@ -643,6 +648,7 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisab verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); @@ -678,6 +684,7 @@ private void testSecurityIndexAccess(String action) { verify(auditLog).logSecurityIndexAttempt(request, action, task); assertThat(presponse.allowed, is(false)); + verify(presponse).addMissingPrivileges(action); verify(presponse).markComplete(); verify(log).isInfoEnabled(); From 41e83b18ec8883107998762c38e69f5f627f1095 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 30 Aug 2024 21:26:31 -0400 Subject: [PATCH 31/62] Fix disabled tests Signed-off-by: Craig Perkins --- .../SystemIndexPermissionDisabledTests.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java index fcb1f9265c..562b3b6963 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java @@ -140,10 +140,10 @@ private void testDeleteWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executeDeleteRequest(index + "/_doc/document1", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:data/write/delete", user); response = restHelper.executeDeleteRequest(index, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/delete", user); } } @@ -169,7 +169,7 @@ public void testCloseOpenAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", allAccessUserHeader); - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:admin/close", allAccessUser); // admin cannot close any system index but can open them response = restHelper.executePostRequest(index + "/_open", "", allAccessUserHeader); @@ -192,7 +192,7 @@ private void testCloseOpenWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/close", user); // normal user cannot open or close security index response = restHelper.executePostRequest(index + "/_open", "", header); @@ -284,10 +284,10 @@ private void testUpdateWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePutRequest(index + "/_mapping", newMappings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/mapping/put", user); response = restHelper.executePutRequest(index + "/_settings", updateIndexSettings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/settings/update", user); } } @@ -346,14 +346,14 @@ public void testSnapshotSystemIndicesAsAdmin() { "", allAccessUserHeader ); - validateForbiddenResponse(res, "", allAccessUser); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", allAccessUser); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", allAccessUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); } } @@ -382,7 +382,7 @@ private void testSnapshotSystemIndexWithUser(String user, Header header) { assertThat(res.getStatusCode(), is(HttpStatus.SC_UNAUTHORIZED)); res = restHelper.executePostRequest("_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "", header); - validateForbiddenResponse(res, "", user); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", user); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", @@ -390,7 +390,7 @@ private void testSnapshotSystemIndexWithUser(String user, Header header) { header ); if (index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN)) { - validateForbiddenResponse(res, "", user); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", user); } else { validateForbiddenResponse(res, "indices:data/write/index, indices:admin/create", user); } From c9f1ff7d7cf07da4258a328e6946ec20169da762 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 30 Aug 2024 22:00:03 -0400 Subject: [PATCH 32/62] Fix more tests Signed-off-by: Craig Perkins --- .../system_indices/SystemIndexDisabledTests.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java index b387070195..7abe20046c 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java @@ -123,7 +123,7 @@ public void testDeleteAsSuperAdmin() { @Test public void testDeleteAsAdmin() { - testDeleteWithUser(allAccessUser, allAccessUserHeader, "", ""); + testDeleteWithUser(allAccessUser, allAccessUserHeader, "indices:admin/delete", "indices:data/write/delete"); } @Test @@ -175,7 +175,7 @@ public void testCloseOpenAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", allAccessUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/close", allAccessUser); // User can open the index but cannot close it response = restHelper.executePostRequest(index + "/_open", "", allAccessUserHeader); @@ -348,14 +348,14 @@ public void testSnapshotSystemIndicesAsAdmin() { assertThat(res.getStatusCode(), is(HttpStatus.SC_UNAUTHORIZED)); res = restHelper.executePostRequest(snapshotRequest + "/_restore?wait_for_completion=true", "", allAccessUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); res = restHelper.executePostRequest( snapshotRequest + "/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", allAccessUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); } } @@ -384,7 +384,9 @@ private void testSnapshotWithUser(String user, Header header) { RestHelper.HttpResponse res = restHelper.executeGetRequest(snapshotRequest); assertThat(res.getStatusCode(), is(HttpStatus.SC_UNAUTHORIZED)); - String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) ? "" : "indices:data/write/index, indices:admin/create"; + String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) + ? "cluster:admin/snapshot/restore" + : "indices:data/write/index, indices:admin/create"; res = restHelper.executePostRequest(snapshotRequest + "/_restore?wait_for_completion=true", "", header); shouldBeAllowedOnlyForAuthorizedIndices(index, res, action, user); From 0a696ff4321f4bbca153bebf6dbc4601aee38db2 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 09:42:03 -0400 Subject: [PATCH 33/62] Add license headers Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubject.java | 9 +++++++++ .../security/identity/SecurityUserSubject.java | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 63ed349ca1..60c8e6108c 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -1,3 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ package org.opensearch.security.identity; import java.security.Principal; diff --git a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java index 76eb17b7cd..9ecaa9c4e8 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java +++ b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java @@ -1,3 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ package org.opensearch.security.identity; import java.security.Principal; From c4b5e17298b944e95958f6f2386f601a76066aec Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 13:01:24 -0400 Subject: [PATCH 34/62] Add tests Signed-off-by: Craig Perkins --- .../identity/SecurityUserSubject.java | 1 - .../ContextProvidingPluginSubjectTests.java | 54 +++++++++++++++++++ .../identity/SecurityUserSubjectTests.java | 52 ++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java create mode 100644 src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java diff --git a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java index 9ecaa9c4e8..5fab960ffd 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java +++ b/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java @@ -44,7 +44,6 @@ public Principal getPrincipal() { @Override public T runAs(Callable callable) throws Exception { try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { - System.out.println("Switching to user: " + user); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); return callable.call(); } diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java new file mode 100644 index 0000000000..562d97a8bd --- /dev/null +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -0,0 +1,54 @@ +/* + * 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.identity; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.plugins.IdentityAwarePlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.security.user.PluginUser; +import org.opensearch.threadpool.TestThreadPool; +import org.opensearch.threadpool.ThreadPool; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER; +import static org.junit.Assert.assertNull; + +public class ContextProvidingPluginSubjectTests { + static class TestIdentityAwarePlugin extends Plugin implements IdentityAwarePlugin { + + } + + @Test + public void testSecurityUserSubjectRunAs() throws Exception { + final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + + final Plugin testPlugin = new TestIdentityAwarePlugin(); + + final PluginUser pluginUser = new PluginUser(testPlugin.getClass().getCanonicalName()); + + ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); + + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); + + subject.runAs(() -> { + assertThat(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER), equalTo(pluginUser)); + return null; + }); + + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); + + SecurityUserSubjectTests.terminate(threadPool); + } +} diff --git a/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java b/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java new file mode 100644 index 0000000000..99ef34549e --- /dev/null +++ b/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java @@ -0,0 +1,52 @@ +/* + * 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.identity; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import org.opensearch.security.user.User; +import org.opensearch.threadpool.TestThreadPool; +import org.opensearch.threadpool.ThreadPool; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER; +import static org.junit.Assert.assertNull; + +public class SecurityUserSubjectTests { + + public static boolean terminate(ThreadPool threadPool) { + return ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); + } + + @Test + public void testSecurityUserSubjectRunAs() throws Exception { + final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + + User user = new User("testUser"); + + SecurityUserSubject subject = new SecurityUserSubject(threadPool, user); + + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); + + subject.runAs(() -> { + assertThat(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER), equalTo(user)); + return null; + }); + + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); + + terminate(threadPool); + } +} From 1d1bb571df835a22e7d85ee321eb181b4325ab3c Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 13:29:04 -0400 Subject: [PATCH 35/62] Test PluginContextSwitcher Signed-off-by: Craig Perkins --- .../ContextProvidingPluginSubjectTests.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index 562d97a8bd..b4a783e71a 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -51,4 +51,30 @@ public void testSecurityUserSubjectRunAs() throws Exception { SecurityUserSubjectTests.terminate(threadPool); } + + @Test + public void testPluginContextSwitcherRunAs() throws Exception { + final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + + final Plugin testPlugin = new TestIdentityAwarePlugin(); + + final PluginContextSwitcher contextSwitcher = new PluginContextSwitcher(); + + final PluginUser pluginUser = new PluginUser(testPlugin.getClass().getCanonicalName()); + + ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); + + contextSwitcher.initialize(subject); + + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); + + subject.runAs(() -> { + assertThat(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER), equalTo(pluginUser)); + return null; + }); + + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); + + SecurityUserSubjectTests.terminate(threadPool); + } } From 27be3c3bfe2a5027173047850a290aef36f48e1c Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 13:32:16 -0400 Subject: [PATCH 36/62] Test getPrincipal Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubjectTests.java | 2 ++ .../opensearch/security/identity/SecurityUserSubjectTests.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index b4a783e71a..e73c8271f4 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -40,6 +40,8 @@ public void testSecurityUserSubjectRunAs() throws Exception { ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); + assertThat(subject.getPrincipal().getName(), equalTo(testPlugin.getClass().getCanonicalName())); + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); subject.runAs(() -> { diff --git a/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java b/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java index 99ef34549e..d6d9da3ba5 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java @@ -38,6 +38,8 @@ public void testSecurityUserSubjectRunAs() throws Exception { SecurityUserSubject subject = new SecurityUserSubject(threadPool, user); + assertThat(subject.getPrincipal().getName(), equalTo(user.getName())); + assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); subject.runAs(() -> { From 634cf7b7f03e893cbf0cd77c6680f1f9deaa0897 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 14:11:54 -0400 Subject: [PATCH 37/62] Add test on createSecurityRole Signed-off-by: Craig Perkins --- .../security/privileges/PrivilegesEvaluator.java | 2 +- .../security/securityconf/ConfigModelV6.java | 9 ++++----- .../security/securityconf/ConfigModelV7.java | 9 ++++----- .../security/securityconf/SecurityRoles.java | 3 ++- .../ssl/transport/SecuritySSLRequestHandler.java | 2 +- .../SecurityRolesPermissionsV6Test.java | 14 ++++++++++++-- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 903c5c2046..238284f747 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -199,7 +199,7 @@ public SecurityRoles getSecurityRoles(Set roles) { public SecurityRoles getSecurityRoleForPlugin(String pluginIdentifier) { SecurityRoles pluginRole = pluginRoles.get(pluginIdentifier); if (pluginRole == null) { - pluginRole = configModel.getSecurityRoles().createSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Set.of(), Set.of()); + pluginRole = configModel.getSecurityRoles().createSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Map.of()); pluginRoles.put(pluginIdentifier, pluginRole); } return pluginRole; diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index dd296931c5..d859b75319 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -363,15 +363,14 @@ public SecurityRoles filter(Set keep) { public SecurityRoles createSecurityRole( String roleName, Set clusterPerms, - Set indexPatterns, - Set allowedActions + Map> indexPatternToAllowedActions ) { SecurityRole role = new SecurityRole(roleName); role.addClusterPerms(clusterPerms); - for (String ip : indexPatterns) { - IndexPattern idxPattern = new IndexPattern(ip); + for (Map.Entry> entry : indexPatternToAllowedActions.entrySet()) { + IndexPattern idxPattern = new IndexPattern(entry.getKey()); TypePerm perms = new TypePerm(""); - perms.addPerms(allowedActions); + perms.addPerms(entry.getValue()); idxPattern.addTypePerms(perms); } SecurityRoles roles = new SecurityRoles(1); diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 263ff0b459..1ab0ba8edf 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -277,13 +277,12 @@ public SecurityRoles filter(Set keep) { public SecurityRoles createSecurityRole( String roleName, Set clusterPerms, - Set indexPatterns, - Set allowedActions + Map> indexPatternToAllowedActions ) { Set ipatterns = new HashSet<>(); - for (String ip : indexPatterns) { - IndexPattern idxPattern = new IndexPattern(ip); - idxPattern.addPerm(allowedActions); + for (Map.Entry> entry : indexPatternToAllowedActions.entrySet()) { + IndexPattern idxPattern = new IndexPattern(entry.getKey()); + idxPattern.addPerm(entry.getValue()); ipatterns.add(idxPattern); } SecurityRole role = new SecurityRole(roleName, ipatterns, WildcardMatcher.from(clusterPerms)); diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index 8c03188c8d..7bf4ee81fd 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -27,6 +27,7 @@ package org.opensearch.security.securityconf; +import java.util.Map; import java.util.Set; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; @@ -97,7 +98,7 @@ Set getAllPermittedIndicesForDashboards( SecurityRoles filter(Set roles); - SecurityRoles createSecurityRole(String roleName, Set clusterPerms, Set indexPatterns, Set allowedActions); + SecurityRoles createSecurityRole(String roleName, Set clusterPerms, Map> indexPatternToAllowedActions); boolean isPermittedOnSystemIndex(String indexName); } diff --git a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java index 758bd12132..7002171595 100644 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java +++ b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java @@ -47,7 +47,7 @@ public class SecuritySSLRequestHandler implements Tr private final String action; private final TransportRequestHandler actualHandler; - protected final ThreadPool threadPool; + private final ThreadPool threadPool; protected final Logger log = LogManager.getLogger(this.getClass()); private final PrincipalExtractor principalExtractor; private final SslExceptionHandler errorHandler; diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java index 530db3211b..914f8750a0 100644 --- a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import com.google.common.collect.ImmutableMap; @@ -41,6 +42,7 @@ import org.mockito.quality.Strictness; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -120,17 +122,25 @@ public void hasExplicitIndexPermission() { .filter(ImmutableSet.of("has_system_index_permission")); user.addSecurityRoles(List.of("has_system_index_permission")); - Assert.assertTrue( + assertTrue( "Should allow system index access with explicit only", securityRoleWithExplicitAccess.hasExplicitIndexPermission(resolved, user, new String[] {}, resolver, cs) ); } + @Test + public void testCreateSecurityRole() { + SecurityRoles securityRoles = configModel.getSecurityRoles() + .createSecurityRole("testRole", Set.of("cluster:monitor/health"), Map.of("*", Set.of("indices:data/read/search"))); + assertTrue(securityRoles.getRoleNames().contains("testRole")); + assertTrue(securityRoles.hasExplicitClusterPermissionPermission("cluster:monitor/health")); + } + @Test public void isPermittedOnSystemIndex() { final SecurityRoles securityRoleWithExplicitAccess = configModel.getSecurityRoles() .filter(ImmutableSet.of("has_system_index_permission")); - Assert.assertTrue(securityRoleWithExplicitAccess.isPermittedOnSystemIndex(TEST_INDEX)); + assertTrue(securityRoleWithExplicitAccess.isPermittedOnSystemIndex(TEST_INDEX)); final SecurityRoles securityRoleWithStarAccess = configModel.getSecurityRoles() .filter(ImmutableSet.of("all_access_without_system_index_permission")); From 1af4b65f1514022ccac1caf816432f45167e331f Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 16:13:35 -0400 Subject: [PATCH 38/62] Increase test coverage Signed-off-by: Craig Perkins --- .../ContextProvidingPluginSubjectTests.java | 21 +++++++++++++++++++ .../SecurityRolesPermissionsTest.java | 10 +++++++++ 2 files changed, 31 insertions(+) diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index e73c8271f4..b9ba435d0a 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -79,4 +79,25 @@ public void testPluginContextSwitcherRunAs() throws Exception { SecurityUserSubjectTests.terminate(threadPool); } + + @Test + public void testPluginContextSwitcherUninitializedRunAs() throws Exception { + final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + + final PluginContextSwitcher contextSwitcher = new PluginContextSwitcher(); + + threadPool.getThreadContext().putHeader("foo", "bar"); + + // should be no noop since contextSwitch is uninitialized + Object returnVal = contextSwitcher.runAs(() -> { + assertThat(threadPool.getThreadContext().getHeader("foo"), equalTo("bar")); + return null; + }); + + assertThat(threadPool.getThreadContext().getHeader("foo"), equalTo("bar")); + + assertNull(returnVal); + + SecurityUserSubjectTests.terminate(threadPool); + } } diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java index f469d1989c..e9c10da015 100644 --- a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java @@ -32,6 +32,7 @@ import java.util.Collection; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -55,6 +56,7 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; +import static org.junit.Assert.assertTrue; public class SecurityRolesPermissionsTest { @@ -217,6 +219,14 @@ public void hasExplicitClusterPermissionPermissionForRestAdmin() { assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.CONFIG, securityConfigAllowRole); } + @Test + public void testCreateSecurityRole() { + SecurityRoles securityRoles = configModel.getSecurityRoles() + .createSecurityRole("testRole", Set.of("cluster:monitor/health"), Map.of("*", Set.of("indices:data/read/search"))); + assertTrue(securityRoles.getRoleNames().contains("testRole")); + assertTrue(securityRoles.hasExplicitClusterPermissionPermission("cluster:monitor/health")); + } + void assertHasNoPermissionsForRestApiAdminOnePermissionRole(final Endpoint allowEndpoint, final SecurityRoles allowOnlyRoleForRole) { final Collection noPermissionEndpoints = ENDPOINTS_WITH_PERMISSIONS.keySet() .stream() From 64fb16123d2a7bec55558188296f2c786fe8c3a8 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Sep 2024 16:40:03 -0400 Subject: [PATCH 39/62] Allow import for AcknowledgedResponse Signed-off-by: Craig Perkins --- .../security/plugin/IndexDocumentIntoSystemIndexResponse.java | 2 ++ .../opensearch/security/plugin/RunClusterHealthResponse.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java index cc72c64887..9779cca252 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexResponse.java @@ -10,6 +10,7 @@ package org.opensearch.security.plugin; +// CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here import java.io.IOException; import org.opensearch.action.support.master.AcknowledgedResponse; @@ -18,6 +19,7 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; +// CS-ENFORCE-SINGLE public class IndexDocumentIntoSystemIndexResponse extends AcknowledgedResponse implements ToXContentObject { diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java index e171d9213a..7a855744dc 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthResponse.java @@ -10,6 +10,7 @@ package org.opensearch.security.plugin; +// CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here import java.io.IOException; import org.opensearch.action.support.master.AcknowledgedResponse; @@ -17,6 +18,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; +// CS-ENFORCE-SINGLE public class RunClusterHealthResponse extends AcknowledgedResponse implements ToXContentObject { From dc8868e8a75ba554c25580d6fdddc9dd56d5bd09 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 6 Sep 2024 13:56:49 -0400 Subject: [PATCH 40/62] Address code review comments Signed-off-by: Craig Perkins --- .../security/OpenSearchSecurityPlugin.java | 4 ++-- .../security/auth/BackendRegistry.java | 6 +++--- .../identity/PluginContextSwitcher.java | 5 ++--- .../privileges/SnapshotRestoreEvaluator.java | 2 +- .../privileges/SystemIndexAccessEvaluator.java | 2 +- .../security/support/ConfigConstants.java | 2 +- .../security/transport/SecurityInterceptor.java | 4 ++-- .../transport/SecurityRequestHandler.java | 6 ------ .../ContextProvidingPluginSubjectTests.java | 17 ++--------------- 9 files changed, 14 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 9fde77ff87..2009efd23d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -220,7 +220,7 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; -import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_SUBJECT; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; @@ -2106,7 +2106,7 @@ private static String handleKeyword(final String field) { @Override public Subject getCurrentSubject() { - return (Subject) threadPool.getThreadContext().getPersistent(OPENDISTRO_SECURITY_SUBJECT); + return (Subject) threadPool.getThreadContext().getPersistent(OPENDISTRO_SECURITY_AUTHENTICATED_USER); } @Override diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index dce8087ea3..229dfdf32d 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -226,7 +226,7 @@ public boolean authenticate(final SecurityRequestChannel request) { // PKI authenticated REST call User superuser = new User(sslPrincipal); UserSubject subject = new SecurityUserSubject(threadPool, superuser); - threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, subject); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); auditLog.logSucceededLogin(sslPrincipal, true, null, request); return true; @@ -391,7 +391,7 @@ public boolean authenticate(final SecurityRequestChannel request) { threadPool.getThreadContext() .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); UserSubject subject = new SecurityUserSubject(threadPool, impersonatedUser == null ? authenticatedUser : impersonatedUser); - threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, subject); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); auditLog.logSucceededLogin( (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), false, @@ -427,7 +427,7 @@ public boolean authenticate(final SecurityRequestChannel request) { UserSubject subject = new SecurityUserSubject(threadPool, anonymousUser); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); - threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, subject); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); auditLog.logSucceededLogin(anonymousUser.getName(), false, null, request); if (isDebugEnabled) { log.debug("Anonymous User is authenticated"); diff --git a/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java b/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java index 52809d4fab..a16671fda4 100644 --- a/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java +++ b/src/main/java/org/opensearch/security/identity/PluginContextSwitcher.java @@ -10,6 +10,7 @@ */ package org.opensearch.security.identity; +import java.util.Objects; import java.util.concurrent.Callable; import org.opensearch.identity.PluginSubject; @@ -24,9 +25,7 @@ public void initialize(PluginSubject pluginSubject) { } public T runAs(Callable callable) { - if (pluginSubject == null) { - return null; - } + Objects.requireNonNull(pluginSubject); try { return pluginSubject.runAs(callable); } catch (Exception e) { diff --git a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java index b206442585..8a11e433e8 100644 --- a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java @@ -109,7 +109,7 @@ public PrivilegesEvaluatorResponse evaluate( auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '{}' as source index is not allowed", action, securityIndex); presponse.allowed = false; - presponse.missingPrivileges.add(action); + presponse.addMissingPrivileges(action); return presponse.markComplete(); } return presponse; diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 89adab64e8..4210dbb931 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -288,7 +288,7 @@ private void evaluateSystemIndicesAccess( presponse.markComplete(); } else { if (log.isInfoEnabled()) { - log.info( + log.warn( "Plugin {} can only perform {} on it's own registered System Indices. System indices from request that match plugin's registered system indices: {}", user.getName(), action, diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index ab99275897..91000e44f2 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -113,7 +113,7 @@ public class ConfigConstants { public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_transport_principal"; public static final String OPENDISTRO_SECURITY_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user"; - public static final String OPENDISTRO_SECURITY_SUBJECT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "subject"; + public static final String OPENDISTRO_SECURITY_AUTHENTICATED_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "authenticated_user"; public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_header"; public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index b73118fc91..e88b91487f 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -131,8 +131,8 @@ public SecurityRequestHandler getHandler(String private User determineUser(Connection connection) { User user0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - // pluginUser did not exist prior to 2.17.0 - if (user0 != null && user0.isPluginUser() && connection.getVersion().before(Version.V_2_17_0)) { + // pluginUser did not exist prior to 2.18.0 + if (user0 != null && user0.isPluginUser() && connection.getVersion().before(Version.V_2_18_0)) { user0 = null; } return user0; diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index 8cff86a2dc..d2dbe50ab7 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -182,12 +182,6 @@ protected void messageReceivedDecorate( } else { User deserializedUser = (User) Base64Helper.deserializeObject(userHeader, useJDKSerialization); getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, deserializedUser); - // if (!deserializedUser.isPluginUser()) { - // getThreadContext().putPersistent( - // ConfigConstants.OPENDISTRO_SECURITY_SUBJECT, - // new SecurityUserSubject(threadPool, deserializedUser) - // ); - // } } String originalRemoteAddress = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER); diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index b9ba435d0a..61be66ac11 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -24,6 +24,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; public class ContextProvidingPluginSubjectTests { static class TestIdentityAwarePlugin extends Plugin implements IdentityAwarePlugin { @@ -82,22 +83,8 @@ public void testPluginContextSwitcherRunAs() throws Exception { @Test public void testPluginContextSwitcherUninitializedRunAs() throws Exception { - final ThreadPool threadPool = new TestThreadPool(getClass().getName()); - final PluginContextSwitcher contextSwitcher = new PluginContextSwitcher(); - threadPool.getThreadContext().putHeader("foo", "bar"); - - // should be no noop since contextSwitch is uninitialized - Object returnVal = contextSwitcher.runAs(() -> { - assertThat(threadPool.getThreadContext().getHeader("foo"), equalTo("bar")); - return null; - }); - - assertThat(threadPool.getThreadContext().getHeader("foo"), equalTo("bar")); - - assertNull(returnVal); - - SecurityUserSubjectTests.terminate(threadPool); + assertThrows(NullPointerException.class, () -> contextSwitcher.runAs(() -> null)); } } From 565fa2056bda13f339e73e054c39f2d55e000680 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 6 Sep 2024 14:12:40 -0400 Subject: [PATCH 41/62] Address code review feedback Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 14 +++--- .../test/framework/cluster/LocalCluster.java | 14 ++---- .../SystemIndexAccessEvaluator.java | 49 ++++++++++--------- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 64266b22d9..75be734f2f 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -35,7 +35,6 @@ import static org.opensearch.security.plugin.SystemIndexPlugin2.SYSTEM_INDEX_2; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; -import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; @@ -50,13 +49,11 @@ public class SystemIndexTests { .anonymousAuth(false) .authc(AUTHC_DOMAIN) .users(USER_ADMIN) - .plugin(List.of(SystemIndexPlugin1.class, SystemIndexPlugin2.class)) + .plugin(SystemIndexPlugin1.class, SystemIndexPlugin2.class) .nodeSettings( Map.of( FeatureFlags.IDENTITY, true, - SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, - true, SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), SECURITY_SYSTEM_INDICES_ENABLED_KEY, @@ -88,10 +85,15 @@ public void adminShouldNotBeAbleToDeleteSecurityIndex() { assertThat(response2.getStatusCode(), equalTo(RestStatus.OK.getStatus())); - // regular use cannot create system index when system index protection is enforced + // regular user can create system index HttpResponse response3 = client.put(".system-index1"); - assertThat(response3.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + assertThat(response3.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + + // regular user cannot delete system index + HttpResponse response4 = client.delete(".system-index1"); + + assertThat(response4.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 9d0b084655..d3998e4793 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -378,20 +378,12 @@ public Builder nodeSettings(Map settings) { return this; } - /** - * Adds additional plugin to the cluster - */ - public Builder plugin(Class plugin) { - this.plugins.add(plugin); - - return this; - } - /** * Adds additional plugins to the cluster */ - public Builder plugin(List> plugins) { - this.plugins.addAll(plugins); + @SafeVarargs + public final Builder plugin(Class... plugins) { + this.plugins.addAll(List.of(plugins)); return this; } diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 4210dbb931..6b58f60d56 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -277,30 +277,6 @@ private void evaluateSystemIndicesAccess( return; } boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); - if (user.isPluginUser()) { - Set matchingSystemIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( - user.getName(), - requestedResolved.getAllIndices() - ); - if (requestedResolved.getAllIndices().equals(matchingSystemIndices)) { - // plugin is authorized to perform any actions on its own registered system indices - presponse.allowed = true; - presponse.markComplete(); - } else { - if (log.isInfoEnabled()) { - log.warn( - "Plugin {} can only perform {} on it's own registered System Indices. System indices from request that match plugin's registered system indices: {}", - user.getName(), - action, - matchingSystemIndices - ); - } - presponse.allowed = false; - presponse.addMissingPrivileges(action); - presponse.markComplete(); - } - return; - } if (containsProtectedIndex) { auditLog.logSecurityIndexAttempt(request, action, task); @@ -340,6 +316,31 @@ private void evaluateSystemIndicesAccess( } } + if (user.isPluginUser()) { + Set matchingSystemIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( + user.getName(), + requestedResolved.getAllIndices() + ); + if (requestedResolved.getAllIndices().equals(matchingSystemIndices)) { + // plugin is authorized to perform any actions on its own registered system indices + presponse.allowed = true; + presponse.markComplete(); + } else { + if (log.isInfoEnabled()) { + log.info( + "Plugin {} can only perform {} on it's own registered System Indices. System indices from request that match plugin's registered system indices: {}", + user.getName(), + action, + matchingSystemIndices + ); + } + presponse.allowed = false; + presponse.addMissingPrivileges(action); + presponse.markComplete(); + } + return; + } + if (isActionAllowed(action)) { if (requestedResolved.isLocalAll()) { if (filterSecurityIndex) { From e5855ab4bb31ea86b74bd35ab27d03309a6751d4 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 20 Sep 2024 15:16:45 -0400 Subject: [PATCH 42/62] Change calls to isPluginUser and create InMemorySecurityRoles Signed-off-by: Craig Perkins --- .../privileges/PrivilegesEvaluator.java | 12 ++++++--- .../SystemIndexAccessEvaluator.java | 3 ++- .../security/securityconf/ConfigModelV6.java | 19 -------------- .../security/securityconf/ConfigModelV7.java | 24 +++-------------- .../securityconf/InMemorySecurityRoles.java | 20 ++++++++++++++ .../securityconf/InMemorySecurityRolesV7.java | 26 +++++++++++++++++++ .../security/securityconf/SecurityRoles.java | 3 --- .../transport/SecurityInterceptor.java | 3 ++- .../org/opensearch/security/user/User.java | 7 ----- .../SecurityRolesPermissionsTest.java | 4 +-- .../SecurityRolesPermissionsV6Test.java | 9 ------- 11 files changed, 63 insertions(+), 67 deletions(-) create mode 100644 src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java create mode 100644 src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 238284f747..c984360cd6 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -90,10 +90,13 @@ import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.InMemorySecurityRoles; +import org.opensearch.security.securityconf.InMemorySecurityRolesV7; import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; @@ -143,7 +146,7 @@ public class PrivilegesEvaluator { private final PitPrivilegesEvaluator pitPrivilegesEvaluator; private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; - private final Map pluginRoles; + private final Map pluginRoles; public PrivilegesEvaluator( final ClusterService clusterService, @@ -197,9 +200,10 @@ public SecurityRoles getSecurityRoles(Set roles) { } public SecurityRoles getSecurityRoleForPlugin(String pluginIdentifier) { - SecurityRoles pluginRole = pluginRoles.get(pluginIdentifier); + InMemorySecurityRoles pluginRole = pluginRoles.get(pluginIdentifier); if (pluginRole == null) { - pluginRole = configModel.getSecurityRoles().createSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Map.of()); + pluginRole = new InMemorySecurityRolesV7(1); + pluginRole.addSecurityRole(pluginIdentifier, Set.of(BulkAction.NAME), Map.of()); pluginRoles.put(pluginIdentifier, pluginRole); } return pluginRole; @@ -292,7 +296,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } presponse.resolvedSecurityRoles.addAll(mappedRoles); final SecurityRoles securityRoles; - if (user.isPluginUser()) { + if (user instanceof PluginUser) { securityRoles = getSecurityRoleForPlugin(user.getName()); } else { securityRoles = getSecurityRoles(mappedRoles); diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 6b58f60d56..6e1168b214 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -48,6 +48,7 @@ import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -316,7 +317,7 @@ private void evaluateSystemIndicesAccess( } } - if (user.isPluginUser()) { + if (user instanceof PluginUser) { Set matchingSystemIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( user.getName(), requestedResolved.getAllIndices() diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index d859b75319..e35fb40a24 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -359,25 +359,6 @@ public SecurityRoles filter(Set keep) { return retVal; } - @Override - public SecurityRoles createSecurityRole( - String roleName, - Set clusterPerms, - Map> indexPatternToAllowedActions - ) { - SecurityRole role = new SecurityRole(roleName); - role.addClusterPerms(clusterPerms); - for (Map.Entry> entry : indexPatternToAllowedActions.entrySet()) { - IndexPattern idxPattern = new IndexPattern(entry.getKey()); - TypePerm perms = new TypePerm(""); - perms.addPerms(entry.getValue()); - idxPattern.addTypePerms(perms); - } - SecurityRoles roles = new SecurityRoles(1); - roles.addSecurityRole(role); - return roles; - } - @Override public EvaluatedDlsFlsConfig getDlsFls( User user, diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 1ab0ba8edf..e41bbf9400 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -219,11 +219,11 @@ public static class SecurityRoles implements org.opensearch.security.securitycon final Set roles; - private SecurityRoles(int roleCount) { + protected SecurityRoles(int roleCount) { roles = new HashSet<>(roleCount); } - private SecurityRoles addSecurityRole(SecurityRole securityRole) { + protected SecurityRoles addSecurityRole(SecurityRole securityRole) { if (securityRole != null) { this.roles.add(securityRole); } @@ -273,24 +273,6 @@ public SecurityRoles filter(Set keep) { return retVal; } - @Override - public SecurityRoles createSecurityRole( - String roleName, - Set clusterPerms, - Map> indexPatternToAllowedActions - ) { - Set ipatterns = new HashSet<>(); - for (Map.Entry> entry : indexPatternToAllowedActions.entrySet()) { - IndexPattern idxPattern = new IndexPattern(entry.getKey()); - idxPattern.addPerm(entry.getValue()); - ipatterns.add(idxPattern); - } - SecurityRole role = new SecurityRole(roleName, ipatterns, WildcardMatcher.from(clusterPerms)); - SecurityRoles roles = new SecurityRoles(1); - roles.addSecurityRole(role); - return roles; - } - @Override public EvaluatedDlsFlsConfig getDlsFls( User user, @@ -554,7 +536,7 @@ public SecurityRole build() { } } - private SecurityRole(String name, Set ipatterns, WildcardMatcher clusterPerms) { + SecurityRole(String name, Set ipatterns, WildcardMatcher clusterPerms) { this.name = Objects.requireNonNull(name); this.ipatterns = ipatterns; this.clusterPerms = clusterPerms; diff --git a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java new file mode 100644 index 0000000000..934533788f --- /dev/null +++ b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java @@ -0,0 +1,20 @@ +/* + * 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.securityconf; + +import java.util.Map; +import java.util.Set; + +public interface InMemorySecurityRoles extends SecurityRoles { + + void addSecurityRole(String roleName, Set clusterPerms, Map> indexPatternToAllowedActions); +} diff --git a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java new file mode 100644 index 0000000000..9c160a3bd7 --- /dev/null +++ b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java @@ -0,0 +1,26 @@ +package org.opensearch.security.securityconf; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.opensearch.security.support.WildcardMatcher; + +public class InMemorySecurityRolesV7 extends ConfigModelV7.SecurityRoles implements InMemorySecurityRoles { + + public InMemorySecurityRolesV7(int roleCount) { + super(roleCount); + } + + @Override + public void addSecurityRole(String roleName, Set clusterPerms, Map> indexPatternToAllowedActions) { + Set ipatterns = new HashSet<>(); + for (Map.Entry> entry : indexPatternToAllowedActions.entrySet()) { + ConfigModelV7.IndexPattern idxPattern = new ConfigModelV7.IndexPattern(entry.getKey()); + idxPattern.addPerm(entry.getValue()); + ipatterns.add(idxPattern); + } + ConfigModelV7.SecurityRole role = new ConfigModelV7.SecurityRole(roleName, ipatterns, WildcardMatcher.from(clusterPerms)); + roles.add(role); + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index 7bf4ee81fd..fb25e1a21f 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -27,7 +27,6 @@ package org.opensearch.security.securityconf; -import java.util.Map; import java.util.Set; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; @@ -98,7 +97,5 @@ Set getAllPermittedIndicesForDashboards( SecurityRoles filter(Set roles); - SecurityRoles createSecurityRole(String roleName, Set clusterPerms, Map> indexPatternToAllowedActions); - boolean isPermittedOnSystemIndex(String indexName); } diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index 099e025c31..f32908a0a7 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -64,6 +64,7 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.SerializationFormat; +import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.Transport.Connection; @@ -132,7 +133,7 @@ public SecurityRequestHandler getHandler(String private User determineUser(Connection connection) { User user0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); // pluginUser did not exist prior to 2.18.0 - if (user0 != null && user0.isPluginUser() && connection.getVersion().before(Version.V_2_18_0)) { + if (user0 != null && user0 instanceof PluginUser && connection.getVersion().before(Version.V_2_18_0)) { user0 = null; } return user0; diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index 67e557bf77..ed48ab7356 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -296,11 +296,4 @@ public boolean isServiceAccount() { Map userAttributesMap = this.getCustomAttributesMap(); return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); } - - /** - * @return true if this instance is of the type PluginUser - */ - public boolean isPluginUser() { - return this instanceof PluginUser; - } } diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java index e9c10da015..b783cfb40d 100644 --- a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java @@ -221,8 +221,8 @@ public void hasExplicitClusterPermissionPermissionForRestAdmin() { @Test public void testCreateSecurityRole() { - SecurityRoles securityRoles = configModel.getSecurityRoles() - .createSecurityRole("testRole", Set.of("cluster:monitor/health"), Map.of("*", Set.of("indices:data/read/search"))); + InMemorySecurityRoles securityRoles = new InMemorySecurityRolesV7(1); + securityRoles.addSecurityRole("testRole", Set.of("cluster:monitor/health"), Map.of("*", Set.of("indices:data/read/search"))); assertTrue(securityRoles.getRoleNames().contains("testRole")); assertTrue(securityRoles.hasExplicitClusterPermissionPermission("cluster:monitor/health")); } diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java index 914f8750a0..281704a4ed 100644 --- a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java @@ -15,7 +15,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.TreeMap; import com.google.common.collect.ImmutableMap; @@ -128,14 +127,6 @@ public void hasExplicitIndexPermission() { ); } - @Test - public void testCreateSecurityRole() { - SecurityRoles securityRoles = configModel.getSecurityRoles() - .createSecurityRole("testRole", Set.of("cluster:monitor/health"), Map.of("*", Set.of("indices:data/read/search"))); - assertTrue(securityRoles.getRoleNames().contains("testRole")); - assertTrue(securityRoles.hasExplicitClusterPermissionPermission("cluster:monitor/health")); - } - @Test public void isPermittedOnSystemIndex() { final SecurityRoles securityRoleWithExplicitAccess = configModel.getSecurityRoles() From b8c333c5a0fd3c11ca38ce7761fad2e36629a621 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 20 Sep 2024 16:52:04 -0400 Subject: [PATCH 43/62] Add missing license header Signed-off-by: Craig Perkins --- .../securityconf/InMemorySecurityRolesV7.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java index 9c160a3bd7..da5a07298c 100644 --- a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java +++ b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java @@ -1,3 +1,14 @@ +/* + * 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.securityconf; import java.util.HashSet; From 9b0c3df1a9b2549aba309b11fe7f445d9ca764ed Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 23 Sep 2024 14:12:08 -0400 Subject: [PATCH 44/62] Use plugin: prefix Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 8 +++-- .../ContextProvidingPluginSubject.java | 8 +++-- .../privileges/PrivilegesEvaluator.java | 3 +- .../SystemIndexAccessEvaluator.java | 5 ++- .../support/SafeSerializationUtils.java | 2 -- .../transport/SecurityInterceptor.java | 3 +- .../opensearch/security/user/PluginUser.java | 34 ------------------- .../org/opensearch/security/user/User.java | 9 +++++ 8 files changed, 23 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/user/PluginUser.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 75be734f2f..97254a8406 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -115,7 +115,9 @@ public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByO assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); assertThat( response.getBody(), - containsString("no permissions for [indices:admin/create] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1") + containsString( + "no permissions for [indices:admin/create] and User [name=plugin:org.opensearch.security.plugin.SystemIndexPlugin1" + ) ); } } @@ -139,7 +141,7 @@ public void testPluginShouldNotBeAbleToRunClusterActions() { assertThat( response.getBody(), containsString( - "no permissions for [cluster:monitor/health] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1" + "no permissions for [cluster:monitor/health] and User [name=plugin:org.opensearch.security.plugin.SystemIndexPlugin1" ) ); } @@ -184,7 +186,7 @@ public void testPluginShouldNotBeAbleToBulkIndexDocumentIntoMixOfSystemIndexWher assertThat( response.getBody(), containsString( - "no permissions for [indices:data/write/bulk[s]] and User [name=org.opensearch.security.plugin.SystemIndexPlugin1" + "no permissions for [indices:data/write/bulk[s]] and User [name=plugin:org.opensearch.security.plugin.SystemIndexPlugin1" ) ); } diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 60c8e6108c..7a096abf08 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -18,19 +18,21 @@ import org.opensearch.identity.PluginSubject; import org.opensearch.plugins.Plugin; import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.PluginUser; +import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; public class ContextProvidingPluginSubject implements PluginSubject { private final ThreadPool threadPool; private final NamedPrincipal pluginPrincipal; - private final PluginUser pluginUser; + private final User pluginUser; public ContextProvidingPluginSubject(ThreadPool threadPool, Settings settings, Plugin plugin) { super(); this.threadPool = threadPool; this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); - this.pluginUser = new PluginUser(pluginPrincipal.getName()); + // Convention for plugin username. Prefixed with 'plugin:'. ':' is forbidden from usernames, so this + // guarantees that a user with this username cannot be created by other means. + this.pluginUser = new User("plugin:" + pluginPrincipal.getName()); } @Override diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index c984360cd6..195f819f7b 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -96,7 +96,6 @@ import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; @@ -296,7 +295,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } presponse.resolvedSecurityRoles.addAll(mappedRoles); final SecurityRoles securityRoles; - if (user instanceof PluginUser) { + if (user.isPluginUser()) { securityRoles = getSecurityRoleForPlugin(user.getName()); } else { securityRoles = getSecurityRoles(mappedRoles); diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 6e1168b214..00a2791ba3 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -48,7 +48,6 @@ import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -317,9 +316,9 @@ private void evaluateSystemIndicesAccess( } } - if (user instanceof PluginUser) { + if (user.isPluginUser()) { Set matchingSystemIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( - user.getName(), + user.getName().replace("plugin:", ""), requestedResolved.getAllIndices() ); if (requestedResolved.getAllIndices().equals(matchingSystemIndices)) { diff --git a/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java b/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java index 1d2289f0ae..b58e4afd35 100644 --- a/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java +++ b/src/main/java/org/opensearch/security/support/SafeSerializationUtils.java @@ -26,7 +26,6 @@ import com.google.common.collect.ImmutableSet; import org.opensearch.security.auth.UserInjector; -import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import com.amazon.dlic.auth.ldap.LdapUser; @@ -49,7 +48,6 @@ public final class SafeSerializationUtils { InetSocketAddress.class, Pattern.class, User.class, - PluginUser.class, UserInjector.InjectedUser.class, SourceFieldsContext.class, LdapUser.class, diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index f32908a0a7..099e025c31 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -64,7 +64,6 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.SerializationFormat; -import org.opensearch.security.user.PluginUser; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.Transport.Connection; @@ -133,7 +132,7 @@ public SecurityRequestHandler getHandler(String private User determineUser(Connection connection) { User user0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); // pluginUser did not exist prior to 2.18.0 - if (user0 != null && user0 instanceof PluginUser && connection.getVersion().before(Version.V_2_18_0)) { + if (user0 != null && user0.isPluginUser() && connection.getVersion().before(Version.V_2_18_0)) { user0 = null; } return user0; diff --git a/src/main/java/org/opensearch/security/user/PluginUser.java b/src/main/java/org/opensearch/security/user/PluginUser.java deleted file mode 100644 index 66ed1b0149..0000000000 --- a/src/main/java/org/opensearch/security/user/PluginUser.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.user; - -import java.io.IOException; - -import org.opensearch.core.common.io.stream.StreamInput; - -public class PluginUser extends User { - private static final long serialVersionUID = -4083322940729403322L; - - public PluginUser(StreamInput in) throws IOException { - super(in); - } - - /** - * Create a new plugin user without roles and attributes - * - * @param name The username (must not be null or empty) - * @throws IllegalArgumentException if name is null or empty - */ - public PluginUser(final String name) { - super(name, null, null); - } -} diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index ed48ab7356..190729623d 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -296,4 +296,13 @@ public boolean isServiceAccount() { Map userAttributesMap = this.getCustomAttributesMap(); return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a plugin account attributes, otherwise false + */ + public boolean isPluginUser() { + return name != null && name.startsWith("plugin:"); + } } From abea484a218a64c05fb713ab0a02ad444d89fea1 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 23 Sep 2024 14:26:38 -0400 Subject: [PATCH 45/62] Remove PluginUser Signed-off-by: Craig Perkins --- .../identity/ContextProvidingPluginSubjectTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index 61be66ac11..533e2eff83 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -16,7 +16,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.plugins.IdentityAwarePlugin; import org.opensearch.plugins.Plugin; -import org.opensearch.security.user.PluginUser; +import org.opensearch.security.user.User; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; @@ -37,7 +37,7 @@ public void testSecurityUserSubjectRunAs() throws Exception { final Plugin testPlugin = new TestIdentityAwarePlugin(); - final PluginUser pluginUser = new PluginUser(testPlugin.getClass().getCanonicalName()); + final User pluginUser = new User(testPlugin.getClass().getCanonicalName()); ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); @@ -63,7 +63,7 @@ public void testPluginContextSwitcherRunAs() throws Exception { final PluginContextSwitcher contextSwitcher = new PluginContextSwitcher(); - final PluginUser pluginUser = new PluginUser(testPlugin.getClass().getCanonicalName()); + final User pluginUser = new User(testPlugin.getClass().getCanonicalName()); ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); From ed13ecbfc09159f8b1e53273d6b65f2732da8c5b Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 1 Oct 2024 16:52:58 -0400 Subject: [PATCH 46/62] Remove Identity FeatureFlag Signed-off-by: Craig Perkins --- .../java/org/opensearch/security/SystemIndexTests.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 97254a8406..c2a5a9f89a 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -18,7 +18,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.common.util.FeatureFlags; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.plugin.SystemIndexPlugin1; import org.opensearch.security.plugin.SystemIndexPlugin2; @@ -52,8 +51,6 @@ public class SystemIndexTests { .plugin(SystemIndexPlugin1.class, SystemIndexPlugin2.class) .nodeSettings( Map.of( - FeatureFlags.IDENTITY, - true, SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), SECURITY_SYSTEM_INDICES_ENABLED_KEY, From 68ff2705e075229b10a92fa02b5227633efa3513 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 1 Oct 2024 18:59:03 -0400 Subject: [PATCH 47/62] Fix tests Signed-off-by: Craig Perkins --- .../security/identity/ContextProvidingPluginSubjectTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index 533e2eff83..0b98913ab8 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -37,7 +37,7 @@ public void testSecurityUserSubjectRunAs() throws Exception { final Plugin testPlugin = new TestIdentityAwarePlugin(); - final User pluginUser = new User(testPlugin.getClass().getCanonicalName()); + final User pluginUser = new User("plugin:" + testPlugin.getClass().getCanonicalName()); ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); @@ -63,7 +63,7 @@ public void testPluginContextSwitcherRunAs() throws Exception { final PluginContextSwitcher contextSwitcher = new PluginContextSwitcher(); - final User pluginUser = new User(testPlugin.getClass().getCanonicalName()); + final User pluginUser = new User("plugin:" + testPlugin.getClass().getCanonicalName()); ContextProvidingPluginSubject subject = new ContextProvidingPluginSubject(threadPool, Settings.EMPTY, testPlugin); From 0216cc051dd2b106dfd0d178959cc3caa160c7a2 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 14 Oct 2024 15:34:57 -0400 Subject: [PATCH 48/62] Address code review feedback Signed-off-by: Craig Perkins --- .../java/org/opensearch/security/auth/BackendRegistry.java | 7 +++---- .../SecurityUserSubject.java => auth/SecurityUser.java} | 6 +++--- .../SecurityUserTests.java} | 6 +++--- .../identity/ContextProvidingPluginSubjectTests.java | 5 +++-- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/main/java/org/opensearch/security/{identity/SecurityUserSubject.java => auth/SecurityUser.java} (89%) rename src/test/java/org/opensearch/security/{identity/SecurityUserSubjectTests.java => auth/SecurityUserTests.java} (90%) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index fb8eae59fe..5be7e9622a 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -64,7 +64,6 @@ import org.opensearch.security.filter.SecurityRequestChannel; import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.http.XFFResolver; -import org.opensearch.security.identity.SecurityUserSubject; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; @@ -226,7 +225,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call User superuser = new User(sslPrincipal); - UserSubject subject = new SecurityUserSubject(threadPool, superuser); + UserSubject subject = new SecurityUser(threadPool, superuser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); auditLog.logSucceededLogin(sslPrincipal, true, null, request); @@ -394,7 +393,7 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); threadPool.getThreadContext() .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); - UserSubject subject = new SecurityUserSubject(threadPool, impersonatedUser == null ? authenticatedUser : impersonatedUser); + UserSubject subject = new SecurityUser(threadPool, impersonatedUser == null ? authenticatedUser : impersonatedUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); auditLog.logSucceededLogin( (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), @@ -428,7 +427,7 @@ public boolean authenticate(final SecurityRequestChannel request) { User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); - UserSubject subject = new SecurityUserSubject(threadPool, anonymousUser); + UserSubject subject = new SecurityUser(threadPool, anonymousUser); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); diff --git a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java b/src/main/java/org/opensearch/security/auth/SecurityUser.java similarity index 89% rename from src/main/java/org/opensearch/security/identity/SecurityUserSubject.java rename to src/main/java/org/opensearch/security/auth/SecurityUser.java index 5fab960ffd..8ce4bfb3a1 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityUserSubject.java +++ b/src/main/java/org/opensearch/security/auth/SecurityUser.java @@ -7,7 +7,7 @@ * compatible open source license. * */ -package org.opensearch.security.identity; +package org.opensearch.security.auth; import java.security.Principal; import java.util.concurrent.Callable; @@ -20,12 +20,12 @@ import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; -public class SecurityUserSubject implements UserSubject { +public class SecurityUser implements UserSubject { private final NamedPrincipal userPrincipal; private final ThreadPool threadPool; private final User user; - public SecurityUserSubject(ThreadPool threadPool, User user) { + SecurityUser(ThreadPool threadPool, User user) { this.threadPool = threadPool; this.user = user; this.userPrincipal = new NamedPrincipal(user.getName()); diff --git a/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java b/src/test/java/org/opensearch/security/auth/SecurityUserTests.java similarity index 90% rename from src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java rename to src/test/java/org/opensearch/security/auth/SecurityUserTests.java index d6d9da3ba5..4ae15ef303 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityUserSubjectTests.java +++ b/src/test/java/org/opensearch/security/auth/SecurityUserTests.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.identity; +package org.opensearch.security.auth; import java.util.concurrent.TimeUnit; @@ -24,7 +24,7 @@ import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER; import static org.junit.Assert.assertNull; -public class SecurityUserSubjectTests { +public class SecurityUserTests { public static boolean terminate(ThreadPool threadPool) { return ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); @@ -36,7 +36,7 @@ public void testSecurityUserSubjectRunAs() throws Exception { User user = new User("testUser"); - SecurityUserSubject subject = new SecurityUserSubject(threadPool, user); + SecurityUser subject = new SecurityUser(threadPool, user); assertThat(subject.getPrincipal().getName(), equalTo(user.getName())); diff --git a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java index 0b98913ab8..48851c48b3 100644 --- a/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java +++ b/src/test/java/org/opensearch/security/identity/ContextProvidingPluginSubjectTests.java @@ -16,6 +16,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.plugins.IdentityAwarePlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.security.auth.SecurityUserTests; import org.opensearch.security.user.User; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; @@ -52,7 +53,7 @@ public void testSecurityUserSubjectRunAs() throws Exception { assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); - SecurityUserSubjectTests.terminate(threadPool); + SecurityUserTests.terminate(threadPool); } @Test @@ -78,7 +79,7 @@ public void testPluginContextSwitcherRunAs() throws Exception { assertNull(threadPool.getThreadContext().getTransient(OPENDISTRO_SECURITY_USER)); - SecurityUserSubjectTests.terminate(threadPool); + SecurityUserTests.terminate(threadPool); } @Test From 05890d5b240e22032fe0125cb6e166a862a2e64b Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 14 Oct 2024 16:02:44 -0400 Subject: [PATCH 49/62] Create common method for getting SecurityRoles and move plugin specific logic into method Signed-off-by: Craig Perkins --- .../SystemIndexSearcherWrapper.java | 2 +- .../privileges/PrivilegesEvaluator.java | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 8e89b60712..c8cc6885d2 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -167,7 +167,7 @@ protected final boolean isBlockedSystemIndexRequest() { } final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); final Set mappedRoles = evaluator.mapRoles(user, caller); - final SecurityRoles securityRoles = evaluator.getSecurityRoles(mappedRoles); + final SecurityRoles securityRoles = evaluator.filterSecurityRolesFromCache(mappedRoles); return !securityRoles.isPermittedOnSystemIndex(index.getName()); } return true; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 195f819f7b..1d904fc5bb 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -194,7 +194,22 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; } - public SecurityRoles getSecurityRoles(Set roles) { + public SecurityRoles getSecurityRoles(User user, Set roles) { + SecurityRoles securityRoles; + if (user.isPluginUser()) { + securityRoles = getSecurityRoleForPlugin(user.getName()); + } else { + securityRoles = filterSecurityRolesFromCache(roles); + + // Add the security roles for this user so that they can be used for DLS parameter substitution. + user.addSecurityRoles(roles); + setUserInfoInThreadContext(user); + } + + return securityRoles; + } + + public SecurityRoles filterSecurityRolesFromCache(Set roles) { return configModel.getSecurityRoles().filter(roles); } @@ -214,7 +229,7 @@ public boolean hasRestAdminPermissions(final User user, final TransportAddress r } private boolean hasRestAdminPermissions(final Set roles, String permission) { - final SecurityRoles securityRoles = getSecurityRoles(roles); + final SecurityRoles securityRoles = filterSecurityRolesFromCache(roles); return securityRoles.hasExplicitClusterPermissionPermission(permission); } @@ -294,16 +309,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) context.setMappedRoles(mappedRoles); } presponse.resolvedSecurityRoles.addAll(mappedRoles); - final SecurityRoles securityRoles; - if (user.isPluginUser()) { - securityRoles = getSecurityRoleForPlugin(user.getName()); - } else { - securityRoles = getSecurityRoles(mappedRoles); - } - - // Add the security roles for this user so that they can be used for DLS parameter substitution. - user.addSecurityRoles(mappedRoles); - setUserInfoInThreadContext(user); + final SecurityRoles securityRoles = getSecurityRoles(user, mappedRoles); final boolean isDebugEnabled = log.isDebugEnabled(); if (isDebugEnabled) { From e0fb8fcb817ac389e8176a9408438b009aa5ef9f Mon Sep 17 00:00:00 2001 From: Muneer Kolarkunnu <33829651+akolarkunnu@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:25:58 +0530 Subject: [PATCH 50/62] Improve error message when a node with an incorrectly configured certificate attempts to connect (#4818) Signed-off-by: Abdul Muneer Kolarkunnu --- .../opensearch/security/ssl/util/ExceptionUtils.java | 10 ++++++++-- .../security/transport/SecurityRequestHandler.java | 2 +- .../security/ccstest/CrossClusterSearchTests.java | 9 ++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java index 83982239f0..4683075f1d 100644 --- a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java +++ b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java @@ -76,7 +76,13 @@ public static OpenSearchException createJwkCreationException(Throwable cause) { return new OpenSearchException("An error occurred during the creation of Jwk: {}", cause, cause.getMessage()); } - public static OpenSearchException createTransportClientNoLongerSupportedException() { - return new OpenSearchException("Transport client authentication no longer supported."); + public static OpenSearchException clusterWrongNodeCertConfigException(String sslPrincipal) { + return new OpenSearchException( + "Node presenting certificate with SSL Principal {" + + sslPrincipal + + "} could" + + " not securely connect to the cluster. Please ensure the principal is correct and present in the" + + " nodes_dn list." + ); } } diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index 5845c63672..18c0c21282 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -290,7 +290,7 @@ protected void messageReceivedDecorate( || HeaderHelper.isTrustedClusterRequest(getThreadContext()) || HeaderHelper.isExtensionRequest(getThreadContext()))) { // CS-ENFORCE-SINGLE - final OpenSearchException exception = ExceptionUtils.createTransportClientNoLongerSupportedException(); + final OpenSearchException exception = ExceptionUtils.clusterWrongNodeCertConfigException(principal); log.error(exception.toString()); transportChannel.sendResponse(exception); return; diff --git a/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java b/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java index 32ab78dbdf..d6a427e581 100644 --- a/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java +++ b/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java @@ -1355,7 +1355,14 @@ public void testCcsWithDiffCertsWithNoNodesDnUpdate() throws Exception { String uri = "cross_cluster_two:twitter/_search?pretty"; HttpResponse ccs = rh1.executeGetRequest(uri, encodeBasicHeader("twitter", "nagilum")); assertThat(ccs.getStatusCode(), equalTo(HttpStatus.SC_INTERNAL_SERVER_ERROR)); - assertThat(ccs.getBody(), containsString("Transport client authentication no longer supported")); + assertThat( + ccs.getBody(), + containsString( + "Node presenting certificate with SSL Principal " + + "{CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE} could not securely connect to the cluster. Please" + + " ensure the principal is correct and present in the nodes_dn list." + ) + ); } @Test From f81faed831eee4acc289eb6b4b61b754405d011a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:55:27 -0400 Subject: [PATCH 51/62] Bump com.google.errorprone:error_prone_annotations from 2.33.0 to 2.34.0 (#4822) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 57a79ae3cd..dde93178d1 100644 --- a/build.gradle +++ b/build.gradle @@ -495,7 +495,7 @@ configurations { // For integrationTest force "org.apache.httpcomponents:httpclient:4.5.14" force "org.apache.httpcomponents:httpcore:4.4.16" - force "com.google.errorprone:error_prone_annotations:2.33.0" + force "com.google.errorprone:error_prone_annotations:2.34.0" force "org.checkerframework:checker-qual:3.48.1" force "ch.qos.logback:logback-classic:1.5.10" force "commons-io:commons-io:2.17.0" @@ -604,7 +604,7 @@ dependencies { runtimeOnly 'com.eclipsesource.minimal-json:minimal-json:0.9.5' runtimeOnly 'commons-codec:commons-codec:1.17.1' runtimeOnly 'org.cryptacular:cryptacular:1.2.7' - compileOnly 'com.google.errorprone:error_prone_annotations:2.33.0' + compileOnly 'com.google.errorprone:error_prone_annotations:2.34.0' runtimeOnly 'com.sun.istack:istack-commons-runtime:4.2.0' runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' runtimeOnly 'org.ow2.asm:asm:9.7.1' From c11a8911905afbe0a1a743c87a84fb0852d35284 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:55:54 -0400 Subject: [PATCH 52/62] Bump org.passay:passay from 1.6.5 to 1.6.6 (#4821) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dde93178d1..75fd1fdb92 100644 --- a/build.gradle +++ b/build.gradle @@ -594,7 +594,7 @@ dependencies { implementation 'org.apache.commons:commons-collections4:4.4' //Password generation - implementation 'org.passay:passay:1.6.5' + implementation 'org.passay:passay:1.6.6' implementation "org.apache.kafka:kafka-clients:${kafka_version}" From 1b207a8332d084420475c6912310cbf825c72495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:44:05 -0400 Subject: [PATCH 53/62] Bump ch.qos.logback:logback-classic from 1.5.10 to 1.5.11 (#4823) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 75fd1fdb92..db494876ca 100644 --- a/build.gradle +++ b/build.gradle @@ -497,7 +497,7 @@ configurations { force "org.apache.httpcomponents:httpcore:4.4.16" force "com.google.errorprone:error_prone_annotations:2.34.0" force "org.checkerframework:checker-qual:3.48.1" - force "ch.qos.logback:logback-classic:1.5.10" + force "ch.qos.logback:logback-classic:1.5.11" force "commons-io:commons-io:2.17.0" } } From 8d7259db2417727628ffa091189c70bad4d5690a Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 21 Oct 2024 12:27:29 -0400 Subject: [PATCH 54/62] Fix issue in HashingStoredFieldVisitor with stored fields (#4826) Signed-off-by: Craig Perkins --- .../security/StoredFieldsTests.java | 110 ++++++++++++++++++ .../configuration/DlsFlsFilterLeafReader.java | 18 ++- 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java diff --git a/src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java b/src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java new file mode 100644 index 0000000000..9bcc0c5526 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java @@ -0,0 +1,110 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.client.Client; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class StoredFieldsTests { + static final TestSecurityConfig.User TEST_USER_MASKED_FIELDS = new TestSecurityConfig.User("test_user_masked_fields").roles( + new TestSecurityConfig.Role("role_masked_fields").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .maskedFields("restricted") + .on("test_index") + ); + + static final TestSecurityConfig.User TEST_USER_FLS = new TestSecurityConfig.User("test_user_fls").roles( + new TestSecurityConfig.Role("role_fls").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .fls("~restricted") + .on("test_index") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(TEST_USER_MASKED_FIELDS, TEST_USER_FLS) + .build(); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + CreateIndexResponse r = client.admin() + .indices() + .prepareCreate("test_index") + .setMapping("raw", "type=keyword,store=true", "restricted", "type=keyword,store=true") + .get(); + + client.prepareIndex("test_index").setRefreshPolicy(IMMEDIATE).setSource("raw", "hello", "restricted", "boo!").get(); + } + } + + @Test + public void testStoredWithWithApplicableMaskedFieldRestrictions() { + try (TestRestClient client = cluster.getRestClient(TEST_USER_MASKED_FIELDS)) { + TestRestClient.HttpResponse normalSearchResponse = client.get("test_index/_search"); + Assert.assertFalse(normalSearchResponse.getBody().contains("boo!")); + + TestRestClient.HttpResponse fieldSearchResponse = client.postJson("test_index/_search", """ + { + "stored_fields": [ + "raw", + "restricted" + ] + } + """); + fieldSearchResponse.assertStatusCode(HttpStatus.SC_OK); + Assert.assertTrue(fieldSearchResponse.getBody().contains("raw")); + Assert.assertTrue(fieldSearchResponse.getBody().contains("hello")); + Assert.assertTrue(fieldSearchResponse.getBody().contains("restricted")); + Assert.assertFalse(fieldSearchResponse.getBody().contains("boo!")); + } + } + + @Test + public void testStoredWithWithApplicableFlsRestrictions() { + try (TestRestClient client = cluster.getRestClient(TEST_USER_FLS)) { + TestRestClient.HttpResponse normalSearchResponse = client.get("test_index/_search"); + Assert.assertFalse(normalSearchResponse.getBody().contains("boo!")); + + TestRestClient.HttpResponse fieldSearchResponse = client.postJson("test_index/_search", """ + { + "stored_fields": [ + "raw", + "restricted" + ] + } + """); + fieldSearchResponse.assertStatusCode(HttpStatus.SC_OK); + Assert.assertTrue(fieldSearchResponse.getBody().contains("raw")); + Assert.assertTrue(fieldSearchResponse.getBody().contains("hello")); + Assert.assertFalse(fieldSearchResponse.getBody().contains("restricted")); + Assert.assertFalse(fieldSearchResponse.getBody().contains("boo!")); + } + } + +} diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java b/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java index b09745727f..b85542393b 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java @@ -663,11 +663,20 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO } delegate.binaryField(fieldInfo, Utils.jsonMapToByteArray(filteredSource)); - } else { + } else if (shouldInclude(fieldInfo.name)) { delegate.binaryField(fieldInfo, value); } } + private boolean shouldInclude(String field) { + if (excludesSet != null && !excludesSet.isEmpty()) { + return !excludesSet.contains(field); + } else if (includesSet != null && !includesSet.isEmpty()) { + return includesSet.contains(field); + } + return true; + } + @Override public Status needsField(final FieldInfo fieldInfo) throws IOException { return isFls(fieldInfo.name) ? delegate.needsField(fieldInfo) : Status.NO; @@ -733,7 +742,12 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO final XContentBuilder xBuilder = XContentBuilder.builder(bytesRefTuple.v1().xContent()).map(filteredSource); delegate.binaryField(fieldInfo, BytesReference.toBytes(BytesReference.bytes(xBuilder))); } else { - delegate.binaryField(fieldInfo, value); + final MaskedField mf = maskedFieldsMap.getMaskedField(fieldInfo.name).orElse(null); + if (mf != null) { + delegate.binaryField(fieldInfo, mf.mask(value)); + } else { + delegate.binaryField(fieldInfo, value); + } } } From 3edfac840ea76752388301107984dc456e7122a3 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Mon, 21 Oct 2024 19:50:07 +0200 Subject: [PATCH 55/62] Bump gradle to 8.10.2 (#4828) Signed-off-by: Andrey Pleskach --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2b189974c2..fb602ee2af 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 811f26de2444b6db41309d8d1c5bcc5debe6ec0e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 21 Oct 2024 13:51:02 -0400 Subject: [PATCH 56/62] Ensure that dual mode enabled flag from cluster settings can get propagated to core (#4820) Signed-off-by: Craig Perkins --- .../EncryptionInTransitMigrationTests.java | 70 +++++++++++++++++++ .../test/framework/cluster/LocalCluster.java | 46 +++++++++++- .../cluster/LocalOpenSearchCluster.java | 12 +++- ...inimumSecuritySettingsSupplierFactory.java | 12 ++++ .../security/OpenSearchSecurityPlugin.java | 4 +- .../ssl/OpenSearchSecureSettingsFactory.java | 16 ++++- .../ssl/OpenSearchSecuritySSLPlugin.java | 4 +- 7 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java diff --git a/src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java b/src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java new file mode 100644 index 0000000000..58eb7218e6 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +/** + * Test related to SSL-only mode of security plugin. In this mode, the security plugin is responsible only for TLS/SSL encryption. + * Therefore, the plugin does not perform authentication and authorization. Moreover, the REST resources (e.g. /_plugins/_security/whoami, + * /_plugins/_security/authinfo, etc.) provided by the plugin are not available. + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class EncryptionInTransitMigrationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) + .anonymousAuth(false) + .loadConfigurationIntoIndex(false) + .nodeSettings(Map.of(ConfigConstants.SECURITY_SSL_ONLY, true)) + .sslOnly(true) + .nodeSpecificSettings(0, Map.of(ConfigConstants.SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED, true)) + .nodeSpecificSettings(1, Map.of(ConfigConstants.SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED, true)) + .extectedNodeStartupCount(2) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .build(); + + @Test + public void shouldOnlyConnectWithThirdNodeAfterDynamicDualModeChange() { + try (TestRestClient client = cluster.getRestClient()) { + TestRestClient.HttpResponse response = client.get("_cat/nodes"); + response.assertStatusCode(200); + + String[] lines = response.getBody().split("\n"); + assertEquals("Expected 2 nodes in the initial response", 2, lines.length); + + String settingsJson = "{\"persistent\": {\"plugins.security_config.ssl_dual_mode_enabled\": false}}"; + TestRestClient.HttpResponse settingsResponse = client.putJson("_cluster/settings", settingsJson); + settingsResponse.assertStatusCode(200); + + await().atMost(10, SECONDS).pollInterval(1, SECONDS).until(() -> { + TestRestClient.HttpResponse secondResponse = client.get("_cat/nodes"); + String[] secondLines = secondResponse.getBody().split("\n"); + return secondLines.length == 3; + }); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 894bb5baa9..d2c53c1de7 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -85,7 +85,9 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope private final List> plugins; private final ClusterManager clusterManager; private final TestSecurityConfig testSecurityConfig; + private Map nodeSpecificOverride; private Settings nodeOverride; + private Integer expectedNodeStartupCount; private final String clusterName; private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; private final TestCertificates testCertificates; @@ -100,6 +102,7 @@ private LocalCluster( String clusterName, TestSecurityConfig testSgConfig, boolean sslOnly, + Map nodeSpecificOverride, Settings nodeOverride, ClusterManager clusterManager, List> plugins, @@ -108,13 +111,15 @@ private LocalCluster( Map remotes, List testIndices, boolean loadConfigurationIntoIndex, - String defaultConfigurationInitDirectory + String defaultConfigurationInitDirectory, + Integer expectedNodeStartupCount ) { this.plugins = plugins; this.testCertificates = testCertificates; this.clusterManager = clusterManager; this.testSecurityConfig = testSgConfig; this.sslOnly = sslOnly; + this.nodeSpecificOverride = nodeSpecificOverride; this.nodeOverride = nodeOverride; this.clusterName = clusterName; this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); @@ -125,6 +130,7 @@ private LocalCluster( if (StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); } + this.expectedNodeStartupCount = expectedNodeStartupCount; } public String getSnapshotDirPath() { @@ -232,6 +238,7 @@ private void start() { try { NodeSettingsSupplier nodeSettingsSupplier = minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings( sslOnly, + nodeSpecificOverride, nodeOverride ); localOpenSearchCluster = new LocalOpenSearchCluster( @@ -239,7 +246,8 @@ private void start() { clusterManager, nodeSettingsSupplier, plugins, - testCertificates + testCertificates, + expectedNodeStartupCount ); localOpenSearchCluster.start(); @@ -312,8 +320,10 @@ public CertificateData getAdminCertificate() { public static class Builder { private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); + private final Map nodeSpecificOverrideSettingsBuilder = new HashMap<>(); private boolean sslOnly = false; + private Integer expectedNodeStartupCount; private final List> plugins = new ArrayList<>(); private Map remoteClusters = new HashMap<>(); private List clusterDependencies = new ArrayList<>(); @@ -365,6 +375,11 @@ public Builder sslOnly(boolean sslOnly) { return this; } + public Builder extectedNodeStartupCount(int expectedNodeStartupCount) { + this.expectedNodeStartupCount = expectedNodeStartupCount; + return this; + } + public Builder nodeSettings(Map settings) { settings.forEach((key, value) -> { if (value instanceof List) { @@ -378,6 +393,25 @@ public Builder nodeSettings(Map settings) { return this; } + public Builder nodeSpecificSettings(int nodeNumber, Map settings) { + if (!nodeSpecificOverrideSettingsBuilder.containsKey(nodeNumber)) { + Settings.Builder builderCopy = Settings.builder(); + builderCopy.put(nodeOverrideSettingsBuilder.build()); + nodeSpecificOverrideSettingsBuilder.put(nodeNumber, builderCopy); + } + Settings.Builder nodeSettingsBuilder = nodeSpecificOverrideSettingsBuilder.get(nodeNumber); + settings.forEach((key, value) -> { + if (value instanceof List) { + List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + nodeSettingsBuilder.putList(key, values); + } else { + nodeSettingsBuilder.put(key, String.valueOf(value)); + } + }); + + return this; + } + /** * Adds additional plugins to the cluster */ @@ -512,10 +546,15 @@ public LocalCluster build() { } clusterName += "_" + num.incrementAndGet(); Settings settings = nodeOverrideSettingsBuilder.build(); + Map nodeSpecificSettings = new HashMap<>(); + for (Map.Entry entry : nodeSpecificOverrideSettingsBuilder.entrySet()) { + nodeSpecificSettings.put(entry.getKey(), entry.getValue().build()); + } return new LocalCluster( clusterName, testSecurityConfig, sslOnly, + nodeSpecificSettings, settings, clusterManager, plugins, @@ -524,7 +563,8 @@ public LocalCluster build() { remoteClusters, testIndices, loadConfigurationIntoIndex, - defaultConfigurationInitDirectory + defaultConfigurationInitDirectory, + expectedNodeStartupCount ); } catch (Exception e) { log.error("Failed to build LocalCluster", e); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 96da63d9fb..8570c3d398 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -97,6 +97,7 @@ public class LocalOpenSearchCluster { private final List> additionalPlugins; private final List nodes = new ArrayList<>(); private final TestCertificates testCertificates; + private final Integer expectedNodeStartupCount; private File clusterHomeDir; private List seedHosts; @@ -112,13 +113,15 @@ public LocalOpenSearchCluster( ClusterManager clusterManager, NodeSettingsSupplier nodeSettingsSupplier, List> additionalPlugins, - TestCertificates testCertificates + TestCertificates testCertificates, + Integer expectedNodeStartCount ) { this.clusterName = clusterName; this.clusterManager = clusterManager; this.nodeSettingsSupplier = nodeSettingsSupplier; this.additionalPlugins = additionalPlugins; this.testCertificates = testCertificates; + this.expectedNodeStartupCount = expectedNodeStartCount; try { createClusterDirectory(clusterName); } catch (IOException e) { @@ -198,7 +201,12 @@ public void start() throws Exception { log.info("Startup finished. Waiting for GREEN"); - waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), nodes.size()); + int expectedCount = nodes.size(); + if (expectedNodeStartupCount != null) { + expectedCount = expectedNodeStartupCount; + } + + waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), expectedCount); log.info("Started: {}", this); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java index 4ad5f8420e..34a105ea39 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -28,6 +28,8 @@ package org.opensearch.test.framework.cluster; +import java.util.Map; + import org.opensearch.common.settings.Settings; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.certificate.TestCertificates; @@ -51,6 +53,16 @@ public NodeSettingsSupplier minimumOpenSearchSettings(boolean sslOnly, Settings return i -> minimumOpenSearchSettingsBuilder(i, sslOnly).put(other).build(); } + public NodeSettingsSupplier minimumOpenSearchSettings(boolean sslOnly, Map nodeOverride, Settings other) { + return i -> { + Settings override = nodeOverride.get(i); + if (override != null) { + return minimumOpenSearchSettingsBuilder(i, sslOnly).put(other).put(override).build(); + } + return minimumOpenSearchSettingsBuilder(i, sslOnly).put(other).build(); + }; + } + private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslOnly) { Settings.Builder builder = Settings.builder(); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 594af15f03..15d5e4c286 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2166,7 +2166,9 @@ public PluginSubject getPluginSubject(Plugin plugin) { @Override public Optional getSecureSettingFactory(Settings settings) { - return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler)); + return Optional.of( + new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler, SSLConfig) + ); } @SuppressWarnings("removal") diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java index 5351eea57e..9d482b18a8 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -27,6 +27,7 @@ import org.opensearch.security.filter.SecurityRestFilter; import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; +import org.opensearch.security.ssl.transport.SSLConfig; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.Transport; import org.opensearch.transport.TransportAdapterProvider; @@ -38,17 +39,20 @@ public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { private final SecurityKeyStore sks; private final SslExceptionHandler sslExceptionHandler; private final SecurityRestFilter restFilter; + private final SSLConfig sslConfig; public OpenSearchSecureSettingsFactory( ThreadPool threadPool, SecurityKeyStore sks, SslExceptionHandler sslExceptionHandler, - SecurityRestFilter restFilter + SecurityRestFilter restFilter, + SSLConfig sslConfig ) { this.threadPool = threadPool; this.sks = sks; this.sslExceptionHandler = sslExceptionHandler; this.restFilter = restFilter; + this.sslConfig = sslConfig; } @Override @@ -64,6 +68,16 @@ public void onError(Throwable t) { }); } + @Override + public Optional parameters(Settings settings) { + return Optional.of(new SecureTransportParameters() { + @Override + public boolean dualModeEnabled() { + return sslConfig.isDualModeEnabled(); + } + }); + } + @Override public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { return Optional.of(sks.createServerTransportSSLEngine()); diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index e6a1b47888..c16706c870 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -674,7 +674,9 @@ public List getSettingsFilter() { @Override public Optional getSecureSettingFactory(Settings settings) { - return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler)); + return Optional.of( + new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) + ); } protected Settings migrateSettings(Settings settings) { From db6e7dc1664694497d80ee0b8b727350c1086b47 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Tue, 22 Oct 2024 18:25:26 +0200 Subject: [PATCH 57/62] Refactor SSL Configuration (#4671) --- build.gradle | 3 + checkstyle/checkstyle.xml | 7 + .../security/OpenSearchSecurityPlugin.java | 15 +- .../dlic/rest/api/SecurityRestApiActions.java | 12 +- .../rest/api/SecuritySSLCertsApiAction.java | 89 ++-- .../TransportCertificatesInfoNodesAction.java | 50 +- .../ssl/OpenSearchSecureSettingsFactory.java | 13 +- .../ssl/OpenSearchSecuritySSLPlugin.java | 14 +- .../security/ssl/SecureSSLSettings.java | 2 +- .../security/ssl/SslConfiguration.java | 148 ++++++ .../security/ssl/SslContextHandler.java | 165 +++++++ .../security/ssl/SslSettingsManager.java | 384 +++++++++++++++ .../security/ssl/config/CertType.java | 33 ++ .../security/ssl/config/Certificate.java | 188 +++++++ .../ssl/config/KeyStoreConfiguration.java | 201 ++++++++ .../security/ssl/config/KeyStoreUtils.java | 218 ++++++++ .../ssl/config/SslCertificatesLoader.java | 171 +++++++ .../security/ssl/config/SslParameters.java | 197 ++++++++ .../ssl/config/TrustStoreConfiguration.java | 185 +++++++ .../ssl/rest/SecuritySSLInfoAction.java | 43 +- .../security/ssl/util/SSLConfigConstants.java | 64 ++- .../security/ssl/CertificatesRule.java | 318 ++++++++++++ .../security/ssl/CertificatesUtils.java | 43 ++ .../ssl/OpenSearchSecuritySSLPluginTest.java | 20 +- .../org/opensearch/security/ssl/SSLTest.java | 2 +- .../SecuritySSLReloadCertsActionTests.java | 9 +- .../security/ssl/SslContextHandlerTest.java | 266 ++++++++++ .../security/ssl/SslSettingsManagerTest.java | 464 ++++++++++++++++++ .../security/ssl/config/CertificateTest.java | 38 ++ .../config/JdkSslCertificatesLoaderTest.java | 318 ++++++++++++ .../config/PemSslCertificatesLoaderTest.java | 174 +++++++ .../ssl/config/SslCertificatesLoaderTest.java | 66 +++ .../ssl/config/SslParametersTest.java | 90 ++++ 33 files changed, 3887 insertions(+), 123 deletions(-) create mode 100644 src/main/java/org/opensearch/security/ssl/SslConfiguration.java create mode 100644 src/main/java/org/opensearch/security/ssl/SslContextHandler.java create mode 100644 src/main/java/org/opensearch/security/ssl/SslSettingsManager.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/CertType.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/Certificate.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/SslParameters.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java create mode 100644 src/test/java/org/opensearch/security/ssl/CertificatesRule.java create mode 100644 src/test/java/org/opensearch/security/ssl/CertificatesUtils.java create mode 100644 src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/CertificateTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java diff --git a/build.gradle b/build.gradle index db494876ca..887966e6c7 100644 --- a/build.gradle +++ b/build.gradle @@ -686,6 +686,9 @@ dependencies { testImplementation('org.awaitility:awaitility:4.2.2') { exclude(group: 'org.hamcrest', module: 'hamcrest') } + testImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" + testImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" + // Only osx-x86_64, osx-aarch_64, linux-x86_64, linux-aarch_64, windows-x86_64 are available if (osdetector.classifier in ["osx-x86_64", "osx-aarch_64", "linux-x86_64", "linux-aarch_64", "windows-x86_64"]) { testImplementation "io.netty:netty-tcnative-classes:2.0.61.Final" diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index 04a36c49c1..a9c1a8f765 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -43,6 +43,13 @@ + + + + + + + diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 15d5e4c286..9076b1da1f 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -648,7 +648,7 @@ public List getRestHandlers( evaluator, threadPool, Objects.requireNonNull(auditLog), - sks, + sslSettingsManager, Objects.requireNonNull(userService), sslCertReloadEnabled, passwordHasher @@ -1207,9 +1207,8 @@ public Collection createComponents( components.add(userService); components.add(passwordHasher); - if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { - components.add(sks); - } + components.add(sslSettingsManager); + final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); final var useClusterState = useClusterStateToInitSecurityConfig(settings); if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) { @@ -2167,7 +2166,13 @@ public PluginSubject getPluginSubject(Plugin plugin) { @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of( - new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler, SSLConfig) + new OpenSearchSecureSettingsFactory( + threadPool, + sslSettingsManager, + evaluateSslExceptionHandler(), + securityRestHandler, + SSLConfig + ) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 3963e443d8..c28a1bdc1d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -25,7 +25,7 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.privileges.PrivilegesEvaluator; -import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.SslSettingsManager; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.user.UserService; import org.opensearch.threadpool.ThreadPool; @@ -46,7 +46,7 @@ public static Collection getHandler( final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final AuditLog auditLog, - final SecurityKeyStore securityKeyStore, + final SslSettingsManager sslSettingsManager, final UserService userService, final boolean certificatesReloadEnabled, final PasswordHasher passwordHasher @@ -97,7 +97,13 @@ public static Collection getHandler( new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies), + new SecuritySSLCertsApiAction( + clusterService, + threadPool, + sslSettingsManager, + certificatesReloadEnabled, + securityApiDependencies + ), new CertificatesApiAction(clusterService, threadPool, securityApiDependencies) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java index 7f4bff50ab..5233149c66 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java @@ -12,11 +12,10 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -31,8 +30,10 @@ import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.ssl.SslContextHandler; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.Certificate; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; @@ -62,23 +63,20 @@ public class SecuritySSLCertsApiAction extends AbstractApiAction { ) ); - private final SecurityKeyStore securityKeyStore; + private final SslSettingsManager sslSettingsManager; private final boolean certificatesReloadEnabled; - private final boolean httpsEnabled; - public SecuritySSLCertsApiAction( final ClusterService clusterService, final ThreadPool threadPool, - final SecurityKeyStore securityKeyStore, + final SslSettingsManager sslSettingsManager, final boolean certificatesReloadEnabled, final SecurityApiDependencies securityApiDependencies ) { super(Endpoint.SSL, clusterService, threadPool, securityApiDependencies); - this.securityKeyStore = securityKeyStore; + this.sslSettingsManager = sslSettingsManager; this.certificatesReloadEnabled = certificatesReloadEnabled; - this.httpsEnabled = securityApiDependencies.settings().getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); this.requestHandlersBuilder.configureRequestHandlers(this::securitySSLCertsRequestHandlers); } @@ -108,10 +106,10 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild .verifyAccessForAllMethods() .override( Method.GET, - (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> loadCertificates(channel, keyStore)) + (channel, request, client) -> withSecurityKeyStore().valid(ignore -> loadCertificates(channel)) .error((status, toXContent) -> response(channel, status, toXContent)) ) - .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> { + .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(ignore -> { if (!certificatesReloadEnabled) { badRequest( channel, @@ -123,7 +121,7 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild ) ); } else { - reloadCertificates(channel, request, keyStore); + reloadCertificates(channel, request); } }).error((status, toXContent) -> response(channel, status, toXContent))); } @@ -138,65 +136,70 @@ boolean accessHandler(final RestRequest request) { } } - ValidationResult withSecurityKeyStore() { - if (securityKeyStore == null) { + ValidationResult withSecurityKeyStore() { + if (sslSettingsManager == null) { return ValidationResult.error(RestStatus.OK, badRequestMessage("keystore is not initialized")); } - return ValidationResult.success(securityKeyStore); + return ValidationResult.success(sslSettingsManager); } - protected void loadCertificates(final RestChannel channel, final SecurityKeyStore keyStore) throws IOException { + protected void loadCertificates(final RestChannel channel) throws IOException { ok( channel, (builder, params) -> builder.startObject() - .field("http_certificates_list", httpsEnabled ? generateCertDetailList(keyStore.getHttpCerts()) : null) - .field("transport_certificates_list", generateCertDetailList(keyStore.getTransportCerts())) + .field( + "http_certificates_list", + generateCertDetailList( + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::keyMaterialCertificates).orElse(null) + ) + ) + .field( + "transport_certificates_list", + generateCertDetailList( + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::keyMaterialCertificates) + .orElse(null) + ) + ) .endObject() ); } - private List> generateCertDetailList(final X509Certificate[] certs) { + private List> generateCertDetailList(final Stream certs) { if (certs == null) { return null; } - return Arrays.stream(certs).map(cert -> { - final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; - final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; - - final String san = securityKeyStore.getSubjectAlternativeNames(cert); - - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; - return ImmutableMap.of( + return certs.map( + c -> ImmutableMap.of( "issuer_dn", - issuerDn, + c.issuer(), "subject_dn", - subjectDn, + c.subject(), "san", - san, + c.subjectAlternativeNames(), "not_before", - notBefore, + c.notBefore(), "not_after", - notAfter - ); - }).collect(Collectors.toList()); + c.notAfter() + ) + ).collect(Collectors.toList()); } - protected void reloadCertificates(final RestChannel channel, final RestRequest request, final SecurityKeyStore keyStore) - throws IOException { + protected void reloadCertificates(final RestChannel channel, final RestRequest request) throws IOException { final String certType = request.param("certType").toLowerCase().trim(); try { switch (certType) { case "http": - if (!httpsEnabled) { + if (sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()) { + sslSettingsManager.reloadSslContext(CertType.HTTP); + ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); + } else { badRequest(channel, "SSL for HTTP is disabled"); - return; } - keyStore.initHttpSSLConfig(); - ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); break; case "transport": - keyStore.initTransportSSLConfig(); + sslSettingsManager.reloadSslContext(CertType.TRANSPORT); + sslSettingsManager.reloadSslContext(CertType.TRANSPORT_CLIENT); ok(channel, (builder, params) -> builder.startObject().field("message", "updated transport certs").endObject()); break; default: diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java index 681c2c01eb..39edfd570f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java @@ -12,22 +12,22 @@ package org.opensearch.security.dlic.rest.api.ssl; import java.io.IOException; -import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; - -import com.google.common.collect.ImmutableList; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.opensearch.action.FailedNodeException; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.nodes.TransportNodesAction; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.ssl.DefaultSecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.ssl.SslContextHandler; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.Certificate; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportService; @@ -38,18 +38,15 @@ public class TransportCertificatesInfoNodesAction extends TransportNodesAction< TransportCertificatesInfoNodesAction.NodeRequest, CertificatesNodesResponse.CertificatesNodeResponse> { - private final DefaultSecurityKeyStore securityKeyStore; - - private final boolean httpsEnabled; + private final SslSettingsManager sslSettingsManager; @Inject public TransportCertificatesInfoNodesAction( - final Settings settings, final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, final ActionFilters actionFilters, - final DefaultSecurityKeyStore securityKeyStore + final SslSettingsManager sslSettingsManager ) { super( CertificatesActionType.NAME, @@ -62,8 +59,7 @@ public TransportCertificatesInfoNodesAction( ThreadPool.Names.GENERIC, CertificatesNodesResponse.CertificatesNodeResponse.class ); - this.httpsEnabled = settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); - this.securityKeyStore = securityKeyStore; + this.sslSettingsManager = sslSettingsManager; } @Override @@ -89,12 +85,6 @@ protected CertificatesNodesResponse.CertificatesNodeResponse newNodeResponse(fin protected CertificatesNodesResponse.CertificatesNodeResponse nodeOperation(final NodeRequest request) { final var sslCertRequest = request.sslCertsInfoNodesRequest; - if (securityKeyStore == null) { - return new CertificatesNodesResponse.CertificatesNodeResponse( - clusterService.localNode(), - new IllegalStateException("keystore is not initialized") - ); - } try { return new CertificatesNodesResponse.CertificatesNodeResponse( clusterService.localNode(), @@ -109,23 +99,27 @@ protected CertificatesInfo loadCertificates(final CertificateType certificateTyp var httpCertificates = List.of(); var transportsCertificates = List.of(); if (CertificateType.isHttp(certificateType)) { - httpCertificates = httpsEnabled ? certificatesDetails(securityKeyStore.getHttpCerts()) : List.of(); + httpCertificates = sslSettingsManager.sslContextHandler(CertType.HTTP) + .map(SslContextHandler::keyMaterialCertificates) + .map(this::certificatesDetails) + .orElse(List.of()); } if (CertificateType.isTransport(certificateType)) { - transportsCertificates = certificatesDetails(securityKeyStore.getTransportCerts()); + transportsCertificates = sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::keyMaterialCertificates) + .map(this::certificatesDetails) + .orElse(List.of()); } return new CertificatesInfo(Map.of(CertificateType.HTTP, httpCertificates, CertificateType.TRANSPORT, transportsCertificates)); } - private List certificatesDetails(final X509Certificate[] certs) { - if (certs == null) { + private List certificatesDetails(final Stream certificateStream) { + if (certificateStream == null) { return null; } - final var certificates = ImmutableList.builder(); - for (final var c : certs) { - certificates.add(CertificateInfo.from(c, securityKeyStore.getSubjectAlternativeNames(c))); - } - return certificates.build(); + return certificateStream.map( + c -> new CertificateInfo(c.subject(), c.subjectAlternativeNames(), c.issuer(), c.notAfter(), c.notBefore()) + ).collect(Collectors.toList()); } public static class NodeRequest extends TransportRequest { diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java index 9d482b18a8..43f6cc4f29 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -25,6 +25,7 @@ import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.plugins.TransportExceptionHandler; import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.ssl.config.CertType; import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; import org.opensearch.security.ssl.transport.SSLConfig; @@ -36,20 +37,20 @@ public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { private final ThreadPool threadPool; - private final SecurityKeyStore sks; + private final SslSettingsManager sslSettingsManager; private final SslExceptionHandler sslExceptionHandler; private final SecurityRestFilter restFilter; private final SSLConfig sslConfig; public OpenSearchSecureSettingsFactory( ThreadPool threadPool, - SecurityKeyStore sks, + SslSettingsManager sslSettingsManager, SslExceptionHandler sslExceptionHandler, SecurityRestFilter restFilter, SSLConfig sslConfig ) { this.threadPool = threadPool; - this.sks = sks; + this.sslSettingsManager = sslSettingsManager; this.sslExceptionHandler = sslExceptionHandler; this.restFilter = restFilter; this.sslConfig = sslConfig; @@ -80,12 +81,12 @@ public boolean dualModeEnabled() { @Override public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { - return Optional.of(sks.createServerTransportSSLEngine()); + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::createSSLEngine); } @Override public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { - return Optional.of(sks.createClientTransportSSLEngine(hostname, port)); + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).map(c -> c.createSSLEngine(hostname, port)); } }); } @@ -142,7 +143,7 @@ public void onError(Throwable t) { @Override public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { - return Optional.of(sks.createHTTPSSLEngine()); + return sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::createSSLEngine); } }); } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index c16706c870..25c55f3cbb 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -126,7 +126,7 @@ public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPl protected final Settings settings; protected volatile SecurityRestFilter securityRestHandler; protected final SharedGroupFactory sharedGroupFactory; - protected final SecurityKeyStore sks; + protected final SslSettingsManager sslSettingsManager; protected PrincipalExtractor principalExtractor; protected final Path configPath; private final static SslExceptionHandler NOOP_SSL_EXCEPTION_HANDLER = new SslExceptionHandler() { @@ -144,7 +144,7 @@ protected OpenSearchSecuritySSLPlugin(final Settings settings, final Path config this.httpSSLEnabled = false; this.transportSSLEnabled = false; this.extendedKeyUsageEnabled = false; - this.sks = null; + this.sslSettingsManager = null; this.configPath = null; SSLConfig = new SSLConfig(false, false); @@ -246,11 +246,7 @@ public Object run() { log.error("SSL not activated for http and/or transport."); } - if (ExternalSecurityKeyStore.hasExternalSslContext(settings)) { - this.sks = new ExternalSecurityKeyStore(settings); - } else { - this.sks = new DefaultSecurityKeyStore(settings, configPath); - } + this.sslSettingsManager = new SslSettingsManager(new Environment(settings, configPath)); } @Override @@ -311,7 +307,7 @@ public List getRestHandlers( final List handlers = new ArrayList(1); if (!client) { - handlers.add(new SecuritySSLInfoAction(settings, configPath, restController, sks, Objects.requireNonNull(principalExtractor))); + handlers.add(new SecuritySSLInfoAction(settings, configPath, sslSettingsManager, Objects.requireNonNull(principalExtractor))); } return handlers; @@ -675,7 +671,7 @@ public List getSettingsFilter() { @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of( - new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) + new OpenSearchSecureSettingsFactory(threadPool, sslSettingsManager, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) ); } diff --git a/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java b/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java index 171bb18bb5..5aad07fbdd 100644 --- a/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java +++ b/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java @@ -36,7 +36,7 @@ public final class SecureSSLSettings { private static final Logger LOG = LogManager.getLogger(SecureSSLSettings.class); - private static final String SECURE_SUFFIX = "_secure"; + public static final String SECURE_SUFFIX = "_secure"; private static final String PREFIX = "plugins.security.ssl"; private static final String HTTP_PREFIX = PREFIX + ".http"; private static final String TRANSPORT_PREFIX = PREFIX + ".transport"; diff --git a/src/main/java/org/opensearch/security/ssl/SslConfiguration.java b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java new file mode 100644 index 0000000000..2332867bd8 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java @@ -0,0 +1,148 @@ +/* + * 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.ssl; + +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.security.ssl.config.Certificate; +import org.opensearch.security.ssl.config.KeyStoreConfiguration; +import org.opensearch.security.ssl.config.SslParameters; +import org.opensearch.security.ssl.config.TrustStoreConfiguration; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + +public class SslConfiguration { + + private final static Logger LOGGER = LogManager.getLogger(SslConfiguration.class); + + private final SslParameters sslParameters; + + private final TrustStoreConfiguration trustStoreConfiguration; + + private final KeyStoreConfiguration keyStoreConfiguration; + + public SslConfiguration( + final SslParameters sslParameters, + final TrustStoreConfiguration trustStoreConfiguration, + final KeyStoreConfiguration keyStoreConfiguration + ) { + this.sslParameters = sslParameters; + this.trustStoreConfiguration = trustStoreConfiguration; + this.keyStoreConfiguration = keyStoreConfiguration; + } + + public List dependentFiles() { + return Stream.concat(keyStoreConfiguration.files().stream(), Stream.of(trustStoreConfiguration.file())) + .collect(Collectors.toList()); + } + + public List certificates() { + return Stream.concat(trustStoreConfiguration.loadCertificates().stream(), keyStoreConfiguration.loadCertificates().stream()) + .collect(Collectors.toList()); + } + + public SslParameters sslParameters() { + return sslParameters; + } + + @SuppressWarnings("removal") + SslContext buildServerSslContext(final boolean validateCertificates) { + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> SslContextBuilder.forServer( + keyStoreConfiguration.createKeyManagerFactory(validateCertificates) + ) + .sslProvider(sslParameters.provider()) + .clientAuth(sslParameters.clientAuth()) + .protocols(sslParameters.allowedProtocols().toArray(new String[0])) + // TODO we always add all HTTP 2 ciphers, while maybe it is better to set them differently + .ciphers( + Stream.concat( + Http2SecurityUtil.CIPHERS.stream(), + StreamSupport.stream(sslParameters.allowedCiphers().spliterator(), false) + ).collect(Collectors.toSet()), + SupportedCipherSuiteFilter.INSTANCE + ) + .sessionCacheSize(0) + .sessionTimeout(0) + .applicationProtocolConfig( + new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1 + ) + ) + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates)) + .build() + ); + } catch (PrivilegedActionException e) { + throw new OpenSearchException("Filed to build server SSL context", e); + } + } + + @SuppressWarnings("removal") + SslContext buildClientSslContext(final boolean validateCertificates) { + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> SslContextBuilder.forClient() + .sslProvider(sslParameters.provider()) + .protocols(sslParameters.allowedProtocols()) + .ciphers(sslParameters.allowedCiphers()) + .applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) + .sessionCacheSize(0) + .sessionTimeout(0) + .sslProvider(sslParameters.provider()) + .keyManager(keyStoreConfiguration.createKeyManagerFactory(validateCertificates)) + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates)) + .build() + ); + } catch (PrivilegedActionException e) { + throw new OpenSearchException("Filed to build client SSL context", e); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslConfiguration that = (SslConfiguration) o; + return Objects.equals(sslParameters, that.sslParameters) + && Objects.equals(trustStoreConfiguration, that.trustStoreConfiguration) + && Objects.equals(keyStoreConfiguration, that.keyStoreConfiguration); + } + + @Override + public int hashCode() { + return Objects.hash(sslParameters, trustStoreConfiguration, keyStoreConfiguration); + } +} diff --git a/src/main/java/org/opensearch/security/ssl/SslContextHandler.java b/src/main/java/org/opensearch/security/ssl/SslContextHandler.java new file mode 100644 index 0000000000..9fda1641af --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslContextHandler.java @@ -0,0 +1,165 @@ +/* + * 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.ssl; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLEngine; + +import org.opensearch.security.ssl.config.Certificate; +import org.opensearch.transport.NettyAllocator; + +import io.netty.handler.ssl.SslContext; + +public class SslContextHandler { + + private SslContext sslContext; + + private final SslConfiguration sslConfiguration; + + private final List loadedCertificates; + + public SslContextHandler(final SslConfiguration sslConfiguration) { + this(sslConfiguration, false); + } + + public SslContextHandler(final SslConfiguration sslConfiguration, final boolean client) { + this.sslContext = client ? sslConfiguration.buildClientSslContext(true) : sslConfiguration.buildServerSslContext(true); + this.sslConfiguration = sslConfiguration; + this.loadedCertificates = sslConfiguration.certificates(); + } + + public SSLEngine createSSLEngine() { + return sslContext.newEngine(NettyAllocator.getAllocator()); + } + + public SSLEngine createSSLEngine(final String hostname, final int port) { + return sslContext.newEngine(NettyAllocator.getAllocator(), hostname, port); + } + + public SslConfiguration sslConfiguration() { + return sslConfiguration; + } + + SslContext sslContext() { + return sslContext; + } + + public Stream keyMaterialCertificates() { + return keyMaterialCertificates(loadedCertificates); + } + + Stream keyMaterialCertificates(final List certificates) { + return certificates.stream().filter(Certificate::hasKey); + } + + void reloadSslContext() throws CertificateException { + final var newCertificates = sslConfiguration.certificates(); + + if (sameCertificates(newCertificates)) { + return; + } + validateNewCertificates(newCertificates); + invalidateSessions(); + if (sslContext.isClient()) { + sslContext = sslConfiguration.buildClientSslContext(false); + } else { + sslContext = sslConfiguration.buildServerSslContext(false); + } + loadedCertificates.clear(); + loadedCertificates.addAll(newCertificates); + } + + private boolean sameCertificates(final List newCertificates) { + final Set currentCertSignatureSet = keyMaterialCertificates().map(Certificate::x509Certificate) + .map(X509Certificate::getSignature) + .map(s -> new String(s, StandardCharsets.UTF_8)) + .collect(Collectors.toSet()); + final Set newCertSignatureSet = keyMaterialCertificates(newCertificates).map(Certificate::x509Certificate) + .map(X509Certificate::getSignature) + .map(s -> new String(s, StandardCharsets.UTF_8)) + .collect(Collectors.toSet()); + return currentCertSignatureSet.equals(newCertSignatureSet); + } + + private void validateSubjectDns(final List newCertificates) throws CertificateException { + final List currentSubjectDNs = keyMaterialCertificates().map(Certificate::subject).sorted().collect(Collectors.toList()); + final List newSubjectDNs = keyMaterialCertificates(newCertificates).map(Certificate::subject) + .sorted() + .collect(Collectors.toList()); + if (!currentSubjectDNs.equals(newSubjectDNs)) { + throw new CertificateException( + "New certificates do not have valid Subject DNs. Current Subject DNs " + + currentSubjectDNs + + " new Subject DNs " + + newSubjectDNs + ); + } + } + + private void validateIssuerDns(final List newCertificates) throws CertificateException { + final List currentIssuerDNs = keyMaterialCertificates().map(Certificate::issuer).sorted().collect(Collectors.toList()); + final List newIssuerDNs = keyMaterialCertificates(newCertificates).map(Certificate::issuer) + .sorted() + .collect(Collectors.toList()); + if (!currentIssuerDNs.equals(newIssuerDNs)) { + throw new CertificateException( + "New certificates do not have valid Issuer DNs. Current Issuer DNs: " + + currentIssuerDNs + + " new Issuer DNs: " + + newIssuerDNs + ); + } + } + + private void validateSans(final List newCertificates) throws CertificateException { + final List currentSans = keyMaterialCertificates().map(Certificate::subjectAlternativeNames) + .sorted() + .collect(Collectors.toList()); + final List newSans = keyMaterialCertificates(newCertificates).map(Certificate::subjectAlternativeNames) + .sorted() + .collect(Collectors.toList()); + if (!currentSans.equals(newSans)) { + throw new CertificateException( + "New certificates do not have valid SANs. Current SANs: " + currentSans + " new SANs: " + newSans + ); + } + } + + private void validateNewCertificates(final List newCertificates) throws CertificateException { + for (final var certificate : newCertificates) { + certificate.x509Certificate().checkValidity(); + } + validateSubjectDns(newCertificates); + validateIssuerDns(newCertificates); + validateSans(newCertificates); + } + + private void invalidateSessions() { + final var sessionContext = sslContext.sessionContext(); + if (sessionContext != null) { + for (final var sessionId : Collections.list(sessionContext.getIds())) { + final var session = sessionContext.getSession(sessionId); + if (session != null) { + session.invalidate(); + } + } + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java new file mode 100644 index 0000000000..381c510894 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java @@ -0,0 +1,384 @@ +/* + * 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.ssl; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import javax.crypto.Cipher; + +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.SslCertificatesLoader; +import org.opensearch.security.ssl.config.SslParameters; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.util.internal.PlatformDependent; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.CLIENT_AUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; + +public class SslSettingsManager { + + private final static Logger LOGGER = LogManager.getLogger(SslSettingsManager.class); + + private final Map sslSettingsContexts; + + public SslSettingsManager(final Environment environment) { + this.sslSettingsContexts = buildSslContexts(environment); + } + + public Optional sslConfiguration(final CertType certType) { + return Optional.ofNullable(sslSettingsContexts.get(certType)).map(SslContextHandler::sslConfiguration); + } + + public Optional sslContextHandler(final CertType sslConfigPrefix) { + return Optional.ofNullable(sslSettingsContexts.get(sslConfigPrefix)); + } + + private Map buildSslContexts(final Environment environment) { + final var contexts = new ImmutableMap.Builder(); + final var configurations = loadConfigurations(environment); + Optional.ofNullable(configurations.get(CertType.HTTP)) + .ifPresentOrElse( + sslConfiguration -> contexts.put(CertType.HTTP, new SslContextHandler(sslConfiguration)), + () -> LOGGER.warn("SSL Configuration for HTTP Layer hasn't been set") + ); + Optional.ofNullable(configurations.get(CertType.TRANSPORT)).ifPresentOrElse(sslConfiguration -> { + contexts.put(CertType.TRANSPORT, new SslContextHandler(sslConfiguration)); + final var transportClientConfiguration = Optional.ofNullable(configurations.get(CertType.TRANSPORT_CLIENT)) + .orElse(sslConfiguration); + contexts.put(CertType.TRANSPORT_CLIENT, new SslContextHandler(transportClientConfiguration, true)); + }, () -> LOGGER.warn("SSL Configuration for Transport Layer hasn't been set")); + return contexts.build(); + } + + public synchronized void reloadSslContext(final CertType certType) { + sslContextHandler(certType).ifPresentOrElse(sscContextHandler -> { + LOGGER.info("Reloading {} SSL context", certType.name()); + try { + sscContextHandler.reloadSslContext(); + } catch (CertificateException e) { + throw new OpenSearchException(e); + } + LOGGER.info("{} SSL context reloaded", certType.name()); + }, () -> LOGGER.error("Missing SSL Context for {}", certType.name())); + } + + private Map loadConfigurations(final Environment environment) { + final var settings = environment.settings(); + final var httpSettings = settings.getByPrefix(CertType.HTTP.sslConfigPrefix()); + final var transpotSettings = settings.getByPrefix(CertType.TRANSPORT.sslConfigPrefix()); + if (httpSettings.isEmpty() && transpotSettings.isEmpty()) { + throw new OpenSearchException("No SSL configuration found"); + } + jceWarnings(); + openSslWarnings(settings); + + final var httpEnabled = httpSettings.getAsBoolean(ENABLED, SECURITY_SSL_HTTP_ENABLED_DEFAULT); + final var transportEnabled = transpotSettings.getAsBoolean(ENABLED, SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT); + + final var configurationBuilder = ImmutableMap.builder(); + if (httpEnabled && !clientNode(settings)) { + validateHttpSettings(httpSettings); + final var httpSslParameters = SslParameters.loader(httpSettings).load(true); + final var httpTrustAndKeyStore = new SslCertificatesLoader(CertType.HTTP.sslConfigPrefix()).loadConfiguration(environment); + configurationBuilder.put( + CertType.HTTP, + new SslConfiguration(httpSslParameters, httpTrustAndKeyStore.v1(), httpTrustAndKeyStore.v2()) + ); + LOGGER.info("TLS HTTP Provider : {}", httpSslParameters.provider()); + LOGGER.info("Enabled TLS protocols for HTTP layer : {}", httpSslParameters.allowedProtocols()); + } + final var transportSslParameters = SslParameters.loader(transpotSettings).load(false); + if (transportEnabled) { + if (hasExtendedKeyUsageEnabled(transpotSettings)) { + validateTransportSettings(transpotSettings); + final var transportServerTrustAndKeyStore = new SslCertificatesLoader( + CertType.TRANSPORT.sslConfigPrefix(), + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + ).loadConfiguration(environment); + configurationBuilder.put( + CertType.TRANSPORT, + new SslConfiguration(transportSslParameters, transportServerTrustAndKeyStore.v1(), transportServerTrustAndKeyStore.v2()) + ); + final var transportClientTrustAndKeyStore = new SslCertificatesLoader( + CertType.TRANSPORT.sslConfigPrefix(), + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + ).loadConfiguration(environment); + configurationBuilder.put( + CertType.TRANSPORT_CLIENT, + new SslConfiguration(transportSslParameters, transportClientTrustAndKeyStore.v1(), transportClientTrustAndKeyStore.v2()) + ); + } else { + validateTransportSettings(transpotSettings); + final var transportTrustAndKeyStore = new SslCertificatesLoader(CertType.TRANSPORT.sslConfigPrefix()).loadConfiguration( + environment + ); + configurationBuilder.put( + CertType.TRANSPORT, + new SslConfiguration(transportSslParameters, transportTrustAndKeyStore.v1(), transportTrustAndKeyStore.v2()) + ); + } + LOGGER.info("TLS Transport Client Provider : {}", transportSslParameters.provider()); + LOGGER.info("TLS Transport Server Provider : {}", transportSslParameters.provider()); + LOGGER.info("Enabled TLS protocols for Transport layer : {}", transportSslParameters.allowedProtocols()); + } + return configurationBuilder.build(); + } + + private boolean clientNode(final Settings settings) { + return !"node".equals(settings.get(OpenSearchSecuritySSLPlugin.CLIENT_TYPE)); + } + + private void validateHttpSettings(final Settings httpSettings) { + if (httpSettings == null) return; + if (!httpSettings.getAsBoolean(ENABLED, SECURITY_SSL_HTTP_ENABLED_DEFAULT)) return; + + final var clientAuth = ClientAuth.valueOf(httpSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT)); + + if (hasPemStoreSettings(httpSettings)) { + if (!httpSettings.hasValue(PEM_CERT_FILEPATH) || !httpSettings.hasValue(PEM_KEY_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + + String.join(", ", SECURITY_SSL_HTTP_PEMCERT_FILEPATH, SECURITY_SSL_HTTP_PEMKEY_FILEPATH) + + " must be set" + ); + } + if (clientAuth == ClientAuth.REQUIRE && !httpSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH + " must be set if client auth is required" + ); + } + } else if (hasKeyOrTrustStoreSettings(httpSettings)) { + if (!httpSettings.hasValue(KEYSTORE_FILEPATH)) { + throw new OpenSearchException("Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_KEYSTORE_FILEPATH + " must be set"); + } + if (clientAuth == ClientAuth.REQUIRE && !httpSettings.hasValue(TRUSTSTORE_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH + " must be set if client auth is required" + ); + } + } else { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure HTTP layer" + ); + } + } + + private void validateTransportSettings(final Settings transportSettings) { + if (!hasExtendedKeyUsageEnabled(transportSettings)) { + if (hasPemStoreSettings(transportSettings)) { + if (!transportSettings.hasValue(PEM_CERT_FILEPATH) + || !transportSettings.hasValue(PEM_KEY_FILEPATH) + || !transportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH + ) + + " must be set" + ); + } + + } else if (hasKeyOrTrustStoreSettings(transportSettings)) { + verifyKeyAndTrustStoreSettings(transportSettings); + } else { + throw new OpenSearchException( + "Wrong Transport SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure Transport layer properly" + ); + } + } else { + final var serverTransportSettings = transportSettings.getByPrefix(SSL_TRANSPORT_SERVER_EXTENDED_PREFIX); + final var clientTransportSettings = transportSettings.getByPrefix(SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX); + if (hasKeyOrTrustStoreSettings(transportSettings)) { + verifyKeyAndTrustStoreSettings(transportSettings); + if (!serverTransportSettings.hasValue(KEYSTORE_ALIAS) + || !serverTransportSettings.hasValue(TRUSTSTORE_ALIAS) + || !clientTransportSettings.hasValue(KEYSTORE_ALIAS) + || !clientTransportSettings.hasValue(TRUSTSTORE_ALIAS)) { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS + ) + + " must be set if " + + SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is set" + ); + } + } else if (!hasKeyOrTrustStoreSettings(transportSettings)) { + if (!serverTransportSettings.hasValue(PEM_CERT_FILEPATH) + || !serverTransportSettings.hasValue(PEM_KEY_FILEPATH) + || !serverTransportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH) + || !clientTransportSettings.hasValue(PEM_CERT_FILEPATH) + || !clientTransportSettings.hasValue(PEM_KEY_FILEPATH) + || !clientTransportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH + ) + + " must be set if " + + SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is set" + ); + } + } else { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure HTTP layer" + ); + } + } + } + + private void verifyKeyAndTrustStoreSettings(final Settings settings) { + if (!settings.hasValue(KEYSTORE_FILEPATH) || !settings.hasValue(TRUSTSTORE_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport/Tran SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure Transport layer properly" + ); + } + } + + private boolean hasExtendedKeyUsageEnabled(final Settings settings) { + return settings.getAsBoolean(EXTENDED_KEY_USAGE_ENABLED, SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT); + } + + private boolean hasKeyOrTrustStoreSettings(final Settings settings) { + return settings.hasValue(KEYSTORE_FILEPATH) || settings.hasValue(TRUSTSTORE_FILEPATH); + } + + private boolean hasPemStoreSettings(final Settings settings) { + return settings.hasValue(PEM_KEY_FILEPATH) || settings.hasValue(PEM_CERT_FILEPATH) || settings.hasValue(PEM_TRUSTED_CAS_FILEPATH); + } + + void jceWarnings() { + try { + final int aesMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); + + if (aesMaxKeyLength < 256) { + // CS-SUPPRESS-SINGLE: RegexpSingleline Java Cryptography Extension is unrelated to OpenSearch extensions + LOGGER.info( + "AES-256 not supported, max key length for AES is {} bit." + + " (This is not an issue, it just limits possible encryption strength. " + + "To enable AES 256, " + + "install 'Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files')", + aesMaxKeyLength + ); + // CS-ENFORCE-SINGLE + } + } catch (final NoSuchAlgorithmException e) { + LOGGER.error("AES encryption not supported (SG 1). ", e); + } + } + + void openSslWarnings(final Settings settings) { + if (!OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED + && OpenSsl.isAvailable() + && (settings.getAsBoolean(SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, true) + || settings.getAsBoolean(SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE, true))) { + if (PlatformDependent.javaVersion() < 12) { + LOGGER.warn( + "Support for OpenSSL with Java 11 or prior versions require using Netty allocator. Set " + + "'opensearch.unsafe.use_netty_default_allocator' system property to true" + ); + } else { + LOGGER.warn("Support for OpenSSL with Java 12+ has been removed from OpenSearch Security. Using JDK SSL instead."); + } + } + if (OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && OpenSsl.isAvailable()) { + LOGGER.info("OpenSSL {} ({}) available", OpenSsl.versionString(), OpenSsl.version()); + + if (OpenSsl.version() < 0x10002000L) { + LOGGER.warn( + "Outdated OpenSSL version detected. You should update to 1.0.2k or later. Currently installed: {}", + OpenSsl.versionString() + ); + } + + if (!OpenSsl.supportsHostnameValidation()) { + LOGGER.warn( + "Your OpenSSL version {} does not support hostname verification. You should update to 1.0.2k or later.", + OpenSsl.versionString() + ); + } + + LOGGER.debug("OpenSSL available ciphers {}", OpenSsl.availableOpenSslCipherSuites()); + } else { + LOGGER.warn( + "OpenSSL not available (this is not an error, we simply fallback to built-in JDK SSL) because of {}", + OpenSsl.unavailabilityCause() + ); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/CertType.java b/src/main/java/org/opensearch/security/ssl/config/CertType.java new file mode 100644 index 0000000000..09a8dcfae9 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/CertType.java @@ -0,0 +1,33 @@ +/* + * 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.ssl.config; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; + +public enum CertType { + HTTP(SSL_HTTP_PREFIX), + TRANSPORT(SSL_TRANSPORT_PREFIX), + TRANSPORT_CLIENT(SSL_TRANSPORT_CLIENT_PREFIX); + + private final String sslConfigPrefix; + + private CertType(String sslConfigPrefix) { + this.sslConfigPrefix = sslConfigPrefix; + } + + public String sslConfigPrefix() { + return sslConfigPrefix; + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/Certificate.java b/src/main/java/org/opensearch/security/ssl/config/Certificate.java new file mode 100644 index 0000000000..534148db57 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/Certificate.java @@ -0,0 +1,188 @@ +/* + * 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.ssl.config; + +import java.lang.reflect.Method; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.ASN1TaggedObject; + +public class Certificate { + + private final static Logger LOGGER = LogManager.getLogger(Certificate.class); + + private final X509Certificate certificate; + + private final String format; + + private final String alias; + + private final boolean hasKey; + + public Certificate(final X509Certificate certificate, final boolean hasKey) { + this(certificate, "pem", null, hasKey); + } + + public Certificate(final X509Certificate certificate, final String format, final String alias, final boolean hasKey) { + this.certificate = certificate; + this.format = format; + this.alias = alias; + this.hasKey = hasKey; + } + + public X509Certificate x509Certificate() { + return certificate; + } + + public String format() { + return format; + } + + public String alias() { + return alias; + } + + public boolean hasKey() { + return hasKey; + } + + public String subjectAlternativeNames() { + return loadSubjectAlternativeNames(); + } + + @Deprecated(since = "since JDK 21", forRemoval = true) + public String loadSubjectAlternativeNames() { + String san = ""; + try { + Collection> altNames = certificate != null && certificate.getSubjectAlternativeNames() != null + ? certificate.getSubjectAlternativeNames() + : null; + if (altNames != null) { + Comparator> comparator = Comparator.comparing((List altName) -> (Integer) altName.get(0)) + .thenComparing((List altName) -> (String) altName.get(1)); + + Set> sans = new TreeSet<>(comparator); + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + // otherName requires parsing to string + if (type == 0) { + List otherName = parseOtherName(altName); + if (otherName != null) { + sans.add(Arrays.asList(type, otherName)); + } + } else { + sans.add(altName); + } + } + san = sans.toString(); + } + } catch (CertificateParsingException e) { + LOGGER.error("Issue parsing SubjectAlternativeName:", e); + } + + return san; + } + + @Deprecated(since = "since JDK 21", forRemoval = true) + private List parseOtherName(List altName) { + if (altName.size() < 2) { + LOGGER.warn("Couldn't parse subject alternative names"); + return null; + } + try (final ASN1InputStream in = new ASN1InputStream((byte[]) altName.get(1))) { + final ASN1Primitive asn1Primitive = in.readObject(); + final ASN1Sequence sequence = ASN1Sequence.getInstance(asn1Primitive); + final ASN1ObjectIdentifier asn1ObjectIdentifier = ASN1ObjectIdentifier.getInstance(sequence.getObjectAt(0)); + final ASN1TaggedObject asn1TaggedObject = ASN1TaggedObject.getInstance(sequence.getObjectAt(1)); + Method getObjectMethod = getObjectMethod(); + ASN1Object maybeTaggedAsn1Primitive = (ASN1Primitive) getObjectMethod.invoke(asn1TaggedObject); + if (maybeTaggedAsn1Primitive instanceof ASN1TaggedObject) { + maybeTaggedAsn1Primitive = (ASN1Primitive) getObjectMethod.invoke(maybeTaggedAsn1Primitive); + } + if (maybeTaggedAsn1Primitive instanceof ASN1String) { + return ImmutableList.of(asn1ObjectIdentifier.getId(), maybeTaggedAsn1Primitive.toString()); + } else { + LOGGER.warn("Couldn't parse subject alternative names"); + return null; + } + } catch (final Exception ioe) { // catch all exception here since BC throws diff exceptions + throw new RuntimeException("Couldn't parse subject alternative names", ioe); + } + } + + static Method getObjectMethod() throws ClassNotFoundException, NoSuchMethodException { + Class asn1TaggedObjectClass = Class.forName("org.bouncycastle.asn1.ASN1TaggedObject"); + try { + return asn1TaggedObjectClass.getMethod("getBaseObject"); + } catch (NoSuchMethodException ex) { + return asn1TaggedObjectClass.getMethod("getObject"); + } + } + + public String serialNumber() { + return certificate.getSerialNumber().toString(); + } + + public String subject() { + return certificate.getSubjectX500Principal() != null ? certificate.getSubjectX500Principal().getName() : null; + } + + public String issuer() { + return certificate.getIssuerX500Principal() != null ? certificate.getIssuerX500Principal().getName() : null; + } + + public String notAfter() { + return certificate.getNotAfter() != null ? certificate.getNotAfter().toInstant().toString() : null; + } + + public String notBefore() { + return certificate.getNotBefore() != null ? certificate.getNotBefore().toInstant().toString() : null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Certificate that = (Certificate) o; + return hasKey == that.hasKey + && Objects.equals(certificate, that.certificate) + && Objects.equals(format, that.format) + && Objects.equals(alias, that.alias); + } + + @Override + public int hashCode() { + return Objects.hash(certificate, format, alias, hasKey); + } + + @Override + public String toString() { + return "Certificate{" + "format='" + format + '\'' + ", alias='" + alias + '\'' + ", hasKey=" + hasKey + '}'; + } +} diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java new file mode 100644 index 0000000000..b1675f093a --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java @@ -0,0 +1,201 @@ +/* + * 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.ssl.config; + +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.net.ssl.KeyManagerFactory; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; + +public interface KeyStoreConfiguration { + + List files(); + + List loadCertificates(); + + default KeyManagerFactory createKeyManagerFactory(boolean validateCertificates) { + final var keyStore = createKeyStore(); + if (validateCertificates) { + KeyStoreUtils.validateKeyStoreCertificates(keyStore.v1()); + } + return buildKeyManagerFactory(keyStore.v1(), keyStore.v2()); + } + + default KeyManagerFactory buildKeyManagerFactory(final KeyStore keyStore, final char[] password) { + try { + final var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + return keyManagerFactory; + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Failed to create KeyManagerFactory", e); + } + } + + Tuple createKeyStore(); + + final class JdkKeyStoreConfiguration implements KeyStoreConfiguration { + private final Path path; + + private final String type; + + private final String alias; + + private final char[] keyStorePassword; + + private final char[] keyPassword; + + public JdkKeyStoreConfiguration( + final Path path, + final String type, + final String alias, + final char[] keyStorePassword, + final char[] keyPassword + ) { + this.path = path; + this.type = type; + this.alias = alias; + this.keyStorePassword = keyStorePassword; + this.keyPassword = keyPassword; + } + + private void loadCertificateChain(final String alias, final KeyStore keyStore, final ImmutableList.Builder listBuilder) + throws KeyStoreException { + final var cc = keyStore.getCertificateChain(alias); + var first = true; + for (final var c : cc) { + if (c instanceof X509Certificate) { + listBuilder.add(new Certificate((X509Certificate) c, type, alias, first)); + first = false; + } + } + } + + @Override + public List loadCertificates() { + final var keyStore = KeyStoreUtils.loadKeyStore(path, type, keyStorePassword); + final var listBuilder = ImmutableList.builder(); + + try { + if (alias != null) { + if (keyStore.isKeyEntry(alias)) { + loadCertificateChain(alias, keyStore, listBuilder); + } + } else { + for (final var a : Collections.list(keyStore.aliases())) { + if (keyStore.isKeyEntry(a)) { + loadCertificateChain(a, keyStore, listBuilder); + } + } + } + final var list = listBuilder.build(); + if (list.isEmpty()) { + throw new OpenSearchException("The file " + path + " does not contain any certificates"); + } + return listBuilder.build(); + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't load certificates from file " + path, e); + } + } + + @Override + public List files() { + return List.of(path); + } + + @Override + public Tuple createKeyStore() { + final var keyStore = KeyStoreUtils.newKeyStore(path, type, alias, keyStorePassword, keyPassword); + return Tuple.tuple(keyStore, keyPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkKeyStoreConfiguration that = (JdkKeyStoreConfiguration) o; + return Objects.equals(path, that.path) + && Objects.equals(type, that.type) + && Objects.equals(alias, that.alias) + && Objects.deepEquals(keyStorePassword, that.keyStorePassword) + && Objects.deepEquals(keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + return Objects.hash(path, type, alias, Arrays.hashCode(keyStorePassword), Arrays.hashCode(keyPassword)); + } + } + + final class PemKeyStoreConfiguration implements KeyStoreConfiguration { + + private final Path certificateChainPath; + + private final Path keyPath; + + private final char[] keyPassword; + + public PemKeyStoreConfiguration(final Path certificateChainPath, final Path keyPath, final char[] keyPassword) { + this.certificateChainPath = certificateChainPath; + this.keyPath = keyPath; + this.keyPassword = keyPassword; + } + + @Override + public List loadCertificates() { + final var certificates = KeyStoreUtils.x509Certificates(certificateChainPath); + final var listBuilder = ImmutableList.builder(); + listBuilder.add(new Certificate(certificates[0], true)); + for (int i = 1; i < certificates.length; i++) { + listBuilder.add(new Certificate(certificates[i], false)); + } + return listBuilder.build(); + } + + @Override + public List files() { + return List.of(certificateChainPath, keyPath); + } + + @Override + public Tuple createKeyStore() { + final var keyStore = KeyStoreUtils.newKeyStoreFromPem(certificateChainPath, keyPath, keyPassword); + return Tuple.tuple(keyStore, keyPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PemKeyStoreConfiguration that = (PemKeyStoreConfiguration) o; + return Objects.equals(certificateChainPath, that.certificateChainPath) + && Objects.equals(keyPath, that.keyPath) + && Objects.deepEquals(keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + return Objects.hash(certificateChainPath, keyPath, Arrays.hashCode(keyPassword)); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java new file mode 100644 index 0000000000..7c063bd312 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java @@ -0,0 +1,218 @@ +/* + * 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.ssl.config; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import javax.crypto.NoSuchPaddingException; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; + +import org.opensearch.OpenSearchException; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ApplicationProtocolNegotiator; +import io.netty.handler.ssl.SslContext; + +final class KeyStoreUtils { + + private final static class SecuritySslContext extends SslContext { + + private SecuritySslContext() {} + + @Override + public boolean isClient() { + throw new UnsupportedOperationException("Method isClient is not supported"); + } + + @Override + public List cipherSuites() { + throw new UnsupportedOperationException("Method cipherSuites is not supported"); + } + + @Override + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + throw new UnsupportedOperationException("Method applicationProtocolNegotiator is not supported"); + } + + @Override + public SSLEngine newEngine(ByteBufAllocator alloc) { + throw new UnsupportedOperationException("Method newEngine is not supported"); + } + + @Override + public SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + throw new UnsupportedOperationException("Method newEngine is not supported"); + } + + @Override + public SSLSessionContext sessionContext() { + throw new UnsupportedOperationException("Method sessionContext is not supported"); + } + + public static X509Certificate[] toX509Certificates(final File file) { + try { + return SslContext.toX509Certificates(file); + } catch (CertificateException e) { + throw new OpenSearchException("Couldn't read SSL certificates from " + file, e); + } + } + + protected static PrivateKey toPrivateKey(File keyFile, String keyPassword) throws InvalidAlgorithmParameterException, + NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, KeyException { + return SslContext.toPrivateKey(keyFile, keyPassword); + } + + } + + public static X509Certificate[] x509Certificates(final Path file) { + final var certificates = SecuritySslContext.toX509Certificates(file.toFile()); + if (certificates == null || certificates.length == 0) { + throw new OpenSearchException("Couldn't read SSL certificates from " + file); + } + return certificates; + } + + public static KeyStore loadTrustStore(final Path path, final String type, final String alias, final char[] password) { + try { + var keyStore = loadKeyStore(path, type, password); + if (alias != null) { + if (!keyStore.isCertificateEntry(alias)) { + throw new OpenSearchException("Alias " + alias + " does not contain a certificate entry"); + } + final var aliasCertificate = (X509Certificate) keyStore.getCertificate(alias); + if (aliasCertificate == null) { + throw new OpenSearchException("Couldn't find SSL certificate for alias " + alias); + } + keyStore = newKeyStore(); + keyStore.setCertificateEntry(alias, aliasCertificate); + } + return keyStore; + } catch (Exception e) { + throw new OpenSearchException("Failed to load trust store from " + path, e); + } + } + + public static KeyStore newTrustStoreFromPem(final Path pemFile) { + try { + final var certs = x509Certificates(pemFile); + final var keyStore = newKeyStore(); + for (int i = 0; i < certs.length; i++) { + final var c = certs[i]; + keyStore.setCertificateEntry("os-sec-plugin-pem-cert-" + i, c); + } + return keyStore; + } catch (final Exception e) { + throw new OpenSearchException("Failed to load SSL certificates from " + pemFile, e); + } + } + + private static KeyStore newKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + return keyStore; + } + + public static void validateKeyStoreCertificates(final KeyStore keyStore) { + try { + final var aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + final var a = aliases.nextElement(); + if (keyStore.isCertificateEntry(a)) { + final var c = (X509Certificate) keyStore.getCertificate(a); + if (c == null) { + throw new CertificateException("Alias " + a + " does not contain a certificate entry"); + } + c.checkValidity(); + } else if (keyStore.isKeyEntry(a)) { + final var cc = keyStore.getCertificateChain(a); + if (cc == null) { + throw new CertificateException("Alias " + a + " does not contain a certificate chain"); + } + for (final var c : cc) { + ((X509Certificate) c).checkValidity(); + } + } + } + } catch (KeyStoreException e) { + throw new OpenSearchException("Couldn't load keys store", e); + } catch (CertificateException e) { + throw new OpenSearchException("Invalid certificates", e); + } + } + + public static KeyStore loadKeyStore(final Path path, final String type, final char[] password) { + try { + final var keyStore = KeyStore.getInstance(type); + try (final var in = Files.newInputStream(path)) { + keyStore.load(in, password); + return keyStore; + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (Exception e) { + throw new OpenSearchException("Failed to load keystore from " + path, e); + } + } + + public static KeyStore newKeyStore( + final Path path, + final String type, + final String alias, + final char[] password, + final char[] keyPassword + ) { + try { + var keyStore = loadKeyStore(path, type, password); + if (alias != null) { + if (!keyStore.isKeyEntry(alias)) { + throw new CertificateException("Couldn't find SSL key for alias " + alias); + } + final var certificateChain = keyStore.getCertificateChain(alias); + if (certificateChain == null) { + throw new CertificateException("Couldn't find certificate chain for alias " + alias); + } + final var key = keyStore.getKey(alias, keyPassword); + keyStore = newKeyStore(); + keyStore.setKeyEntry(alias, key, keyPassword, certificateChain); + } + return keyStore; + } catch (final Exception e) { + throw new OpenSearchException("Failed to load key store from " + path, e); + } + } + + public static KeyStore newKeyStoreFromPem(final Path certificateChainPath, final Path keyPath, final char[] keyPassword) { + try { + final var certificateChain = x509Certificates(certificateChainPath); + final var keyStore = newKeyStore(); + final var key = SecuritySslContext.toPrivateKey(keyPath.toFile(), keyPassword != null ? new String(keyPassword) : null); + keyStore.setKeyEntry("key", key, keyPassword, certificateChain); + return keyStore; + } catch (Exception e) { + throw new OpenSearchException("Failed read key from " + keyPath, e); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java b/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java new file mode 100644 index 0000000000..a3f0c39eed --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java @@ -0,0 +1,171 @@ +/* + * 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.ssl.config; + +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.KeyStore; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.SecureSetting; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; + +import static org.opensearch.security.ssl.SecureSSLSettings.SECURE_SUFFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_KEY_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; + +public class SslCertificatesLoader { + + private final static Logger LOGGER = LogManager.getLogger(SslCertificatesLoader.class); + + private final String sslConfigSuffix; + + private final String fullSslConfigSuffix; + + public SslCertificatesLoader(final String sslConfigSuffix) { + this(sslConfigSuffix, null); + } + + public SslCertificatesLoader(final String sslConfigSuffix, final String extendedSslConfigSuffix) { + this.sslConfigSuffix = sslConfigSuffix; + this.fullSslConfigSuffix = extendedSslConfigSuffix != null ? sslConfigSuffix + extendedSslConfigSuffix : sslConfigSuffix; + } + + public Tuple loadConfiguration(final Environment environment) { + final var settings = environment.settings(); + final var sslConfigSettings = settings.getByPrefix(fullSslConfigSuffix); + if (settings.hasValue(sslConfigSuffix + KEYSTORE_FILEPATH)) { + return Tuple.tuple( + environment.settings().hasValue(sslConfigSuffix + TRUSTSTORE_FILEPATH) + ? buildJdkTrustStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(sslConfigSuffix + TRUSTSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD) + ) + : TrustStoreConfiguration.EMPTY_CONFIGURATION, + buildJdkKeyStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(sslConfigSuffix + KEYSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD), + resolvePassword(fullSslConfigSuffix + KEYSTORE_KEY_PASSWORD, settings, DEFAULT_STORE_PASSWORD) + ) + ); + } else { + return Tuple.tuple( + sslConfigSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH) + ? new TrustStoreConfiguration.PemTrustStoreConfiguration( + resolvePath(sslConfigSettings.get(PEM_TRUSTED_CAS_FILEPATH), environment) + ) + : TrustStoreConfiguration.EMPTY_CONFIGURATION, + buildPemKeyStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(fullSslConfigSuffix + PEM_KEY_PASSWORD, settings, null) + ) + ); + } + } + + private char[] resolvePassword(final String legacyPasswordSettings, final Settings settings, final String defaultPassword) { + final var securePasswordSetting = String.format("%s%s", legacyPasswordSettings, SECURE_SUFFIX); + final var securePassword = SecureSetting.secureString(securePasswordSetting, null).get(settings); + final var legacyPassword = settings.get(legacyPasswordSettings, defaultPassword); + if (!securePassword.isEmpty() && legacyPassword != null && !legacyPassword.equals(defaultPassword)) { + throw new OpenSearchException("One of " + legacyPasswordSettings + " or " + securePasswordSetting + " must be set not both"); + } + if (!securePassword.isEmpty()) { + return securePassword.getChars(); + } else { + if (legacyPassword != null) { + LOGGER.warn( + "Setting [{}] has a secure counterpart [{}] which should be used instead - allowing for legacy SSL setups", + legacyPasswordSettings, + securePasswordSetting + ); + return legacyPassword.toCharArray(); + } + } + return null; + } + + private KeyStoreConfiguration.JdkKeyStoreConfiguration buildJdkKeyStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] keyStorePassword, + final char[] keyPassword + ) { + return new KeyStoreConfiguration.JdkKeyStoreConfiguration( + resolvePath(environment.settings().get(sslConfigSuffix + KEYSTORE_FILEPATH), environment), + environment.settings().get(sslConfigSuffix + KEYSTORE_TYPE, KeyStore.getDefaultType()), + settings.get(KEYSTORE_ALIAS, null), + keyStorePassword, + keyPassword + ); + } + + private TrustStoreConfiguration.JdkTrustStoreConfiguration buildJdkTrustStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] trustStorePassword + ) { + return new TrustStoreConfiguration.JdkTrustStoreConfiguration( + resolvePath(environment.settings().get(sslConfigSuffix + TRUSTSTORE_FILEPATH), environment), + environment.settings().get(sslConfigSuffix + TRUSTSTORE_TYPE, KeyStore.getDefaultType()), + settings.get(TRUSTSTORE_ALIAS, null), + trustStorePassword + ); + } + + private KeyStoreConfiguration.PemKeyStoreConfiguration buildPemKeyStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] pemKeyPassword + ) { + return new KeyStoreConfiguration.PemKeyStoreConfiguration( + resolvePath(settings.get(PEM_CERT_FILEPATH), environment), + resolvePath(settings.get(PEM_KEY_FILEPATH), environment), + pemKeyPassword + ); + } + + private Path resolvePath(final String filePath, final Environment environment) { + final var path = environment.configDir().resolve(Path.of(filePath)); + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException(filePath + " - is a directory"); + } + if (!Files.isReadable(path)) { + throw new OpenSearchException( + "Unable to read the file " + filePath + ". Please make sure this files exists and is readable regarding to permissions" + ); + } + return path; + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/SslParameters.java b/src/main/java/org/opensearch/security/ssl/config/SslParameters.java new file mode 100644 index 0000000000..eef14cea0a --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/SslParameters.java @@ -0,0 +1,197 @@ +/* + * 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.ssl.config; + +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_HTTP_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.CLIENT_AUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.OPENSSL_AVAILABLE; + +public class SslParameters { + + private final SslProvider provider; + + private final ClientAuth clientAuth; + + private final List protocols; + + private final List ciphers; + + private SslParameters(SslProvider provider, final ClientAuth clientAuth, List protocols, List ciphers) { + this.provider = provider; + this.ciphers = ciphers; + this.protocols = protocols; + this.clientAuth = clientAuth; + } + + public ClientAuth clientAuth() { + return clientAuth; + } + + public SslProvider provider() { + return provider; + } + + public List allowedCiphers() { + return ciphers; + } + + public List allowedProtocols() { + return protocols; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslParameters that = (SslParameters) o; + return provider == that.provider && Objects.equals(ciphers, that.ciphers) && Objects.equals(protocols, that.protocols); + } + + @Override + public int hashCode() { + return Objects.hash(provider, ciphers, protocols); + } + + public static Loader loader(final Settings sslConfigSettings) { + return new Loader(sslConfigSettings); + } + + public static final class Loader { + + private final static Logger LOGGER = LogManager.getLogger(SslParameters.class); + + private final Settings sslConfigSettings; + + public Loader(final Settings sslConfigSettings) { + this.sslConfigSettings = sslConfigSettings; + } + + private SslProvider provider(final Settings settings) { + final var useOpenSslIfAvailable = settings.getAsBoolean(ENABLE_OPENSSL_IF_AVAILABLE, true); + if (OPENSSL_AVAILABLE && useOpenSslIfAvailable) { + return SslProvider.OPENSSL; + } else { + return SslProvider.JDK; + } + } + + private List protocols(final SslProvider provider, final Settings settings, boolean http) { + final var allowedProtocols = settings.getAsList(ENABLED_PROTOCOLS, List.of(ALLOWED_SSL_PROTOCOLS)); + if (provider == SslProvider.OPENSSL) { + final String[] supportedProtocols; + if (OpenSsl.version() > OPENSSL_1_1_1_BETA_9) { + supportedProtocols = http ? ALLOWED_OPENSSL_HTTP_PROTOCOLS : ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS; + } else { + supportedProtocols = http + ? ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 + : ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; + } + return openSslProtocols(allowedProtocols, supportedProtocols); + } else { + return jdkProtocols(allowedProtocols); + } + } + + private List openSslProtocols(final List allowedSslProtocols, final String... supportedProtocols) { + LOGGER.debug("OpenSSL supports the following {} protocols {}", supportedProtocols.length, supportedProtocols); + return Stream.of(supportedProtocols).filter(allowedSslProtocols::contains).collect(Collectors.toList()); + } + + private List jdkProtocols(final List allowedSslProtocols) { + try { + final var supportedProtocols = SSLContext.getDefault().getDefaultSSLParameters().getProtocols(); + LOGGER.debug("JVM supports the following {} protocols {}", supportedProtocols.length, supportedProtocols); + return Stream.of(supportedProtocols).filter(allowedSslProtocols::contains).collect(Collectors.toList()); + } catch (final NoSuchAlgorithmException e) { + throw new OpenSearchException("Unable to determine supported protocols", e); + } + } + + private List ciphers(final SslProvider provider, final Settings settings) { + final var allowed = settings.getAsList(ENABLED_CIPHERS, List.of(ALLOWED_SSL_CIPHERS)); + final Stream allowedCiphers; + if (provider == SslProvider.OPENSSL) { + LOGGER.debug( + "OpenSSL {} supports the following ciphers (java-style) {}", + OpenSsl.versionString(), + OpenSsl.availableJavaCipherSuites() + ); + LOGGER.debug( + "OpenSSL {} supports the following ciphers (openssl-style) {}", + OpenSsl.versionString(), + OpenSsl.availableOpenSslCipherSuites() + ); + allowedCiphers = allowed.stream().filter(OpenSsl::isCipherSuiteAvailable); + } else { + try { + final var supportedCiphers = SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites(); + LOGGER.debug("JVM supports the following {} ciphers {}", supportedCiphers.length, supportedCiphers); + allowedCiphers = Stream.of(supportedCiphers).filter(allowed::contains); + } catch (final NoSuchAlgorithmException e) { + throw new OpenSearchException("Unable to determine ciphers protocols", e); + } + } + return allowedCiphers.sorted(String::compareTo).collect(Collectors.toList()); + } + + public SslParameters load(final boolean http) { + final var clientAuth = http + ? ClientAuth.valueOf(sslConfigSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT)) + : ClientAuth.REQUIRE; + + final var provider = provider(sslConfigSettings); + final var sslParameters = new SslParameters( + provider, + clientAuth, + protocols(provider, sslConfigSettings, http), + ciphers(provider, sslConfigSettings) + ); + if (sslParameters.allowedProtocols().isEmpty()) { + throw new OpenSearchSecurityException("No ssl protocols for " + (http ? "HTTP" : "Transport") + " layer"); + } + if (sslParameters.allowedCiphers().isEmpty()) { + throw new OpenSearchSecurityException("No valid cipher suites for " + (http ? "HTTP" : "Transport") + " layer"); + } + return sslParameters; + } + + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java new file mode 100644 index 0000000000..4965aa3216 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java @@ -0,0 +1,185 @@ +/* + * 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.ssl.config; + +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.TrustManagerFactory; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.OpenSearchException; + +public interface TrustStoreConfiguration { + + TrustStoreConfiguration EMPTY_CONFIGURATION = new TrustStoreConfiguration() { + @Override + public Path file() { + return null; + } + + @Override + public List loadCertificates() { + return List.of(); + } + + @Override + public KeyStore createTrustStore() { + return null; + } + + @Override + public TrustManagerFactory createTrustManagerFactory(boolean validateCertificates) { + return null; + } + }; + + Path file(); + + List loadCertificates(); + + default TrustManagerFactory createTrustManagerFactory(boolean validateCertificates) { + final var trustStore = createTrustStore(); + if (validateCertificates) { + KeyStoreUtils.validateKeyStoreCertificates(trustStore); + } + return buildTrustManagerFactory(trustStore); + } + + default TrustManagerFactory buildTrustManagerFactory(final KeyStore keyStore) { + try { + final var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory; + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't initialize TrustManagerFactory", e); + } + } + + KeyStore createTrustStore(); + + final class JdkTrustStoreConfiguration implements TrustStoreConfiguration { + + private final Path path; + + private final String type; + + private final String alias; + + private final char[] password; + + public JdkTrustStoreConfiguration(final Path path, final String type, final String alias, final char[] password) { + this.path = path; + this.type = type; + this.alias = alias; + this.password = password; + } + + @Override + public List loadCertificates() { + final var keyStore = KeyStoreUtils.loadKeyStore(path, type, password); + final var listBuilder = ImmutableList.builder(); + try { + if (alias != null) { + listBuilder.add(new Certificate((X509Certificate) keyStore.getCertificate(alias), type, alias, false)); + } else { + for (final var a : Collections.list(keyStore.aliases())) { + if (!keyStore.isCertificateEntry(a)) continue; + final var c = keyStore.getCertificate(a); + if (c instanceof X509Certificate) { + listBuilder.add(new Certificate((X509Certificate) c, type, a, false)); + } + } + } + final var list = listBuilder.build(); + if (list.isEmpty()) { + throw new OpenSearchException("The file " + path + " does not contain any certificates"); + } + return listBuilder.build(); + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't load certificates from file " + path, e); + } + } + + @Override + public Path file() { + return path; + } + + @Override + public KeyStore createTrustStore() { + return KeyStoreUtils.loadTrustStore(path, type, alias, password); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkTrustStoreConfiguration that = (JdkTrustStoreConfiguration) o; + return Objects.equals(path, that.path) + && Objects.equals(type, that.type) + && Objects.equals(alias, that.alias) + && Objects.deepEquals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(path, type, alias, Arrays.hashCode(password)); + } + } + + final class PemTrustStoreConfiguration implements TrustStoreConfiguration { + + private final Path path; + + public PemTrustStoreConfiguration(final Path path) { + this.path = path; + } + + @Override + public List loadCertificates() { + return Stream.of(KeyStoreUtils.x509Certificates(path)).map(c -> new Certificate(c, false)).collect(Collectors.toList()); + } + + @Override + public Path file() { + return path; + } + + @Override + public KeyStore createTrustStore() { + return KeyStoreUtils.newTrustStoreFromPem(path); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PemTrustStoreConfiguration that = (PemTrustStoreConfiguration) o; + return Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hashCode(path); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java index b9f9e949ec..203a0c7965 100644 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java +++ b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java @@ -35,11 +35,13 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.filter.SecurityRequestFactory; -import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.SslConfiguration; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.SslParameters; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.ssl.util.SSLRequestHelper.SSLInfo; @@ -50,7 +52,7 @@ public class SecuritySSLInfoAction extends BaseRestHandler { private static final List routes = Collections.singletonList(new Route(Method.GET, "/_opendistro/_security/sslinfo")); private final Logger log = LogManager.getLogger(this.getClass()); - private final SecurityKeyStore sks; + private final SslSettingsManager sslSettingsManager; final PrincipalExtractor principalExtractor; private final Path configPath; private final Settings settings; @@ -58,13 +60,12 @@ public class SecuritySSLInfoAction extends BaseRestHandler { public SecuritySSLInfoAction( final Settings settings, final Path configPath, - final RestController controller, - final SecurityKeyStore sks, + final SslSettingsManager sslSettingsManager, final PrincipalExtractor principalExtractor ) { super(); this.settings = settings; - this.sks = sks; + this.sslSettingsManager = sslSettingsManager; this.principalExtractor = principalExtractor; this.configPath = configPath; } @@ -103,13 +104,15 @@ public void accept(RestChannel channel) throws Exception { if (showDn == Boolean.TRUE) { builder.field( "peer_certificates_list", - certs == null ? null : Arrays.stream(certs).map(c -> c.getSubjectDN().getName()).collect(Collectors.toList()) + certs == null + ? null + : Arrays.stream(certs).map(c -> c.getSubjectX500Principal().getName()).collect(Collectors.toList()) ); builder.field( "local_certificates_list", localCerts == null ? null - : Arrays.stream(localCerts).map(c -> c.getSubjectDN().getName()).collect(Collectors.toList()) + : Arrays.stream(localCerts).map(c -> c.getSubjectX500Principal().getName()).collect(Collectors.toList()) ); } @@ -122,9 +125,27 @@ public void accept(RestChannel channel) throws Exception { builder.field("ssl_openssl_non_available_cause", openSslUnavailCause == null ? "" : openSslUnavailCause.toString()); builder.field("ssl_openssl_supports_key_manager_factory", OpenSsl.supportsKeyManagerFactory()); builder.field("ssl_openssl_supports_hostname_validation", OpenSsl.supportsHostnameValidation()); - builder.field("ssl_provider_http", sks.getHTTPProviderName()); - builder.field("ssl_provider_transport_server", sks.getTransportServerProviderName()); - builder.field("ssl_provider_transport_client", sks.getTransportClientProviderName()); + builder.field( + "ssl_provider_http", + sslSettingsManager.sslConfiguration(CertType.HTTP) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); + builder.field( + "ssl_provider_transport_server", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); + builder.field( + "ssl_provider_transport_client", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index a3b9348496..dfc9ae567e 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -22,9 +22,53 @@ import java.util.List; import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; + +import io.netty.handler.ssl.OpenSsl; public final class SSLConfigConstants { + public static final String SSL_PREFIX = "plugins.security.ssl."; + + public static final String HTTP_SETTINGS = "http"; + + public static final String TRANSPORT_SETTINGS = "transport"; + + public static final String SSL_HTTP_PREFIX = SSL_PREFIX + HTTP_SETTINGS + "."; + + public static final String SSL_TRANSPORT_PREFIX = SSL_PREFIX + TRANSPORT_SETTINGS + "."; + + public static final String SSL_TRANSPORT_SERVER_EXTENDED_PREFIX = "server."; + + public static final String SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX = "client."; + + public static final String SSL_TRANSPORT_CLIENT_PREFIX = SSL_PREFIX + TRANSPORT_SETTINGS + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; + + public static final String ENABLED = "enabled"; + + public static final String CLIENT_AUTH_MODE = "clientauth_mode"; + + public static final String KEYSTORE_TYPE = "keystore_type"; + public static final String KEYSTORE_ALIAS = "keystore_alias"; + public static final String KEYSTORE_FILEPATH = "keystore_filepath"; + public static final String KEYSTORE_PASSWORD = "keystore_password"; + public static final String KEYSTORE_KEY_PASSWORD = "keystore_keypassword"; + + public static final String TRUSTSTORE_ALIAS = "truststore_alias"; + public static final String TRUSTSTORE_FILEPATH = "truststore_filepath"; + public static final String TRUSTSTORE_TYPE = "truststore_type"; + public static final String TRUSTSTORE_PASSWORD = "truststore_password"; + + public static final String PEM_KEY_FILEPATH = "pemkey_filepath"; + public static final String PEM_CERT_FILEPATH = "pemcert_filepath"; + public static final String PEM_TRUSTED_CAS_FILEPATH = "pemtrustedcas_filepath"; + public static final String EXTENDED_KEY_USAGE_ENABLED = "extended_key_usage_enabled"; + + public static final String ENABLE_OPENSSL_IF_AVAILABLE = "enable_openssl_if_available"; + public static final String ENABLED_PROTOCOLS = "enabled_protocols"; + public static final String ENABLED_CIPHERS = "enabled_ciphers"; + public static final String PEM_KEY_PASSWORD = "pemkey_password"; + public static final String SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE = "plugins.security.ssl.http.enable_openssl_if_available"; public static final String SECURITY_SSL_HTTP_ENABLED = "plugins.security.ssl.http.enabled"; public static final boolean SECURITY_SSL_HTTP_ENABLED_DEFAULT = false; @@ -99,7 +143,19 @@ public final class SSLConfigConstants { public static final String JDK_TLS_REJECT_CLIENT_INITIATED_RENEGOTIATION = "jdk.tls.rejectClientInitiatedRenegotiation"; - private static final String[] _SECURE_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + public static final Long OPENSSL_1_1_1_BETA_9 = 0x10101009L; + + public static final String[] ALLOWED_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + + public static final String[] ALLOWED_OPENSSL_HTTP_PROTOCOLS = ALLOWED_SSL_PROTOCOLS; + + public static final String[] ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 = { "TLSv1.2", "TLSv1.1", "TLSv1" }; + + public static final String[] ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS = ALLOWED_SSL_PROTOCOLS; + + public static final String[] ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 = { "TLSv1.2", "TLSv1.1" }; + + public static final boolean OPENSSL_AVAILABLE = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && OpenSsl.isAvailable(); public static String[] getSecureSSLProtocols(Settings settings, boolean http) { List configuredProtocols = null; @@ -116,11 +172,11 @@ public static String[] getSecureSSLProtocols(Settings settings, boolean http) { return configuredProtocols.toArray(new String[0]); } - return _SECURE_SSL_PROTOCOLS.clone(); + return ALLOWED_SSL_PROTOCOLS.clone(); } // @formatter:off - private static final String[] _SECURE_SSL_CIPHERS = { + public static final String[] ALLOWED_SSL_CIPHERS = { // TLS__WITH_ // Example (including unsafe ones) @@ -249,7 +305,7 @@ public static List getSecureSSLCiphers(Settings settings, boolean http) return configuredCiphers; } - return Collections.unmodifiableList(Arrays.asList(_SECURE_SSL_CIPHERS)); + return Collections.unmodifiableList(Arrays.asList(ALLOWED_SSL_CIPHERS)); } private SSLConfigConstants() { diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesRule.java b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java new file mode 100644 index 0000000000..a27de233dc --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java @@ -0,0 +1,318 @@ +/* + * 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.ssl; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import org.opensearch.common.collect.Tuple; + +public class CertificatesRule extends ExternalResource { + + private final static BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); + + private final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + final static String DEFAULT_SUBJECT_NAME = "CN=some_access,OU=client,O=client,L=test,C=de"; + + private Path configRootFolder; + + private final String privateKeyPassword = RandomStringUtils.randomAlphabetic(10); + + private X509CertificateHolder caCertificateHolder; + + private X509CertificateHolder accessCertificateHolder; + + private PrivateKey accessCertificatePrivateKey; + + @Override + protected void before() throws Throwable { + super.before(); + temporaryFolder.create(); + configRootFolder = temporaryFolder.newFolder("esHome").toPath(); + final var keyPair = generateKeyPair(); + caCertificateHolder = generateCaCertificate(keyPair); + final var keyAndCertificate = generateAccessCertificate(keyPair); + accessCertificatePrivateKey = keyAndCertificate.v1(); + accessCertificateHolder = keyAndCertificate.v2(); + } + + @Override + protected void after() { + super.after(); + temporaryFolder.delete(); + } + + public Path configRootFolder() { + return configRootFolder; + } + + public String privateKeyPassword() { + return privateKeyPassword; + } + + public X509CertificateHolder caCertificateHolder() { + return caCertificateHolder; + } + + public X509CertificateHolder accessCertificateHolder() { + return accessCertificateHolder; + } + + public X509Certificate x509CaCertificate() throws CertificateException { + return toX509Certificate(caCertificateHolder); + } + + public X509Certificate x509AccessCertificate() throws CertificateException { + return toX509Certificate(accessCertificateHolder); + } + + public PrivateKey accessCertificatePrivateKey() { + return accessCertificatePrivateKey; + } + + public KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_PROVIDER); + generator.initialize(4096); + return generator.generateKeyPair(); + } + + public X509CertificateHolder generateCaCertificate(final KeyPair parentKeyPair) throws IOException, NoSuchAlgorithmException, + OperatorCreationException { + return generateCaCertificate(parentKeyPair, generateSerialNumber()); + } + + public X509CertificateHolder generateCaCertificate(final KeyPair parentKeyPair, final BigInteger serialNumber) throws IOException, + NoSuchAlgorithmException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + return createCertificateBuilder( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair.getPublic(), + parentKeyPair.getPublic(), + serialNumber, + startAndEndDate.v1(), + startAndEndDate.v2() + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) + .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)) + .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_PROVIDER).build(parentKeyPair.getPrivate())); + // CS-ENFORCE-SINGLE + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair) throws NoSuchAlgorithmException, + IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair, final BigInteger serialNumber) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAdnEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + serialNumber, + startAdnEndDate.v1(), + startAdnEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final Instant startDate, + final Instant endDate + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startDate, + endDate, + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final Instant startDate, + final Instant endDate, + List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startDate, + endDate, + sans + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final String subject, + final String issuer + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + subject, + issuer, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair, final List sans) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + sans + ); + } + + public Tuple generateAccessCertificate( + final String subject, + final String issuer, + final KeyPair parentKeyPair, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate, + final List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var keyPair = generateKeyPair(); + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var certificate = createCertificateBuilder( + subject, + issuer, + keyPair.getPublic(), + parentKeyPair.getPublic(), + serialNumber, + startDate, + endDate + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(false)) + .addExtension( + Extension.keyUsage, + true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment) + ) + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)) + .addExtension(Extension.subjectAlternativeName, false, new DERSequence(sans.toArray(sans.toArray(new ASN1Encodable[0])))) + .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_PROVIDER).build(parentKeyPair.getPrivate())); + // CS-ENFORCE-SINGLE + return Tuple.tuple(keyPair.getPrivate(), certificate); + } + + private List defaultSubjectAlternativeNames() { + return List.of( + new GeneralName(GeneralName.registeredID, "1.2.3.4.5.5"), + new GeneralName(GeneralName.dNSName, "localhost"), + new GeneralName(GeneralName.iPAddress, "127.0.0.1") + ); + } + + public X509Certificate toX509Certificate(final X509CertificateHolder x509CertificateHolder) throws CertificateException { + return new JcaX509CertificateConverter().getCertificate(x509CertificateHolder); + } + + private X509v3CertificateBuilder createCertificateBuilder( + final String subject, + final String issuer, + final PublicKey certificatePublicKey, + final PublicKey parentPublicKey, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate + ) throws NoSuchAlgorithmException, CertIOException { + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var subjectName = new X500Name(RFC4519Style.INSTANCE, subject); + final var issuerName = new X500Name(RFC4519Style.INSTANCE, issuer); + final var extUtils = new JcaX509ExtensionUtils(); + return new X509v3CertificateBuilder( + issuerName, + serialNumber, + Date.from(startDate), + Date.from(endDate), + subjectName, + SubjectPublicKeyInfo.getInstance(certificatePublicKey.getEncoded()) + ).addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(parentPublicKey)) + .addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(certificatePublicKey)); + // CS-ENFORCE-SINGLE + } + + Tuple generateStartAndEndDate() { + final var startDate = Instant.now().minusMillis(24 * 3600 * 1000); + final var endDate = Instant.from(startDate).plus(10, ChronoUnit.DAYS); + return Tuple.tuple(startDate, endDate); + } + + public BigInteger generateSerialNumber() { + return BigInteger.valueOf(Instant.now().plusMillis(100).getEpochSecond()); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java new file mode 100644 index 0000000000..7b6ee9fc74 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java @@ -0,0 +1,43 @@ +/* + * 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.ssl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.SecureRandom; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.util.io.pem.PemObject; + +public class CertificatesUtils { + + public static void writePemContent(final Path path, final Object pemContent) throws IOException { + try (JcaPEMWriter writer = new JcaPEMWriter(Files.newBufferedWriter(path))) { + writer.writeObject(pemContent); + } + } + + public static PemObject privateKeyToPemObject(final PrivateKey privateKey, final String password) throws Exception { + return new PKCS8Generator( + PrivateKeyInfo.getInstance(privateKey.getEncoded()), + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + .setPassword(password.toCharArray()) + .build() + ).generate(); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java index aefb12c0db..e7e5abaeda 100644 --- a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java +++ b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java @@ -9,6 +9,7 @@ package org.opensearch.security.ssl; import java.io.IOException; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Map; @@ -26,6 +27,7 @@ import org.opensearch.common.network.NetworkModule; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; @@ -55,17 +57,17 @@ public class OpenSearchSecuritySSLPluginTest extends AbstractSecurityUnitTest { private SecureTransportSettingsProvider secureTransportSettingsProvider; private ClusterSettings clusterSettings; + private Path osPathHome; + @Before public void setUp() { + osPathHome = FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks").getParent().getParent(); settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), osPathHome) .put( SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") ) - .put( - SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") - ) .put( SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/truststore.jks") @@ -116,7 +118,7 @@ public Optional buildSecureHttpServerEngine(Settings settings, HttpSe @Test public void testRegisterSecureHttpTransport() throws IOException { - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, osPathHome, false)) { final Map> transports = plugin.getSecureHttpTransports( settings, MOCK_POOL, @@ -140,7 +142,7 @@ public void testRegisterSecureHttpTransport() throws IOException { @Test public void testRegisterSecureTransport() throws IOException { - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( settings, MOCK_POOL, @@ -165,7 +167,7 @@ public void testRegisterSecureTransportWithDeprecatedSecuirtyPluginSettings() th .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( deprecated, MOCK_POOL, @@ -190,7 +192,7 @@ public void testRegisterSecureTransportWithNetworkModuleSettings() throws IOExce .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( migrated, MOCK_POOL, @@ -229,7 +231,7 @@ public void testRegisterSecureTransportWithDuplicateSettings() throws IOExceptio .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( migrated, MOCK_POOL, diff --git a/src/test/java/org/opensearch/security/ssl/SSLTest.java b/src/test/java/org/opensearch/security/ssl/SSLTest.java index a6013c7823..20887fccdf 100644 --- a/src/test/java/org/opensearch/security/ssl/SSLTest.java +++ b/src/test/java/org/opensearch/security/ssl/SSLTest.java @@ -569,7 +569,7 @@ public void testHttpsAndNodeSSLFailedCipher() throws Exception { Assert.fail(); } catch (Exception e1) { Throwable e = ExceptionUtils.getRootCause(e1); - Assert.assertTrue(e.toString(), e.toString().contains("no valid cipher")); + Assert.assertTrue(e.toString(), e.toString().contains("No valid cipher")); } } diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index 244967cf76..30635477eb 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -147,9 +147,12 @@ public void testSSLReloadFail_InvalidDNAndDate() throws Exception { RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); assertThat(reloadCertsResponse.getStatusCode(), is(500)); assertThat( - "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " - + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];", - is(DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText()) + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "java.security.cert.CertificateException: " + + "New certificates do not have valid Subject DNs. Current Subject DNs [CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE] " + + "new Subject DNs [CN=node-2.example.com,OU=SSL,O=Test,L=Test,C=DE]" + ) ); } diff --git a/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java b/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java new file mode 100644 index 0000000000..4dea300754 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java @@ -0,0 +1,266 @@ +/* + * 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.ssl; + +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.cert.X509CertificateHolder; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.config.KeyStoreConfiguration; +import org.opensearch.security.ssl.config.SslParameters; +import org.opensearch.security.ssl.config.TrustStoreConfiguration; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.junit.Assert.assertThrows; + +public class SslContextHandlerTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + Path caCertificatePath; + + Path accessCertificatePath; + + Path accessCertificatePrivateKeyPath; + + @Before + public void setUp() throws Exception { + caCertificatePath = certificatesRule.configRootFolder().resolve("ca_certificate.pem"); + accessCertificatePath = certificatesRule.configRootFolder().resolve("access_certificate.pem"); + accessCertificatePrivateKeyPath = certificatesRule.configRootFolder().resolve("access_certificate_pk.pem"); + writeCertificates( + certificatesRule.caCertificateHolder(), + certificatesRule.accessCertificateHolder(), + certificatesRule.accessCertificatePrivateKey() + ); + } + + void writeCertificates( + final X509CertificateHolder caCertificate, + final X509CertificateHolder accessCertificate, + final PrivateKey accessPrivateKey + ) throws Exception { + writePemContent(caCertificatePath, caCertificate); + writePemContent(accessCertificatePath, accessCertificate); + writePemContent(accessCertificatePrivateKeyPath, privateKeyToPemObject(accessPrivateKey, certificatesRule.privateKeyPassword())); + } + + @Test + public void doesNothingIfCertificatesAreSame() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + sslContextHandler.reloadSslContext(); + + assertThat("SSL Context is the same", sslContextBefore.equals(sslContextHandler.sslContext())); + } + + @Test + public void failsIfCertificatesHasInvalidDates() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var accessCertificate = certificatesRule.x509AccessCertificate(); + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + accessCertificate.getNotBefore().toInstant(), + accessCertificate.getNotAfter().toInstant().minus(10, ChronoUnit.DAYS) + ); + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + + newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + accessCertificate.getNotBefore().toInstant().plus(10, ChronoUnit.DAYS), + accessCertificate.getNotAfter().toInstant().plus(20, ChronoUnit.DAYS) + ); + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + } + + @Test + public void filesIfHasNotValidSubjectDNs() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + "CN=ddddd,O=client,L=test,C=de", + currentAccessCertificate.getIssuerX500Principal().getName() + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid Subject DNs. " + + "Current Subject DNs [CN=some_access,OU=client,O=client,L=test,C=de] " + + "new Subject DNs [CN=ddddd,O=client,L=test,C=de]" + ) + ); + } + + @Test + public void filesIfHasNotValidIssuerDNs() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getSubjectX500Principal().getName(), + "CN=ddddd,O=client,L=test,C=de" + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid Issuer DNs. " + + "Current Issuer DNs: [CN=some_access,OU=client,O=client,L=test,C=de] " + + "new Issuer DNs: [CN=ddddd,O=client,L=test,C=de]" + ) + ); + } + + @Test + public void filesIfHasNotValidSans() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + List.of(new GeneralName(GeneralName.iPAddress, "127.0.0.3")) + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid SANs. " + + "Current SANs: [[[2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]] " + + "new SANs: [[[7, 127.0.0.3]]]" + ) + ); + } + + @Test + public void reloadSslContext() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getNotBefore().toInstant(), + currentAccessCertificate.getNotAfter().toInstant().plus(10, ChronoUnit.MINUTES) + ); + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + sslContextHandler.reloadSslContext(); + + assertThat("Context reloaded", is(not(sslContextBefore.equals(sslContextHandler.sslContext())))); + } + + @Test + public void reloadSslContextForShuffledSameSans() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.accessCertificateHolder(); + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getNotBefore().toInstant(), + currentAccessCertificate.getNotAfter().toInstant().plus(10, ChronoUnit.MINUTES), + shuffledSans(currentAccessCertificate.getExtension(Extension.subjectAlternativeName)) + ); + // CS-ENFORCE-SINGLE + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + sslContextHandler.reloadSslContext(); + + assertThat("Context reloaded", is(not(sslContextBefore.equals(sslContextHandler.sslContext())))); + } + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + List shuffledSans(Extension currentSans) { + final var san1Sequence = ASN1Sequence.getInstance(currentSans.getParsedValue().toASN1Primitive()); + + final var shuffledSans = new ArrayList(); + final var objects = san1Sequence.getObjects(); + while (objects.hasMoreElements()) { + shuffledSans.add(GeneralName.getInstance(objects.nextElement())); + } + + for (int i = 0; i < 5; i++) + Collections.shuffle(shuffledSans); + return shuffledSans; + } + // CS-ENFORCE-SINGLE + + SslContextHandler sslContextHandler() { + final var sslParameters = SslParameters.loader(Settings.EMPTY).load(false); + final var trustStoreConfiguration = new TrustStoreConfiguration.PemTrustStoreConfiguration(caCertificatePath); + final var keyStoreConfiguration = new KeyStoreConfiguration.PemKeyStoreConfiguration( + accessCertificatePath, + accessCertificatePrivateKeyPath, + certificatesRule.privateKeyPassword().toCharArray() + ); + + SslConfiguration sslConfiguration = new SslConfiguration(sslParameters, trustStoreConfiguration, keyStoreConfiguration); + return new SslContextHandler(sslConfiguration, false); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java new file mode 100644 index 0000000000..1aa2c47eb3 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java @@ -0,0 +1,464 @@ +/* + * 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.ssl; + +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.env.TestEnvironment; +import org.opensearch.security.ssl.config.CertType; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_ONLY; +import static org.junit.Assert.assertThrows; + +public class SslSettingsManagerTest extends RandomizedTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + @BeforeClass + public static void setUp() throws Exception { + writeCertificates("ca_http_certificate.pem", "access_http_certificate.pem", "access_http_certificate_pk.pem"); + writeCertificates("ca_transport_certificate.pem", "access_transport_certificate.pem", "access_transport_certificate_pk.pem"); + } + + static void writeCertificates(final String trustedFileName, final String accessFileName, final String accessPkFileName) + throws Exception { + writePemContent(path(trustedFileName), certificatesRule.caCertificateHolder()); + writePemContent(path(accessFileName), certificatesRule.accessCertificateHolder()); + writePemContent( + path(accessPkFileName), + privateKeyToPemObject(certificatesRule.accessCertificatePrivateKey(), certificatesRule.privateKeyPassword()) + ); + } + + static Path path(final String fileName) { + return certificatesRule.configRootFolder().resolve(fileName); + } + + @Test + public void failsIfNoSslSet() throws Exception { + final var settings = defaultSettingsBuilder().build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void transportFailsIfNoConfigDefine() throws Exception { + final var noTransportSettings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true).build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettings))); + } + + @Test + public void transportFailsIfConfigEnabledButNotDefined() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true).build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfJdkTrustStoreHasNotBeenSet() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfExtendedKeyUsageEnabledForJdkKeyStoreButNotConfigured() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfExtendedKeyUsageEnabledForPemKeyStoreButNotConfigured() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfConfigDisabled() throws Exception { + Settings settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_ENABLED, false) + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfBothPemAndJDKSettingsWereSet() throws Exception { + final var keyStoreSettings = randomFrom(List.of(SECURITY_SSL_HTTP_KEYSTORE_FILEPATH)); + final var pemKeyStoreSettings = randomFrom( + List.of(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, SECURITY_SSL_HTTP_PEMCERT_FILEPATH, SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH) + ); + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(keyStoreSettings, "aaa") + .put(pemKeyStoreSettings, "bbb") + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfHttpEnabledButButNotDefined() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true).build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfClientAuthRequiredAndJdkTrustStoreNotSet() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .put(SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfClientAuthRequiredAndPemTrustedCasNotSet() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .put(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, "aaa") + .put(SECURITY_SSL_HTTP_PEMCERT_FILEPATH, "bbb") + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void loadConfigurationAndBuildHSslContextForSslOnlyMode() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var transportEnabled = randomBoolean(); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, transportEnabled).put(SECURITY_SSL_ONLY, true).build() + ) + ); + + assertThat("Loaded HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()); + if (transportEnabled) { + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + } else { + assertThat("Didn't load Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isEmpty()); + assertThat( + "Didn't load Transport Client configuration", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isEmpty() + ); + } + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isPresent()); + if (transportEnabled) { + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + } else { + assertThat("Didn't build Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isEmpty()); + assertThat("Didn't build Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isEmpty()); + } + + assertThat( + "Built Server SSL context for HTTP", + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::sslContext).map(SslContext::isServer).orElse(false) + ); + } + + @Test + public void loadConfigurationAndBuildSslContextForClientNode() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + settingsBuilder.put("client.type", "client").put(SECURITY_SSL_HTTP_ENABLED, randomBoolean()).build() + ) + ); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat("Didn't build HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildSslContexts() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var sslSettingsManager = new SslSettingsManager(TestEnvironment.newEnvironment(settingsBuilder.build())); + assertThat("Loaded HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isPresent()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for HTTP", + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::sslContext).map(SslContext::isServer).orElse(false) + ); + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildTransportSslContext() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + final var sslSettingsManager = new SslSettingsManager(TestEnvironment.newEnvironment(settingsBuilder.build())); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + assertThat( + "SSL configuration for Transport and Transport Client is the same", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .flatMap(t -> sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).map(tc -> tc.equals(t))) + .orElse(false) + ); + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildExtendedTransportSslContexts() throws Exception { + writeCertificates( + "ca_server_transport_certificate.pem", + "access_server_transport_certificate.pem", + "access_server_transport_certificate_pk.pem" + ); + writeCertificates( + "ca_client_transport_certificate.pem", + "access_client_transport_certificate.pem", + "access_client_transport_certificate_pk.pem" + ); + + final var securitySettings = new MockSecureSettings(); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "pemkey_password_secure", + certificatesRule.privateKeyPassword() + ); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "pemkey_password_secure", + certificatesRule.privateKeyPassword() + ); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, path("ca_server_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, path("access_server_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, path("access_server_transport_certificate_pk.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, path("ca_client_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, path("access_client_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, path("access_client_transport_certificate_pk.pem")) + .setSecureSettings(securitySettings) + .build() + ) + ); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + assertThat( + "SSL configuration for Transport and Transport Client is not the same", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .flatMap(t -> sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).map(tc -> !tc.equals(t))) + .orElse(true) + ); + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + private void withTransportSslSettings( + final Settings.Builder settingsBuilder, + final String caFileName, + final String accessFileName, + final String accessPkFileName + ) { + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, path(caFileName)) + .put(SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, path(accessFileName)) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, path(accessPkFileName)); + } + + private void withHttpSslSettings(final Settings.Builder settingsBuilder) { + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, path("ca_http_certificate.pem")) + .put(SECURITY_SSL_HTTP_PEMCERT_FILEPATH, path("access_http_certificate.pem")) + .put(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, path("access_http_certificate_pk.pem")); + } + + Settings.Builder defaultSettingsBuilder() { + return Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), certificatesRule.configRootFolder().toString()) + .put("client.type", "node"); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java b/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java new file mode 100644 index 0000000000..5fe2185d44 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java @@ -0,0 +1,38 @@ +/* + * 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.ssl.config; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +public class CertificateTest { + + @Test + public void testGetObjectMethod() { + try { + final Method method = Certificate.getObjectMethod(); + assertThat("Method should not be null", method, notNullValue()); + assertThat( + "One of the expected methods should be available", + method.getName().equals("getBaseObject") || method.getName().equals("getObject") + ); + } catch (ClassNotFoundException | NoSuchMethodException e) { + fail("Exception should not be thrown: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java new file mode 100644 index 0000000000..174f6c0fd5 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java @@ -0,0 +1,318 @@ +/* + * 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.ssl.config; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Test; + +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.env.TestEnvironment; + +import static java.util.Objects.isNull; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; + +public class JdkSslCertificatesLoaderTest extends SslCertificatesLoaderTest { + + static final Function resolveKeyStoreType = s -> isNull(s) ? KeyStore.getDefaultType() : s; + + static final String SERVER_TRUSTSTORE_ALIAS = "server-truststore-alias"; + + static final String SERVER_KEYSTORE_ALIAS = "server-keystore-alias"; + + static final String CLIENT_TRUSTSTORE_ALIAS = "client-truststore-alias"; + + static final String CLIENT_KEYSTORE_ALIAS = "client-keystore-alias"; + + @Test + public void loadHttpSslConfigurationFromKeyAndTrustStoreFiles() throws Exception { + testJdkBasedSslConfiguration(SSL_HTTP_PREFIX, randomBoolean()); + } + + @Test + public void loadTransportJdkBasedSslConfiguration() throws Exception { + testJdkBasedSslConfiguration(SSL_TRANSPORT_PREFIX, true); + } + + @Test + public void loadTransportJdkBasedSslExtendedConfiguration() throws Exception { + final var clientKeyPair = certificatesRule.generateKeyPair(); + + final var serverCaCertificate = certificatesRule.x509CaCertificate(); + final var clientCaCertificate = certificatesRule.toX509Certificate(certificatesRule.generateCaCertificate(clientKeyPair)); + + final var serverAccessCertificateKey = certificatesRule.accessCertificatePrivateKey(); + final var serverAccessCertificate = certificatesRule.x509AccessCertificate(); + + final var clientAccessCertificateAndKey = certificatesRule.generateAccessCertificate(clientKeyPair); + + final var clientAccessCertificateKey = clientAccessCertificateAndKey.v1(); + final var clientAccessCertificate = certificatesRule.toX509Certificate(clientAccessCertificateAndKey.v2()); + + final var trustStoreType = randomKeyStoreType(); + final var keyStoreType = randomKeyStoreType(); + + final var useSecurePassword = randomBoolean(); + final var trustStorePassword = randomKeyStorePassword(useSecurePassword); + final var keyStorePassword = randomKeyStorePassword(useSecurePassword); + + final var trustStorePath = createTrustStore( + trustStoreType, + trustStorePassword, + Map.of(SERVER_TRUSTSTORE_ALIAS, serverCaCertificate, CLIENT_TRUSTSTORE_ALIAS, clientCaCertificate) + ); + final var keyStorePath = createKeyStore( + keyStoreType, + keyStorePassword, + Map.of( + SERVER_KEYSTORE_ALIAS, + Tuple.tuple(serverAccessCertificateKey, serverAccessCertificate), + CLIENT_KEYSTORE_ALIAS, + Tuple.tuple(clientAccessCertificateKey, clientAccessCertificate) + ) + ); + + final var settingsBuilder = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE, trustStoreType) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, trustStorePath) + .put(SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, SERVER_TRUSTSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS, CLIENT_TRUSTSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, keyStoreType) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, keyStorePath) + .put(SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, SERVER_KEYSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, CLIENT_KEYSTORE_ALIAS); + + if (useSecurePassword) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "keystore_password_secure", keyStorePassword); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "truststore_password_secure", trustStorePassword); + + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "keystore_keypassword_secure", + certificatesRule.privateKeyPassword() + ); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "keystore_keypassword_secure", + certificatesRule.privateKeyPassword() + ); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "keystore_password", keyStorePassword); + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "truststore_password", trustStorePassword); + + settingsBuilder.put( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "keystore_keypassword", + certificatesRule.privateKeyPassword() + ); + settingsBuilder.put( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "keystore_keypassword", + certificatesRule.privateKeyPassword() + ); + } + final var settings = settingsBuilder.build(); + + final var serverConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_SERVER_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + serverConfiguration.v1(), + trustStorePath, + new Certificate(serverCaCertificate, resolveKeyStoreType.apply(trustStoreType), SERVER_TRUSTSTORE_ALIAS, false) + ); + assertKeyStoreConfiguration( + serverConfiguration.v2(), + List.of(keyStorePath), + new Certificate(serverAccessCertificate, resolveKeyStoreType.apply(keyStoreType), SERVER_KEYSTORE_ALIAS, true) + ); + + final var clientConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + clientConfiguration.v1(), + trustStorePath, + new Certificate(clientCaCertificate, resolveKeyStoreType.apply(trustStoreType), CLIENT_TRUSTSTORE_ALIAS, false) + ); + assertKeyStoreConfiguration( + clientConfiguration.v2(), + List.of(keyStorePath), + new Certificate(clientAccessCertificate, resolveKeyStoreType.apply(keyStoreType), CLIENT_KEYSTORE_ALIAS, true) + ); + } + + private void testJdkBasedSslConfiguration(final String sslConfigPrefix, final boolean useAuthorityCertificate) throws Exception { + final var useSecurePassword = randomBoolean(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var trustStoreCertificates = Map.of( + "default-truststore-alias", + certificatesRule.x509CaCertificate(), + "another-truststore-alias", + certificatesRule.toX509Certificate(certificatesRule.generateCaCertificate(keyPair)) + ); + + final var keysAndCertificate = certificatesRule.generateAccessCertificate(keyPair); + final var keyStoreCertificates = Map.of( + "default-keystore-alias", + Tuple.tuple(certificatesRule.accessCertificatePrivateKey(), certificatesRule.x509AccessCertificate()), + "another-keystore-alias", + Tuple.tuple(keysAndCertificate.v1(), certificatesRule.toX509Certificate(keysAndCertificate.v2())) + ); + + final var trustStoreAlias = randomFrom(new String[] { "default-truststore-alias", "another-truststore-alias", null }); + final var keyStoreAlias = (String) null;// randomFrom(new String[] { "default-keystore-alias", "another-keystore-alias", null }); + + final var keyStorePassword = randomKeyStorePassword(useSecurePassword); + final var trustStorePassword = randomKeyStorePassword(useSecurePassword); + + final var keyStoreType = randomKeyStoreType(); + final var keyStorePath = createKeyStore(keyStoreType, keyStorePassword, keyStoreCertificates); + + final var trustStoreType = randomKeyStoreType(); + final var trustStorePath = createTrustStore(trustStoreType, trustStorePassword, trustStoreCertificates); + + final var settingsBuilder = defaultSettingsBuilder().put(sslConfigPrefix + ENABLED, true) + .put(sslConfigPrefix + KEYSTORE_FILEPATH, keyStorePath) + .put(sslConfigPrefix + KEYSTORE_ALIAS, keyStoreAlias) + .put(sslConfigPrefix + KEYSTORE_TYPE, keyStoreType); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + TRUSTSTORE_FILEPATH, trustStorePath) + .put(sslConfigPrefix + TRUSTSTORE_ALIAS, trustStoreAlias) + .put(sslConfigPrefix + TRUSTSTORE_TYPE, trustStoreType); + } + if (useSecurePassword) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(sslConfigPrefix + "keystore_password_secure", keyStorePassword); + securitySettings.setString(sslConfigPrefix + "keystore_keypassword_secure", certificatesRule.privateKeyPassword()); + if (useAuthorityCertificate) { + securitySettings.setString(sslConfigPrefix + "truststore_password_secure", trustStorePassword); + } + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(sslConfigPrefix + "keystore_password", keyStorePassword); + settingsBuilder.put(sslConfigPrefix + "keystore_keypassword", certificatesRule.privateKeyPassword()); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + "truststore_password", trustStorePassword); + } + } + + final var configuration = new SslCertificatesLoader(sslConfigPrefix).loadConfiguration( + TestEnvironment.newEnvironment(settingsBuilder.build()) + ); + + if (useAuthorityCertificate) { + final var expectedTrustStoreCertificates = isNull(trustStoreAlias) + ? trustStoreCertificates.entrySet() + .stream() + .map(e -> new Certificate(e.getValue(), resolveKeyStoreType.apply(trustStoreType), e.getKey(), false)) + .toArray(Certificate[]::new) + : trustStoreCertificates.entrySet() + .stream() + .filter(e -> e.getKey().equals(trustStoreAlias)) + .map(e -> new Certificate(e.getValue(), resolveKeyStoreType.apply(trustStoreType), e.getKey(), false)) + .toArray(Certificate[]::new); + assertTrustStoreConfiguration(configuration.v1(), trustStorePath, expectedTrustStoreCertificates); + } else { + assertThat(configuration.v1(), is(TrustStoreConfiguration.EMPTY_CONFIGURATION)); + } + + final var expectedKeyStoreCertificates = isNull(keyStoreAlias) + ? keyStoreCertificates.entrySet() + .stream() + .map(e -> new Certificate(e.getValue().v2(), resolveKeyStoreType.apply(keyStoreType), e.getKey(), true)) + .toArray(Certificate[]::new) + : keyStoreCertificates.entrySet() + .stream() + .filter(e -> e.getKey().equals(keyStoreAlias)) + .map(e -> new Certificate(e.getValue().v2(), resolveKeyStoreType.apply(keyStoreType), e.getKey(), true)) + .toArray(Certificate[]::new); + assertKeyStoreConfiguration(configuration.v2(), List.of(keyStorePath), expectedKeyStoreCertificates); + } + + String randomKeyStoreType() { + return randomFrom(new String[] { "jks", "pkcs12", null }); + } + + String randomKeyStorePassword(final boolean useSecurePassword) { + return useSecurePassword ? randomAsciiAlphanumOfLength(10) : randomFrom(new String[] { randomAsciiAlphanumOfLength(10), null }); + } + + Path createTrustStore(final String type, final String password, Map certificates) throws Exception { + final var keyStore = keyStore(type); + for (final var alias : certificates.keySet()) { + keyStore.setCertificateEntry(alias, certificates.get(alias)); + } + final var trustStorePath = path(String.format("truststore.%s", isNull(type) ? "jsk" : type)); + storeKeyStore(keyStore, trustStorePath, password); + return trustStorePath; + } + + Path createKeyStore(final String type, final String password, final Map> keysAndCertificates) + throws Exception { + final var keyStore = keyStore(type); + final var keyStorePath = path(String.format("keystore.%s", isNull(type) ? "jsk" : type)); + for (final var alias : keysAndCertificates.keySet()) { + final var keyAndCertificate = keysAndCertificates.get(alias); + keyStore.setKeyEntry( + alias, + keyAndCertificate.v1(), + certificatesRule.privateKeyPassword().toCharArray(), + new X509Certificate[] { keyAndCertificate.v2() } + ); + } + storeKeyStore(keyStore, keyStorePath, password); + return keyStorePath; + } + + KeyStore keyStore(final String type) throws Exception { + final var keyStore = KeyStore.getInstance(isNull(type) ? KeyStore.getDefaultType() : type); + keyStore.load(null, null); + return keyStore; + } + + void storeKeyStore(final KeyStore keyStore, final Path path, final String password) throws Exception { + try (final var out = Files.newOutputStream(path)) { + keyStore.store(out, isNull(password) ? DEFAULT_STORE_PASSWORD.toCharArray() : password.toCharArray()); + } + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java new file mode 100644 index 0000000000..d03bf9c59d --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java @@ -0,0 +1,174 @@ +/* + * 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.ssl.config; + +import java.security.SecureRandom; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; + +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.env.TestEnvironment; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; + +public class PemSslCertificatesLoaderTest extends SslCertificatesLoaderTest { + + final static String PEM_CA_CERTIFICATE_FILE_NAME = "ca_certificate.pem"; + + final static String PEM_KEY_CERTIFICATE_FILE_NAME = "key_certificate.pem"; + + final static String PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME = "private_key.pem"; + + @BeforeClass + public static void setup() throws Exception { + writePemContent(path(PEM_CA_CERTIFICATE_FILE_NAME), certificatesRule.caCertificateHolder()); + writePemContent(path(PEM_KEY_CERTIFICATE_FILE_NAME), certificatesRule.accessCertificateHolder()); + writePemContent( + path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME), + new PKCS8Generator( + PrivateKeyInfo.getInstance(certificatesRule.accessCertificatePrivateKey().getEncoded()), + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + .setPassword(certificatesRule.privateKeyPassword().toCharArray()) + .build() + ).generate() + ); + } + + @Test + public void loadHttpSslConfigurationFromPemFiles() throws Exception { + testLoadPemBasedConfiguration(SSL_HTTP_PREFIX, randomBoolean()); + } + + @Test + public void loadTransportSslConfigurationFromPemFiles() throws Exception { + testLoadPemBasedConfiguration(SSL_HTTP_PREFIX, false); + } + + void testLoadPemBasedConfiguration(final String sslConfigPrefix, final boolean useAuthorityCertificate) throws Exception { + final var settingsBuilder = defaultSettingsBuilder().put(sslConfigPrefix + ENABLED, true) + .put(sslConfigPrefix + PEM_CERT_FILEPATH, path(PEM_KEY_CERTIFICATE_FILE_NAME)) + .put(sslConfigPrefix + PEM_KEY_FILEPATH, path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + PEM_TRUSTED_CAS_FILEPATH, path(PEM_CA_CERTIFICATE_FILE_NAME)); + } + if (randomBoolean()) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(sslConfigPrefix + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(sslConfigPrefix + "pemkey_password", certificatesRule.privateKeyPassword()); + } + + final var settings = settingsBuilder.build(); + final var configuration = new SslCertificatesLoader(SSL_HTTP_PREFIX).loadConfiguration(TestEnvironment.newEnvironment(settings)); + if (useAuthorityCertificate) { + assertTrustStoreConfiguration( + configuration.v1(), + path(PEM_CA_CERTIFICATE_FILE_NAME), + new Certificate(certificatesRule.x509CaCertificate(), false) + ); + } else { + assertThat(configuration.v1(), is(TrustStoreConfiguration.EMPTY_CONFIGURATION)); + } + assertKeyStoreConfiguration( + configuration.v2(), + List.of(path(PEM_KEY_CERTIFICATE_FILE_NAME), path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)), + new Certificate(certificatesRule.x509AccessCertificate(), true) + ); + } + + @Test + public void loadExtendedTransportSslConfigurationFromPemFiles() throws Exception { + final var keyPair = certificatesRule.generateKeyPair(); + final var clientCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var keyAndCertificate = certificatesRule.generateAccessCertificate(keyPair); + final var clientCaCertificatePath = "client_ca_certificate.pem"; + final var clientKeyCertificatePath = "client_key_certificate.pem"; + final var clientPrivateKeyCertificatePath = "client_private_key_certificate.pem"; + final var clientPrivateKeyPassword = RandomStringUtils.randomAlphabetic(10); + + writePemContent(path(clientCaCertificatePath), clientCaCertificate); + writePemContent(path(clientKeyCertificatePath), keyAndCertificate.v2()); + writePemContent(path(clientPrivateKeyCertificatePath), privateKeyToPemObject(keyAndCertificate.v1(), clientPrivateKeyPassword)); + + final var settingsBuilder = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, path(PEM_CA_CERTIFICATE_FILE_NAME)) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, path(PEM_KEY_CERTIFICATE_FILE_NAME)) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)) + + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, path(clientCaCertificatePath)) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, path(clientKeyCertificatePath)) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, path(clientPrivateKeyCertificatePath)); + if (randomBoolean()) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "server.pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "client.pemkey_password_secure", clientPrivateKeyPassword); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "server.pemkey_password", certificatesRule.privateKeyPassword()); + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "client.pemkey_password", clientPrivateKeyPassword); + } + final var settings = settingsBuilder.build(); + + final var transportServerConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_SERVER_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + transportServerConfiguration.v1(), + path(PEM_CA_CERTIFICATE_FILE_NAME), + new Certificate(certificatesRule.x509CaCertificate(), false) + ); + assertKeyStoreConfiguration( + transportServerConfiguration.v2(), + List.of(path(PEM_KEY_CERTIFICATE_FILE_NAME), path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)), + new Certificate(certificatesRule.x509AccessCertificate(), true) + ); + final var transportClientConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + transportClientConfiguration.v1(), + path(clientCaCertificatePath), + new Certificate(certificatesRule.toX509Certificate(clientCaCertificate), false) + ); + assertKeyStoreConfiguration( + transportClientConfiguration.v2(), + List.of(path(clientKeyCertificatePath), path(clientPrivateKeyCertificatePath)), + new Certificate(certificatesRule.toX509Certificate(keyAndCertificate.v2()), true) + ); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java new file mode 100644 index 0000000000..0dfc02b386 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java @@ -0,0 +1,66 @@ +/* + * 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.ssl.config; + +import java.nio.file.Path; +import java.util.List; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import org.junit.ClassRule; + +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.security.ssl.CertificatesRule; + +import static java.util.Objects.nonNull; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; + +public abstract class SslCertificatesLoaderTest extends RandomizedTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + static Path path(final String fileName) { + return certificatesRule.configRootFolder().resolve(fileName); + } + + Settings.Builder defaultSettingsBuilder() throws Exception { + return Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), certificatesRule.caCertificateHolder().toString()); + } + + void assertTrustStoreConfiguration( + final TrustStoreConfiguration trustStoreConfiguration, + final Path expectedFile, + final Certificate... expectedCertificates + ) { + assertThat("Truststore configuration created", nonNull(trustStoreConfiguration)); + assertThat(trustStoreConfiguration.file(), is(expectedFile)); + assertThat(trustStoreConfiguration.loadCertificates(), containsInAnyOrder(expectedCertificates)); + assertThat(trustStoreConfiguration.createTrustManagerFactory(true), is(notNullValue())); + } + + void assertKeyStoreConfiguration( + final KeyStoreConfiguration keyStoreConfiguration, + final List expectedFiles, + final Certificate... expectedCertificates + ) { + assertThat("Keystore configuration created", nonNull(keyStoreConfiguration)); + assertThat(keyStoreConfiguration.files(), contains(expectedFiles.toArray(new Path[0]))); + assertThat(keyStoreConfiguration.loadCertificates(), containsInAnyOrder(expectedCertificates)); + assertThat(keyStoreConfiguration.createKeyManagerFactory(true), is(notNullValue())); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java b/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java new file mode 100644 index 0000000000..d95c336e15 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java @@ -0,0 +1,90 @@ +/* + * 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.ssl.config; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslProvider; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; + +public class SslParametersTest { + + @Test + public void testDefaultSslParameters() throws Exception { + final var settings = Settings.EMPTY; + final var httpSslParameters = SslParameters.loader(settings).load(true); + final var transportSslParameters = SslParameters.loader(settings).load(false); + + final var defaultCiphers = List.of(ALLOWED_SSL_CIPHERS); + final var finalDefaultCiphers = Stream.of(SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites()) + .filter(defaultCiphers::contains) + .sorted(String::compareTo) + .collect(Collectors.toList()); + + assertThat(httpSslParameters.provider(), is(SslProvider.JDK)); + assertThat(transportSslParameters.provider(), is(SslProvider.JDK)); + + assertThat(httpSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(httpSslParameters.allowedCiphers(), is(finalDefaultCiphers)); + + assertThat(transportSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(transportSslParameters.allowedCiphers(), is(finalDefaultCiphers)); + + assertThat(httpSslParameters.clientAuth(), is(ClientAuth.OPTIONAL)); + assertThat(transportSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + } + + @Test + public void testCustomSSlParameters() { + final var settings = Settings.builder() + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .putList(SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, List.of("TLSv1.2", "TLSv1")) + .putList(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of("TLS_AES_256_GCM_SHA384")) + .putList(SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, List.of("TLSv1.3", "TLSv1.2")) + .putList(SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS, List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384")) + .build(); + final var httpSslParameters = SslParameters.loader(settings.getByPrefix(SSL_HTTP_PREFIX)).load(true); + final var transportSslParameters = SslParameters.loader(settings.getByPrefix(SSL_TRANSPORT_PREFIX)).load(false); + + assertThat(httpSslParameters.provider(), is(SslProvider.JDK)); + assertThat(transportSslParameters.provider(), is(SslProvider.JDK)); + + assertThat(httpSslParameters.allowedProtocols(), is(List.of("TLSv1.2"))); + assertThat(httpSslParameters.allowedCiphers(), is(List.of("TLS_AES_256_GCM_SHA384"))); + + assertThat(transportSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(transportSslParameters.allowedCiphers(), is(List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"))); + + assertThat(httpSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + assertThat(transportSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + } + +} From 8b71209e8452212fdd88839bd8cbbc20524ad137 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 22 Oct 2024 15:27:04 -0400 Subject: [PATCH 58/62] Add release notes for 2.18 (#4834) Signed-off-by: Derek Ho --- ...nsearch-security.release-notes-2.18.0.0.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 release-notes/opensearch-security.release-notes-2.18.0.0.md diff --git a/release-notes/opensearch-security.release-notes-2.18.0.0.md b/release-notes/opensearch-security.release-notes-2.18.0.0.md new file mode 100644 index 0000000000..b6faf32235 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.18.0.0.md @@ -0,0 +1,48 @@ +## Version 2.18.0 Release Notes + +Compatible with OpenSearch and OpenSearch Dashboards version 2.18.0 + +### Enhancements +* Improve error message when a node with an incorrectly configured certificate attempts to connect ([#4819](https://github.com/opensearch-project/security/pull/4819)) +* Support datastreams as an AuditLog Sink ([#4756](https://github.com/opensearch-project/security/pull/4756)) +* Auto-convert V6 configuration instances into V7 configuration instances (for OpenSearch 2.x only) ([#4753](https://github.com/opensearch-project/security/pull/4753)) +* Add can trip circuit breaker override ([#4779](https://github.com/opensearch-project/security/pull/4779)) +* Adding index permissions for remote index in AD ([#4721](https://github.com/opensearch-project/security/pull/4721)) +* Fix env var password hashing for PBKDF2 ([#4778](https://github.com/opensearch-project/security/pull/4778)) +* Add ensureCustomSerialization to ensure that headers are serialized correctly with multiple transport hops ([#4741](https://github.com/opensearch-project/security/pull/4741)) + +### Bug Fixes +* Handle non-flat yaml settings for demo configuration detection ([#4798](https://github.com/opensearch-project/security/pull/4798)) +* Fix bug where admin can read system index ([#4775](https://github.com/opensearch-project/security/pull/4775)) +* Ensure that dual mode enabled flag from cluster settings can get propagated to core ([#4830](https://github.com/opensearch-project/security/pull/4830)) +* Remove failed login attempt for saml authenticator ([#4770](https://github.com/opensearch-project/security/pull/4770)) +* Fix issue in HashingStoredFieldVisitor with stored fields ([#4827](https://github.com/opensearch-project/security/pull/4827)) +* Fix issue with Get mappings on a Closed index ([#4777](https://github.com/opensearch-project/security/pull/4777)) +* changing comments permission for alerting_ack_alerts role ([#4723](https://github.com/opensearch-project/security/pull/4723)) +* Fixed use of rolesMappingConfiguration in InternalUsersApiActionValidationTest ([#4754](https://github.com/opensearch-project/security/pull/4754)) +* Use evaluateSslExceptionHandler() when constructing OpenSearchSecureSettingsFactory ([#4726](https://github.com/opensearch-project/security/pull/4726)) + +### Maintenance +* Bump gradle to 8.10.2 ([#4829](https://github.com/opensearch-project/security/pull/4829)) +* Bump ch.qos.logback:logback-classic from 1.5.8 to 1.5.11 ([#4807](https://github.com/opensearch-project/security/pull/4807)) ([#4825](https://github.com/opensearch-project/security/pull/4825)) +* Bump org.passay:passay from 1.6.5 to 1.6.6 ([#4824](https://github.com/opensearch-project/security/pull/4824)) +* Bump org.junit.jupiter:junit-jupiter from 5.11.0 to 5.11.2 ([#4767](https://github.com/opensearch-project/security/pull/4767)) ([#4811](https://github.com/opensearch-project/security/pull/4811)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.27 to 4.2.28 ([#4789](https://github.com/opensearch-project/security/pull/4789)) +* Bump com.nimbusds:nimbus-jose-jwt from 9.40 to 9.41.2 ([#4737](https://github.com/opensearch-project/security/pull/4737)) ([#4787](https://github.com/opensearch-project/security/pull/4787)) +* Bump org.ow2.asm:asm from 9.7 to 9.7.1 ([#4788](https://github.com/opensearch-project/security/pull/4788)) +* Bump com.google.googlejavaformat:google-java-format from 1.23.0 to 1.24.0 ([#4786](https://github.com/opensearch-project/security/pull/4786)) +* Bump org.xerial.snappy:snappy-java from 1.1.10.6 to 1.1.10.7 ([#4738](https://github.com/opensearch-project/security/pull/4738)) +* Bump org.gradle.test-retry from 1.5.10 to 1.6.0 ([#4736](https://github.com/opensearch-project/security/pull/4736)) +* Moves @cliu123 to emeritus status ([#4667](https://github.com/opensearch-project/security/pull/4667)) +* Add Derek Ho (github: derek-ho) as a maintainer ([#4796](https://github.com/opensearch-project/security/pull/4796)) +* Add deprecation warning for GET/POST/PUT cache ([#4776](https://github.com/opensearch-project/security/pull/4776)) +* Fix for: CVE-2024-47554 ([#4792](https://github.com/opensearch-project/security/pull/4792)) +* Move Stephen to emeritus ([#4804](https://github.com/opensearch-project/security/pull/4804)) +* Undeprecate securityadmin script ([#4768](https://github.com/opensearch-project/security/pull/4768)) +* Bump commons-io:commons-io from 2.16.1 to 2.17.0 ([#4750](https://github.com/opensearch-project/security/pull/4750)) +* Bump org.scala-lang:scala-library from 2.13.14 to 2.13.15 ([#4749](https://github.com/opensearch-project/security/pull/4749)) +* org.checkerframework:checker-qual and ch.qos.logback:logback-classic to new versions ([#4717](https://github.com/opensearch-project/security/pull/4717)) +* Add isActionPaginated to DelegatingRestHandler ([#4765](https://github.com/opensearch-project/security/pull/4765)) +* Refactor ASN1 call ([#4740](https://github.com/opensearch-project/security/pull/4740)) +* Fix 'integTest' not called with test workflows during release ([#4815](https://github.com/opensearch-project/security/pull/4815)) +* Fixed bulk index requests in BWC tests and hardened assertions ([#4831](https://github.com/opensearch-project/security/pull/4831)) From eb7f8218be7ed49da4d76fbb22b7b473847c4068 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 23 Oct 2024 13:59:21 -0400 Subject: [PATCH 59/62] Generalize public key reading in the JWT authenticator (#4833) Signed-off-by: Craig Perkins --- .../opensearch/security/util/KeyUtils.java | 4 +- .../http/jwt/HTTPJwtAuthenticatorTest.java | 72 +++++++++++++------ 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java index 920cf198be..bb2abea795 100644 --- a/src/main/java/org/opensearch/security/util/KeyUtils.java +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -52,8 +52,8 @@ public JwtParserBuilder run() { } else { try { PublicKey key = null; - - final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") + final String minimalKeyFormat = signingKey.replaceAll("\\r|\\n", "") + .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "") .trim(); final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java index 4214e8ed06..48a14916a0 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java @@ -389,7 +389,6 @@ public void testNbf() throws Exception { @Test public void testRS256() throws Exception { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); @@ -397,27 +396,61 @@ public void testRS256() throws Exception { PublicKey pub = pair.getPublic(); String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(priv, SignatureAlgorithm.RS256).compact(); - Settings settings = Settings.builder() - .put( - "signing_key", - "-----BEGIN PUBLIC KEY-----\n" + BaseEncoding.base64().encode(pub.getEncoded()) + "-----END PUBLIC KEY-----" - ) - .build(); + String signingKey = "-----BEGIN PUBLIC KEY-----\n" + BaseEncoding.base64().encode(pub.getEncoded()) + "-----END PUBLIC KEY-----"; + AuthCredentials creds = testJwtAuthenticationWithSigningKey(signingKey, jwsToken); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); - Map headers = new HashMap(); - headers.put("Authorization", "Bearer " + jwsToken); + Assert.assertNotNull(creds); + assertThat(creds.getUsername(), is("Leonard McCoy")); + assertThat(creds.getBackendRoles().size(), is(0)); + } - AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), - null - ); + private static String formatKeyWithNewlines(String keyAsString) { + StringBuilder result = new StringBuilder(); + int lineLength = 64; + int length = keyAsString.length(); + + for (int i = 0; i < length; i += lineLength) { + if (i + lineLength < length) { + result.append(keyAsString, i, i + lineLength); + } else { + result.append(keyAsString.substring(i)); + } + result.append("\n"); + } + + return result.toString().trim(); + } + + @Test + public void testRS256WithNewlines() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair = keyGen.generateKeyPair(); + PrivateKey priv = pair.getPrivate(); + PublicKey pub = pair.getPublic(); + + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(priv, SignatureAlgorithm.RS256).compact(); + + String signingKey = "-----BEGIN PUBLIC KEY-----\n" + + formatKeyWithNewlines(BaseEncoding.base64().encode(pub.getEncoded())) + + "\n-----END PUBLIC KEY-----"; + AuthCredentials creds = testJwtAuthenticationWithSigningKey(signingKey, jwsToken); Assert.assertNotNull(creds); assertThat(creds.getUsername(), is("Leonard McCoy")); assertThat(creds.getBackendRoles().size(), is(0)); } + private AuthCredentials testJwtAuthenticationWithSigningKey(String signingKey, String jwsToken) throws NoSuchAlgorithmException { + Settings settings = Settings.builder().put("signing_key", signingKey).build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), null); + } + @Test public void testES512() throws Exception { @@ -427,17 +460,10 @@ public void testES512() throws Exception { PrivateKey priv = pair.getPrivate(); PublicKey pub = pair.getPublic(); - Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(pub.getEncoded())).build(); + String signingKey = BaseEncoding.base64().encode(pub.getEncoded()); String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(priv, SignatureAlgorithm.ES512).compact(); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); - Map headers = new HashMap(); - headers.put("Authorization", jwsToken); - - AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), - null - ); + AuthCredentials creds = testJwtAuthenticationWithSigningKey(signingKey, jwsToken); Assert.assertNotNull(creds); assertThat(creds.getUsername(), is("Leonard McCoy")); From c8cacf1259a611934da6aa631a65d5ed0fe3d216 Mon Sep 17 00:00:00 2001 From: parisl <11188036+parislarkins@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:38:25 +1100 Subject: [PATCH 60/62] Allow skipping hot reload dn validation (#4752) Signed-off-by: Paris Larkins Signed-off-by: Paris Larkins --- .../security/ssl/DefaultSecurityKeyStore.java | 31 ++- .../ssl/OpenSearchSecuritySSLPlugin.java | 17 ++ .../security/ssl/util/SSLConfigConstants.java | 4 + .../SecuritySSLReloadCertsActionTests.java | 217 +++++++++++++++--- src/test/resources/ssl/reload/README.txt | 29 +++ .../resources/ssl/reload/node-new-ca.crt.pem | 82 +++++++ .../resources/ssl/reload/node-new-ca.key.pem | 28 +++ .../ssl/reload/secondary-root-ca.pem | 29 +++ .../ssl/reload/secondary-signing-ca.pem | 29 +++ src/test/resources/ssl/reload/truststore.jks | Bin 1398 -> 4230 bytes 10 files changed, 432 insertions(+), 34 deletions(-) create mode 100644 src/test/resources/ssl/reload/README.txt create mode 100644 src/test/resources/ssl/reload/node-new-ca.crt.pem create mode 100644 src/test/resources/ssl/reload/node-new-ca.key.pem create mode 100644 src/test/resources/ssl/reload/secondary-root-ca.pem create mode 100644 src/test/resources/ssl/reload/secondary-signing-ca.pem diff --git a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java index 8dbd2f139a..61dac199ae 100644 --- a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java +++ b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java @@ -133,7 +133,9 @@ private void printJCEWarnings() { public final SslProvider sslTransportServerProvider; public final SslProvider sslTransportClientProvider; private final boolean httpSSLEnabled; + private final boolean httpSSLEnforceCertReloadDnVerification; private final boolean transportSSLEnabled; + private final boolean transportSSLEnforceCertReloadDnVerification; private ArrayList enabledHttpCiphersJDKProvider; private ArrayList enabledHttpCiphersOpenSSLProvider; @@ -166,10 +168,18 @@ public DefaultSecurityKeyStore(final Settings settings, final Path configPath) { SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_DEFAULT ); + httpSSLEnforceCertReloadDnVerification = settings.getAsBoolean( + SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true + ); transportSSLEnabled = settings.getAsBoolean( SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT ); + transportSSLEnforceCertReloadDnVerification = settings.getAsBoolean( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true + ); final boolean useOpenSSLForHttpIfAvailable = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, true); final boolean useOpenSSLForTransportIfAvailable = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED @@ -422,7 +432,7 @@ public void initTransportSSLConfig() { certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias); } - validateNewCerts(transportCerts, certFromKeystore.getCerts()); + validateNewCerts(transportCerts, certFromKeystore.getCerts(), transportSSLEnforceCertReloadDnVerification); transportServerSslContext = buildSSLServerContext( certFromKeystore.getServerKey(), certFromKeystore.getServerCert(), @@ -473,7 +483,7 @@ public void initTransportSSLConfig() { certFromFile = new CertFromFile(certProps); } - validateNewCerts(transportCerts, certFromFile.getCerts()); + validateNewCerts(transportCerts, certFromFile.getCerts(), transportSSLEnforceCertReloadDnVerification); transportServerSslContext = buildSSLServerContext( certFromFile.getServerPemKey(), certFromFile.getServerPemCert(), @@ -571,7 +581,7 @@ public void initHttpSSLConfig() { certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias); } - validateNewCerts(httpCerts, certFromKeystore.getCerts()); + validateNewCerts(httpCerts, certFromKeystore.getCerts(), httpSSLEnforceCertReloadDnVerification); httpSslContext = buildSSLServerContext( certFromKeystore.getServerKey(), certFromKeystore.getServerCert(), @@ -602,7 +612,7 @@ public void initHttpSSLConfig() { ); CertFromFile certFromFile = new CertFromFile(certFileProps); - validateNewCerts(httpCerts, certFromFile.getCerts()); + validateNewCerts(httpCerts, certFromFile.getCerts(), httpSSLEnforceCertReloadDnVerification); httpSslContext = buildSSLServerContext( certFromFile.getServerPemKey(), certFromFile.getServerPemCert(), @@ -633,11 +643,16 @@ public void initHttpSSLConfig() { * If the current and new certificates are same, skip remaining checks. * For new X509 cert to be valid Issuer, Subject DN must be the same and * new certificates should expire after current ones. - * @param currentX509Certs Array of current x509 certificates - * @param newX509Certs Array of x509 certificates which will replace our current cert + * @param currentX509Certs Array of current x509 certificates + * @param newX509Certs Array of x509 certificates which will replace our current cert + * @param verifyValidDNs Whether to verify that new certs have valid IssuerDN, SubjectDN and SAN * @throws Exception if certificate is invalid */ - private void validateNewCerts(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) throws Exception { + private void validateNewCerts( + final X509Certificate[] currentX509Certs, + final X509Certificate[] newX509Certs, + final boolean verifyValidDNs + ) throws Exception { // First time we init certs ignore validity check if (currentX509Certs == null) { @@ -654,7 +669,7 @@ private void validateNewCerts(final X509Certificate[] currentX509Certs, final X5 } // Check if new X509 certs have valid IssuerDN, SubjectDN or SAN - if (!hasValidDNs(currentX509Certs, newX509Certs)) { + if (verifyValidDNs && !hasValidDNs(currentX509Certs, newX509Certs)) { throw new Exception("New Certs do not have valid Issuer DN, Subject DN or SAN."); } } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index 25c55f3cbb..c12424f028 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -634,6 +634,23 @@ public List> getSettings() { Setting.longSetting(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_VALIDATION_DATE, -1, -1, Property.NodeScope, Property.Filtered) ); + settings.add( + Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true, + Property.NodeScope, + Property.Filtered + ) + ); + return settings; } diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index dfc9ae567e..003c46b093 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -82,6 +82,8 @@ public final class SSLConfigConstants { public static final String SECURITY_SSL_HTTP_TRUSTSTORE_ALIAS = "plugins.security.ssl.http.truststore_alias"; public static final String SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH = "plugins.security.ssl.http.truststore_filepath"; public static final String SECURITY_SSL_HTTP_TRUSTSTORE_TYPE = "plugins.security.ssl.http.truststore_type"; + public static final String SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION = + "plugins.security.ssl.http.enforce_cert_reload_dn_verification"; public static final String SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE = "plugins.security.ssl.transport.enable_openssl_if_available"; public static final String SECURITY_SSL_TRANSPORT_ENABLED = "plugins.security.ssl.transport.enabled"; @@ -91,6 +93,8 @@ public final class SSLConfigConstants { public static final String SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME = "plugins.security.ssl.transport.resolve_hostname"; + public static final String SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION = + "plugins.security.ssl.transport.enforce_cert_reload_dn_verification"; public static final String SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS = "plugins.security.ssl.transport.keystore_alias"; public static final String SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS = "plugins.security.ssl.transport.server.keystore_alias"; public static final String SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS = "plugins.security.ssl.transport.client.keystore_alias"; diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index 30635477eb..d3b428e9b2 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.Objects; import com.fasterxml.jackson.databind.JsonNode; import org.junit.After; @@ -44,10 +43,8 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { private final String RELOAD_HTTP_CERTS_ENDPOINT = "_opendistro/_security/api/ssl/http/reloadcerts"; @Rule public TemporaryFolder testFolder = new TemporaryFolder(); - private final String HTTP_CERTIFICATES_LIST_KEY = "http_certificates_list"; - private final String TRANSPORT_CERTIFICATES_LIST_KEY = "transport_certificates_list"; - private final List> NODE_CERT_DETAILS = List.of( + private final List> INITIAL_NODE_CERT_DETAILS = List.of( Map.of( "issuer_dn", "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", @@ -77,6 +74,21 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { ) ); + private final List> NEW_CA_NODE_CERT_DETAILS = List.of( + Map.of( + "issuer_dn", + "CN=Example Com Inc. Secondary Signing CA,OU=Example Com Inc. Secondary Signing CA,O=Example Com Inc.,DC=example,DC=com", + "subject_dn", + "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE", + "san", + "[[2, localhost], [2, node-1.example.com], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", + "not_before", + "2024-09-17T00:15:48Z", + "not_after", + "2034-09-15T00:15:48Z" + ) + ); + private String pemCertFilePath; private String pemKeyFilePath; private final String defaultCertFilePath = "ssl/reload/node.crt.pem"; @@ -116,7 +128,7 @@ public void testReloadTransportSSLCertsPass() throws Exception { updateFiles(newCertFilePath, pemCertFilePath); updateFiles(newKeyFilePath, pemKeyFilePath); - assertReloadCertificateSuccess(rh, "transport", getUpdatedCertDetailsExpectedResponse("transport")); + assertReloadCertificateSuccess(rh, "transport", getCertDetailsExpectedResponse(INITIAL_NODE_CERT_DETAILS, NEW_NODE_CERT_DETAILS)); } @Test @@ -133,7 +145,7 @@ public void testReloadHttpSSLCertsPass() throws Exception { updateFiles(newCertFilePath, pemCertFilePath); updateFiles(newKeyFilePath, pemKeyFilePath); - assertReloadCertificateSuccess(rh, "http", getUpdatedCertDetailsExpectedResponse("http")); + assertReloadCertificateSuccess(rh, "http", getCertDetailsExpectedResponse(NEW_NODE_CERT_DETAILS, INITIAL_NODE_CERT_DETAILS)); } @Test @@ -189,6 +201,148 @@ public void testReloadHttpSSLSameCertsPass() throws Exception { assertReloadCertificateSuccess(rh, "http", getInitCertDetailsExpectedResponse()); } + @Test + public void testReloadHttpCertDifferentTrustChain_skipDnValidationPass() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, false, true); + + RestHelper rh = getRestHelperAdminUser(); + // Change http certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(200)); + final var expectedJsonResponse = DefaultObjectMapper.objectMapper.createObjectNode(); + expectedJsonResponse.put("message", "updated http certs"); + assertThat(reloadCertsResponse.getBody(), is(expectedJsonResponse.toString())); + + String certDetailsResponse = rh.executeSimpleRequest(GET_CERT_DETAILS_ENDPOINT); + assertThat( + DefaultObjectMapper.readTree(certDetailsResponse), + is(getCertDetailsExpectedResponse(NEW_CA_NODE_CERT_DETAILS, INITIAL_NODE_CERT_DETAILS)) + ); + } + + @Test + public void testReloadHttpCertDifferentTrustChain_noSkipDnValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true); + + RestHelper rh = getRestHelperAdminUser(); + // Change http certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing http SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + + @Test + public void testReloadHttpCertDifferentTrustChain_defaultSettingValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, null, null); + + RestHelper rh = getRestHelperAdminUser(); + // Change http certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing http SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + + @Test + public void testReloadTransportCertDifferentTrustChain_skipDnValidationPass() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, false); + + RestHelper rh = getRestHelperAdminUser(); + // Change transport certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(200)); + final var expectedJsonResponse = DefaultObjectMapper.objectMapper.createObjectNode(); + expectedJsonResponse.put("message", "updated transport certs"); + assertThat(reloadCertsResponse.getBody(), is(expectedJsonResponse.toString())); + + String certDetailsResponse = rh.executeSimpleRequest(GET_CERT_DETAILS_ENDPOINT); + assertThat( + DefaultObjectMapper.readTree(certDetailsResponse), + is(getCertDetailsExpectedResponse(INITIAL_NODE_CERT_DETAILS, NEW_CA_NODE_CERT_DETAILS)) + ); + } + + @Test + public void testReloadTransportCertDifferentTrustChain_noSkipDnValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true); + + RestHelper rh = getRestHelperAdminUser(); + // Change transport certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + + @Test + public void testReloadTransportCertDifferentTrustChain_defaultSettingValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, null, null); + + RestHelper rh = getRestHelperAdminUser(); + // Change transport certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + /** * * @param rh RestHelper to perform rest actions on the cluster @@ -214,20 +368,18 @@ private void updateFiles(String srcFile, String dstFile) { FileHelper.copyFileContents(FileHelper.getAbsoluteFilePathFromClassPath(srcFile).toString(), dstFile); } - private JsonNode getUpdatedCertDetailsExpectedResponse(String updateChannel) { - String updateKey = (Objects.equals(updateChannel, "http")) ? HTTP_CERTIFICATES_LIST_KEY : TRANSPORT_CERTIFICATES_LIST_KEY; - String oldKey = (Objects.equals(updateChannel, "http")) ? TRANSPORT_CERTIFICATES_LIST_KEY : HTTP_CERTIFICATES_LIST_KEY; + private JsonNode getCertDetailsExpectedResponse( + List> httpCertDetails, + List> transportCertDetails + ) { final var updatedCertDetailsResponse = DefaultObjectMapper.objectMapper.createObjectNode(); - updatedCertDetailsResponse.set(updateKey, buildCertsInfoNode(NEW_NODE_CERT_DETAILS)); - updatedCertDetailsResponse.set(oldKey, buildCertsInfoNode(NODE_CERT_DETAILS)); + updatedCertDetailsResponse.set("http_certificates_list", buildCertsInfoNode(httpCertDetails)); + updatedCertDetailsResponse.set("transport_certificates_list", buildCertsInfoNode(transportCertDetails)); return updatedCertDetailsResponse; } private JsonNode getInitCertDetailsExpectedResponse() { - final var initCertDetailsResponse = DefaultObjectMapper.objectMapper.createObjectNode(); - initCertDetailsResponse.set(HTTP_CERTIFICATES_LIST_KEY, buildCertsInfoNode(NODE_CERT_DETAILS)); - initCertDetailsResponse.set(TRANSPORT_CERTIFICATES_LIST_KEY, buildCertsInfoNode(NODE_CERT_DETAILS)); - return initCertDetailsResponse; + return getCertDetailsExpectedResponse(INITIAL_NODE_CERT_DETAILS, INITIAL_NODE_CERT_DETAILS); } private JsonNode buildCertsInfoNode(final List> certsInfo) { @@ -273,25 +425,29 @@ private RestHelper getRestHelperNonAdminUser() { private void initClusterWithTestCerts() throws Exception { updateFiles(defaultCertFilePath, pemCertFilePath); updateFiles(defaultKeyFilePath, pemKeyFilePath); - initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true); } /** * Helper method to initialize test cluster for SSL Certificate Reload Tests - * @param transportPemCertFilePath Absolute Path to transport pem cert file - * @param transportPemKeyFilePath Absolute Path to transport pem key file - * @param httpPemCertFilePath Absolute Path to transport pem cert file - * @param httpPemKeyFilePath Absolute Path to transport pem key file - * @param sslCertReload Sets the ssl cert reload flag + * @param transportPemCertFilePath Absolute Path to transport pem cert file + * @param transportPemKeyFilePath Absolute Path to transport pem key file + * @param httpPemCertFilePath Absolute Path to transport pem cert file + * @param httpPemKeyFilePath Absolute Path to transport pem key file + * @param sslCertReload Sets the ssl cert reload flag + * @param httpEnforceReloadDnVerification Sets the http enforce reload dn verification flag if non-null + * @param transportEnforceReloadDnVerification Sets the transport enforce reload dn verification flag if non-null */ private void initTestCluster( final String transportPemCertFilePath, final String transportPemKeyFilePath, final String httpPemCertFilePath, final String httpPemKeyFilePath, - final boolean sslCertReload + final boolean sslCertReload, + final Boolean httpEnforceReloadDnVerification, + final Boolean transportEnforceReloadDnVerification ) throws Exception { - final Settings settings = Settings.builder() + final Settings.Builder settingsBuilder = Settings.builder() .putList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, "CN=kirk,OU=client,O=client,L=Test,C=DE") .putList(ConfigConstants.SECURITY_NODES_DN, "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE") .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true) @@ -310,8 +466,17 @@ private void initTestCluster( SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/reload/root-ca.pem") ) - .put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, sslCertReload) - .build(); + .put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, sslCertReload); + + if (httpEnforceReloadDnVerification != null) settingsBuilder.put( + SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + httpEnforceReloadDnVerification + ); + + if (transportEnforceReloadDnVerification != null) settingsBuilder.put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + transportEnforceReloadDnVerification + ); final Settings initTransportClientSettings = Settings.builder() .put( @@ -325,7 +490,7 @@ private void initTestCluster( ) .build(); - setup(initTransportClientSettings, new DynamicSecurityConfig(), settings, true, clusterConfiguration); + setup(initTransportClientSettings, new DynamicSecurityConfig(), settingsBuilder.build(), true, clusterConfiguration); } } diff --git a/src/test/resources/ssl/reload/README.txt b/src/test/resources/ssl/reload/README.txt new file mode 100644 index 0000000000..a149c42284 --- /dev/null +++ b/src/test/resources/ssl/reload/README.txt @@ -0,0 +1,29 @@ +Commands to generate node-new-ca.crt.pem, node-new-ca.key.pem, secondary-root-ca.pem, secondary-signing-ca.pem: + +# generate new secondary root CA +openssl genrsa -out secondary-root-ca-key.pem 2048 +openssl req -new -x509 -sha256 -days 3650 -key secondary-root-ca-key.pem -subj "/DC=com/DC=example/O=Example Com Inc./OU=Example Com Inc. Secondary Root CA/CN=Example Com Inc. Secondary Root CA" -addext "basicConstraints = critical,CA:TRUE" -addext "keyUsage = critical, digitalSignature, keyCertSign, cRLSign" -addext "subjectKeyIdentifier = hash" -addext "authorityKeyIdentifier = keyid:always,issuer:always" -out secondary-root-ca.pem + +# generate new secondary signing CA, signed by the new secondary root CA + +openssl genrsa -out secondary-signing-ca-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in secondary-signing-ca-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out secondary-signing-ca-key.pem +openssl req -new -key secondary-signing-ca-key.pem -subj "/DC=com/DC=example/O=Example Com Inc./OU=Example Com Inc. Secondary Signing CA/CN=Example Com Inc. Secondary Signing CA" -out secondary-signing-ca-key.csr +printf "basicConstraints = critical,CA:TRUE" > secondary-signing-ca_ext.conf +printf "basicConstraints = critical,CA:TRUE\nkeyUsage = critical, digitalSignature, keyCertSign, cRLSign\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always" > secondary-signing-ca_ext.conf +openssl x509 -req -in secondary-signing-ca-key.csr -out secondary-signing-ca.pem -CA secondary-root-ca.pem -CAkey secondary-root-ca-key.pem -CAcreateserial -days 3650 -extfile secondary-signing-ca_ext.conf + +# generate a new node cert, signed by the new secondary signing key CA +openssl genrsa -out node-new-ca-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in node-new-ca-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out node-new-ca.key.pem +openssl req -new -key node-new-ca.key.pem -subj "/C=DE/L=Test/O=Test/OU=SSL/CN=node-1.example.com" -out node-new-ca.csr +printf "subjectAltName = RID:1.2.3.4.5.5, DNS:node-1.example.com, DNS:localhost, IP:127.0.0.1" > node-new-ca_ext.conf +openssl x509 -req -in node-new-ca.csr -out node-new-ca.pem -CA secondary-signing-ca.pem -CAkey secondary-signing-ca-key.pem -CAcreateserial -days 3650 -extfile node-new-ca_ext.conf + +cat node-new-ca.pem > node-new-ca.crt.pem +cat secondary-signing-ca.pem >> node-new-ca.crt.pem +cat secondary-root-ca.pem >> node-new-ca.crt.pem + +# for tests to pass, the new secondary-signing-ca.pem and secondary-root-ca.pem keys should also be added to the truststore.jks file, e.g.: +keytool -import -alias secondary-root-ca -file secondary-root-ca.pem -storetype JKS -keystore truststore.jks +keytool -import -alias secondary-signing-ca -file secondary-signing-ca.pem -storetype JKS -keystore truststore.jks diff --git a/src/test/resources/ssl/reload/node-new-ca.crt.pem b/src/test/resources/ssl/reload/node-new-ca.crt.pem new file mode 100644 index 0000000000..2bf9284f09 --- /dev/null +++ b/src/test/resources/ssl/reload/node-new-ca.crt.pem @@ -0,0 +1,82 @@ +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgIUUN4lYU0yobNFo1xcluReeadmlaUwDQYJKoZIhvcNAQEL +BQAwgakxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMS4wLAYDVQQLDCVFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMS4wLAYDVQQDDCVFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMB4XDTI0MDkxNzAwMTU0OFoX +DTM0MDkxNTAwMTU0OFowVjELMAkGA1UEBhMCREUxDTALBgNVBAcMBFRlc3QxDTAL +BgNVBAoMBFRlc3QxDDAKBgNVBAsMA1NTTDEbMBkGA1UEAwwSbm9kZS0xLmV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtJG372aVfTYZ +tQ6udEQzC9RNy8+SqBZEproPBdYupOZ2l0tKgGykoAI0iX/p3gzQlYBSmSVduKLZ +n5E/nQCb+Rqbi1uoZrojEQxq538RXWmI9X72MyKFnqcgjZW9qCBn0ok5J0fSp7kS +55I6IzJhrJFqJKdn/i1dTReyg0tjSa/dR2yHbFj97gdXAnnte7xa87ounKZoFtme +rhhfVfbnkxQfSFecg0AltBiuhB9TxovRTo1TOVPpAUhBFBaj4ILSyGJdG9qQ11OM +L+QUd6TjQB0qFSVaf/BGu/0Umz1lp1OrrQkouaTQfuQ+3tOY3hwCM4PdL03YbBYX +r/H7EmhJ2QIDAQABo3kwdzA1BgNVHREELjAsiAUqAwQFBYISbm9kZS0xLmV4YW1w +bGUuY29tgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFCBH9UyAoNd2nq/4PHuP +6XqCvcMnMB8GA1UdIwQYMBaAFFLGvw6mimoIfjgzjHjhUfVNAeAIMA0GCSqGSIb3 +DQEBCwUAA4IBAQAOhTfnE+uTD0PQy+/HT2uQKRMsn+f6CeiHTgTWkA7+XXECXyBI +B8cGnXEqNRg7gInrnYpsNv19Q5v4fghMG+5mTO0iDhSCL3ttXVy3J7yvb9IWgc12 +34YC7BeTe8DB+vATTnxEibOqXX8YhB/n9pB/xoqs7XUTVTP56QYcMZZvjzdIJhp9 +kpydel7TIDqJmG7HPkjVn0caxdsGFaBF5XmI4o73xlJVEZrN5OMy9yao6kXrNiqD +GPRg6y3KTtrGXNImTs9+iJhLfBtT3i8/UU7T8vC9yfU6JDC6CWDRIwHNtIBY2Yp+ +cLMxoh/SZHFqLPguzH2RWmwa7mgEOet1RYVe +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFCDCCA/CgAwIBAgIUfUpmQ/BPCGTsPLW7rrPbkEU1RcwwDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgakxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMS4wLAYDVQQLDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMS4wLAYDVQQDDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjUd34V1uf+OwGXIBce+4O/UX35yCxY0LHt48 +wNIGkEs3StbTG5/qjkeAIFr2EUpRX5c0n5sIWdJX1cV/drWrhUzy6Ya1jvQiTA+i +k4YVVkFsz9QajgP+UPS06ZLkFldBofd/Su4GW5YEBlOBfxbsr8+E+73M/8sU1/wD +QLwPZGrkN7Cc37qi0Sf3blCNsjwfZPrGm+J/4hxdlJKuimo3Ctfwtlv/cIJZv7aG +RPksgsiirrk//nrW24wCQjqernuRayT0+2KL7OIn7UH2XL4nUUKU4cHYJOeiTNz+ +ds/uP1FG5WAvQ7CEyh6z2aXxGhZ89ZquATFy2paLpqfWgARiiwIDAQABo4IBKjCC +ASYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFFLG +vw6mimoIfjgzjHjhUfVNAeAIMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK +1x/HzhmeoYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPy +LGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UE +CwwiRXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwi +RXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K21 +3Aw9UyZJF+8wDQYJKoZIhvcNAQELBQADggEBAJo5QHvLgfH3VVJlNwFrGdNH1dCh +/mqPpqhjHCG8OUl2H8+dFsu/WfY7k/tcrMHCJHSVMbBiPxKM1MlR2aSIrGW7SNVZ +mrk0QfBHvhKdXOnUcPjp6CL7BAwgrKT9h0/v5ky/GutAL0L7N1Enntw+WWdI0SAn +JIaCzEN4s3VniDSyULZ7J3E4z7wmeLhzHf1ugyEoPOehP1RZzVJDLExZ30dXDUlG +qUQaUkBAjclD4i5vybF+CGGhCzIi0UTb+VmHNfi3yqYwltYGJzELvYw3ce/cVkSm +B4Qqx0niiFQfguX6MduWB067IXDLKu51ovnA+h72FKd7iZSrKg+qCEy0eYA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFAjCCA+qgAwIBAgIULRaaYWRGTgsg6K213Aw9UyZJF+8wDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFt +cGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqRYm2F+Ejfg3Vqmld+dXzIThERJuzyqpq7FUNegfAebM +e7aelHiAMSecYroxj990HdCp1yDhryRxzdGHkPK7VHzH695th8N1su7wO37cspjX +ZxMexiZuwV1t/N8khi20MItqa6sYY4gkBLoGiT5DdJNTJHv3Ammx+PmYHIRF0S1P +P1j2nd+Kxaj1Il4sInUo7BqbmO794QdICgJQ5XFeXmEV+4uhoPSHEoOfAlWUTKA9 +a9rugrY0k3JlUTF0tIPLEWOcMxEcQj6uYFehxakwiOnZwgkJMCSbhsoEBzq+i7Eb +2Wob7d2Gn0De3Z+ZruVIJzY0MpHWrDUyny/Qi17nDwIDAQABo4IBKjCCASYwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFLmdh4hHmYd6 +TqjvfdgK1x/HzhmeMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK1x/Hzhme +oYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH +ZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UECwwiRXhh +bXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwiRXhhbXBs +ZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K213Aw9UyZJ +F+8wDQYJKoZIhvcNAQELBQADggEBAI6PJGGXH2fIlrZQFZXkuLqjMrR8K+/60cq0 +4qqjTg8p+vQyB66BJSh4BiUM2sh7SwGKpehNB8QQXEZoyzpWY+Cdcm4ty4F430xS +uz/uW0NObhnJnyURlHf1szHTr91/1yX7eCtpUA1X9cjtXYS/uR911BCotdgmp3N9 +lHp+DjMx3j/xsGAuC1B2vmuLaMXA8SeYziDx+9KUHidMM7v/JsDZwc8XKCK+i12s +yIAv7Tuk5drq3x7ZCA3k9Xja/YqpaPNSP6iVsdM57NLPfZA9ilNuSMD49No6q9wW +dJ7sJEGDdICEBTuL9bCnwv/PZQ8ohJMJ+7Ike8f6tz8TsH3C+fg= +-----END CERTIFICATE----- diff --git a/src/test/resources/ssl/reload/node-new-ca.key.pem b/src/test/resources/ssl/reload/node-new-ca.key.pem new file mode 100644 index 0000000000..1fa12aa5e5 --- /dev/null +++ b/src/test/resources/ssl/reload/node-new-ca.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC0kbfvZpV9Nhm1 +Dq50RDML1E3Lz5KoFkSmug8F1i6k5naXS0qAbKSgAjSJf+neDNCVgFKZJV24otmf +kT+dAJv5GpuLW6hmuiMRDGrnfxFdaYj1fvYzIoWepyCNlb2oIGfSiTknR9KnuRLn +kjojMmGskWokp2f+LV1NF7KDS2NJr91HbIdsWP3uB1cCee17vFrzui6cpmgW2Z6u +GF9V9ueTFB9IV5yDQCW0GK6EH1PGi9FOjVM5U+kBSEEUFqPggtLIYl0b2pDXU4wv +5BR3pONAHSoVJVp/8Ea7/RSbPWWnU6utCSi5pNB+5D7e05jeHAIzg90vTdhsFhev +8fsSaEnZAgMBAAECggEABVlpxwxVcmOnaE86iNQ6ZOfRtC9+iz85omzRpB0fvZ/c +NIg0+U/+ooTeNJKXBY6AoWUvTT0npSAh7VG6vjZ16G/K2tqIxx5NiqRBCIGhrJBD +T+6GcaZcqgIOe1NLzo7DNJ//EvRUP8bCUhzpXwCPlzKpn9Nbx9JlOLLyhWQ22Uhx +NQVIOd3qvhAvU/LF03fMPAqxeHXD0KOQNNxNPqwTWW1rRi6bzvMud0icQhDjTVBf +gUhowZFdDnt3NIiMmh997Fnjbx2J5BTJ/tWnyG4pVO8d4JX5RDcZOx+MFBO9ypQ/ +FqhGu+J3xjMzSP+Y7kKHI60KBMCRnz2hEUP2IN+xxwKBgQDkiBtpJkRshaP27Bbw +xpuSvsKRAYdMDSlfReKQHEs3hJ0w3wFT0ofgMS49PyzHu0TyQ39jNAJ3YkdpRpIC +nI9fcDV4xS6G5kz72U4yamm1RF7TdMoU1WWMqmtm7i9Cdjrd7z/WGnBUH2pmenJ7 +IDoeALme+GxNwyakaSKaemOlGwKBgQDKRc/uln6AIxtlWBqgo8XGLyzReK0EYdnN +jwp7CcOuza//q9/P1fh/NA4rj0uiPD0SYX92DmaGRuuF/FwQ7OnAw9x81JlQeBbW +iSH3IzFSXP6kuY+SpUkRuWLXuNByIbsICOWN761PyIDV7TJAG/e6G/SLHUoOA6+G +aOQopG+gGwKBgDdmIzbvNuET2HaQLtN5YddF9QaP10uBWUkmOND0eutfc3eYZ8r5 +G0Umxu3D9cgJRqJv6F6VChAEvAjyOYz2hO7+1YeMTUYYaAsZV5JzJ2Lwywf5pM8+ +F9rsqRKPpNc4r/aC+/eb+yT5ZKKpBj2Ax3XkeRrnX+HN7/0lG2VVS/iHAoGAFAvN +KqkRimNwUJ/lq6vvas+8ElpyUy/bZQrbEAyMryNFYQJIoRFkmj6vdNOzvDVaHBs7 +hZixwnb+2n2DJk5EcE046cosE5SDNunKSvLa7X234t1dBDyLPE1yJUz0o4sCPS6c +iW+KbpDBa/Ig+8eJypEAsFTLxQ3KdHiqu/hn86cCgYBygyypD9R11k8X46ayXFcV +C+bpC4GsRliRvA0smFIAC75urzngLYyyP2ueonusE0cNYARlcbV3FcLDLuvEGoJL +enlYi4k9F+nCZaQ2ylWl2H6ud0/kCnIfjv+4Knf15Wz58VkH09AvbmqJNyd6kB0e +19JMEgOOugnWgfUNsBipfQ== +-----END PRIVATE KEY----- diff --git a/src/test/resources/ssl/reload/secondary-root-ca.pem b/src/test/resources/ssl/reload/secondary-root-ca.pem new file mode 100644 index 0000000000..81d8309898 --- /dev/null +++ b/src/test/resources/ssl/reload/secondary-root-ca.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFAjCCA+qgAwIBAgIULRaaYWRGTgsg6K213Aw9UyZJF+8wDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFt +cGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqRYm2F+Ejfg3Vqmld+dXzIThERJuzyqpq7FUNegfAebM +e7aelHiAMSecYroxj990HdCp1yDhryRxzdGHkPK7VHzH695th8N1su7wO37cspjX +ZxMexiZuwV1t/N8khi20MItqa6sYY4gkBLoGiT5DdJNTJHv3Ammx+PmYHIRF0S1P +P1j2nd+Kxaj1Il4sInUo7BqbmO794QdICgJQ5XFeXmEV+4uhoPSHEoOfAlWUTKA9 +a9rugrY0k3JlUTF0tIPLEWOcMxEcQj6uYFehxakwiOnZwgkJMCSbhsoEBzq+i7Eb +2Wob7d2Gn0De3Z+ZruVIJzY0MpHWrDUyny/Qi17nDwIDAQABo4IBKjCCASYwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFLmdh4hHmYd6 +TqjvfdgK1x/HzhmeMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK1x/Hzhme +oYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH +ZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UECwwiRXhh +bXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwiRXhhbXBs +ZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K213Aw9UyZJ +F+8wDQYJKoZIhvcNAQELBQADggEBAI6PJGGXH2fIlrZQFZXkuLqjMrR8K+/60cq0 +4qqjTg8p+vQyB66BJSh4BiUM2sh7SwGKpehNB8QQXEZoyzpWY+Cdcm4ty4F430xS +uz/uW0NObhnJnyURlHf1szHTr91/1yX7eCtpUA1X9cjtXYS/uR911BCotdgmp3N9 +lHp+DjMx3j/xsGAuC1B2vmuLaMXA8SeYziDx+9KUHidMM7v/JsDZwc8XKCK+i12s +yIAv7Tuk5drq3x7ZCA3k9Xja/YqpaPNSP6iVsdM57NLPfZA9ilNuSMD49No6q9wW +dJ7sJEGDdICEBTuL9bCnwv/PZQ8ohJMJ+7Ike8f6tz8TsH3C+fg= +-----END CERTIFICATE----- diff --git a/src/test/resources/ssl/reload/secondary-signing-ca.pem b/src/test/resources/ssl/reload/secondary-signing-ca.pem new file mode 100644 index 0000000000..53f989d0b3 --- /dev/null +++ b/src/test/resources/ssl/reload/secondary-signing-ca.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCDCCA/CgAwIBAgIUfUpmQ/BPCGTsPLW7rrPbkEU1RcwwDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgakxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMS4wLAYDVQQLDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMS4wLAYDVQQDDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjUd34V1uf+OwGXIBce+4O/UX35yCxY0LHt48 +wNIGkEs3StbTG5/qjkeAIFr2EUpRX5c0n5sIWdJX1cV/drWrhUzy6Ya1jvQiTA+i +k4YVVkFsz9QajgP+UPS06ZLkFldBofd/Su4GW5YEBlOBfxbsr8+E+73M/8sU1/wD +QLwPZGrkN7Cc37qi0Sf3blCNsjwfZPrGm+J/4hxdlJKuimo3Ctfwtlv/cIJZv7aG +RPksgsiirrk//nrW24wCQjqernuRayT0+2KL7OIn7UH2XL4nUUKU4cHYJOeiTNz+ +ds/uP1FG5WAvQ7CEyh6z2aXxGhZ89ZquATFy2paLpqfWgARiiwIDAQABo4IBKjCC +ASYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFFLG +vw6mimoIfjgzjHjhUfVNAeAIMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK +1x/HzhmeoYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPy +LGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UE +CwwiRXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwi +RXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K21 +3Aw9UyZJF+8wDQYJKoZIhvcNAQELBQADggEBAJo5QHvLgfH3VVJlNwFrGdNH1dCh +/mqPpqhjHCG8OUl2H8+dFsu/WfY7k/tcrMHCJHSVMbBiPxKM1MlR2aSIrGW7SNVZ +mrk0QfBHvhKdXOnUcPjp6CL7BAwgrKT9h0/v5ky/GutAL0L7N1Enntw+WWdI0SAn +JIaCzEN4s3VniDSyULZ7J3E4z7wmeLhzHf1ugyEoPOehP1RZzVJDLExZ30dXDUlG +qUQaUkBAjclD4i5vybF+CGGhCzIi0UTb+VmHNfi3yqYwltYGJzELvYw3ce/cVkSm +B4Qqx0niiFQfguX6MduWB067IXDLKu51ovnA+h72FKd7iZSrKg+qCEy0eYA= +-----END CERTIFICATE----- diff --git a/src/test/resources/ssl/reload/truststore.jks b/src/test/resources/ssl/reload/truststore.jks index c750f9807aa22339c19d05e510990a57ceef4758..217c4d09b4f5b162bf6ede35fb2fd19594d7643b 100644 GIT binary patch delta 4196 zcmV-q5S#Dz3Wgy-FoF<*0s#Xsf)FeQ2`Yw2hW8Bt2LYgh5FG@95E(Fn5EU?j5D^9m zDuzgg_YDCD2B3lv0x*IP{sI92FoF;Fkw7aH3y2teV%KE-MHD{guQeLr8q(9yk$)tA zFM<4bb*1t?DM&>|2%CerL^6{oqhGZlovK>+EUsZblPP^hD~L%q zxW~7Pddh0Quz8MtIjhRlcHKX!)?~_n9VJ04tbRUiid|(je>zqAnluKlCsI%Q{d3+9Z|8OGY3x7y zE<%>#Q(8y4VXvO3xvRq5;)Gs5s5vxA$NYR{X7piJUYrJl>2Cc8_;Y`3exC-y%I1fu z|NEzt!9|78QrN}*5I9V@zo`9xF>?D|Jw)0&wUcAKd~)7nqACu2VfqH|Qlk?ksH9rv*UjIeMYJ!$5891rewLTb^}}Hu$h0 zPAh=ST!yzDAX#n?{32YgS;v4MGNIIf>Vj^#dL!B6y+^!%2J#D}cC_B(eRMjy?I&d6 zQ#XARl>73#`fz&sK||&YdDz9(H!CH=%_W2#z!a7c=F56HHo{>$QC~GGj$iaMdHlsW zq`O;q4zH)#*rSu-oRiL$Wy#A4oFd%mrPW1m?d-Wym>G{@7;_kdcGorAAkif=MGJBGbYSSH$%cSDZ7oW200}*ovw67d9V`cS($YR2iLE%&gL>%oKJg#!cNpcVYle zlLvfIG|VSIadx&k+Kq?z67}{awDo+EaOsa?eBNKg6T-viRPWK>|4Hi+a;Yg?m+}J$=Fb^DN=&hCpJAnnCB?TEGC%gWEy-nOm-EO%uAZe$+^1?b zE`fp{%v5&q$%gyd2_457A=xVl@+ylDQb`!N3RjjT{S6}iVxQc-oNY)em5Pp0nlc?`;`TZyx&?>rr%QZw_H4}BJkMLEwq{J@o=7vTZb3ho!V&I}9gOG){N##Qc+ z%25D(;BXxH<9YB`$+5V&SMVskNkbp5f?NG zqh9mr7%6)TSL!WR-t#m>3=BwjgEby$Ig0iu7{qR$xJyM60oGycF#dSI9qVSj27@udR)=?(~`0@$pGegMv2Ow?3NU) zJ$aSmId9+@mCM0n7 zctJ!HpH6eG0>f;6ENJ@TF`9)h%uOQ>E&9vHVKamsG942JJ%T`$grWT~KJlB_4;8~Hen_?Bp*W(Eo`+JqajMOOrZ$4FO}459xo zIQny(s>$7dorWQ=|0GyXEx3+@tN$FI!=9rC@>H%mwt7Kg!S+Hy9T$r@K;c51wS&z} zibe4eBh&$i{Dl%E3CywYySs&vNSY5{A-Ar|t(+Slb(GM{CHtbuS;u*ShlO{)+z3T) z@aR7ZjG@leT+)MEy}dhWTy=n~xAls{>kaaW(X4HMhUWSh!PK3D__)v3DPR^PLZ_$m zCn9yZnEpoS4(xcBL;Sdt7%5Gxmwx2H2vU!|9(OD`;Bg8aqk$=2h2AV9@=2qirW`=o zS!qSbf_NcqscGAXzCLi~g8oZlXkyszsc7Hoz*)X&(oa!9EJxH`Em+EGfgSLqB6$63 zHwZg_uGcd}(^GsPeLrYC^SL-@gD(%g6*1p>UWfm~(rQfn3LYr>UI4%SSXXv~d0kpZ z&_Kk&Aj<(j#grr>%fm~cblR(U9wZax)d{<^;K-t)!!2f3h0Ff<;GiK+0W%G&+nz!C zWsk0i`jMm87>Q%b#5P^Cn@{KIiGytCug1=Q$+@X;sai!*QJO`Wbs}E}y}yvUhkpK; zAN%~N)SIFSg;tGHUB%I+(PIS)f$fPcNSQvT84MFe3~a_XT7^i48zg5$P)*vc%+&R= zf}6Ppzz6fRY`+pOjSBLSASTqG!b5)9{F=T}mv!~#v5_9?q6P+^Ph7m}RSKUyxhc$l z?}K!eD1B}9MkH>@jmYHp6Ucvoe^>64>kXNL=Ab~*zmSciqDuk45NwHOqpBT3P+WW+ z?+?pvP`gx-YXI|D*F23>;IEX8*ajn0|-pbN)#y9=d!=lE=2v`(1Zh9{;$(Qz`! zV1T;jS(7#KQRfG?<2{!`j4ZW{;F~&TB{RSr@}JtnV(Fhz*ujaIzDsP) z5JRm7`XP)5R@G^*t0Kr0omt;i5M0Gf{}oIn?=)H_YkS!2bH98s9EP#L3G+ch#guG4 z#CdcX(R31XC`=5qgf#z>ZT>5wAE@Pw^(g;|m z-88M6cJ5U)Bi~+>tGsIlg?xn6IU`U;ng0!KzNtfl_QDN`|6$*dLIsuAv{Nyw01(>n ziIV8_7bv{6)EkUku<>N$DlI;9z4y%-_&_3%2FBZh)UOvllP;gBjdA1Tm1q90w)k5e;_LW delta 1341 zcmV-D1;YAM1qCpI1px*L zDuzgg_YDCD2B3lj@-Tt~?g9Y-FoFcRae3egG^!`N|uiWS)NYr0K$2~!PvU^FxeAQr*)jTJ(#*e8yQ zdcgN_Ejb(F3;ReP4(W)YJ0@Andbbu39;Wvl3&}V3y>Ru8O*2*B5t|npn$-pwtvDDK z#nee&HaB;>dd6al&|l+*{rU^+1qrf22Pw@lw;tB<97!(Q)l*A`9evzu*HbU_zspJ|K6Grpsj3}c=pbJ8GXhugda6pwmX+^%yH(H;PEx#ae_nT6iteH|! zqP(-g3b;0PIVidgGdFRJ=MnhC&SmBa;DqpVHP4x}30bDu%Urtl`R2~uH7fCmKWmSh zH@qcMNG~GFvdzjgelvsYh)kJ((#z;r&_jR&+UL66E!L5NSR-J`JdK9Q|2dl>4AEWf z+69VFS)i4{?eB-Fy@-b6qf5$;5&iS88Rx=Mw6ac*hgaUJmLj&&G?Pb(s)6lrQ&wCX zb8o$m@gEHpW5-dC&DUAs3N#^}=^UZ^;6U&Ozi4`FAd|0qw$$vXIx*9K;C1)4sDW@4 zy^U)cDCczg=MYcVJ}nbB;+}UHnd^(>&HqByd8Cf@*+$)94c3cjRox1s{dILT==n1O z0yl?hu<8FDpV|`{T1;72`>#<8;BtCgncM>(J9mfRTwCl|pgx~{b&<=pRWbqcfibzk~3_+%DH#Ji1rLu6+N8Vk)0mm(;fbkb+V!5m`MJmV2d zu}&Ja%Dm^rgxZmXPj4qm_p=fC}7s;{??PX(k%9YpYSGAYgyel5=)xnW(zZa#~j zxFIn7)h150{p+gR<4fM1ta?K42VI3$_)lkIH1>Vitduu=o;V71SM^bK557wGkDZ6B zX@bUa_C2rs2FiSYio$o`Iw16uS;MC!Bfu-FGSSaJ1>2EmegJ#9T~L>tX&$Sa-6jM* zenjwtLnq|l7Xhg4J}X5p#*xWy9t~14JkZE)L5mKL4iJt#U;iVJMR9}D!D%(KO(s6P z-JNYM?L;b;*8{l%3cRo4y+}GFzyCj*-R}$LC9Q#PlS~tTX-j~+08esmOkFCVjf4j= z$;XoCTg!V`wl>gsIZXR&{m@;8n7JN3#=M=OsZP4%+grOum!AXs%2`Q)O-1klK+-XH zJ!T}tXb|IW_)Op)cW5GX@0W7C?UCy7+=C7GFKI~B%I!;A#jrdQb>PoIz1nyy_AQ1^ z?N{(iLBLF0rv!Yw^DQ?TKziP&yD&{KF)$4V31Egu0c8UO0s#d81R!5j#|R$F6Yf5- zUZVTP!BjaL^Nd Date: Thu, 24 Oct 2024 16:53:00 -0400 Subject: [PATCH 61/62] WIP on adapting to optimized privilege evaluation Signed-off-by: Craig Perkins --- .../opensearch/security/SystemIndexTests.java | 2 + .../IndexDocumentIntoSystemIndexAction.java | 2 +- ...ortIndexDocumentIntoSystemIndexAction.java | 2 + .../privileges/ActionPrivilegesTest.java | 3 +- .../RestEndpointPermissionTests.java | 2 - .../security/privileges/ActionPrivileges.java | 63 +++++++++++++++++-- .../privileges/PrivilegesEvaluator.java | 31 ++++++++- .../PrivilegesEvaluatorResponse.java | 4 -- .../privileges/SnapshotRestoreEvaluator.java | 1 - .../SystemIndexAccessEvaluator.java | 7 --- .../securityconf/InMemorySecurityRoles.java | 20 ------ .../securityconf/InMemorySecurityRolesV7.java | 37 ----------- .../SystemIndexAccessEvaluatorTest.java | 8 --- 13 files changed, 96 insertions(+), 86 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java delete mode 100644 src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java diff --git a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java index 0c77cf43f6..a131bb9891 100644 --- a/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/SystemIndexTests.java @@ -99,6 +99,8 @@ public void testPluginShouldBeAbleToIndexDocumentIntoItsSystemIndex() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse response = client.put("try-create-and-index/" + SYSTEM_INDEX_1); + System.out.println("response: " + response.getBody()); + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); assertThat(response.getBody(), containsString(SystemIndexPlugin1.class.getCanonicalName())); } diff --git a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java index 8621f2a609..9a60de201c 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/IndexDocumentIntoSystemIndexAction.java @@ -14,7 +14,7 @@ public class IndexDocumentIntoSystemIndexAction extends ActionType { public static final IndexDocumentIntoSystemIndexAction INSTANCE = new IndexDocumentIntoSystemIndexAction(); - public static final String NAME = "mock:systemindex/index"; + public static final String NAME = "cluster:mock/systemindex/index"; private IndexDocumentIntoSystemIndexAction() { super(NAME, IndexDocumentIntoSystemIndexResponse::new); diff --git a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java index c549da7809..19ec06ca7d 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/TransportIndexDocumentIntoSystemIndexAction.java @@ -62,6 +62,7 @@ protected void doExecute( String indexName = request.getIndexName(); String runAs = request.getRunAs(); Subject userSubject = identityService.getCurrentSubject(); + System.out.println("User Subject: " + userSubject); try { contextSwitcher.runAs(() -> { client.admin().indices().create(new CreateIndexRequest(indexName), ActionListener.wrap(r -> { @@ -83,6 +84,7 @@ protected void doExecute( .source("{\"content\":1}", XContentType.JSON), ActionListener.wrap(r2 -> { User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + System.out.println("Test User: " + user); actionListener.onResponse(new IndexDocumentIntoSystemIndexResponse(true, user.getName())); }, actionListener::onFailure) ); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java index 70621acdbb..b23e47c314 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -447,7 +447,8 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes Settings.EMPTY, WellKnownActions.CLUSTER_ACTIONS, WellKnownActions.INDEX_ACTIONS, - WellKnownActions.INDEX_ACTIONS + WellKnownActions.INDEX_ACTIONS, + Map.of() ); if (statefulness == Statefulness.STATEFUL) { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index c424891a8c..1e61aa0206 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -32,7 +32,6 @@ import java.util.Collection; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -57,7 +56,6 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; -import static org.junit.Assert.assertTrue; /** * Moved from https://github.com/opensearch-project/security/blob/54361468f5c4b3a57f3ecffaf1bbe8dccee562be/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 32b4bf3825..389926179d 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -113,9 +113,10 @@ public ActionPrivileges( Settings settings, ImmutableSet wellKnownClusterActions, ImmutableSet wellKnownIndexActions, - ImmutableSet explicitlyRequiredIndexActions + ImmutableSet explicitlyRequiredIndexActions, + Map> pluginToClusterActions ) { - this.cluster = new ClusterPrivileges(roles, actionGroups, wellKnownClusterActions); + this.cluster = new ClusterPrivileges(roles, actionGroups, wellKnownClusterActions, pluginToClusterActions); this.index = new IndexPrivileges(roles, actionGroups, wellKnownIndexActions, explicitlyRequiredIndexActions); this.roles = roles; this.actionGroups = actionGroups; @@ -142,7 +143,27 @@ public ActionPrivileges( settings, WellKnownActions.CLUSTER_ACTIONS, WellKnownActions.INDEX_ACTIONS, - WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS, + Map.of() + ); + } + + public ActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + Supplier> indexMetadataSupplier, + Settings settings, + Map> pluginToClusterActions + ) { + this( + roles, + actionGroups, + indexMetadataSupplier, + settings, + WellKnownActions.CLUSTER_ACTIONS, + WellKnownActions.INDEX_ACTIONS, + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS, + pluginToClusterActions ); } @@ -375,6 +396,8 @@ static class ClusterPrivileges { */ private final ImmutableMap rolesToActionMatcher; + private final ImmutableMap usersToActionMatcher; + private final ImmutableSet wellKnownClusterActions; /** @@ -388,7 +411,8 @@ static class ClusterPrivileges { ClusterPrivileges( SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups, - ImmutableSet wellKnownClusterActions + ImmutableSet wellKnownClusterActions, + Map> pluginToClusterActions ) { DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( roles.getCEntries().keySet() @@ -396,6 +420,7 @@ static class ClusterPrivileges { Map> actionToRoles = new HashMap<>(); ImmutableSet.Builder rolesWithWildcardPermissions = ImmutableSet.builder(); ImmutableMap.Builder rolesToActionMatcher = ImmutableMap.builder(); + ImmutableMap.Builder usersToActionMatcher = ImmutableMap.builder(); for (Map.Entry entry : roles.getCEntries().entrySet()) { try { @@ -445,6 +470,14 @@ static class ClusterPrivileges { } } + if (pluginToClusterActions != null) { + for (String pluginIdentifier : pluginToClusterActions.keySet()) { + Set clusterActions = pluginToClusterActions.get(pluginIdentifier); + WildcardMatcher matcher = WildcardMatcher.from(clusterActions); + usersToActionMatcher.put(pluginIdentifier, matcher); + } + } + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); this.actionToRoles = actionToRoles.entrySet() @@ -452,6 +485,7 @@ static class ClusterPrivileges { .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); this.rolesToActionMatcher = rolesToActionMatcher.build(); + this.usersToActionMatcher = usersToActionMatcher.build(); this.wellKnownClusterActions = wellKnownClusterActions; } @@ -485,6 +519,17 @@ PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext contex } } + System.out.println("context: " + context); + System.out.println("usersToActionMatcher: " + usersToActionMatcher); + + // 4: If plugin is performing the action, check if plugin has permission + if (context.getUser().isPluginUser()) { + WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + return PrivilegesEvaluatorResponse.insufficient(action, context); } @@ -554,6 +599,16 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con } } + // 4: If plugin is performing the action, check if plugin has permission + if (context.getUser().isPluginUser()) { + WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); + for (String action : actions) { + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + if (actions.size() == 1) { return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next(), context); } else { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index cda5755ad6..617c14f7a7 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -143,6 +143,7 @@ public class PrivilegesEvaluator { private final boolean checkSnapshotRestoreWritePrivileges; private final ClusterInfoHolder clusterInfoHolder; + private final ConfigurationRepository configurationRepository; private ConfigModel configModel; private final IndexResolverReplacer irr; private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; @@ -153,6 +154,7 @@ public class PrivilegesEvaluator { private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; private final Settings settings; + private final Map> pluginToClusterActions; private final AtomicReference actionPrivileges = new AtomicReference<>(); public PrivilegesEvaluator( @@ -176,6 +178,7 @@ public PrivilegesEvaluator( this.threadContext = threadContext; this.privilegesInterceptor = privilegesInterceptor; + this.pluginToClusterActions = new HashMap<>(); this.clusterStateSupplier = clusterStateSupplier; this.settings = settings; @@ -192,6 +195,7 @@ public PrivilegesEvaluator( termsAggregationEvaluator = new TermsAggregationEvaluator(); pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); this.namedXContentRegistry = namedXContentRegistry; + this.configurationRepository = configurationRepository; if (configurationRepository != null) { configurationRepository.subscribeOnChange(configMap -> { @@ -228,11 +232,14 @@ void updateConfiguration( ? DynamicConfigFactory.addStatics(actionGroupsConfiguration.clone()) : DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)); FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsWithStatics); + System.out.println("updateConfiguration"); + System.out.println("pluginToClusterActions: " + pluginToClusterActions); ActionPrivileges actionPrivileges = new ActionPrivileges( DynamicConfigFactory.addStatics(rolesConfiguration.clone()), flattenedActionGroups, () -> clusterStateSupplier.get().metadata().getIndicesLookup(), - settings + settings, + pluginToClusterActions ); Metadata metadata = clusterStateSupplier.get().metadata(); actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); @@ -296,6 +303,22 @@ public PrivilegesEvaluationContext createContext( throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); } + if (user.isPluginUser()) { + String pluginIdentifier = user.getName(); + Set clusterActions = pluginToClusterActions.get(pluginIdentifier); + if (clusterActions == null) { + clusterActions = new HashSet<>(); + clusterActions.add(BulkAction.NAME); + pluginToClusterActions.put(pluginIdentifier, clusterActions); + SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( + CType.ACTIONGROUPS + ); + SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); + + this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration); + } + } + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); @@ -392,11 +415,16 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) return presponse; } + System.out.println("Calling systemIndexAccessEvaluator.evaluate"); + System.out.println("user: " + user); + System.out.println("action: " + action0); // Security index access if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) .isComplete()) { + System.out.println("Returning presponse: " + presponse); return presponse; } + System.out.println("After systemIndexAccessEvaluator.evaluate"); // Protected index access if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { @@ -532,6 +560,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); + System.out.println("allIndexPermsRequired: " + allIndexPermsRequired); presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, requestedResolved); if (presponse.isPartiallyOk()) { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index 54eeda3191..3330219a4f 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -133,10 +133,6 @@ public String getPrivilegeMatrix() { return result; } - public boolean addMissingPrivileges(String action) { - return missingPrivileges.add(action); - } - public Set getMissingSecurityRoles() { return new HashSet<>(missingSecurityRoles); } diff --git a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java index 8a11e433e8..23612e1a52 100644 --- a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java @@ -109,7 +109,6 @@ public PrivilegesEvaluatorResponse evaluate( auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '{}' as source index is not allowed", action, securityIndex); presponse.allowed = false; - presponse.addMissingPrivileges(action); return presponse.markComplete(); } return presponse; diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 0ebac48974..d966f1e020 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -268,7 +268,6 @@ private void evaluateSystemIndicesAccess( log.debug("Service account cannot access regular indices: {}", regularIndices); } presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); return; } @@ -285,7 +284,6 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); return; } else if (containsSystemIndex @@ -300,7 +298,6 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); return; } @@ -325,7 +322,6 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); } return; @@ -348,7 +344,6 @@ private void evaluateSystemIndicesAccess( auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '_all' indices is not allowed for a regular user", action); presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); } } @@ -363,7 +358,6 @@ else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { log.debug("Filtered '{}' but resulting list is empty", securityIndex); } presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); return; } @@ -376,7 +370,6 @@ else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { final String foundSystemIndexes = String.join(", ", getAllSystemIndices(requestedResolved)); log.warn("{} for '{}' index is not allowed for a regular user", action, foundSystemIndexes); presponse.allowed = false; - presponse.addMissingPrivileges(action); presponse.markComplete(); } } diff --git a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java deleted file mode 100644 index 934533788f..0000000000 --- a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRoles.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.securityconf; - -import java.util.Map; -import java.util.Set; - -public interface InMemorySecurityRoles extends SecurityRoles { - - void addSecurityRole(String roleName, Set clusterPerms, Map> indexPatternToAllowedActions); -} diff --git a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java b/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java deleted file mode 100644 index da5a07298c..0000000000 --- a/src/main/java/org/opensearch/security/securityconf/InMemorySecurityRolesV7.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.securityconf; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.opensearch.security.support.WildcardMatcher; - -public class InMemorySecurityRolesV7 extends ConfigModelV7.SecurityRoles implements InMemorySecurityRoles { - - public InMemorySecurityRolesV7(int roleCount) { - super(roleCount); - } - - @Override - public void addSecurityRole(String roleName, Set clusterPerms, Map> indexPatternToAllowedActions) { - Set ipatterns = new HashSet<>(); - for (Map.Entry> entry : indexPatternToAllowedActions.entrySet()) { - ConfigModelV7.IndexPattern idxPattern = new ConfigModelV7.IndexPattern(entry.getKey()); - idxPattern.addPerm(entry.getValue()); - ipatterns.add(idxPattern); - } - ConfigModelV7.SecurityRole role = new ConfigModelV7.SecurityRole(roleName, ipatterns, WildcardMatcher.from(clusterPerms)); - roles.add(role); - } -} diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index cbb004be17..878033fd5c 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -393,7 +393,6 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled ); verify(log).debug("Disable search request cache for this request"); verify(log).debug("Disable realtime for this request"); - verify(presponse, times(3)).addMissingPrivileges(UNPROTECTED_ACTION); } @Test @@ -427,7 +426,6 @@ public void testProtectedActionLocalAll_systemIndexDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '_all' indices is not allowed for a regular user", "indices:data/write"); } @@ -442,7 +440,6 @@ public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); } @@ -457,7 +454,6 @@ public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); } @@ -516,7 +512,6 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, TEST_SYSTEM_INDEX); } @@ -563,7 +558,6 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); @@ -579,7 +573,6 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisab verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(PROTECTED_ACTION); verify(presponse).markComplete(); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); @@ -615,7 +608,6 @@ private void testSecurityIndexAccess(String action) { verify(auditLog).logSecurityIndexAttempt(request, action, task); assertThat(presponse.allowed, is(false)); - verify(presponse).addMissingPrivileges(action); verify(presponse).markComplete(); verify(log).isInfoEnabled(); From 0a8924d901d85aa93c687f1823ae0f97377857a4 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 25 Oct 2024 10:28:00 -0400 Subject: [PATCH 62/62] Get cluster actions working Signed-off-by: Craig Perkins --- .../plugin/RunClusterHealthAction.java | 2 +- .../security/OpenSearchSecurityPlugin.java | 8 +++- .../security/filter/SecurityFilter.java | 5 +++ .../ContextProvidingPluginSubject.java | 5 ++- .../privileges/PrivilegesEvaluator.java | 44 ++++++++++--------- .../PrivilegesEvaluatorResponse.java | 2 + .../SystemIndexAccessEvaluator.java | 1 + 7 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java index b819465a35..4234879bb8 100644 --- a/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java +++ b/src/integrationTest/java/org/opensearch/security/plugin/RunClusterHealthAction.java @@ -14,7 +14,7 @@ public class RunClusterHealthAction extends ActionType { public static final RunClusterHealthAction INSTANCE = new RunClusterHealthAction(); - public static final String NAME = "mock:cluster/monitor/health"; + public static final String NAME = "cluster:mock/monitor/health"; private RunClusterHealthAction() { super(NAME, RunClusterHealthResponse::new); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 7680398468..39e0e0a676 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -43,6 +43,7 @@ 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.Objects; @@ -69,6 +70,7 @@ import org.opensearch.SpecialPermission; import org.opensearch.Version; import org.opensearch.action.ActionRequest; +import org.opensearch.action.bulk.BulkAction; import org.opensearch.action.search.PitService; import org.opensearch.action.search.SearchScrollAction; import org.opensearch.action.support.ActionFilter; @@ -2126,7 +2128,11 @@ public SecurityTokenManager getTokenManager() { @Override public PluginSubject getPluginSubject(Plugin plugin) { - return new ContextProvidingPluginSubject(threadPool, settings, plugin); + Set clusterActions = new HashSet<>(); + clusterActions.add(BulkAction.NAME); + PluginSubject subject = new ContextProvidingPluginSubject(threadPool, settings, plugin); + sf.updatePluginToClusterAction(subject.getPrincipal().getName(), clusterActions); + return subject; } @Override diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 3670bf8319..31ea5ac259 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -453,6 +453,7 @@ public void onFailure(Exception e) { }); } } else { + System.out.println("No permissions for " + user); auditLog.logMissingPrivileges(action, request, task); String err; if (!pres.getMissingSecurityRoles().isEmpty()) { @@ -529,6 +530,10 @@ private boolean checkImmutableIndices(Object request, ActionListener listener) { return false; } + public void updatePluginToClusterAction(String pluginIdentifier, Set clusterActions) { + evalp.updatePluginToClusterActions(pluginIdentifier, clusterActions); + } + private boolean isRequestIndexImmutable(Object request) { final IndexResolverReplacer.Resolved resolved = indexResolverReplacer.resolveRequest(request); if (resolved.isLocalAll()) { diff --git a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java index 7a096abf08..ab6dddceba 100644 --- a/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java +++ b/src/main/java/org/opensearch/security/identity/ContextProvidingPluginSubject.java @@ -29,10 +29,11 @@ public class ContextProvidingPluginSubject implements PluginSubject { public ContextProvidingPluginSubject(ThreadPool threadPool, Settings settings, Plugin plugin) { super(); this.threadPool = threadPool; - this.pluginPrincipal = new NamedPrincipal(plugin.getClass().getCanonicalName()); + String principal = "plugin:" + plugin.getClass().getCanonicalName(); + this.pluginPrincipal = new NamedPrincipal(principal); // Convention for plugin username. Prefixed with 'plugin:'. ':' is forbidden from usernames, so this // guarantees that a user with this username cannot be created by other means. - this.pluginUser = new User("plugin:" + pluginPrincipal.getName()); + this.pluginUser = new User(principal); } @Override diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 617c14f7a7..a2e863b0b2 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -303,22 +303,6 @@ public PrivilegesEvaluationContext createContext( throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); } - if (user.isPluginUser()) { - String pluginIdentifier = user.getName(); - Set clusterActions = pluginToClusterActions.get(pluginIdentifier); - if (clusterActions == null) { - clusterActions = new HashSet<>(); - clusterActions.add(BulkAction.NAME); - pluginToClusterActions.put(pluginIdentifier, clusterActions); - SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( - CType.ACTIONGROUPS - ); - SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); - - this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration); - } - } - TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); @@ -412,7 +396,11 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) // check snapshot/restore requests if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { - return presponse; + if (!presponse.isAllowed()) { + return PrivilegesEvaluatorResponse.insufficient(action0, context); + } else { + return presponse; + } } System.out.println("Calling systemIndexAccessEvaluator.evaluate"); @@ -422,18 +410,30 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) .isComplete()) { System.out.println("Returning presponse: " + presponse); - return presponse; + if (!presponse.isAllowed()) { + return PrivilegesEvaluatorResponse.insufficient(action0, context); + } else { + return presponse; + } } System.out.println("After systemIndexAccessEvaluator.evaluate"); // Protected index access if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { - return presponse; + if (!presponse.isAllowed()) { + return PrivilegesEvaluatorResponse.insufficient(action0, context); + } else { + return presponse; + } } // check access for point in time requests if (pitPrivilegesEvaluator.evaluate(request, context, actionPrivileges, action0, presponse, irr).isComplete()) { - return presponse; + if (!presponse.isAllowed()) { + return PrivilegesEvaluatorResponse.insufficient(action0, context); + } else { + return presponse; + } } final boolean dnfofEnabled = dcm.isDnfofEnabled(); @@ -868,4 +868,8 @@ private List toString(List aliases) { return Collections.unmodifiableList(ret); } + + public void updatePluginToClusterActions(String pluginIdentifier, Set clusterActions) { + pluginToClusterActions.put(pluginIdentifier, clusterActions); + } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index 3330219a4f..98840ac5a6 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -188,6 +188,7 @@ public static PrivilegesEvaluatorResponse partiallyOk( } public static PrivilegesEvaluatorResponse insufficient(String missingPrivilege, PrivilegesEvaluationContext context) { + System.out.println("missingPrivilege: " + missingPrivilege); PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); response.indexToActionCheckTable = CheckTable.create(ImmutableSet.of("_"), ImmutableSet.of(missingPrivilege)); return response; @@ -197,6 +198,7 @@ public static PrivilegesEvaluatorResponse insufficient( CheckTable indexToActionCheckTable, PrivilegesEvaluationContext context ) { + System.out.println("indexToActionCheckTable: " + indexToActionCheckTable); PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); response.indexToActionCheckTable = indexToActionCheckTable; return response; diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index d966f1e020..8fc2cd8539 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -322,6 +322,7 @@ private void evaluateSystemIndicesAccess( ); } presponse.allowed = false; + presponse.getMissingPrivileges(); presponse.markComplete(); } return;