diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java index 38095b8573f07..5ef73751999b2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java @@ -27,6 +27,7 @@ import java.util.function.Supplier; import static org.elasticsearch.ingest.ConfigurationUtils.newConfigurationException; +import static org.elasticsearch.ingest.ConfigurationUtils.readBooleanProperty; import static org.elasticsearch.ingest.ConfigurationUtils.readOptionalList; import static org.elasticsearch.ingest.ConfigurationUtils.readStringProperty; @@ -43,10 +44,14 @@ public final class SetSecurityUserProcessor extends AbstractProcessor { private final XPackLicenseState licenseState; private final String field; private final Set properties; + private final boolean ecsCompliant; public SetSecurityUserProcessor(String tag, SecurityContext securityContext, XPackLicenseState licenseState, String field, - Set properties) { + Set properties, boolean ecsCompliant) { super(tag); + if (ecsCompliant && false == "user".equals(field)) { + throw newConfigurationException(TYPE, tag, "ecs_compliant", "ESC compliance requires [field] value to be 'user'"); + } this.securityContext = securityContext; this.licenseState = Objects.requireNonNull(licenseState, "license state cannot be null"); if (licenseState.isAuthAllowed() == false) { @@ -57,6 +62,7 @@ public SetSecurityUserProcessor(String tag, SecurityContext securityContext, XPa } this.field = field; this.properties = properties; + this.ecsCompliant = ecsCompliant; } @Override @@ -93,7 +99,11 @@ public IngestDocument execute(IngestDocument ingestDocument) throws Exception { switch (property) { case USERNAME: if (user.principal() != null) { - userObject.put("username", user.principal()); + if (ecsCompliant) { + userObject.put("name", user.principal()); + } else { + userObject.put("username", user.principal()); + } } break; case FULL_NAME: @@ -179,6 +189,10 @@ Set getProperties() { return properties; } + boolean isEcsCompliant() { + return ecsCompliant; + } + public static final class Factory implements Processor.Factory { private final Supplier securityContext; @@ -193,6 +207,7 @@ public Factory(Supplier securityContext, Supplier processorFactories, String tag, Map config) throws Exception { String field = readStringProperty(TYPE, tag, config, "field"); + final Boolean ecsCompliant = readBooleanProperty(TYPE, tag, config, "ecs_compliant", true); List propertyNames = readOptionalList(TYPE, tag, config, "properties"); Set properties; if (propertyNames != null) { @@ -203,7 +218,8 @@ public SetSecurityUserProcessor create(Map processorF } else { properties = EnumSet.allOf(Property.class); } - return new SetSecurityUserProcessor(tag, securityContext.get(), licenseState.get(), field, properties); + return new SetSecurityUserProcessor(tag, securityContext.get(), licenseState.get(), field, properties, + ecsCompliant); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorFactoryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorFactoryTests.java index ed06f4d1a29e5..34e688e5ec3ec 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorFactoryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorFactoryTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.ingest; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -20,6 +21,7 @@ import java.util.HashMap; import java.util.Map; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Mockito.when; @@ -40,6 +42,7 @@ public void testProcessor() throws Exception { SetSecurityUserProcessor.Factory factory = new SetSecurityUserProcessor.Factory(() -> securityContext, () -> licenseState); Map config = new HashMap<>(); config.put("field", "_field"); + config.put("ecs_compliant", false); SetSecurityUserProcessor processor = factory.create(null, "_tag", config); assertThat(processor.getField(), equalTo("_field")); assertThat(processor.getProperties(), equalTo(EnumSet.allOf(Property.class))); @@ -58,6 +61,7 @@ public void testProcessor_validProperties() throws Exception { SetSecurityUserProcessor.Factory factory = new SetSecurityUserProcessor.Factory(() -> securityContext, () -> licenseState); Map config = new HashMap<>(); config.put("field", "_field"); + config.put("ecs_compliant", false); config.put("properties", Arrays.asList(Property.USERNAME.name(), Property.ROLES.name())); SetSecurityUserProcessor processor = factory.create(null, "_tag", config); assertThat(processor.getField(), equalTo("_field")); @@ -80,8 +84,26 @@ public void testCanConstructorProcessorWithoutSecurityEnabled() throws Exception SetSecurityUserProcessor.Factory factory = new SetSecurityUserProcessor.Factory(() -> null, () -> licenseState); Map config = new HashMap<>(); config.put("field", "_field"); + config.put("ecs_compliant", false); final SetSecurityUserProcessor processor = factory.create(null, "_tag", config); assertThat(processor, notNullValue()); } + public void testEcsCompliantDefaultToTrue() throws Exception { + SetSecurityUserProcessor.Factory factory = new SetSecurityUserProcessor.Factory(() -> securityContext, () -> licenseState); + Map config = new HashMap<>(); + config.put("field", "user"); + SetSecurityUserProcessor processor = factory.create(null, "_tag", config); + assertTrue(processor.isEcsCompliant()); + } + + public void testEcsCompliantRequiresParentFieldToBeUser() throws Exception { + SetSecurityUserProcessor.Factory factory = new SetSecurityUserProcessor.Factory(() -> securityContext, () -> licenseState); + Map config = new HashMap<>(); + config.put("field", "_field"); + config.put("ecs_compliant", true); + final ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> factory.create(null, "_tag", config)); + assertThat(e.getMessage(), containsString("[ecs_compliant] ESC compliance requires [field] value to be 'user'")); + } + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java index 3cd0efdd418ec..ca96c08ad93a7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.ingest; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -27,7 +28,10 @@ import java.util.HashMap; import java.util.Map; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.when; public class SetSecurityUserProcessorTests extends ESTestCase { @@ -52,7 +56,7 @@ public void testProcessorWithData() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); processor.execute(ingestDocument); Map result = ingestDocument.getFieldValue("_field", Map.class); @@ -82,7 +86,7 @@ public void testProcessorWithEmptyUserData() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); processor.execute(ingestDocument); Map result = ingestDocument.getFieldValue("_field", Map.class); // Still holds data for realm and authentication type @@ -95,7 +99,7 @@ public void testProcessorWithEmptyUserData() throws Exception { public void testNoCurrentUser() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); IllegalStateException e = expectThrows(IllegalStateException.class, () -> processor.execute(ingestDocument)); assertThat(e.getMessage(), equalTo("There is no authenticated user - the [set_security_user] processor requires an authenticated user")); @@ -105,7 +109,7 @@ public void testSecurityDisabled() throws Exception { when(licenseState.isAuthAllowed()).thenReturn(false); IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); IllegalStateException e = expectThrows(IllegalStateException.class, () -> processor.execute(ingestDocument)); assertThat(e.getMessage(), equalTo("Security (authentication) is not enabled on this cluster, so there is no active user" + " - the [set_security_user] processor cannot be used without security")); @@ -118,7 +122,7 @@ public void testUsernameProperties() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.USERNAME)); + "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.USERNAME), false); processor.execute(ingestDocument); @SuppressWarnings("unchecked") @@ -134,7 +138,7 @@ public void testRolesProperties() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.ROLES)); + "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.ROLES), false); processor.execute(ingestDocument); @SuppressWarnings("unchecked") @@ -150,7 +154,7 @@ public void testFullNameProperties() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor - = new SetSecurityUserProcessor("_tag", securityContext, licenseState, "_field", EnumSet.of(Property.FULL_NAME)); + = new SetSecurityUserProcessor("_tag", securityContext, licenseState, "_field", EnumSet.of(Property.FULL_NAME), false); processor.execute(ingestDocument); @SuppressWarnings("unchecked") @@ -166,7 +170,7 @@ public void testEmailProperties() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.EMAIL)); + "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.EMAIL), false); processor.execute(ingestDocument); @SuppressWarnings("unchecked") @@ -182,7 +186,7 @@ public void testMetadataProperties() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.METADATA)); + "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.METADATA), false); processor.execute(ingestDocument); @SuppressWarnings("unchecked") @@ -198,7 +202,7 @@ public void testOverwriteExistingField() throws Exception { new Authentication(user, realmRef, null).writeToContext(threadContext); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.USERNAME)); + "_tag", securityContext, licenseState, "_field", EnumSet.of(Property.USERNAME), false); IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); ingestDocument.setFieldValue("_field", "test"); @@ -238,7 +242,7 @@ public void testApiKeyPopulation() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); processor.execute(ingestDocument); Map result = ingestDocument.getFieldValue("_field", Map.class); @@ -269,7 +273,7 @@ public void testWillNotOverwriteExistingApiKeyAndRealm() throws Exception { "_field", Map.of("api_key", Map.of("version", 42), "realm", Map.of("id", 7)) )), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); processor.execute(ingestDocument); Map result = ingestDocument.getFieldValue("_field", Map.class); @@ -292,7 +296,7 @@ public void testWillSetRunAsRealmForNonApiAuth() throws Exception { IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); SetSecurityUserProcessor processor = new SetSecurityUserProcessor( - "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class)); + "_tag", securityContext, licenseState, "_field", EnumSet.allOf(Property.class), false); processor.execute(ingestDocument); Map result = ingestDocument.getFieldValue("_field", Map.class); @@ -301,4 +305,31 @@ public void testWillSetRunAsRealmForNonApiAuth() throws Exception { assertThat(((Map) result.get("realm")).get("type"), equalTo(lookedUpRealmRef.getType())); } + public void testWillEnsureEcsComplianceWhenParentFieldIsUser() throws Exception { + User user = new User("_username", null, null); + Authentication.RealmRef realmRef = new Authentication.RealmRef("_name", "_type", "_node_name"); + new Authentication(user, realmRef, null).writeToContext(threadContext); + + IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); + SetSecurityUserProcessor processor = new SetSecurityUserProcessor( + "_tag", securityContext, licenseState, "user", EnumSet.of(Property.USERNAME), true); + processor.execute(ingestDocument); + + @SuppressWarnings("unchecked") + Map result = ingestDocument.getFieldValue("user", Map.class); + assertThat(result.size(), equalTo(1)); + assertThat(result.get("name"), equalTo("_username")); + assertThat(result.get("username"), is(nullValue())); + } + + public void testWillThrowWhenEcsComplianceIsSetButParentFieldIsNotUser() throws Exception { + User user = new User("_username", null, null); + Authentication.RealmRef realmRef = new Authentication.RealmRef("_name", "_type", "_node_name"); + new Authentication(user, realmRef, null).writeToContext(threadContext); + + IngestDocument ingestDocument = new IngestDocument(new HashMap<>(), new HashMap<>()); + ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> new SetSecurityUserProcessor( + "_tag", securityContext, licenseState, "auth", EnumSet.of(Property.USERNAME), true)); + assertThat(e.getMessage(), containsString("[ecs_compliant] ESC compliance requires [field] value to be 'user'")); + } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/set_security_user/10_small_users_one_index.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/set_security_user/10_small_users_one_index.yml index 80a1ea12dec3d..8fc1c804583bb 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/set_security_user/10_small_users_one_index.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/set_security_user/10_small_users_one_index.yml @@ -127,7 +127,7 @@ teardown: index: shared_logs body: { "query" : { "match_all" : {} } } - match: { hits.total: 1} - - match: { hits.hits.0._source.user.username: joe} + - match: { hits.hits.0._source.user.name: joe} - match: { hits.hits.0._source.user.roles.0: company_x_logs_role} # John searches: @@ -139,5 +139,5 @@ teardown: index: shared_logs body: { "query" : { "match_all" : {} } } - match: { hits.total: 1} - - match: { hits.hits.0._source.user.username: john} + - match: { hits.hits.0._source.user.name: john} - match: { hits.hits.0._source.user.roles.0: company_y_logs_role} diff --git a/x-pack/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/20_small_users_one_index.yml b/x-pack/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/20_small_users_one_index.yml index 4c4e673cd29ef..7bcba1ad0b61b 100644 --- a/x-pack/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/20_small_users_one_index.yml +++ b/x-pack/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/20_small_users_one_index.yml @@ -75,7 +75,7 @@ teardown: "query" : { "template" : { "source" : { - "term" : { "user.username" : "{{_user.username}}" } + "term" : { "user.name" : "{{_user.username}}" } } } } @@ -116,7 +116,7 @@ teardown: index: shared_logs body: { "query" : { "match_all" : {} } } - match: { hits.total: 1} - - match: { hits.hits.0._source.user.username: joe} + - match: { hits.hits.0._source.user.name: joe} # John searches: - do: @@ -127,7 +127,7 @@ teardown: index: shared_logs body: { "query" : { "match_all" : {} } } - match: { hits.total: 1} - - match: { hits.hits.0._source.user.username: john} + - match: { hits.hits.0._source.user.name: john} --- "Test shared index separating user by using DLS role query with user's metadata": @@ -184,7 +184,7 @@ teardown: index: shared_logs body: { "query" : { "match_all" : {} } } - match: { hits.total: 1} - - match: { hits.hits.0._source.user.username: joe} + - match: { hits.hits.0._source.user.name: joe} # John searches: - do: @@ -195,4 +195,4 @@ teardown: index: shared_logs body: { "query" : { "match_all" : {} } } - match: { hits.total: 1} - - match: { hits.hits.0._source.user.username: john} + - match: { hits.hits.0._source.user.name: john}