diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityLicensingAndFeatureUsageRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityLicensingAndFeatureUsageRestIT.java index 9883911e5c07c..5b777a59d1069 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityLicensingAndFeatureUsageRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityLicensingAndFeatureUsageRestIT.java @@ -53,7 +53,7 @@ public class RemoteClusterSecurityLicensingAndFeatureUsageRestIT extends Abstrac .name("fulfilling-cluster") .nodes(1) .apply(commonClusterConfig) - .setting("xpack.license.self_generated.type", "basic") + .setting("xpack.license.self_generated.type", "trial") .setting("remote_cluster_server.enabled", "true") .setting("remote_cluster.port", "0") .setting("xpack.security.remote_cluster_server.ssl.enabled", "true") @@ -113,7 +113,6 @@ protected void configureRemoteCluster(boolean isProxyMode) throws Exception { } public void testCrossClusterAccessFeatureTrackingAndLicensing() throws Exception { - assertBasicLicense(fulfillingClusterClient); assertBasicLicense(client()); final boolean useProxyMode = randomBoolean(); @@ -167,7 +166,6 @@ public void testCrossClusterAccessFeatureTrackingAndLicensing() throws Exception assertRequestFailsDueToUnsupportedLicense(() -> performRequestWithRemoteSearchUser(searchRequest)); // We start the trial license which supports all features. - startTrialLicense(fulfillingClusterClient); startTrialLicense(client()); // Check that feature is not tracked before we send CCS request. diff --git a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/SecurityWithBasicLicenseIT.java b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/SecurityWithBasicLicenseIT.java index f0ea4b34ba0b2..6c834301f03c8 100644 --- a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/SecurityWithBasicLicenseIT.java +++ b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/SecurityWithBasicLicenseIT.java @@ -50,6 +50,7 @@ public void testWithBasicLicense() throws Exception { assertUserProfileFeatures(false); checkRemoteIndicesXPackUsage(); + assertFailToCreateAndUpdateCrossClusterApiKeys(); } public void testWithTrialLicense() throws Exception { @@ -80,6 +81,7 @@ public void testWithTrialLicense() throws Exception { assertReadWithApiKey(apiKeyCredentials2, "/index*/_search", true); assertUserProfileFeatures(true); checkRemoteIndicesXPackUsage(); + assertSuccessToCreateAndUpdateCrossClusterApiKeys(); } finally { revertTrial(); assertAuthenticateWithToken(accessToken, false); @@ -95,6 +97,7 @@ public void testWithTrialLicense() throws Exception { assertReadWithApiKey(apiKeyCredentials2, "/index1/_doc/1", false); assertUserProfileFeatures(false); checkRemoteIndicesXPackUsage(); + assertFailToCreateAndUpdateCrossClusterApiKeys(); } } @@ -567,4 +570,54 @@ private void assertUserProfileFeatures(boolean clusterHasTrialLicense) throws IO assertThat(e.getMessage(), containsString("current license is non-compliant for [user-profile-collaboration]")); } } + + private void assertFailToCreateAndUpdateCrossClusterApiKeys() { + if (false == TcpTransport.isUntrustedRemoteClusterEnabled()) { + return; + } + + final Request createRequest = new Request("POST", "/_security/cross_cluster/api_key"); + createRequest.setJsonEntity(""" + { + "name": "cc-key", + "access": { + "search": [ { "names": ["*"] } ] + } + }"""); + final ResponseException e1 = expectThrows(ResponseException.class, () -> adminClient().performRequest(createRequest)); + assertThat(e1.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(e1.getMessage(), containsString("current license is non-compliant for [advanced-remote-cluster-security]")); + + final Request updateRequest = new Request("PUT", "/_security/cross_cluster/api_key/" + randomAlphaOfLength(20)); + updateRequest.setJsonEntity(""" + { + "metadata": { } + }"""); + final ResponseException e2 = expectThrows(ResponseException.class, () -> adminClient().performRequest(updateRequest)); + assertThat(e2.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(e2.getMessage(), containsString("current license is non-compliant for [advanced-remote-cluster-security]")); + } + + private void assertSuccessToCreateAndUpdateCrossClusterApiKeys() throws IOException { + if (false == TcpTransport.isUntrustedRemoteClusterEnabled()) { + return; + } + + final Request createRequest = new Request("POST", "/_security/cross_cluster/api_key"); + createRequest.setJsonEntity(""" + { + "name": "cc-key", + "access": { + "search": [ { "names": ["*"] } ] + } + }"""); + final ObjectPath createResponse = assertOKAndCreateObjectPath(adminClient().performRequest(createRequest)); + + final Request updateRequest = new Request("PUT", "/_security/cross_cluster/api_key/" + createResponse.evaluate("id")); + updateRequest.setJsonEntity(""" + { + "metadata": { } + }"""); + assertOK(adminClient().performRequest(updateRequest)); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index 9f003314c7898..469571798680b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -10,6 +10,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; @@ -26,6 +27,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.xpack.security.Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE; /** * Rest action to create an API key specific to cross cluster access via the dedicate remote cluster server port @@ -79,4 +81,16 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin new RestToXContentListener<>(channel) ); } + + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + final Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.checkWithoutTracking(licenseState)) { + return null; + } else { + return LicenseUtils.newComplianceException(ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.getName()); + } + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyAction.java index 7453609f6bbe0..0623009529984 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyAction.java @@ -9,6 +9,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; @@ -24,6 +25,7 @@ import static org.elasticsearch.rest.RestRequest.Method.PUT; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.xpack.security.Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE; public final class RestUpdateCrossClusterApiKeyAction extends ApiKeyBaseRestHandler { @@ -64,5 +66,17 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin ); } + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + final Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.checkWithoutTracking(licenseState)) { + return null; + } else { + return LicenseUtils.newComplianceException(ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.getName()); + } + } + record Payload(CrossClusterApiKeyRoleDescriptorBuilder builder, Map metadata) {} } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java index d971e06f09481..d722eae69f883 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java @@ -7,11 +7,14 @@ package org.elasticsearch.xpack.security.rest.action.apikey; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.rest.AbstractRestChannel; import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestResponse; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -20,10 +23,12 @@ import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequest; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.security.Security; import org.mockito.ArgumentCaptor; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -31,9 +36,21 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class RestCreateCrossClusterApiKeyActionTests extends ESTestCase { + private MockLicenseState licenseState; + private RestCreateCrossClusterApiKeyAction action; + + @Override + public void setUp() throws Exception { + super.setUp(); + licenseState = MockLicenseState.createMock(); + when(licenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(true); + action = new RestCreateCrossClusterApiKeyAction(Settings.EMPTY, licenseState); + } + public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { final FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent(new BytesArray(""" { @@ -49,7 +66,6 @@ public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { } }"""), XContentType.JSON).build(); - final var action = new RestCreateCrossClusterApiKeyAction(Settings.EMPTY, mock(XPackLicenseState.class)); final NodeClient client = mock(NodeClient.class); action.handleRequest(restRequest, mock(RestChannel.class), client); @@ -80,4 +96,39 @@ public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { ); assertThat(request.getMetadata(), nullValue()); } + + public void testLicenseEnforcement() throws Exception { + // Disallow by license + when(licenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(false); + + final FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent(new BytesArray(""" + { + "name": "my-key", + "access": { + "search": [ + { + "names": [ + "logs" + ] + } + ] + } + }"""), XContentType.JSON).build(); + final SetOnce responseSetOnce = new SetOnce<>(); + final RestChannel restChannel = new AbstractRestChannel(restRequest, randomBoolean()) { + @Override + public void sendResponse(RestResponse restResponse) { + responseSetOnce.set(restResponse); + } + }; + + action.handleRequest(restRequest, restChannel, mock(NodeClient.class)); + + final RestResponse restResponse = responseSetOnce.get(); + assertThat(restResponse.status().getStatus(), equalTo(403)); + assertThat( + restResponse.content().utf8ToString(), + containsString("current license is non-compliant for [advanced-remote-cluster-security]") + ); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyActionTests.java index fcedb5fa5e6da..f9fa9269c4ef1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestUpdateCrossClusterApiKeyActionTests.java @@ -7,12 +7,15 @@ package org.elasticsearch.xpack.security.rest.action.apikey; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.rest.AbstractRestChannel; import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestResponse; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -21,12 +24,14 @@ import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import org.elasticsearch.xpack.core.security.action.apikey.UpdateCrossClusterApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.UpdateCrossClusterApiKeyRequest; +import org.elasticsearch.xpack.security.Security; import org.mockito.ArgumentCaptor; import java.util.List; import java.util.Map; import static org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequestTests.randomCrossClusterApiKeyAccessField; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -34,9 +39,21 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class RestUpdateCrossClusterApiKeyActionTests extends ESTestCase { + private MockLicenseState licenseState; + private RestUpdateCrossClusterApiKeyAction action; + + @Override + public void setUp() throws Exception { + super.setUp(); + licenseState = MockLicenseState.createMock(); + when(licenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(true); + action = new RestUpdateCrossClusterApiKeyAction(Settings.EMPTY, licenseState); + } + public void testUpdateHasTypeOfCrossCluster() throws Exception { final String id = randomAlphaOfLength(10); final String access = randomCrossClusterApiKeyAccessField(); @@ -49,7 +66,6 @@ public void testUpdateHasTypeOfCrossCluster() throws Exception { XContentType.JSON ).withParams(Map.of("id", id)).build(); - final var action = new RestUpdateCrossClusterApiKeyAction(Settings.EMPTY, mock(XPackLicenseState.class)); final NodeClient client = mock(NodeClient.class); action.handleRequest(restRequest, mock(RestChannel.class), client); @@ -68,4 +84,33 @@ public void testUpdateHasTypeOfCrossCluster() throws Exception { assertThat(request.getMetadata(), nullValue()); } } + + public void testLicenseEnforcement() throws Exception { + // Disallow by license + when(licenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(false); + + final FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent( + new BytesArray(""" + { + "metadata": {} + }"""), + XContentType.JSON + ).withParams(Map.of("id", randomAlphaOfLength(10))).build(); + final SetOnce responseSetOnce = new SetOnce<>(); + final RestChannel restChannel = new AbstractRestChannel(restRequest, randomBoolean()) { + @Override + public void sendResponse(RestResponse restResponse) { + responseSetOnce.set(restResponse); + } + }; + + action.handleRequest(restRequest, restChannel, mock(NodeClient.class)); + + final RestResponse restResponse = responseSetOnce.get(); + assertThat(restResponse.status().getStatus(), equalTo(403)); + assertThat( + restResponse.content().utf8ToString(), + containsString("current license is non-compliant for [advanced-remote-cluster-security]") + ); + } }