From ee66d6f11f9af2d7b6d20fae742fbc77d284820b Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:44:57 +0100 Subject: [PATCH] Support audit ignore policy by actions (#67477) * Support audit ignore policy by index privileges Adding new audit ignore policy - privileges For example, following policy will filter out all events, which actions minimal required privilege is either "read" or "delete": xpack.security.audit.logfile.events.ignore_filters: example: privileges: ["read", "delete"] Resolve: #60877 Related: #10836 Related: #37148 * Support audit ignore policy by index privileges Adding new audit ignore policy - privileges For example, following policy will filter out all events, which actions required privilege is either "read" or "delete": xpack.security.audit.logfile.events.ignore_filters: example: privileges: ["read", "delete"] Resolve: #60877 Related: #10836 Related: #37148 * To avoid ambiguity (as cluster and index policies may have the same name) changing implementation to have to separate policies for `index_privileges` and `cluster_privileges`. If both are set for the same policy, throw the IllegalArgumentException. * To avoid ambiguity (as cluster and index policies may have the same name) changing implementation to have to separate policies for `index_privileges` and `cluster_privileges`. If both are set for the same policy, throw the IllegalArgumentException. * Fixing Api key related privilege check which expects request and authentication by introducing overloaded version of findPrivilegesThatGrant just checking if privileges which can grant the action regardless of the request and authentication context. * Fixing a test; adding a caching mechanism to avoid calling findPrivilegesThatGrant each time. * Support audit ignore policy by index privileges Addressing review feedback * Support audit ignore policy by index privileges Addressing review comments + changing approach: - use permission check instead of simple "checkIfGrants" - adding more testing * Support audit ignore policy by index privileges Addressing review comments + changing approach: - use permission check instead of simple "checkIfGrants" - adding more testing * Support audit ignore policy by index privileges Addressing review comments + changing approach: - use permission check instead of simple "checkIfGrants" - adding more testing * Support audit ignore policy by index privileges Addressing review comments + changing approach: - use permission check instead of simple "checkIfGrants" - adding more testing * Revert "Support audit ignore policy by index privileges" This reverts commit 152821e7 * Revert "Support audit ignore policy by index privileges" This reverts commit 79649e9a * Revert "Support audit ignore policy by index privileges" This reverts commit 96d22a42 * Revert "Support audit ignore policy by index privileges" This reverts commit 67574b2f * Revert "Support audit ignore policy by index privileges" This reverts commit 35573c8b * Revert "Fixing a test; adding a caching mechanism to avoid calling findPrivilegesThatGrant each time." This reverts commit 7faa52f3 * Revert "Fixing Api key related privilege check which expects request and authentication by introducing overloaded version of findPrivilegesThatGrant just checking if privileges which can grant the action regardless of the request and authentication context." This reverts commit 72b9aefe * Revert "To avoid ambiguity (as cluster and index policies may have the same name) changing implementation to have to separate policies for `index_privileges` and `cluster_privileges`. If both are set for the same policy, throw the IllegalArgumentException." This reverts commit 7dd8fe7d * Revert "To avoid ambiguity (as cluster and index policies may have the same name) changing implementation to have to separate policies for `index_privileges` and `cluster_privileges`. If both are set for the same policy, throw the IllegalArgumentException." This reverts commit cb5bc09c * Revert "Support audit ignore policy by index privileges" This reverts commit a918da10 * Support audit ignore policy by actions Getting back to action filtering * Support audit ignore policy by actions Cleaning up some tests * Support audit ignore policy by actions Cleaning up some tests Co-authored-by: Elastic Machine --- .../settings/audit-settings.asciidoc | 9 + .../AuditTrailSettingsUpdateTests.java | 9 +- .../audit/logfile/LoggingAuditTrail.java | 86 ++-- .../logfile/LoggingAuditTrailFilterTests.java | 410 ++++++++++++++++-- .../audit/logfile/LoggingAuditTrailTests.java | 12 +- 5 files changed, 466 insertions(+), 60 deletions(-) diff --git a/docs/reference/settings/audit-settings.asciidoc b/docs/reference/settings/audit-settings.asciidoc index a293444e2a25..280078fbe4b1 100644 --- a/docs/reference/settings/audit-settings.asciidoc +++ b/docs/reference/settings/audit-settings.asciidoc @@ -149,6 +149,15 @@ A list of authentication realm names or wildcards. The specified policy will not print audit events for users in these realms. // end::xpack-sa-lf-events-ignore-realms-tag[] +[[xpack-sa-lf-events-ignore-actions]] +// tag::xpack-sa-lf-events-ignore-actions-tag[] +`xpack.security.audit.logfile.events.ignore_filters..actions`:: +(<>) +A list of action names or wildcards. Action name can be found in the `action` +field of the audit event. The specified policy will not print audit events +for actions matching these values. +// end::xpack-sa-lf-events-ignore-actions-tag[] + [[xpack-sa-lf-events-ignore-roles]] // tag::xpack-sa-lf-events-ignore-roles-tag[] `xpack.security.audit.logfile.events.ignore_filters..roles`:: diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java index eff07f844e8c..9d86a7fb5206 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java @@ -96,7 +96,8 @@ public class AuditTrailSettingsUpdateTests extends SecurityIntegTestCase { final String[] allSettingsKeys = new String[] { "xpack.security.audit.logfile.events.ignore_filters.invalid.users", "xpack.security.audit.logfile.events.ignore_filters.invalid.realms", "xpack.security.audit.logfile.events.ignore_filters.invalid.roles", - "xpack.security.audit.logfile.events.ignore_filters.invalid.indices" }; + "xpack.security.audit.logfile.events.ignore_filters.invalid.indices", + "xpack.security.audit.logfile.events.ignore_filters.invalid.actions"}; settingsBuilder.put(randomFrom(allSettingsKeys), invalidLuceneRegex); final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> client().admin().cluster().prepareUpdateSettings().setTransientSettings(settingsBuilder.build()).get()); @@ -223,6 +224,12 @@ public class AuditTrailSettingsUpdateTests extends SecurityIntegTestCase { final List filteredIndices = randomNonEmptyListOfFilteredNames(); settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters." + policyName + ".indices", filteredIndices); } + if (randomBoolean()) { + // filter by actions + final List filteredActions = randomNonEmptyListOfFilteredNames(); + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters." + policyName + ".actions", + filteredActions); + } } while (settingsBuilder.build().isEmpty()); assertFalse(settingsBuilder.build().isEmpty()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index e560744ad854..71ca8ec39b62 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -218,6 +218,10 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { Setting.affixKeySetting(FILTER_POLICY_PREFIX, "indices", (key) -> Setting.listSetting(key, Collections.singletonList("*"), Function.identity(), value -> EventFilterPolicy.parsePredicate(value), Property.NodeScope, Property.Dynamic)); + protected static final Setting.AffixSetting> FILTER_POLICY_IGNORE_ACTIONS = + Setting.affixKeySetting(FILTER_POLICY_PREFIX, "actions", + (key) -> Setting.listSetting(key, Collections.singletonList("*"), Function.identity(), + value -> EventFilterPolicy.parsePredicate(value), Property.NodeScope, Property.Dynamic)); private static final Marker AUDIT_MARKER = MarkerManager.getMarker("org.elasticsearch.xpack.security.audit"); @@ -277,6 +281,12 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)).changeIndicesFilter(filtersList); this.eventFilterPolicyRegistry.set(policyName, newPolicy); }, (policyName, filtersList) -> EventFilterPolicy.parsePredicate(filtersList)); + clusterService.getClusterSettings().addAffixUpdateConsumer(FILTER_POLICY_IGNORE_ACTIONS, (policyName, filtersList) -> { + final Optional policy = eventFilterPolicyRegistry.get(policyName); + final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)). + changeActionsFilter(filtersList); + this.eventFilterPolicyRegistry.set(policyName, newPolicy); + }, (policyName, filtersList) -> EventFilterPolicy.parsePredicate(filtersList)); // this log filter ensures that audit events are not filtered out because of the log level final LoggerContext ctx = LoggerContext.getContext(false); MarkerFilter auditMarkerFilter = MarkerFilter.createFilter(AUDIT_MARKER.getName(), Result.ACCEPT, Result.NEUTRAL); @@ -295,6 +305,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), Optional.empty(), + Optional.empty(), Optional.empty())) == false) { // this is redundant information maintained for bwc purposes final String authnRealm = authentication.getAuthenticatedBy().getName(); @@ -323,7 +334,8 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), Optional.empty(), - indices)) == false) { + indices, + Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "authentication_success") @@ -345,7 +357,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (events.contains(ANONYMOUS_ACCESS_DENIED)) { final Optional indices = indices(transportRequest); if (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices)) == false) { + .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") @@ -383,7 +395,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (events.contains(AUTHENTICATION_FAILED)) { final Optional indices = indices(transportRequest); if (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), indices)) == false) { + .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") @@ -421,7 +433,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (events.contains(AUTHENTICATION_FAILED)) { final Optional indices = indices(transportRequest); if (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices)) == false) { + .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") @@ -440,7 +452,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { @Override public void authenticationFailed(String requestId, AuthenticationToken token, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), Optional.empty())) == false) { + .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), Optional.empty(), Optional.empty())) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") @@ -461,7 +473,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (events.contains(REALM_AUTHENTICATION_FAILED)) { final Optional indices = indices(transportRequest); if (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), indices)) == false) { + .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") @@ -482,7 +494,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { @Override public void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request) { if (events.contains(REALM_AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), Optional.empty())) == false) { + .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), Optional.empty(), Optional.empty())) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") @@ -508,7 +520,8 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(user), // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), - Optional.of(authorizationInfo), indices)) == false) { + Optional.of(authorizationInfo), indices, + Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "access_granted") @@ -593,7 +606,8 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { .test(new AuditEventMetaInfo(Optional.of(user), // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), - Optional.of(authorizationInfo), Optional.ofNullable(indices))) == false) { + Optional.of(authorizationInfo), Optional.ofNullable(indices), + Optional.of(action))) == false) { final LogEntryBuilder logEntryBuilder = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, eventType == ACCESS_DENIED ? "access_denied" : "access_granted") @@ -628,7 +642,8 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), - Optional.of(authorizationInfo), indices)) == false) { + Optional.of(authorizationInfo), indices, + Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "access_denied") @@ -667,7 +682,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (events.contains(TAMPERED_REQUEST)) { final Optional indices = indices(transportRequest); if (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices)) == false) { + .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "tampered_request") @@ -692,7 +707,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), Optional.empty(), - indices)) == false) { + indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "tampered_request") @@ -751,7 +766,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), - Optional.of(authorizationInfo), indices)) == false) { + Optional.of(authorizationInfo), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "run_as_granted") @@ -777,7 +792,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), - Optional.of(authorizationInfo), indices)) == false) { + Optional.of(authorizationInfo), indices, Optional.of(action))) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") @@ -801,7 +816,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { new AuditEventMetaInfo(Optional.of(authentication.getUser()), // can be null for API keys created before version 7.7 Optional.ofNullable(ApiKeyService.getCreatorRealmName(authentication)), - Optional.of(authorizationInfo), Optional.empty())) == false) { + Optional.of(authorizationInfo), Optional.empty(), Optional.empty())) == false) { new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") @@ -1327,6 +1342,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { settings.add(FILTER_POLICY_IGNORE_INDICES); settings.add(FILTER_POLICY_IGNORE_ROLES); settings.add(FILTER_POLICY_IGNORE_REALMS); + settings.add(FILTER_POLICY_IGNORE_ACTIONS); } /** @@ -1342,12 +1358,14 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { private final Predicate ignoreRealmsPredicate; private final Predicate ignoreRolesPredicate; private final Predicate ignoreIndicesPredicate; + private final Predicate ignoreActionsPredicate; EventFilterPolicy(String name, Settings settings) { this(name, parsePredicate(FILTER_POLICY_IGNORE_PRINCIPALS.getConcreteSettingForNamespace(name).get(settings)), parsePredicate(FILTER_POLICY_IGNORE_REALMS.getConcreteSettingForNamespace(name).get(settings)), parsePredicate(FILTER_POLICY_IGNORE_ROLES.getConcreteSettingForNamespace(name).get(settings)), - parsePredicate(FILTER_POLICY_IGNORE_INDICES.getConcreteSettingForNamespace(name).get(settings))); + parsePredicate(FILTER_POLICY_IGNORE_INDICES.getConcreteSettingForNamespace(name).get(settings)), + parsePredicate(FILTER_POLICY_IGNORE_ACTIONS.getConcreteSettingForNamespace(name).get(settings))); } /** @@ -1356,33 +1374,40 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { * a singleton list of the empty string ([""]). */ EventFilterPolicy(String name, Predicate ignorePrincipalsPredicate, Predicate ignoreRealmsPredicate, - Predicate ignoreRolesPredicate, Predicate ignoreIndicesPredicate) { + Predicate ignoreRolesPredicate, Predicate ignoreIndicesPredicate, + Predicate ignoreActionsPredicate) { this.name = name; // "null" values are "unexpected" and should not match any ignore policy this.ignorePrincipalsPredicate = ignorePrincipalsPredicate; this.ignoreRealmsPredicate = ignoreRealmsPredicate; this.ignoreRolesPredicate = ignoreRolesPredicate; this.ignoreIndicesPredicate = ignoreIndicesPredicate; + this.ignoreActionsPredicate = ignoreActionsPredicate; } private EventFilterPolicy changePrincipalsFilter(List filtersList) { return new EventFilterPolicy(name, parsePredicate(filtersList), ignoreRealmsPredicate, ignoreRolesPredicate, - ignoreIndicesPredicate); + ignoreIndicesPredicate, ignoreActionsPredicate); } private EventFilterPolicy changeRealmsFilter(List filtersList) { return new EventFilterPolicy(name, ignorePrincipalsPredicate, parsePredicate(filtersList), ignoreRolesPredicate, - ignoreIndicesPredicate); + ignoreIndicesPredicate, ignoreActionsPredicate); } private EventFilterPolicy changeRolesFilter(List filtersList) { return new EventFilterPolicy(name, ignorePrincipalsPredicate, ignoreRealmsPredicate, parsePredicate(filtersList), - ignoreIndicesPredicate); + ignoreIndicesPredicate, ignoreActionsPredicate); } private EventFilterPolicy changeIndicesFilter(List filtersList) { return new EventFilterPolicy(name, ignorePrincipalsPredicate, ignoreRealmsPredicate, ignoreRolesPredicate, - parsePredicate(filtersList)); + parsePredicate(filtersList), ignoreActionsPredicate); + } + + private EventFilterPolicy changeActionsFilter(List filtersList) { + return new EventFilterPolicy(name, ignorePrincipalsPredicate, ignoreRealmsPredicate, ignoreRolesPredicate, + ignoreIndicesPredicate, parsePredicate(filtersList)); } static Predicate parsePredicate(List l) { @@ -1408,16 +1433,20 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { * predicate of the corresponding field. */ Predicate ignorePredicate() { - return eventInfo -> eventInfo.principal != null && ignorePrincipalsPredicate.test(eventInfo.principal) + return eventInfo -> { + return eventInfo.principal != null && ignorePrincipalsPredicate.test(eventInfo.principal) && eventInfo.realm != null && ignoreRealmsPredicate.test(eventInfo.realm) + && eventInfo.action != null && ignoreActionsPredicate.test(eventInfo.action) && eventInfo.roles.get().allMatch(role -> role != null && ignoreRolesPredicate.test(role)) && eventInfo.indices.get().allMatch(index -> index != null && ignoreIndicesPredicate.test(index)); + }; } @Override public String toString() { return "[users]:" + ignorePrincipalsPredicate.toString() + "&[realms]:" + ignoreRealmsPredicate.toString() + "&[roles]:" - + ignoreRolesPredicate.toString() + "&[indices]:" + ignoreIndicesPredicate.toString(); + + ignoreRolesPredicate.toString() + "&[indices]:" + ignoreIndicesPredicate.toString() + "&[actions]:" + + ignoreActionsPredicate.toString(); } } @@ -1477,11 +1506,13 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { static final class AuditEventMetaInfo { final String principal; final String realm; + final String action; final Supplier> roles; final Supplier> indices; // empty is used for events can be filtered out only by the lack of a field - static final AuditEventMetaInfo EMPTY = new AuditEventMetaInfo(Optional.empty(), Optional.empty(), Optional.empty()); + static final AuditEventMetaInfo EMPTY = new AuditEventMetaInfo(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty()); /** * If a field is missing for an event, its value for filtering purposes is the @@ -1492,9 +1523,10 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { * "elastic" username. */ AuditEventMetaInfo(Optional user, Optional realm, Optional authorizationInfo, - Optional indices) { + Optional indices, Optional action) { this.principal = user.map(u -> u.principal()).orElse(""); this.realm = realm.orElse(""); + this.action = action.orElse(""); // Supplier indirection and lazy generation of Streams serves 2 purposes: // 1. streams might not get generated due to short circuiting logical // conditions on the `principal` and `realm` fields @@ -1510,9 +1542,11 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener { .map(Arrays::stream).orElse(Stream.of("")); } - AuditEventMetaInfo(Optional authenticationToken, Optional realm, Optional indices) { + AuditEventMetaInfo(Optional authenticationToken, Optional realm, Optional indices, + Optional action) { this.principal = authenticationToken.map(u -> u.principal()).orElse(""); this.realm = realm.orElse(""); + this.action = action.orElse(""); this.roles = () -> Stream.of(""); this.indices = () -> indices.filter(r -> r.length != 0).map(i -> Arrays.stream(i)).orElse(Stream.of("")); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index c9ffd2364309..61ac598ba1b9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -54,6 +54,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Random; import java.util.stream.Collectors; import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; @@ -120,12 +121,17 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { // filter by indices final List filteredIndices = randomNonEmptyListOfFilteredNames(); settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.indicesPolicy.indices", filteredIndices); + // filter by actions + final List filteredActions = randomNonEmptyListOfFilteredActions(); + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.actionsPolicy.actions", + filteredActions); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); // user field matches assertTrue("Matches the user filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test( - new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.empty(), Optional.empty(), Optional.empty()))); + new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty()))); final User unfilteredUser = mock(User.class); if (randomBoolean()) { when(unfilteredUser.authenticatedUser()).thenReturn(new User(randomFrom(filteredUsernames))); @@ -133,39 +139,52 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { // null user field does NOT match assertFalse("Does not match the user filter predicate because of null username.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.empty(), Optional.empty(), Optional.empty()))); + .test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty()))); // realm field matches assertTrue("Matches the realm filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test( - new AuditEventMetaInfo(Optional.empty(), Optional.of(randomFrom(filteredRealms)), Optional.empty(), Optional.empty()))); + new AuditEventMetaInfo(Optional.empty(), Optional.of(randomFrom(filteredRealms)), Optional.empty(), Optional.empty(), + Optional.empty()))); // null realm field does NOT match assertFalse("Does not match the realm filter predicate because of null realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.empty(), Optional.ofNullable(null), Optional.empty(), Optional.empty()))); + .test(new AuditEventMetaInfo(Optional.empty(), Optional.ofNullable(null), Optional.empty(), Optional.empty(), + Optional.empty()))); // role field matches assertTrue("Matches the role filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), Optional.of(authzInfo( randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0]))), - Optional.empty()))); + Optional.empty(), Optional.empty()))); + // action field matches + Random random = random(); + assertTrue("Matches the actions filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test( + new AuditEventMetaInfo(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.of(randomFrom(filteredActions))))); + // null privilege field does NOT match + assertFalse("Does not matches the actions filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() + .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.ofNullable(null)))); final List unfilteredRoles = new ArrayList<>(); unfilteredRoles.add(null); unfilteredRoles.addAll(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles)); // null role among roles field does NOT match assertFalse("Does not match the role filter predicate because of null role.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), - Optional.of(authzInfo(unfilteredRoles.toArray(new String[0]))), Optional.empty()))); + Optional.of(authzInfo(unfilteredRoles.toArray(new String[0]))), Optional.empty(), Optional.empty()))); // indices field matches assertTrue("Matches the index filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), Optional.empty(), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.empty()))); final List unfilteredIndices = new ArrayList<>(); unfilteredIndices.add(null); unfilteredIndices.addAll(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices)); // null index among indices field does NOT match assertFalse("Does not match the indices filter predicate because of null index.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), - Optional.empty(), Optional.of(unfilteredIndices.toArray(new String[0]))))); + Optional.empty(), Optional.of(unfilteredIndices.toArray(new String[0])), Optional.empty()))); } public void testSingleCompletePolicyPredicate() throws Exception { @@ -192,15 +211,21 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { // filter by indices final List filteredIndices = randomNonEmptyListOfFilteredNames(); settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.completeFilterPolicy.indices", filteredIndices); + // filter by actions + final List filteredActions = randomNonEmptyListOfFilteredActions(); + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.completeFilterPolicy.actions", + filteredActions); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); // all fields match + Random random = random(); assertTrue("Matches the filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo( Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final User unfilteredUser; if (randomBoolean()) { unfilteredUser = new User(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)); @@ -214,25 +239,36 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertFalse("Does not match the filter predicate because of the empty user.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertFalse("Does not match the filter predicate because of the realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertFalse("Does not match the filter predicate because of the empty realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.empty(), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); + assertFalse("Does not match the filter predicate because of the empty actions.", + auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), + Optional.of(randomFrom(filteredRealms)), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.empty()))); final List someRolesDoNotMatch = new ArrayList<>(randomSubsetOf(randomIntBetween(0, filteredRoles.size()), filteredRoles)); for (int i = 0; i < randomIntBetween(1, 8); i++) { someRolesDoNotMatch.add(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)); @@ -240,12 +276,14 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { assertFalse("Does not match the filter predicate because of some of the roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final Optional emptyRoles = randomBoolean() ? Optional.empty() : Optional.of(authzInfo(new String[0])); assertFalse("Does not match the filter predicate because of the empty roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), emptyRoles, - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final List someIndicesDoNotMatch = new ArrayList<>( randomSubsetOf(randomIntBetween(0, filteredIndices.size()), filteredIndices)); for (int i = 0; i < randomIntBetween(1, 8); i++) { @@ -256,14 +294,15 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); + Optional.of(someIndicesDoNotMatch.toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final Optional emptyIndices = randomBoolean() ? Optional.empty() : Optional.of(new String[0]); assertFalse("Does not match the filter predicate because of the empty indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - emptyIndices))); + emptyIndices, Optional.of(randomFrom(filteredActions))))); } public void testSingleCompleteWithEmptyFieldPolicyPredicate() throws Exception { @@ -297,16 +336,22 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { filteredIndices.add(""); // filter by missing index name settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.completeFilterPolicy.indices", filteredIndices); filteredIndices.remove(""); + // filter by actions + final List filteredActions = randomNonEmptyListOfFilteredActions(); + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.completeFilterPolicy.actions", + filteredActions); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); // all fields match + Random random = random(); assertTrue("Matches the filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final User unfilteredUser; if (randomBoolean()) { unfilteredUser = new User(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)); @@ -320,25 +365,36 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertTrue("Matches the filter predicate because of the empty user.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertFalse("Does not match the filter predicate because of the realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertTrue("Matches the filter predicate because of the empty realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.empty(), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); + assertFalse("Does not match the filter predicate because of the pivilege.", + auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), + Optional.of(randomFrom(filteredRealms)), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8))))); final List someRolesDoNotMatch = new ArrayList<>(randomSubsetOf(randomIntBetween(0, filteredRoles.size()), filteredRoles)); for (int i = 0; i < randomIntBetween(1, 8); i++) { someRolesDoNotMatch.add(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)); @@ -346,12 +402,14 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { assertFalse("Does not match the filter predicate because of some of the roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final Optional emptyRoles = randomBoolean() ? Optional.empty() : Optional.of(authzInfo(new String[0])); assertTrue("Matches the filter predicate because of the empty roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), emptyRoles, - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); final List someIndicesDoNotMatch = new ArrayList<>( randomSubsetOf(randomIntBetween(0, filteredIndices.size()), filteredIndices)); for (int i = 0; i < randomIntBetween(1, 8); i++) { @@ -362,22 +420,24 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); + Optional.of(someIndicesDoNotMatch.toArray(new String[0])), + Optional.of(randomFrom(filteredActions))))); assertTrue("Matches the filter predicate because of the empty indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo( randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0]))), - Optional.empty()))); + Optional.empty(), Optional.of(randomFrom(filteredActions))))); assertTrue("Matches the filter predicate because of the empty indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo( randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0]))), - Optional.of(new String[0])))); + Optional.of(new String[0]), Optional.of(randomFrom(filteredActions))))); assertTrue("Matches the filter predicate because of the empty indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo( randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0]))), - Optional.of(new String[] { null })))); + Optional.of(new String[] { null }), + Optional.of(randomFrom(filteredActions))))); } public void testTwoPolicyPredicatesWithMissingFields() throws Exception { @@ -427,27 +487,29 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.empty()))); // matches first policy but not the second assertTrue("Matches the first filter predicate but not the second.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) .toArray(new String[0]))), - Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); + Optional.of(someIndicesDoNotMatch.toArray(new String[0])), Optional.of("_action")))); // matches the second policy but not the first assertTrue("Matches the second filter predicate but not the first.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); + Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0])), + Optional.empty()))); // matches neither the first nor the second policies assertFalse("Matches neither the first nor the second filter predicates.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), - Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); + Optional.of(someIndicesDoNotMatch.toArray(new String[0])), Optional.empty()))); } public void testUsersFilter() throws Exception { @@ -1817,6 +1879,231 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { threadContext.stashContext(); } + public void testActionsFilter() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + final List filteredActions = randomNonEmptyListOfFilteredActions(); + + final Settings.Builder settingsBuilder = Settings.builder().put(settings); + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.actionsPolicy.actions", + filteredActions); + // a filter for a field consisting of an empty string ("") or an empty list([]) + // will match events that lack that field + final boolean filterMissingAction = randomBoolean(); + if (filterMissingAction) { + if (randomBoolean()) { + filteredActions.add(""); + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.missingPolicy.actions", + filteredActions); + } else { + settingsBuilder.putList("xpack.security.audit.logfile.events.ignore_filters.missingPolicy.actions", + Collections.emptyList()); + } + } + final String filteredAction = randomFrom(filteredActions); + final String unfilteredAction = "mock_action/mock_action"; + User user; + if (randomBoolean()) { + user = new User("user1", new String[] { "r1" }, new User("authUsername", new String[] { "r2" })); + } else { + user = new User("user1", new String[] { "r1" }); + } + final TransportRequest request = randomBoolean() ? new MockRequest(threadContext) + : new MockIndicesRequest(threadContext, new String[] { "idx1", "idx2" }); + final MockToken authToken = new MockToken("token1"); + final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); + final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); + + // anonymous accessDenied + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), filteredAction, request); + assertThat("Anonymous message: not filtered out by the action filter", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), getRestRequest()); + if (filterMissingAction){ + assertThat("Anonymous rest request: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("Anonymous rest request: filtered out by action filter", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + // authenticationFailed + auditTrail.authenticationFailed(randomAlphaOfLength(8), getRestRequest()); + if (filterMissingAction){ + assertThat("AuthenticationFailed: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("AuthenticationFailed: filtered out by action filter", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, filteredAction, request); + assertThat("AuthenticationFailed: not filtered out by the action filter", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationFailed(randomAlphaOfLength(8), filteredAction, request); + assertThat("AuthenticationFailed no token message: not filtered out by the action filter", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, getRestRequest()); + if (filterMissingAction) { + assertThat("AuthenticationFailed rest request: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("AuthenticationFailed rest request: filtered out by action filter", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationFailed(randomAlphaOfLength(8), "realm", authToken, unfilteredAction, request); + assertThat("AuthenticationFailed realm message: unfiltered action is filtered out", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationFailed(randomAlphaOfLength(8), "realm", authToken, filteredAction, request); + assertThat("AuthenticationFailed realm message: filtered action is not filtered out", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationFailed(randomAlphaOfLength(8), "realm", authToken, getRestRequest()); + if (filterMissingAction) { + assertThat("AuthenticationFailed realm rest request: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("AuthenticationFailed realm rest request: filtered out by the action filters", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + // accessGranted + Authentication authentication = createAuthentication(user, "realm"); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, filteredAction, request, authzInfo(new String[]{"role1"})); + assertThat("AccessGranted message: not filtered out by the action filters", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, unfilteredAction, request, authzInfo(new String[]{"role1"})); + assertThat("AccessGranted message: unfiltered action filtered out by the action filter", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + logOutput.clear(); + threadContext.stashContext(); + + // accessDenied + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, filteredAction, request, authzInfo(new String[]{"role1"})); + assertThat("AccessDenied message: not filtered out by the action filters", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, unfilteredAction, request, authzInfo(new String[]{"role1"})); + assertThat("AccessDenied message: unfiltered action filtered out by the action filter", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + + // tamperedRequest + auditTrail.tamperedRequest(randomAlphaOfLength(8), getRestRequest()); + if (filterMissingAction) { + assertThat("Tampered rest: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("Tampered rest: filtered out by the action filters", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.tamperedRequest(randomAlphaOfLength(8), filteredAction, request); + assertThat("Tampered message: not filtered out by the action filters", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.tamperedRequest(randomAlphaOfLength(8), authentication, filteredAction, request); + assertThat("Tampered message: not filtered out by the action filters", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.tamperedRequest(randomAlphaOfLength(8), authentication, unfilteredAction, request); + assertThat("Tampered message: unfiltered action filtered out by the action filter", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + + // connection denied + auditTrail.connectionDenied(InetAddress.getLoopbackAddress(), "default", new SecurityIpFilterRule(false, "_all")); + if (filterMissingAction) { + assertThat("Connection denied: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("Connection denied: filtered out by the action filters", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + // connection granted + auditTrail.connectionGranted(InetAddress.getLoopbackAddress(), "default", new SecurityIpFilterRule(false, "_all")); + if (filterMissingAction) { + assertThat("Connection granted: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("Connection granted: filtered out by the action filters", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + // runAsGranted + auditTrail.runAsGranted(randomAlphaOfLength(8), createAuthentication(user, "realm"), filteredAction, + new MockRequest(threadContext), authzInfo(new String[] { "role1" })); + assertThat("RunAsGranted message: not filtered out by the action filters", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.runAsGranted(randomAlphaOfLength(8), createAuthentication(user, "realm"), unfilteredAction, + new MockRequest(threadContext), authzInfo(new String[] { "role1" })); + assertThat("RunAsGranted message: unfiltered action is filtered out", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + + // runAsDenied + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, "realm"), filteredAction, new MockRequest(threadContext), + authzInfo(new String[] { "role1" })); + assertThat("RunAsDenied message: not filtered out by the action filters", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, "realm"), unfilteredAction, + new MockRequest(threadContext), authzInfo(new String[] { "role1" })); + assertThat("RunAsDenied message: unfiltered action filtered out by the action filters", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, "realm"), getRestRequest(), + authzInfo(new String[] { "role1" })); + if (filterMissingAction) { + assertThat("RunAsDenied rest request: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("RunAsDenied rest request: filtered out by the action filters", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + // authentication Success + auditTrail.authenticationSuccess(randomAlphaOfLength(8), createAuthentication(user, "realm"), getRestRequest()); + if (filterMissingAction) { + assertThat("AuthenticationSuccess rest request: not filtered out by the missing action filter", logOutput.size(), is(0)); + } else { + assertThat("AuthenticationSuccess rest request: filtered out by the action filters", logOutput.size(), is(1)); + } + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationSuccess(randomAlphaOfLength(8), createAuthentication(user, "realm"), filteredAction, request); + assertThat("AuthenticationSuccess message: filtered action is not filtered out", logOutput.size(), is(0)); + logOutput.clear(); + threadContext.stashContext(); + + auditTrail.authenticationSuccess(randomAlphaOfLength(8), createAuthentication(user, "realm"), unfilteredAction, request); + assertThat("AuthenticationSuccess message: unfiltered action is filtered out", logOutput.size(), is(1)); + logOutput.clear(); + threadContext.stashContext(); + } + private List randomListFromLengthBetween(List l, int min, int max) { assert (min >= 0) && (min <= max) && (max <= l.size()); final int len = randomIntBetween(min, max); @@ -1851,6 +2138,65 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { return filtered; } + private List randomNonEmptyListOfFilteredActions() { + final List filtered = new ArrayList<>(4); + final String[] actionPatterns = { + "internal:transport/proxy/indices:*", + "indices:data/read/*", + "internal:transport/proxy/indices:data/read/*", + "indices:data/write/index*", + "indices:data/write/bulk*", + "indices:data/write/index", + "indices:data/write/index[*", + "indices:data/write/index:op_type/create", + "indices:data/write/update*", + "indices:data/write/delete*", + "indices:data/write/*", + "indices:monitor/*", + "indices:admin/*", + "indices:admin/ilm/*", + "indices:admin/refresh*", + "indices:admin/flush*", + "indices:admin/synced_flush", + "indices:admin/forcemerge*", + "cluster:admin/xpack/security/*", + "cluster:admin/xpack/security/saml/*", + "cluster:admin/xpack/security/oidc/*", + "cluster:admin/xpack/security/token/*", + "cluster:admin/xpack/security/api_key/*", + "cluster:monitor/*", + "cluster:monitor/xpack/ml/*", + "cluster:monitor/text_structure/*", + "cluster:monitor/data_frame/*", + "cluster:monitor/xpack/watcher/*", + "cluster:monitor/xpack/rollup/*", + "cluster:*", + "indices:admin/index_template/*", + "indices:admin/data_stream/*", + "cluster:admin/xpack/ml/*", + "cluster:admin/data_frame/*", + "cluster:monitor/data_frame/*", + "cluster:monitor/transform/*", + "cluster:admin/transform/*", + "cluster:admin/xpack/watcher/*", + "cluster:monitor/nodes/liveness", + "cluster:monitor/state", + "indices:admin/template/*", + "cluster:admin/component_template/*", + "cluster:admin/ingest/pipeline/*", + "cluster:admin/xpack/rollup/*", + "cluster:admin/xpack/ccr/*", + "cluster:admin/ilm/*", + "cluster:admin/slm/*", + "cluster:admin/xpack/enrich/*"}; + Random random = random(); + for (int i = 0; i < randomIntBetween(1, 4); i++) { + Object name = actionPatterns[random.nextInt(actionPatterns.length)]; + filtered.add((String)name); + } + return filtered; + } + private RestRequest getRestRequest() throws IOException { final RestContent content = randomFrom(RestContent.values()); final FakeRestRequest.Builder builder = new Builder(NamedXContentRegistry.EMPTY); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index b043c2d201a7..bad457d612e7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -273,7 +273,8 @@ public class LoggingAuditTrailTests extends ESTestCase { LoggingAuditTrail.INCLUDE_EVENT_SETTINGS, LoggingAuditTrail.EXCLUDE_EVENT_SETTINGS, LoggingAuditTrail.INCLUDE_REQUEST_BODY, LoggingAuditTrail.FILTER_POLICY_IGNORE_PRINCIPALS, LoggingAuditTrail.FILTER_POLICY_IGNORE_REALMS, LoggingAuditTrail.FILTER_POLICY_IGNORE_ROLES, - LoggingAuditTrail.FILTER_POLICY_IGNORE_INDICES, Loggers.LOG_LEVEL_SETTING)); + LoggingAuditTrail.FILTER_POLICY_IGNORE_INDICES, LoggingAuditTrail.FILTER_POLICY_IGNORE_ACTIONS, + Loggers.LOG_LEVEL_SETTING)); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); commonFields = new LoggingAuditTrail.EntryCommonFields(settings, localNode).commonFields; threadContext = new ThreadContext(Settings.EMPTY); @@ -342,6 +343,15 @@ public class LoggingAuditTrailTests extends ESTestCase { e = expectThrows(IllegalArgumentException.class, () -> LoggingAuditTrail.FILTER_POLICY_IGNORE_INDICES.getConcreteSettingForNamespace("filter4").get(settings4)); assertThat(e, hasToString(containsString("invalid pattern [/no-inspiration]"))); + + Settings settings5 = Settings.builder() + .putList(prefix + "ignore_filters.filter2.users", Arrays.asList("tom", "cruise")) + .putList(prefix + "ignore_filters.filter2.actions", Arrays.asList("indices:data/read/*", "/foo")).build(); + assertThat(LoggingAuditTrail.FILTER_POLICY_IGNORE_PRINCIPALS.getConcreteSettingForNamespace("filter2").get(settings5), + containsInAnyOrder("tom", "cruise")); + e = expectThrows(IllegalArgumentException.class, + () -> LoggingAuditTrail.FILTER_POLICY_IGNORE_ACTIONS.getConcreteSettingForNamespace("filter2").get(settings5)); + assertThat(e, hasToString(containsString("invalid pattern [/foo]"))); } public void testSecurityConfigChangeEventFormattingForRoles() throws IOException {