From 4966365adbe68efdde84088bb1afe0ca253fcfb8 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 19 Nov 2020 19:19:17 +1100 Subject: [PATCH 01/23] WIP: operator privileges initial working code with smoke tests --- .../license/XPackLicenseState.java | 4 +- .../security/authc/AuthenticationField.java | 2 + .../xpack/security/Security.java | 10 +- .../security/authc/AuthenticationService.java | 8 +- .../security/authz/AuthorizationService.java | 13 +- .../operator/CompositeOperatorOnly.java | 120 ++++++++ .../xpack/security/operator/OperatorOnly.java | 45 +++ .../security/operator/OperatorPrivileges.java | 62 ++++ .../operator/OperatorUserDescriptor.java | 269 ++++++++++++++++++ .../authc/AuthenticationServiceTests.java | 22 +- .../support/SecondaryAuthenticatorTests.java | 2 +- .../authz/AuthorizationServiceTests.java | 9 +- .../operator/OperatorUserDescriptorTests.java | 36 +++ .../qa/operator-privileges-tests/build.gradle | 24 ++ .../operator/OperatorPrivilegesIT.java | 63 ++++ .../src/test/resources/operator_users.yml | 2 + .../src/test/resources/roles.yml | 3 + 17 files changed, 673 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java create mode 100644 x-pack/qa/operator-privileges-tests/build.gradle create mode 100644 x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java create mode 100644 x-pack/qa/operator-privileges-tests/src/test/resources/operator_users.yml create mode 100644 x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 753ae5de3b44c..8a2a5e0b08fb3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -98,7 +98,9 @@ public enum Feature { ANALYTICS(OperationMode.MISSING, true), - SEARCHABLE_SNAPSHOTS(OperationMode.ENTERPRISE, true); + SEARCHABLE_SNAPSHOTS(OperationMode.ENTERPRISE, true), + + OPERATOR_PRIVILEGES(OperationMode.ENTERPRISE, true); final OperationMode minimumOperationMode; final boolean needsActive; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java index 12fab154b8ef7..e90bfa7b7c251 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java @@ -10,6 +10,8 @@ public final class AuthenticationField { public static final String AUTHENTICATION_KEY = "_xpack_security_authentication"; public static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors"; public static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors"; + public static final String PRIVILEGE_CATEGORY_KEY = "_security_privilege_category"; + public static final String PRIVILEGE_CATEGORY_VALUE_OPERATOR = "operator"; private AuthenticationField() {} } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 8cd10494e0c11..2c32accca422c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -214,6 +214,9 @@ import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor; +import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges; +import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestClearApiKeyCacheAction; @@ -473,8 +476,10 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste getLicenseState().addListener(new SecurityStatusChangeListener(getLicenseState())); final AuthenticationFailureHandler failureHandler = createAuthenticationFailureHandler(realms, extensionComponents); + final OperatorPrivileges operatorPrivileges = new OperatorPrivileges(settings, getLicenseState(), + new OperatorUserDescriptor(environment, resourceWatcherService), new CompositeOperatorOnly()); authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool, - anonymousUser, tokenService, apiKeyService)); + anonymousUser, tokenService, apiKeyService, operatorPrivileges)); components.add(authcService.get()); securityIndex.get().addIndexStateListener(authcService.get()::onSecurityIndexStateChange); @@ -492,7 +497,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine(), requestInterceptors, - getLicenseState(), expressionResolver); + getLicenseState(), expressionResolver, operatorPrivileges); components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions @@ -673,6 +678,7 @@ public static List> getSettings(List securityExten settingsList.add(ApiKeyService.DOC_CACHE_TTL_SETTING); settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING); + settingsList.add(OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED); // hide settings settingsList.add(Setting.listSetting(SecurityField.setting("hide_settings"), Collections.emptyList(), Function.identity(), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 42cab54b0ca4a..4f97550d77ea4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -32,6 +32,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -46,6 +47,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; @@ -86,13 +88,15 @@ public class AuthenticationService { private final Cache lastSuccessfulAuthCache; private final AtomicLong numInvalidation = new AtomicLong(); private final ApiKeyService apiKeyService; + private final OperatorPrivileges operatorPrivileges; private final boolean runAsEnabled; private final boolean isAnonymousUserEnabled; private final AuthenticationContextSerializer authenticationSerializer; public AuthenticationService(Settings settings, Realms realms, AuditTrailService auditTrailService, AuthenticationFailureHandler failureHandler, ThreadPool threadPool, - AnonymousUser anonymousUser, TokenService tokenService, ApiKeyService apiKeyService) { + AnonymousUser anonymousUser, TokenService tokenService, ApiKeyService apiKeyService, + OperatorPrivileges operatorPrivileges) { this.nodeName = Node.NODE_NAME_SETTING.get(settings); this.realms = realms; this.auditTrailService = auditTrailService; @@ -111,6 +115,7 @@ public AuthenticationService(Settings settings, Realms realms, AuditTrailService this.lastSuccessfulAuthCache = null; } this.apiKeyService = apiKeyService; + this.operatorPrivileges = operatorPrivileges; this.authenticationSerializer = new AuthenticationContextSerializer(); } @@ -689,6 +694,7 @@ void writeAuthToContext(Authentication authentication) { try { authenticationSerializer.writeToContext(authentication, threadContext); request.authenticationSuccess(authentication); + operatorPrivileges.maybeMarkOperatorUser(authentication, threadContext); } catch (Exception e) { action = () -> { logger.debug( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index ecb9fd43cc7f1..9651d49aa047b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -73,6 +73,7 @@ import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import java.util.ArrayList; import java.util.Collection; @@ -118,6 +119,7 @@ public class AuthorizationService { private final AuthorizationEngine authorizationEngine; private final Set requestInterceptors; private final XPackLicenseState licenseState; + private final OperatorPrivileges operatorPrivileges; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; @@ -125,7 +127,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C AuditTrailService auditTrailService, AuthenticationFailureHandler authcFailureHandler, ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine, Set requestInterceptors, XPackLicenseState licenseState, - IndexNameExpressionResolver resolver) { + IndexNameExpressionResolver resolver, OperatorPrivileges operatorPrivileges) { this.clusterService = clusterService; this.auditTrailService = auditTrailService; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver); @@ -139,6 +141,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.requestInterceptors = requestInterceptors; this.settings = settings; this.licenseState = licenseState; + this.operatorPrivileges = operatorPrivileges; } public void checkPrivileges(Authentication authentication, HasPrivilegesRequest request, @@ -184,6 +187,14 @@ public void authorize(final Authentication authentication, final String action, // if there is already an original action, that stays put (eg. the current action is a child action) putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action); + // Check operator privileges first if applicable + final ElasticsearchSecurityException operatorException = operatorPrivileges.check(action, originalRequest, threadContext); + if (operatorException != null) { + // TODO: audit + listener.onFailure(denialException(authentication, action, operatorException)); + return; + } + String auditId = AuditUtil.extractRequestId(threadContext); if (auditId == null) { // We would like to assert that there is an existing request-id, but if this is a system action, then that might not be diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java new file mode 100644 index 0000000000000..de62039f7cde4 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction; +import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction; +import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryAction; +import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.transport.TransportRequest; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_HTTP_SETTING; +import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_SETTING; + +public class CompositeOperatorOnly implements OperatorOnly { + + private final List checks; + + public CompositeOperatorOnly() { + checks = List.of(new ActionOperatorOnly(), new SettingOperatorOnly()); + } + + @Override + public Result check(String action, TransportRequest request) { + return checks.stream() + .map(c -> c.check(action, request)) + .filter(r -> r.getStatus() != Status.ABSTAIN) + .findFirst() + .orElse(OperatorOnly.RESULT_FALSE); + } + + public static final class ActionOperatorOnly implements OperatorOnly { + + public static final Set SIMPLE_ACTIONS = Set.of( + AddVotingConfigExclusionsAction.NAME, + ClearVotingConfigExclusionsAction.NAME, + // TODO: Use literal strings due to dependency. + // Alternatively we can let each plugin publish names of operator actions. It would require changes to Plugin framework + "cluster:admin/autoscaling/put_autoscaling_policy", + "cluster:admin/autoscaling/delete_autoscaling_policy" + ); + + // This map is just to showcase how "partial" operator-only API would work. + // It will not be included in phase 1 delivery. + public static final Map> PARAMETER_SENSITIVE_ACTIONS = Map.of( + DeleteRepositoryAction.NAME, (request) -> { + assert request instanceof DeleteRepositoryRequest; + final DeleteRepositoryRequest deleteRepositoryRequest = (DeleteRepositoryRequest) request; + if ("found-snapshots".equals(deleteRepositoryRequest.name())) { + return OperatorOnly.Result.matched( + () -> "action [" + DeleteRepositoryAction.NAME + "] with repository [" + deleteRepositoryRequest.name()); + } else { + return OperatorOnly.RESULT_FALSE; + } + } + ); + + @Override + public Result check(String action, TransportRequest request) { + if (SIMPLE_ACTIONS.contains(action)) { + return OperatorOnly.Result.matched(() -> "action [" + action + "]"); + } else if (PARAMETER_SENSITIVE_ACTIONS.containsKey(action)) { + return PARAMETER_SENSITIVE_ACTIONS.get(action).apply(request); + } else { + return OperatorOnly.RESULT_CONTINUE; + } + } + } + + // This class is a prototype to showcase what it would look like for operator only settings + // It may not be included in phase 1 delivery + public static final class SettingOperatorOnly implements OperatorOnly { + + public static final Set SIMPLE_SETTINGS = Set.of( + IP_FILTER_ENABLED_HTTP_SETTING.getKey(), + IP_FILTER_ENABLED_SETTING.getKey(), + // TODO: Use literal strings due to dependency. Alternatively we can let each plugin publish names of operator settings + "xpack.ml.max_machine_memory_percent", + "xpack.ml.max_model_memory_limit" + ); + + @Override + public Result check(String action, TransportRequest request) { + if (false == ClusterUpdateSettingsAction.NAME.equals(action)) { + return OperatorOnly.RESULT_CONTINUE; + } + assert request instanceof ClusterUpdateSettingsRequest; + final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = (ClusterUpdateSettingsRequest) request; + + final boolean hasNoOverlap = + Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.persistentSettings().keySet()) + && Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.transientSettings().keySet()); + + if (hasNoOverlap) { + return OperatorOnly.RESULT_FALSE; + } else { + final HashSet requestedSettings = new HashSet<>(clusterUpdateSettingsRequest.persistentSettings().keySet()); + requestedSettings.addAll(clusterUpdateSettingsRequest.transientSettings().keySet()); + requestedSettings.retainAll(SIMPLE_SETTINGS); + return OperatorOnly.Result.matched( + () -> requestedSettings.size() > 1 ? "settings" : "setting" + +" [" + Strings.collectionToCommaDelimitedString(requestedSettings) + "]"); + } + } + } +} + diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java new file mode 100644 index 0000000000000..f96b75810b4c3 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.transport.TransportRequest; + +import java.util.function.Supplier; + +public interface OperatorOnly { + + Result check(String action, TransportRequest request); + + enum Status { + MATCHED, UNMATCHED, ABSTAIN; + } + + final class Result { + private final Status status; + private final Supplier messageSupplier; + + private Result(Status status, Supplier messageSupplier) { + this.status = status; + this.messageSupplier = messageSupplier; + } + + static Result matched(Supplier messageSupplier) { + return new Result(Status.MATCHED, messageSupplier); + } + + public Status getStatus() { + return status; + } + + public String getMessage() { + return messageSupplier.get(); + } + } + + Result RESULT_FALSE = new Result(Status.UNMATCHED, null); + Result RESULT_CONTINUE = new Result(Status.ABSTAIN, null); +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java new file mode 100644 index 0000000000000..33ba91ec22f36 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; + +public class OperatorPrivileges { + + public static final Setting OPERATOR_PRIVILEGES_ENABLED = + Setting.boolSetting("xpack.security.operator_privileges.enabled", false, Setting.Property.NodeScope); + + private final OperatorUserDescriptor operatorUserDescriptor; + private final CompositeOperatorOnly compositeOperatorOnly; + private final XPackLicenseState licenseState; + private final boolean enabled; + + public OperatorPrivileges( + Settings settings, XPackLicenseState licenseState, OperatorUserDescriptor operatorUserDescriptor, CompositeOperatorOnly compositeOperatorOnly) { + this.operatorUserDescriptor = operatorUserDescriptor; + this.compositeOperatorOnly = compositeOperatorOnly; + this.licenseState = licenseState; + this.enabled = OPERATOR_PRIVILEGES_ENABLED.get(settings); + } + + public void maybeMarkOperatorUser(Authentication authentication, ThreadContext threadContext) { + if (shouldProcess() && operatorUserDescriptor.isOperatorUser(authentication)) { + threadContext.putHeader( + AuthenticationField.PRIVILEGE_CATEGORY_KEY, + AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR); + } + } + + public ElasticsearchSecurityException check(String action, TransportRequest request, ThreadContext threadContext) { + if (false == shouldProcess()) { + return null; + } + final OperatorOnly.Result operatorOnlyCheckResult = compositeOperatorOnly.check(action, request); + if (operatorOnlyCheckResult.getStatus() == OperatorOnly.Status.MATCHED) { + if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( + threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { + return new ElasticsearchSecurityException( + "Operator privileges are required for " + operatorOnlyCheckResult.getMessage()); + } + } + return null; + } + + private boolean shouldProcess() { + return enabled && licenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java new file mode 100644 index 0000000000000..2c33771d9a8fe --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.env.Environment; +import org.elasticsearch.watcher.FileChangesListener; +import org.elasticsearch.watcher.FileWatcher; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +public class OperatorUserDescriptor { + private static final Logger logger = LogManager.getLogger(OperatorUserDescriptor.class); + + private final Path file; + private volatile List groups; + + public OperatorUserDescriptor(Environment env, ResourceWatcherService watcherService) { + this.file = XPackPlugin.resolveConfigFile(env, "operator_users.yml"); + this.groups = parseFileLenient(file, logger); + FileWatcher watcher = new FileWatcher(file.getParent()); + watcher.addListener(new OperatorUserDescriptor.FileListener()); + try { + watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); + } catch (IOException e) { + throw new ElasticsearchException("failed to start watching the operator users file [" + file.toAbsolutePath() + "]", e); + } + } + + public boolean isOperatorUser(Authentication authentication) { + if (authentication.getUser().isRunAs()) { + return false; + } else if (User.isInternal(authentication.getUser())) { + // Internal user are considered operator users + return true; + } + + return groups.stream().anyMatch(group -> { + final Authentication.RealmRef realm = authentication.getSourceRealm(); + return group.usernames.contains(authentication.getUser().principal()) + && group.authenticationType == authentication.getAuthenticationType() + && realm.getType().equals(group.realmType) + && (realm.getType().equals(FileRealmSettings.TYPE) || realm.getName().equals(group.realmName)); + }); + } + + public static final class Group { + private final Set usernames; + private final String realmName; + private final String realmType; + private final Authentication.AuthenticationType authenticationType; + + public Group(Set usernames) { + this(usernames, null, FileRealmSettings.TYPE, Authentication.AuthenticationType.REALM); + } + + public Group(Set usernames, String realmName) { + this(usernames, realmName, FileRealmSettings.TYPE, Authentication.AuthenticationType.REALM); + } + + public Group( + Set usernames, String realmName, String realmType, Authentication.AuthenticationType authenticationType) { + this.usernames = usernames; + this.realmName = realmName; + this.realmType = realmType; + this.authenticationType = authenticationType; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Group["); + sb.append("usernames=").append(usernames); + if (realmName != null) { + sb.append(", realm_name=").append(realmName); + } + if (realmType != null) { + sb.append(", realm_type=").append(realmType); + } + if (authenticationType != null) { + sb.append(", auth_type=").append(authenticationType.name().toLowerCase(Locale.ROOT)); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Group group = (Group) o; + return usernames.equals(group.usernames) && Objects.equals(realmName, + group.realmName) && realmType.equals(group.realmType) && authenticationType == group.authenticationType; + } + + @Override + public int hashCode() { + return Objects.hash(usernames, realmName, realmType, authenticationType); + } + } + + public static List parseFileLenient(Path path, Logger logger) { + if (false == Files.exists(path)) { + logger.debug("Skip reading operator user file since it does not exist"); + return List.of(); + } + logger.debug("Reading operator users file [{}]", path.toAbsolutePath()); + try { + return parseFile(path); + } catch (IOException | RuntimeException e) { + logger.error("Failed to parse operator_users file [" + path + "].", e); + return List.of(); + } + } + + public static List parseFile(Path path) throws IOException { + try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { + return parseConfig(in); + } + } + + public static List parseConfig(InputStream in) throws IOException { + final List groups = new ArrayList<>(); + try (XContentParser parser = yamlParser(in)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); + final String categoryName = parser.currentName(); + if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals(categoryName)) { + throw new IllegalArgumentException("Operator user config file must begin with operator, got [" + categoryName + "]"); + } + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser); + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + groups.add(parseOneGroup(parser)); + } + } + } + return List.copyOf(groups); + } + + private static Group parseOneGroup(XContentParser parser) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + String[] usernames = null; + String realmName = null; + String realmType = FileRealmSettings.TYPE; + Authentication.AuthenticationType authenticationType = Authentication.AuthenticationType.REALM; + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Fields.USERNAMES.match(currentFieldName, parser.getDeprecationHandler())) { + usernames = XContentUtils.readStringArray(parser, false); + } else if (Fields.REALM_NAME.match(currentFieldName, parser.getDeprecationHandler())) { + if (token == XContentParser.Token.VALUE_STRING) { + realmName = parser.text(); + } else { + throw mismatchedFieldException("realm_name", currentFieldName, "string", token); + } + } else if (Fields.REALM_TYPE.match(currentFieldName, parser.getDeprecationHandler())) { + if (token == XContentParser.Token.VALUE_STRING) { + realmType = parser.text(); + } else { + throw mismatchedFieldException("realm type", currentFieldName, "string", token); + } + } else if (Fields.AUTH_TYPE.match(currentFieldName, parser.getDeprecationHandler())) { + if (token == XContentParser.Token.VALUE_STRING) { + authenticationType = Authentication.AuthenticationType.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } else { + throw mismatchedFieldException("authentication type", currentFieldName, "string", token); + } + } else { + throw unexpectedFieldException("user", currentFieldName); + } + } + if (usernames == null) { + throw missingRequiredFieldException("usernames", Fields.USERNAMES.getPreferredName()); + } + return new Group(Set.of(usernames), realmName, realmType, authenticationType); + } + + private static ElasticsearchParseException mismatchedFieldException(String entityName, + String fieldName, String expectedType, + XContentParser.Token token) { + return new ElasticsearchParseException( + "failed to parse {}. " + + "expected field [{}] value to be {}, but found an element of type [{}]", + entityName, fieldName, expectedType, token); + } + + private static ElasticsearchParseException missingRequiredFieldException(String entityName, + String fieldName) { + return new ElasticsearchParseException( + "failed to parse {}. missing required [{}] field", + entityName, fieldName); + } + + private static ElasticsearchParseException unexpectedFieldException(String entityName, + String fieldName) { + return new ElasticsearchParseException( + "failed to parse {}. unexpected field [{}]", + entityName, fieldName); + } + + private static XContentParser yamlParser(InputStream in) throws IOException { + return XContentType.YAML.xContent().createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, in); + } + + public interface Fields { + ParseField USERNAMES = new ParseField("usernames"); + ParseField REALM_NAME = new ParseField("realm_name"); + ParseField REALM_TYPE = new ParseField("realm_type"); + ParseField AUTH_TYPE = new ParseField("auth_type"); + } + + private class FileListener implements FileChangesListener { + @Override + public void onFileCreated(Path file) { + onFileChanged(file); + } + + @Override + public void onFileDeleted(Path file) { + onFileChanged(file); + } + + @Override + public void onFileChanged(Path file) { + if (file.equals(OperatorUserDescriptor.this.file)) { + final List previousGroups = groups; + groups = parseFileLenient(file, logger); + + if (groups.equals(previousGroups) == false) { + logger.info("operator users file [{}] changed. updating operator users...", file.toAbsolutePath()); + } + } + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index badfd859e80d1..12f85648f6fdc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -259,7 +259,7 @@ public void init() throws Exception { clusterService); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, new AnonymousUser(settings), tokenService, apiKeyService); + threadPool, new AnonymousUser(settings), tokenService, apiKeyService, null); } @After @@ -464,7 +464,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { .build(); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService); + tokenService, apiKeyService, null); User user = new User("_username", "r1"); when(firstRealm.supports(token)).thenReturn(true); mockAuthenticate(firstRealm, token, null); @@ -734,7 +734,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { ThreadContext threadContext1 = threadPool1.getThreadContext(); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool1, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService); + tokenService, apiKeyService, null); threadContext1.putTransient(AuthenticationField.AUTHENTICATION_KEY, authRef.get()); threadContext1.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); @@ -758,7 +758,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { try (ThreadContext.StoredContext ignore = threadContext2.stashContext()) { service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService); + tokenService, apiKeyService, null); threadContext2.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); BytesStreamOutput output = new BytesStreamOutput(); @@ -772,7 +772,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { threadPool2.getThreadContext().putHeader(AuthenticationField.AUTHENTICATION_KEY, header); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService); + tokenService, apiKeyService, null); service.authenticate("_action", new InternalRequest(), SystemUser.INSTANCE, ActionListener.wrap(result -> { assertThat(result, notNullValue()); assertThat(result.getUser(), equalTo(user1)); @@ -810,7 +810,7 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { Settings anonymousEnabledSettings = builder.build(); final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService, null); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); @@ -833,7 +833,7 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { Settings anonymousEnabledSettings = builder.build(); final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService, null); doAnswer(invocationOnMock -> { final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0]; final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; @@ -864,7 +864,7 @@ public void testAnonymousUserRest() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService); + threadPool, anonymousUser, tokenService, apiKeyService, null); RestRequest request = new FakeRestRequest(); Authentication result = authenticateBlocking(request); @@ -890,7 +890,7 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService); + threadPool, anonymousUser, tokenService, apiKeyService, null); RestRequest request = new FakeRestRequest(); PlainActionFuture future = new PlainActionFuture<>(); @@ -913,7 +913,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService); + threadPool, anonymousUser, tokenService, apiKeyService, null); InternalRequest message = new InternalRequest(); Authentication result = authenticateBlocking("_action", message, null); @@ -930,7 +930,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService); + threadPool, anonymousUser, tokenService, apiKeyService, null); InternalRequest message = new InternalRequest(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 0f254a908441e..24bb596d7462b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -124,7 +124,7 @@ public void setupMocks() throws Exception { securityIndex, clusterService, mock(CacheInvalidatorRegistry.class),threadPool); authenticationService = new AuthenticationService(settings, realms, auditTrail, failureHandler, threadPool, anonymous, - tokenService, apiKeyService); + tokenService, apiKeyService, null); authenticator = new SecondaryAuthenticator(securityContext, authenticationService); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index fa1edf32e7d85..1e782ec80790f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -269,7 +269,8 @@ public void setup() { roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), - null, Collections.emptySet(), licenseState, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY))); + null, Collections.emptySet(), licenseState, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -913,7 +914,7 @@ public void testDenialForAnonymousUser() throws IOException { final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet(), - new XPackLicenseState(settings, () -> 0), new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY))); + new XPackLicenseState(settings, () -> 0), new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -942,7 +943,7 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() throws IO authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), new XPackLicenseState(settings, () -> 0), - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY))); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1684,7 +1685,7 @@ public void getUserPrivileges(Authentication authentication, AuthorizationInfo a authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), engine, Collections.emptySet(), licenseState, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY))); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null); Authentication authentication; try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { authentication = createAuthentication(new User("test user", "a_all")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java new file mode 100644 index 0000000000000..1ad2658875704 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.test.ESTestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; + +public class OperatorUserDescriptorTests extends ESTestCase { + + public void testParseConfig() throws IOException { + final String config = "" + + "operator:\n" + + " - usernames: [\"operator_1\",\"operator_2\"]\n" + + " realm_name: \"found\"\n" + + " realm_type: \"file\"\n" + + " auth_type: \"REALM\"\n" + + " - usernames: [\"internal_system\"]\n"; + + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final List groups = OperatorUserDescriptor.parseConfig(in); + assertEquals(2, groups.size()); + System.out.println(groups); + assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), "found"), groups.get(0)); + assertEquals(new OperatorUserDescriptor.Group(Set.of("internal_system")), groups.get(1)); + } + } +} diff --git a/x-pack/qa/operator-privileges-tests/build.gradle b/x-pack/qa/operator-privileges-tests/build.gradle new file mode 100644 index 0000000000000..67942cb2d3c08 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'elasticsearch.testclusters' +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-test' + +dependencies { + testImplementation project(path: xpackModule('core'), configuration: 'default') + testImplementation project(path: xpackModule('core'), configuration: 'testArtifacts') + testImplementation project(path: xpackModule('security'), configuration: 'testArtifacts') +} + +testClusters.integTest { + testDistribution = 'DEFAULT' + + extraConfigFile 'operator_users.yml', file('src/test/resources/operator_users.yml') + extraConfigFile 'roles.yml', file('src/test/resources/roles.yml') + + setting 'xpack.license.self_generated.type', 'trial' + setting 'xpack.security.enabled', 'true' + setting 'xpack.security.http.ssl.enabled', 'false' + setting 'xpack.security.operator_privileges.enabled', 'true' + + user username: "test_admin", password: 'x-pack-test-password', role: "superuser" + user username: "test_operator", password: 'x-pack-test-password', role: "limited_operator" +} diff --git a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java new file mode 100644 index 0000000000000..eceb90ee43dae --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.rest.ESRestTestCase; + +import java.io.IOException; +import java.util.Base64; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class OperatorPrivilegesIT extends ESRestTestCase { + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); + return Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token) + .build(); + } + + public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApi() throws IOException { + final Request postVotingConfigExclusionsRequest = new Request( + "POST", "_cluster/voting_config_exclusions?node_names=foo"); + final ResponseException responseException = expectThrows( + ResponseException.class, + () -> client().performRequest(postVotingConfigExclusionsRequest)); + assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(responseException.getMessage(), containsString("Operator privileges are required for action")); + } + + public void testOperatorUserWillSucceedToCallOperatorOnlyApi() throws IOException { + final Request postVotingConfigExclusionsRequest = new Request( + "POST", "_cluster/voting_config_exclusions?node_names=foo"); + final String authHeader = "Basic " + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes()); + postVotingConfigExclusionsRequest.setOptions( + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); + client().performRequest(postVotingConfigExclusionsRequest); + } + + public void testOperatorUserWillFailToCallOperatorOnlyApiIfRbacFails() throws IOException { + final Request deleteVotingConfigExclusionsRequest = new Request( + "DELETE", "_cluster/voting_config_exclusions"); + final String authHeader = "Basic " + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes()); + deleteVotingConfigExclusionsRequest.setOptions( + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); + final ResponseException responseException = expectThrows(ResponseException.class, + () -> client().performRequest(deleteVotingConfigExclusionsRequest)); + assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(responseException.getMessage(), containsString("is unauthorized for user")); + } +} diff --git a/x-pack/qa/operator-privileges-tests/src/test/resources/operator_users.yml b/x-pack/qa/operator-privileges-tests/src/test/resources/operator_users.yml new file mode 100644 index 0000000000000..1535ef271dd00 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/test/resources/operator_users.yml @@ -0,0 +1,2 @@ +operator: + - usernames: ["test_operator"] diff --git a/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml b/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml new file mode 100644 index 0000000000000..06be15922f89f --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml @@ -0,0 +1,3 @@ +limited_operator: + cluster: + - "cluster:admin/voting_config/add_exclusions" From 3d5f3b0417388819e4bc8bef73cc76321b16e046 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 23 Nov 2020 21:49:12 +1100 Subject: [PATCH 02/23] WIP: working on tests --- .../test/SecuritySingleNodeTestCase.java | 9 + .../operator/NonOperatorOnlyActions.java | 13 + .../OperatorPrivilegesSingleNodeTests.java | 286 ++++++++++++++++++ .../security/authc/AuthenticationService.java | 1 - .../operator/CompositeOperatorOnly.java | 25 +- .../xpack/security/operator/OperatorOnly.java | 10 +- .../security/operator/OperatorPrivileges.java | 6 +- .../operator/OperatorUserDescriptor.java | 11 +- .../test/SecuritySettingsSource.java | 6 + .../authc/AuthenticationServiceTests.java | 31 +- .../support/SecondaryAuthenticatorTests.java | 8 +- .../authz/AuthorizationServiceTests.java | 15 +- .../operator/CompositeOperatorOnlyTests.java | 29 ++ .../operator/OperatorPrivilegesTests.java | 109 +++++++ .../operator/OperatorUserDescriptorTests.java | 110 ++++++- .../security/operator/operator_users.yml | 5 + .../qa/operator-privileges-tests/build.gradle | 6 +- .../operator/OperatorPrivilegesIT.java | 40 ++- .../src/test/resources/roles.yml | 1 + 19 files changed, 672 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java index 168a24e8a3c90..7a386cf42772e 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java @@ -208,6 +208,10 @@ protected String configRoles() { return SECURITY_DEFAULT_SETTINGS.configRoles(); } + protected String configOperatorUsers() { + return SECURITY_DEFAULT_SETTINGS.configOperatorUsers(); + } + /** * Allows to override the node client username */ @@ -250,6 +254,11 @@ protected String configRoles() { return SecuritySingleNodeTestCase.this.configRoles(); } + @Override + protected String configOperatorUsers() { + return SecuritySingleNodeTestCase.this.configOperatorUsers(); + } + @Override protected String nodeClientUsername() { return SecuritySingleNodeTestCase.this.nodeClientUsername(); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java new file mode 100644 index 0000000000000..661fd81565150 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +class NonOperatorOnlyActions { + + + +} diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java new file mode 100644 index 0000000000000..18c14a191ece6 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction; +import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsRequest; +import org.elasticsearch.action.support.TransportAction; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Binding; +import org.elasticsearch.common.inject.Injector; +import org.elasticsearch.common.inject.TypeLiteral; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.SecuritySingleNodeTestCase; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.test.SecuritySettingsSource.TEST_PASSWORD_HASHED; +import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD; +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; + +public class OperatorPrivilegesSingleNodeTests extends SecuritySingleNodeTestCase { + + private static final String OPERATOR_USER_NAME = "test_operator"; + + @Override + protected String configUsers() { + return super.configUsers() + + OPERATOR_USER_NAME + ":" + TEST_PASSWORD_HASHED + "\n"; + } + + @Override + protected String configRoles() { + return super.configRoles() + + "limited_operator:\n" + + " cluster:\n" + + " - 'cluster:admin/voting_config/clear_exclusions'\n" + + " - 'monitor'\n"; + } + + @Override + protected String configUsersRoles() { + return super.configUsersRoles() + + "limited_operator:" + OPERATOR_USER_NAME + "\n"; + } + + @Override + protected String configOperatorUsers() { + return super.configOperatorUsers() + + "operator:\n" + + " - usernames: ['" + OPERATOR_USER_NAME + "']\n"; + } + + @Override + protected Settings nodeSettings() { + Settings.Builder builder = Settings.builder().put(super.nodeSettings()); + // Ensure the new settings can be configured + builder.put("xpack.security.operator_privileges.enabled", "true"); + return builder.build(); + } + + // TODO: Not all plugins are available in internal cluster tests. Hence not all action names can be checked. + public void testActionsAreEitherOperatorOnlyOrNot() { + final Injector injector = node().injector(); + final List> bindings = injector.findBindingsByType(TypeLiteral.get(TransportAction.class)); + + final List allActionNames = new ArrayList<>(bindings.size()); + for (final Binding binding : bindings) { + allActionNames.add(binding.getProvider().get().actionName); + } + + final Set nonOperatorActions = Set.of(NON_OPERATOR_ACTIONS); + final Set expectedOperatorOnlyActions = Sets.difference(Set.copyOf(allActionNames), nonOperatorActions); + final Set actualOperatorOnlyActions = new HashSet<>(CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS); + assertTrue(actualOperatorOnlyActions.containsAll(expectedOperatorOnlyActions)); + assertFalse(actualOperatorOnlyActions.removeAll(nonOperatorActions)); + } + + public void testSuperuserWillFailToCallOperatorOnlyAction() { + final ClearVotingConfigExclusionsRequest clearVotingConfigExclusionsRequest = new ClearVotingConfigExclusionsRequest(); + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> client().execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet()); + assertThat(e.getCause().getMessage(), containsString("Operator privileges are required for action")); + } + + public void testOperatorUserWillSucceedToCallOperatorOnlyAction() { + final Client client = client().filterWithHeader(Map.of( + "Authorization", + basicAuthHeaderValue(OPERATOR_USER_NAME, new SecureString(TEST_PASSWORD.toCharArray())))); + final ClearVotingConfigExclusionsRequest clearVotingConfigExclusionsRequest = new ClearVotingConfigExclusionsRequest(); + client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet(); + } + + public static final String[] NON_OPERATOR_ACTIONS = new String[] { + "cluster:admin/component_template/delete", + "cluster:admin/component_template/get", + "cluster:admin/component_template/put", + "cluster:admin/indices/dangling/delete", + "cluster:admin/indices/dangling/find", + "cluster:admin/indices/dangling/import", + "cluster:admin/indices/dangling/list", + "cluster:admin/ingest/pipeline/delete", + "cluster:admin/ingest/pipeline/get", + "cluster:admin/ingest/pipeline/put", + "cluster:admin/ingest/pipeline/simulate", + "cluster:admin/nodes/reload_secure_settings", + "cluster:admin/persistent/completion", + "cluster:admin/persistent/remove", + "cluster:admin/persistent/start", + "cluster:admin/persistent/update_status", + "cluster:admin/reindex/rethrottle", + "cluster:admin/repository/_cleanup", + "cluster:admin/repository/delete", + "cluster:admin/repository/get", + "cluster:admin/repository/put", + "cluster:admin/repository/verify", + "cluster:admin/reroute", + "cluster:admin/script/delete", + "cluster:admin/script/get", + "cluster:admin/script/put", + "cluster:admin/script_context/get", + "cluster:admin/script_language/get", + "cluster:admin/settings/update", + "cluster:admin/snapshot/clone", + "cluster:admin/snapshot/create", + "cluster:admin/snapshot/delete", + "cluster:admin/snapshot/get", + "cluster:admin/snapshot/restore", + "cluster:admin/snapshot/status", + "cluster:admin/snapshot/status[nodes]", + "cluster:admin/tasks/cancel", + "cluster:admin/xpack/license/basic_status", + "cluster:admin/xpack/license/feature_usage", + "cluster:admin/xpack/license/start_basic", + "cluster:admin/xpack/license/start_trial", + "cluster:admin/xpack/license/trial_status", + "cluster:admin/xpack/monitoring/bulk", + "cluster:admin/xpack/security/api_key/create", + "cluster:admin/xpack/security/api_key/get", + "cluster:admin/xpack/security/api_key/grant", + "cluster:admin/xpack/security/api_key/invalidate", + "cluster:admin/xpack/security/cache/clear", + "cluster:admin/xpack/security/delegate_pki", + "cluster:admin/xpack/security/oidc/authenticate", + "cluster:admin/xpack/security/oidc/logout", + "cluster:admin/xpack/security/oidc/prepare", + "cluster:admin/xpack/security/privilege/builtin/get", + "cluster:admin/xpack/security/privilege/cache/clear", + "cluster:admin/xpack/security/privilege/delete", + "cluster:admin/xpack/security/privilege/get", + "cluster:admin/xpack/security/privilege/put", + "cluster:admin/xpack/security/realm/cache/clear", + "cluster:admin/xpack/security/role/delete", + "cluster:admin/xpack/security/role/get", + "cluster:admin/xpack/security/role/put", + "cluster:admin/xpack/security/role_mapping/delete", + "cluster:admin/xpack/security/role_mapping/get", + "cluster:admin/xpack/security/role_mapping/put", + "cluster:admin/xpack/security/roles/cache/clear", + "cluster:admin/xpack/security/saml/authenticate", + "cluster:admin/xpack/security/saml/complete_logout", + "cluster:admin/xpack/security/saml/invalidate", + "cluster:admin/xpack/security/saml/logout", + "cluster:admin/xpack/security/saml/prepare", + "cluster:admin/xpack/security/token/create", + "cluster:admin/xpack/security/token/invalidate", + "cluster:admin/xpack/security/token/refresh", + "cluster:admin/xpack/security/user/authenticate", + "cluster:admin/xpack/security/user/change_password", + "cluster:admin/xpack/security/user/delete", + "cluster:admin/xpack/security/user/get", + "cluster:admin/xpack/security/user/has_privileges", + "cluster:admin/xpack/security/user/list_privileges", + "cluster:admin/xpack/security/user/put", + "cluster:admin/xpack/security/user/set_enabled", + "cluster:monitor/allocation/explain", + "cluster:monitor/health", + "cluster:monitor/main", + "cluster:monitor/nodes/hot_threads", + "cluster:monitor/nodes/info", + "cluster:monitor/nodes/stats", + "cluster:monitor/nodes/usage", + "cluster:monitor/remote/info", + "cluster:monitor/state", + "cluster:monitor/stats", + "cluster:monitor/task", + "cluster:monitor/task/get", + "cluster:monitor/tasks/lists", + "cluster:monitor/xpack/info", + "cluster:monitor/xpack/info/data_tiers", + "cluster:monitor/xpack/info/monitoring", + "cluster:monitor/xpack/info/security", + "cluster:monitor/xpack/license/get", + "cluster:monitor/xpack/security/saml/metadata", + "cluster:monitor/xpack/ssl/certificates/get", + "cluster:monitor/xpack/usage", + "cluster:monitor/xpack/usage/data_tiers", + "cluster:monitor/xpack/usage/monitoring", + "cluster:monitor/xpack/usage/security", + "indices:admin/aliases", + "indices:admin/aliases/get", + "indices:admin/analyze", + "indices:admin/auto_create", + "indices:admin/block/add", + "indices:admin/block/add[s]", + "indices:admin/cache/clear", + "indices:admin/close", + "indices:admin/close[s]", + "indices:admin/create", + "indices:admin/delete", + "indices:admin/flush", + "indices:admin/flush[s]", + "indices:admin/forcemerge", + "indices:admin/get", + "indices:admin/index_template/delete", + "indices:admin/index_template/get", + "indices:admin/index_template/put", + "indices:admin/index_template/simulate", + "indices:admin/index_template/simulate_index", + "indices:admin/mapping/auto_put", + "indices:admin/mapping/put", + "indices:admin/mappings/fields/get", + "indices:admin/mappings/fields/get[index]", + "indices:admin/mappings/get", + "indices:admin/open", + "indices:admin/refresh", + "indices:admin/refresh[s]", + "indices:admin/reload_analyzers", + "indices:admin/resize", + "indices:admin/resolve/index", + "indices:admin/rollover", + "indices:admin/seq_no/add_retention_lease", + "indices:admin/seq_no/global_checkpoint_sync", + "indices:admin/seq_no/remove_retention_lease", + "indices:admin/seq_no/renew_retention_lease", + "indices:admin/settings/update", + "indices:admin/shards/search_shards", + "indices:admin/template/delete", + "indices:admin/template/get", + "indices:admin/template/put", + "indices:admin/validate/query", + "indices:data/read/async_search/delete", + "indices:data/read/close_point_in_time", + "indices:data/read/explain", + "indices:data/read/field_caps", + "indices:data/read/field_caps[index]", + "indices:data/read/get", + "indices:data/read/mget", + "indices:data/read/mget[shard]", + "indices:data/read/msearch", + "indices:data/read/mtv", + "indices:data/read/mtv[shard]", + "indices:data/read/open_point_in_time", + "indices:data/read/scroll", + "indices:data/read/scroll/clear", + "indices:data/read/search", + "indices:data/read/tv", + "indices:data/write/bulk", + "indices:data/write/bulk[s]", + "indices:data/write/delete", + "indices:data/write/delete/byquery", + "indices:data/write/index", + "indices:data/write/reindex", + "indices:data/write/update", + "indices:data/write/update/byquery", + "indices:monitor/recovery", + "indices:monitor/segments", + "indices:monitor/settings/get", + "indices:monitor/shard_stores", + "indices:monitor/stats", + "internal:cluster/nodes/indices/shard/store", + "internal:gateway/local/meta_state", + "internal:gateway/local/started_shards" + }; +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 4f97550d77ea4..883943c4f4ff4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -32,7 +32,6 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; -import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java index de62039f7cde4..d690f4f4d176a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java @@ -14,6 +14,8 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.license.DeleteLicenseAction; +import org.elasticsearch.license.PutLicenseAction; import org.elasticsearch.transport.TransportRequest; import java.util.HashSet; @@ -37,9 +39,9 @@ public CompositeOperatorOnly() { public Result check(String action, TransportRequest request) { return checks.stream() .map(c -> c.check(action, request)) - .filter(r -> r.getStatus() != Status.ABSTAIN) + .filter(r -> r.getStatus() != Status.CONTINUE) .findFirst() - .orElse(OperatorOnly.RESULT_FALSE); + .orElse(OperatorOnly.RESULT_NO); } public static final class ActionOperatorOnly implements OperatorOnly { @@ -47,10 +49,13 @@ public static final class ActionOperatorOnly implements OperatorOnly { public static final Set SIMPLE_ACTIONS = Set.of( AddVotingConfigExclusionsAction.NAME, ClearVotingConfigExclusionsAction.NAME, - // TODO: Use literal strings due to dependency. - // Alternatively we can let each plugin publish names of operator actions. It would require changes to Plugin framework + PutLicenseAction.NAME, + DeleteLicenseAction.NAME, + // Autoscaling does not publish its actions to core, literal strings are needed. "cluster:admin/autoscaling/put_autoscaling_policy", - "cluster:admin/autoscaling/delete_autoscaling_policy" + "cluster:admin/autoscaling/delete_autoscaling_policy", + "cluster:admin/autoscaling/get_autoscaling_policy", + "cluster:admin/autoscaling/get_autoscaling_capacity" ); // This map is just to showcase how "partial" operator-only API would work. @@ -60,10 +65,10 @@ public static final class ActionOperatorOnly implements OperatorOnly { assert request instanceof DeleteRepositoryRequest; final DeleteRepositoryRequest deleteRepositoryRequest = (DeleteRepositoryRequest) request; if ("found-snapshots".equals(deleteRepositoryRequest.name())) { - return OperatorOnly.Result.matched( + return OperatorOnly.Result.yes( () -> "action [" + DeleteRepositoryAction.NAME + "] with repository [" + deleteRepositoryRequest.name()); } else { - return OperatorOnly.RESULT_FALSE; + return OperatorOnly.RESULT_NO; } } ); @@ -71,7 +76,7 @@ public static final class ActionOperatorOnly implements OperatorOnly { @Override public Result check(String action, TransportRequest request) { if (SIMPLE_ACTIONS.contains(action)) { - return OperatorOnly.Result.matched(() -> "action [" + action + "]"); + return OperatorOnly.Result.yes(() -> "action [" + action + "]"); } else if (PARAMETER_SENSITIVE_ACTIONS.containsKey(action)) { return PARAMETER_SENSITIVE_ACTIONS.get(action).apply(request); } else { @@ -105,12 +110,12 @@ public Result check(String action, TransportRequest request) { && Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.transientSettings().keySet()); if (hasNoOverlap) { - return OperatorOnly.RESULT_FALSE; + return OperatorOnly.RESULT_NO; } else { final HashSet requestedSettings = new HashSet<>(clusterUpdateSettingsRequest.persistentSettings().keySet()); requestedSettings.addAll(clusterUpdateSettingsRequest.transientSettings().keySet()); requestedSettings.retainAll(SIMPLE_SETTINGS); - return OperatorOnly.Result.matched( + return OperatorOnly.Result.yes( () -> requestedSettings.size() > 1 ? "settings" : "setting" +" [" + Strings.collectionToCommaDelimitedString(requestedSettings) + "]"); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java index f96b75810b4c3..6d21642500bdc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java @@ -15,7 +15,7 @@ public interface OperatorOnly { Result check(String action, TransportRequest request); enum Status { - MATCHED, UNMATCHED, ABSTAIN; + YES, NO, CONTINUE; } final class Result { @@ -27,8 +27,8 @@ private Result(Status status, Supplier messageSupplier) { this.messageSupplier = messageSupplier; } - static Result matched(Supplier messageSupplier) { - return new Result(Status.MATCHED, messageSupplier); + static Result yes(Supplier messageSupplier) { + return new Result(Status.YES, messageSupplier); } public Status getStatus() { @@ -40,6 +40,6 @@ public String getMessage() { } } - Result RESULT_FALSE = new Result(Status.UNMATCHED, null); - Result RESULT_CONTINUE = new Result(Status.ABSTAIN, null); + Result RESULT_NO = new Result(Status.NO, null); + Result RESULT_CONTINUE = new Result(Status.CONTINUE, null); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java index 33ba91ec22f36..ad17337a06097 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -25,8 +25,8 @@ public class OperatorPrivileges { private final XPackLicenseState licenseState; private final boolean enabled; - public OperatorPrivileges( - Settings settings, XPackLicenseState licenseState, OperatorUserDescriptor operatorUserDescriptor, CompositeOperatorOnly compositeOperatorOnly) { + public OperatorPrivileges(Settings settings, XPackLicenseState licenseState, + OperatorUserDescriptor operatorUserDescriptor, CompositeOperatorOnly compositeOperatorOnly) { this.operatorUserDescriptor = operatorUserDescriptor; this.compositeOperatorOnly = compositeOperatorOnly; this.licenseState = licenseState; @@ -46,7 +46,7 @@ public ElasticsearchSecurityException check(String action, TransportRequest requ return null; } final OperatorOnly.Result operatorOnlyCheckResult = compositeOperatorOnly.check(action, request); - if (operatorOnlyCheckResult.getStatus() == OperatorOnly.Status.MATCHED) { + if (operatorOnlyCheckResult.getStatus() == OperatorOnly.Status.YES) { if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { return new ElasticsearchSecurityException( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java index 2c33771d9a8fe..839813d8116a3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java @@ -73,6 +73,11 @@ public boolean isOperatorUser(Authentication authentication) { }); } + // Package private for tests + List getGroups() { + return groups; + } + public static final class Group { private final Set usernames; private final String realmName; @@ -257,11 +262,11 @@ public void onFileDeleted(Path file) { @Override public void onFileChanged(Path file) { if (file.equals(OperatorUserDescriptor.this.file)) { - final List previousGroups = groups; - groups = parseFileLenient(file, logger); + List newGroups = parseFileLenient(file, logger); - if (groups.equals(previousGroups) == false) { + if (groups.equals(newGroups) == false) { logger.info("operator users file [{}] changed. updating operator users...", file.toAbsolutePath()); + groups = newGroups; } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java index e2bda4522f322..7d31eb8e703b3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java @@ -129,6 +129,7 @@ public Settings nodeSettings(int nodeOrdinal) { writeFile(xpackConf, "roles.yml", configRoles()); writeFile(xpackConf, "users", configUsers()); writeFile(xpackConf, "users_roles", configUsersRoles()); + writeFile(xpackConf, "operator_users.yml", configOperatorUsers()); Settings.Builder builder = Settings.builder() .put(Environment.PATH_HOME_SETTING.getKey(), home) @@ -179,6 +180,11 @@ protected String configRoles() { return CONFIG_ROLE_ALLOW_ALL; } + protected String configOperatorUsers() { + // By default, no operator user is configured + return ""; + } + protected String nodeClientUsername() { return TEST_USER_NAME; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 12f85648f6fdc..93564db4ad81b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -85,6 +85,9 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges; +import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; @@ -167,7 +170,7 @@ public class AuthenticationServiceTests extends ESTestCase { private SecurityIndexManager securityIndex; private Client client; private InetSocketAddress remoteAddress; - + private OperatorPrivileges operatorPrivileges; private String concreteSecurityIndexName; @Before @@ -257,9 +260,11 @@ public void init() throws Exception { mock(CacheInvalidatorRegistry.class), threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); + final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); + operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new CompositeOperatorOnly()); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, new AnonymousUser(settings), tokenService, apiKeyService, null); + threadPool, new AnonymousUser(settings), tokenService, apiKeyService, operatorPrivileges); } @After @@ -464,7 +469,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { .build(); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, null); + tokenService, apiKeyService, operatorPrivileges); User user = new User("_username", "r1"); when(firstRealm.supports(token)).thenReturn(true); mockAuthenticate(firstRealm, token, null); @@ -734,7 +739,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { ThreadContext threadContext1 = threadPool1.getThreadContext(); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool1, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, null); + tokenService, apiKeyService, operatorPrivileges); threadContext1.putTransient(AuthenticationField.AUTHENTICATION_KEY, authRef.get()); threadContext1.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); @@ -758,7 +763,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { try (ThreadContext.StoredContext ignore = threadContext2.stashContext()) { service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, null); + tokenService, apiKeyService, operatorPrivileges); threadContext2.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); BytesStreamOutput output = new BytesStreamOutput(); @@ -772,7 +777,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { threadPool2.getThreadContext().putHeader(AuthenticationField.AUTHENTICATION_KEY, header); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, null); + tokenService, apiKeyService, operatorPrivileges); service.authenticate("_action", new InternalRequest(), SystemUser.INSTANCE, ActionListener.wrap(result -> { assertThat(result, notNullValue()); assertThat(result.getUser(), equalTo(user1)); @@ -810,7 +815,8 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { Settings anonymousEnabledSettings = builder.build(); final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService, null); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, + tokenService, apiKeyService, operatorPrivileges); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); @@ -833,7 +839,8 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { Settings anonymousEnabledSettings = builder.build(); final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService, null); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, + tokenService, apiKeyService, operatorPrivileges); doAnswer(invocationOnMock -> { final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0]; final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; @@ -864,7 +871,7 @@ public void testAnonymousUserRest() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, null); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); RestRequest request = new FakeRestRequest(); Authentication result = authenticateBlocking(request); @@ -890,7 +897,7 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, null); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); RestRequest request = new FakeRestRequest(); PlainActionFuture future = new PlainActionFuture<>(); @@ -913,7 +920,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, null); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); InternalRequest message = new InternalRequest(); Authentication result = authenticateBlocking("_action", message, null); @@ -930,7 +937,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, null); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); InternalRequest message = new InternalRequest(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 24bb596d7462b..4161ce30085cc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -46,6 +46,9 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges; +import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.test.SecurityMocks; @@ -79,6 +82,7 @@ public class SecondaryAuthenticatorTests extends ESTestCase { private SecurityContext securityContext; private TokenService tokenService; private Client client; + private OperatorPrivileges operatorPrivileges; @Before public void setupMocks() throws Exception { @@ -123,8 +127,10 @@ public void setupMocks() throws Exception { final ApiKeyService apiKeyService = new ApiKeyService(settings, clock, client, licenseState, securityIndex, clusterService, mock(CacheInvalidatorRegistry.class),threadPool); + final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); + operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new CompositeOperatorOnly()); authenticationService = new AuthenticationService(settings, realms, auditTrail, failureHandler, threadPool, anonymous, - tokenService, apiKeyService, null); + tokenService, apiKeyService, operatorPrivileges); authenticator = new SecondaryAuthenticator(securityContext, authenticationService); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 1e782ec80790f..1b5d7280dcedf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -147,6 +147,9 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; +import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges; +import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.sql.action.SqlQueryAction; import org.elasticsearch.xpack.sql.action.SqlQueryRequest; import org.junit.Before; @@ -211,6 +214,7 @@ public class AuthorizationServiceTests extends ESTestCase { private ThreadPool threadPool; private Map roleMap = new HashMap<>(); private CompositeRolesStore rolesStore; + private OperatorPrivileges operatorPrivileges; @SuppressWarnings("unchecked") @Before @@ -267,10 +271,12 @@ public void setup() { return Void.TYPE; }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), any(ActionListener.class)); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); + final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); + operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new CompositeOperatorOnly()); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), licenseState, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null); + operatorPrivileges); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -914,7 +920,8 @@ public void testDenialForAnonymousUser() throws IOException { final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet(), - new XPackLicenseState(settings, () -> 0), new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null); + new XPackLicenseState(settings, () -> 0), + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivileges); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -943,7 +950,7 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() throws IO authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), new XPackLicenseState(settings, () -> 0), - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivileges); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1685,7 +1692,7 @@ public void getUserPrivileges(Authentication authentication, AuthorizationInfo a authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), engine, Collections.emptySet(), licenseState, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), null); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivileges); Authentication authentication; try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { authentication = createAuthentication(new User("test user", "a_all")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java new file mode 100644 index 0000000000000..ff49722daf03a --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.action.main.MainAction; +import org.elasticsearch.test.ESTestCase; + +public class CompositeOperatorOnlyTests extends ESTestCase { + + public void testSimpleOperatorOnlyApi() { + final CompositeOperatorOnly compositeOperatorOnly = new CompositeOperatorOnly(); + for (final String actionName : CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS) { + final OperatorOnly.Result result = compositeOperatorOnly.check(actionName, null); + assertEquals(OperatorOnly.Status.YES, result.getStatus()); + assertNotNull(result.getMessage()); + } + } + + public void testNonOperatorOnlyApi() { + final CompositeOperatorOnly compositeOperatorOnly = new CompositeOperatorOnly(); + final OperatorOnly.Result result = compositeOperatorOnly.check(MainAction.NAME, null); + assertEquals(result, OperatorOnly.RESULT_NO); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java new file mode 100644 index 0000000000000..326dc2e7db306 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.junit.Before; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class OperatorPrivilegesTests extends ESTestCase { + + private XPackLicenseState xPackLicenseState; + private OperatorUserDescriptor operatorUserDescriptor; + private CompositeOperatorOnly compositeOperatorOnly; + + @Before + public void init() { + xPackLicenseState = mock(XPackLicenseState.class); + operatorUserDescriptor = mock(OperatorUserDescriptor.class); + compositeOperatorOnly = mock(CompositeOperatorOnly.class); + } + + public void testWillNotProcessWhenFeatureIsDisabledOrLicenseDoesNotSupport() { + final Settings settings = Settings.builder() + .put("xpack.security.operator_privileges.enabled", randomBoolean()) + .build(); + when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(false); + + final OperatorPrivileges operatorPrivileges = + new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, compositeOperatorOnly); + final ThreadContext threadContext = new ThreadContext(settings); + + operatorPrivileges.maybeMarkOperatorUser(mock(Authentication.class), threadContext); + verifyZeroInteractions(operatorUserDescriptor); + + final ElasticsearchSecurityException e = + operatorPrivileges.check("cluster:action", mock(TransportRequest.class), threadContext); + assertNull(e); + verifyZeroInteractions(compositeOperatorOnly); + } + + public void testMarkOperatorUser() { + final Settings settings = Settings.builder() + .put("xpack.security.operator_privileges.enabled", true) + .build(); + when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(true); + final Authentication operatorAuth = mock(Authentication.class); + final Authentication nonOperatorAuth = mock(Authentication.class); + when(operatorUserDescriptor.isOperatorUser(operatorAuth)).thenReturn(true); + when(operatorUserDescriptor.isOperatorUser(nonOperatorAuth)).thenReturn(false); + + final OperatorPrivileges operatorPrivileges = + new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, compositeOperatorOnly); + ThreadContext threadContext = new ThreadContext(settings); + + operatorPrivileges.maybeMarkOperatorUser(operatorAuth, threadContext); + assertEquals(AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR, + threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY)); + + threadContext = new ThreadContext(settings); + operatorPrivileges.maybeMarkOperatorUser(nonOperatorAuth, threadContext); + assertNull(threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY)); + } + + public void testCheck() { + final Settings settings = Settings.builder() + .put("xpack.security.operator_privileges.enabled", true) + .build(); + when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(true); + + final String operatorAction = "cluster:operator_only/action"; + final String nonOperatorAction = "cluster:non_operator/action"; + final String message = "[" + operatorAction + "]"; + when(compositeOperatorOnly.check(eq(operatorAction), any())).thenReturn(OperatorOnly.Result.yes(() -> message)); + when(compositeOperatorOnly.check(eq(nonOperatorAction), any())).thenReturn(OperatorOnly.RESULT_NO); + + final OperatorPrivileges operatorPrivileges = + new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, compositeOperatorOnly); + + ThreadContext threadContext = new ThreadContext(settings); + if (randomBoolean()) { + threadContext.putHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY, AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR); + assertNull(operatorPrivileges.check(operatorAction, mock(TransportRequest.class), threadContext)); + } else { + final ElasticsearchSecurityException e = operatorPrivileges.check(operatorAction, mock(TransportRequest.class), threadContext); + assertNotNull(e); + assertThat(e.getMessage(), containsString("Operator privileges are required for " + message)); + } + + assertNull(operatorPrivileges.check(nonOperatorAction, mock(TransportRequest.class), threadContext)); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java index 1ad2658875704..ef1946e678722 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java @@ -6,16 +6,113 @@ package org.elasticsearch.xpack.security.operator; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.security.audit.logfile.CapturingLogger; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.junit.After; +import org.junit.Before; +import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.util.List; import java.util.Set; public class OperatorUserDescriptorTests extends ESTestCase { + private Settings settings; + private Environment env; + private ThreadPool threadPool; + + @Before + public void init() { + settings = Settings.builder() + .put("resource.reload.interval.high", "100ms") + .put("path.home", createTempDir()) + .build(); + env = TestEnvironment.newEnvironment(settings); + threadPool = new TestThreadPool("test"); + } + + @After + public void shutdown() throws InterruptedException { + terminate(threadPool); + } + + public void testFileAutoReload() throws Exception { + Path operatorUsers = getDataPath("operator_users.yml"); + Path tmp = getOperatorUsersPath(); + Files.copy(operatorUsers, tmp, StandardCopyOption.REPLACE_EXISTING); + + try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { + final OperatorUserDescriptor operatorUserDescriptor = new OperatorUserDescriptor(env, watcherService); + final List groups = operatorUserDescriptor.getGroups(); + + assertEquals(1, groups.size()); + assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), + "file", "file", Authentication.AuthenticationType.REALM), groups.get(0)); + + // Content does not change, the groups should not be updated + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append("\n"); + } + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + assertSame(groups, operatorUserDescriptor.getGroups()); + + // Add one more entry + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append(" - usernames: [ 'operator_3' ]\n"); + } + assertBusy(() -> { + final List newGroups = operatorUserDescriptor.getGroups(); + assertEquals(2, newGroups.size()); + assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), + "file", "file", Authentication.AuthenticationType.REALM), newGroups.get(0)); + assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_3")), newGroups.get(1)); + }); + + // Add mal-formatted entry + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append(" - blah\n"); + } + assertBusy(() -> { + assertEquals(0, operatorUserDescriptor.getGroups().size()); + }); + } + } + + public void testMalFormattedOrEmptyFile() throws IOException { + // Mal-formatted file is functionally equivalent to an empty file + writeOperatorUsers(randomBoolean() ? "foobar" : ""); + try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { + final OperatorUserDescriptor operatorUserDescriptor = new OperatorUserDescriptor(env, watcherService); + assertEquals(0, operatorUserDescriptor.getGroups().size()); + } + } + + public void testParseFileWhenFileDoesNotExist() throws Exception { + Path file = createTempDir().resolve(randomAlphaOfLength(10)); + Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); + final List groups = OperatorUserDescriptor.parseFileLenient(file, logger); + assertEquals(0, groups.size()); + List events = CapturingLogger.output(logger.getName(), Level.DEBUG); + assertEquals(1, events.size()); + assertEquals("Skip reading operator user file since it does not exist", events.get(0)); + } + public void testParseConfig() throws IOException { final String config = "" + "operator:\n" @@ -28,9 +125,20 @@ public void testParseConfig() throws IOException { try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { final List groups = OperatorUserDescriptor.parseConfig(in); assertEquals(2, groups.size()); - System.out.println(groups); assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), "found"), groups.get(0)); assertEquals(new OperatorUserDescriptor.Group(Set.of("internal_system")), groups.get(1)); } } + + private Path getOperatorUsersPath() throws IOException { + Path xpackConf = env.configFile(); + Files.createDirectories(xpackConf); + return xpackConf.resolve("operator_users.yml"); + } + + private Path writeOperatorUsers(String input) throws IOException { + Path file = getOperatorUsersPath(); + Files.write(file, input.getBytes(StandardCharsets.UTF_8)); + return file; + } } diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml new file mode 100644 index 0000000000000..2227d1e3ae708 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml @@ -0,0 +1,5 @@ +operator: + - usernames: ['operator_1', 'operator_2'] + realm_name: file + realm_type: file + auth_type: realm diff --git a/x-pack/qa/operator-privileges-tests/build.gradle b/x-pack/qa/operator-privileges-tests/build.gradle index 67942cb2d3c08..6a672774beae3 100644 --- a/x-pack/qa/operator-privileges-tests/build.gradle +++ b/x-pack/qa/operator-privileges-tests/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.info.BuildParams + apply plugin: 'elasticsearch.testclusters' apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' @@ -8,6 +10,8 @@ dependencies { testImplementation project(path: xpackModule('security'), configuration: 'testArtifacts') } +boolean enableOperatorPrivileges = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() + testClusters.integTest { testDistribution = 'DEFAULT' @@ -17,7 +21,7 @@ testClusters.integTest { setting 'xpack.license.self_generated.type', 'trial' setting 'xpack.security.enabled', 'true' setting 'xpack.security.http.ssl.enabled', 'false' - setting 'xpack.security.operator_privileges.enabled', 'true' + setting 'xpack.security.operator_privileges.enabled', enableOperatorPrivileges.toString() user username: "test_admin", password: 'x-pack-test-password', role: "superuser" user username: "test_operator", password: 'x-pack-test-password', role: "limited_operator" diff --git a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index eceb90ee43dae..7201e5e750c38 100644 --- a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -14,7 +14,9 @@ import org.elasticsearch.test.rest.ESRestTestCase; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Map; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.containsString; @@ -30,20 +32,32 @@ protected Settings restClientSettings() { .build(); } - public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApi() throws IOException { + @SuppressWarnings("unchecked") + public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApiWhenOperatorPrivilegesIsEnabled() throws IOException { + final Request getClusterSettingsRequest = new Request("GET", + "_cluster/settings?flat_settings&include_defaults&filter_path=defaults.*operator_privileges*"); + final Map settingsMap = entityAsMap(client().performRequest(getClusterSettingsRequest)); + final Map defaults = (Map) settingsMap.get("defaults"); + final Object isOperatorPrivilegesEnabled = defaults.get("xpack.security.operator_privileges.enabled"); + final Request postVotingConfigExclusionsRequest = new Request( "POST", "_cluster/voting_config_exclusions?node_names=foo"); - final ResponseException responseException = expectThrows( - ResponseException.class, - () -> client().performRequest(postVotingConfigExclusionsRequest)); - assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat(responseException.getMessage(), containsString("Operator privileges are required for action")); + if ("true".equals(isOperatorPrivilegesEnabled)) { + final ResponseException responseException = expectThrows( + ResponseException.class, + () -> client().performRequest(postVotingConfigExclusionsRequest)); + assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(responseException.getMessage(), containsString("Operator privileges are required for action")); + } else { + client().performRequest(postVotingConfigExclusionsRequest); + } } public void testOperatorUserWillSucceedToCallOperatorOnlyApi() throws IOException { final Request postVotingConfigExclusionsRequest = new Request( "POST", "_cluster/voting_config_exclusions?node_names=foo"); - final String authHeader = "Basic " + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes()); + final String authHeader = "Basic " + Base64.getEncoder().encodeToString( + "test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); postVotingConfigExclusionsRequest.setOptions( RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); client().performRequest(postVotingConfigExclusionsRequest); @@ -52,7 +66,8 @@ public void testOperatorUserWillSucceedToCallOperatorOnlyApi() throws IOExceptio public void testOperatorUserWillFailToCallOperatorOnlyApiIfRbacFails() throws IOException { final Request deleteVotingConfigExclusionsRequest = new Request( "DELETE", "_cluster/voting_config_exclusions"); - final String authHeader = "Basic " + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes()); + final String authHeader = "Basic " + Base64.getEncoder().encodeToString( + "test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); deleteVotingConfigExclusionsRequest.setOptions( RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); final ResponseException responseException = expectThrows(ResponseException.class, @@ -60,4 +75,13 @@ public void testOperatorUserWillFailToCallOperatorOnlyApiIfRbacFails() throws IO assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); assertThat(responseException.getMessage(), containsString("is unauthorized for user")); } + + public void testOperatorUserCanCallNonOperatorOnlyApi() throws IOException { + final Request mainRequest = new Request("GET", "/"); + final String authHeader = "Basic " + Base64.getEncoder().encodeToString( + "test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); + mainRequest.setOptions( + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); + client().performRequest(mainRequest); + } } diff --git a/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml b/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml index 06be15922f89f..ac6d3a00dacad 100644 --- a/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml +++ b/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml @@ -1,3 +1,4 @@ limited_operator: cluster: - "cluster:admin/voting_config/add_exclusions" + - "monitor" From cd5f33999d1edbb12a58c9bdfda5e97b7cf9379c Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 24 Nov 2020 16:07:47 +1100 Subject: [PATCH 03/23] Remove unnecessary file --- .../security/operator/NonOperatorOnlyActions.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java deleted file mode 100644 index 661fd81565150..0000000000000 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/NonOperatorOnlyActions.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.security.operator; - -class NonOperatorOnlyActions { - - - -} From 9834e47680d6aeec804f98807566a2727a543e04 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 24 Nov 2020 16:22:19 +1100 Subject: [PATCH 04/23] Use javaRestTest instead of test to align with recent changes --- .../qa/operator-privileges-tests/build.gradle | 18 +++++++++--------- .../operator/OperatorPrivilegesIT.java | 0 .../resources/operator_users.yml | 0 .../{test => javaRestTest}/resources/roles.yml | 0 4 files changed, 9 insertions(+), 9 deletions(-) rename x-pack/qa/operator-privileges-tests/src/{test => javaRestTest}/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java (100%) rename x-pack/qa/operator-privileges-tests/src/{test => javaRestTest}/resources/operator_users.yml (100%) rename x-pack/qa/operator-privileges-tests/src/{test => javaRestTest}/resources/roles.yml (100%) diff --git a/x-pack/qa/operator-privileges-tests/build.gradle b/x-pack/qa/operator-privileges-tests/build.gradle index 6a672774beae3..ff784e57c3a73 100644 --- a/x-pack/qa/operator-privileges-tests/build.gradle +++ b/x-pack/qa/operator-privileges-tests/build.gradle @@ -1,22 +1,22 @@ import org.elasticsearch.gradle.info.BuildParams -apply plugin: 'elasticsearch.testclusters' -apply plugin: 'elasticsearch.standalone-rest-test' -apply plugin: 'elasticsearch.rest-test' +apply plugin: 'elasticsearch.java-rest-test' dependencies { - testImplementation project(path: xpackModule('core'), configuration: 'default') - testImplementation project(path: xpackModule('core'), configuration: 'testArtifacts') - testImplementation project(path: xpackModule('security'), configuration: 'testArtifacts') + compileOnly project(':x-pack:plugin:core') + javaRestTestImplementation project(':x-pack:plugin:core') + javaRestTestImplementation project(':client:rest-high-level') + // let the javaRestTest see the classpath of main + javaRestTestImplementation project.sourceSets.main.runtimeClasspath } boolean enableOperatorPrivileges = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() -testClusters.integTest { +testClusters.all { testDistribution = 'DEFAULT' - extraConfigFile 'operator_users.yml', file('src/test/resources/operator_users.yml') - extraConfigFile 'roles.yml', file('src/test/resources/roles.yml') + extraConfigFile 'operator_users.yml', file('src/javaRestTest/resources/operator_users.yml') + extraConfigFile 'roles.yml', file('src/javaRestTest/resources/roles.yml') setting 'xpack.license.self_generated.type', 'trial' setting 'xpack.security.enabled', 'true' diff --git a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java rename to x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java diff --git a/x-pack/qa/operator-privileges-tests/src/test/resources/operator_users.yml b/x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/test/resources/operator_users.yml rename to x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml diff --git a/x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml b/x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/test/resources/roles.yml rename to x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml From 9b63010318840d5ccd19ca3b4242b25c4d39942c Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 24 Nov 2020 19:14:18 +1100 Subject: [PATCH 05/23] Add a test plugin to ensure every action is declared either operator-only or not --- .../qa/operator-privileges-tests/build.gradle | 10 + .../xpack/security/operator/Constants.java | 421 ++++++++++++++++++ .../operator/OperatorPrivilegesIT.java | 13 + .../elasticsearch/example/OpTestPlugin.java | 47 ++ .../example/actions/GetActionsAction.java | 19 + .../example/actions/GetActionsRequest.java | 28 ++ .../example/actions/GetActionsResponse.java | 43 ++ .../example/actions/RestGetActionsAction.java | 35 ++ .../actions/TransportGetActionsAction.java | 44 ++ 9 files changed, 660 insertions(+) create mode 100644 x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java diff --git a/x-pack/qa/operator-privileges-tests/build.gradle b/x-pack/qa/operator-privileges-tests/build.gradle index ff784e57c3a73..c2b57c506e72b 100644 --- a/x-pack/qa/operator-privileges-tests/build.gradle +++ b/x-pack/qa/operator-privileges-tests/build.gradle @@ -1,11 +1,21 @@ import org.elasticsearch.gradle.info.BuildParams +apply plugin: 'elasticsearch.esplugin' apply plugin: 'elasticsearch.java-rest-test' +esplugin { + name 'op-test' + description 'An test plugin for testing hard to get internals' + classname 'org.elasticsearch.example.OpTestPlugin' + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') +} + dependencies { compileOnly project(':x-pack:plugin:core') javaRestTestImplementation project(':x-pack:plugin:core') javaRestTestImplementation project(':client:rest-high-level') + javaRestTestImplementation project(':x-pack:plugin:security') // let the javaRestTest see the classpath of main javaRestTestImplementation project.sourceSets.main.runtimeClasspath } diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java new file mode 100644 index 0000000000000..c95dd789b9cea --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -0,0 +1,421 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import java.util.Set; + +public class Constants { + + public static final Set NON_OPERATOR_ACTIONS = Set.of( +// "cluster:admin/autoscaling/delete_autoscaling_policy", +// "cluster:admin/autoscaling/get_autoscaling_capacity", +// "cluster:admin/autoscaling/get_autoscaling_policy", +// "cluster:admin/autoscaling/put_autoscaling_policy", + "cluster:admin/component_template/delete", + "cluster:admin/component_template/get", + "cluster:admin/component_template/put", + "cluster:admin/data_frame/delete", + "cluster:admin/data_frame/preview", + "cluster:admin/data_frame/put", + "cluster:admin/data_frame/start", + "cluster:admin/data_frame/stop", + "cluster:admin/data_frame/update", + "cluster:admin/ilm/_move/post", + "cluster:admin/ilm/delete", + "cluster:admin/ilm/get", + "cluster:admin/ilm/operation_mode/get", + "cluster:admin/ilm/put", + "cluster:admin/ilm/start", + "cluster:admin/ilm/stop", + "cluster:admin/indices/dangling/delete", + "cluster:admin/indices/dangling/find", + "cluster:admin/indices/dangling/import", + "cluster:admin/indices/dangling/list", + "cluster:admin/ingest/pipeline/delete", + "cluster:admin/ingest/pipeline/get", + "cluster:admin/ingest/pipeline/put", + "cluster:admin/ingest/pipeline/simulate", + "cluster:admin/ingest/processor/grok/get", + "cluster:admin/logstash/pipeline/delete", + "cluster:admin/logstash/pipeline/get", + "cluster:admin/logstash/pipeline/put", + "cluster:admin/nodes/reload_secure_settings", + "cluster:admin/persistent/completion", + "cluster:admin/persistent/remove", + "cluster:admin/persistent/start", + "cluster:admin/persistent/update_status", + "cluster:admin/reindex/rethrottle", + "cluster:admin/repository/_cleanup", + "cluster:admin/repository/delete", + "cluster:admin/repository/get", + "cluster:admin/repository/put", + "cluster:admin/repository/verify", + "cluster:admin/reroute", + "cluster:admin/script/delete", + "cluster:admin/script/get", + "cluster:admin/script/put", + "cluster:admin/script_context/get", + "cluster:admin/script_language/get", + "cluster:admin/scripts/painless/context", + "cluster:admin/scripts/painless/execute", + "cluster:admin/settings/update", + "cluster:admin/slm/delete", + "cluster:admin/slm/execute", + "cluster:admin/slm/execute-retention", + "cluster:admin/slm/get", + "cluster:admin/slm/put", + "cluster:admin/slm/start", + "cluster:admin/slm/stats", + "cluster:admin/slm/status", + "cluster:admin/slm/stop", + "cluster:admin/snapshot/clone", + "cluster:admin/snapshot/create", + "cluster:admin/snapshot/delete", + "cluster:admin/snapshot/get", + "cluster:admin/snapshot/mount", + "cluster:admin/snapshot/restore", + "cluster:admin/snapshot/status", + "cluster:admin/snapshot/status[nodes]", + "cluster:admin/tasks/cancel", + "cluster:admin/transform/delete", + "cluster:admin/transform/preview", + "cluster:admin/transform/put", + "cluster:admin/transform/start", + "cluster:admin/transform/stop", + "cluster:admin/transform/update", +// "cluster:admin/voting_config/add_exclusions", +// "cluster:admin/voting_config/clear_exclusions", + "cluster:admin/xpack/ccr/auto_follow_pattern/activate", + "cluster:admin/xpack/ccr/auto_follow_pattern/delete", + "cluster:admin/xpack/ccr/auto_follow_pattern/get", + "cluster:admin/xpack/ccr/auto_follow_pattern/put", + "cluster:admin/xpack/ccr/pause_follow", + "cluster:admin/xpack/ccr/resume_follow", + "cluster:admin/xpack/deprecation/info", + "cluster:admin/xpack/deprecation/nodes/info", + "cluster:admin/xpack/enrich/delete", + "cluster:admin/xpack/enrich/execute", + "cluster:admin/xpack/enrich/get", + "cluster:admin/xpack/enrich/put", + "cluster:admin/xpack/license/basic_status", +// "cluster:admin/xpack/license/delete", + "cluster:admin/xpack/license/feature_usage", +// "cluster:admin/xpack/license/put", + "cluster:admin/xpack/license/start_basic", + "cluster:admin/xpack/license/start_trial", + "cluster:admin/xpack/license/trial_status", + "cluster:admin/xpack/ml/calendars/delete", + "cluster:admin/xpack/ml/calendars/events/delete", + "cluster:admin/xpack/ml/calendars/events/post", + "cluster:admin/xpack/ml/calendars/jobs/update", + "cluster:admin/xpack/ml/calendars/put", + "cluster:admin/xpack/ml/data_frame/analytics/delete", + "cluster:admin/xpack/ml/data_frame/analytics/explain", + "cluster:admin/xpack/ml/data_frame/analytics/put", + "cluster:admin/xpack/ml/data_frame/analytics/start", + "cluster:admin/xpack/ml/data_frame/analytics/stop", + "cluster:admin/xpack/ml/data_frame/analytics/update", + "cluster:admin/xpack/ml/datafeed/start", + "cluster:admin/xpack/ml/datafeed/stop", + "cluster:admin/xpack/ml/datafeeds/delete", + "cluster:admin/xpack/ml/datafeeds/preview", + "cluster:admin/xpack/ml/datafeeds/put", + "cluster:admin/xpack/ml/datafeeds/update", + "cluster:admin/xpack/ml/delete_expired_data", + "cluster:admin/xpack/ml/filters/delete", + "cluster:admin/xpack/ml/filters/get", + "cluster:admin/xpack/ml/filters/put", + "cluster:admin/xpack/ml/filters/update", + "cluster:admin/xpack/ml/inference/delete", + "cluster:admin/xpack/ml/inference/put", + "cluster:admin/xpack/ml/job/close", + "cluster:admin/xpack/ml/job/data/post", + "cluster:admin/xpack/ml/job/delete", + "cluster:admin/xpack/ml/job/estimate_model_memory", + "cluster:admin/xpack/ml/job/flush", + "cluster:admin/xpack/ml/job/forecast", + "cluster:admin/xpack/ml/job/forecast/delete", + "cluster:admin/xpack/ml/job/model_snapshots/delete", + "cluster:admin/xpack/ml/job/model_snapshots/revert", + "cluster:admin/xpack/ml/job/model_snapshots/update", + "cluster:admin/xpack/ml/job/model_snapshots/upgrade", + "cluster:admin/xpack/ml/job/open", + "cluster:admin/xpack/ml/job/persist", + "cluster:admin/xpack/ml/job/put", + "cluster:admin/xpack/ml/job/update", + "cluster:admin/xpack/ml/job/validate", + "cluster:admin/xpack/ml/job/validate/detector", + "cluster:admin/xpack/ml/upgrade_mode", + "cluster:admin/xpack/monitoring/bulk", + "cluster:admin/xpack/rollup/delete", + "cluster:admin/xpack/rollup/put", + "cluster:admin/xpack/rollup/start", + "cluster:admin/xpack/rollup/stop", + "cluster:admin/xpack/searchable_snapshots/cache/clear", + "cluster:admin/xpack/security/api_key/create", + "cluster:admin/xpack/security/api_key/get", + "cluster:admin/xpack/security/api_key/grant", + "cluster:admin/xpack/security/api_key/invalidate", + "cluster:admin/xpack/security/cache/clear", + "cluster:admin/xpack/security/delegate_pki", + "cluster:admin/xpack/security/oidc/authenticate", + "cluster:admin/xpack/security/oidc/logout", + "cluster:admin/xpack/security/oidc/prepare", + "cluster:admin/xpack/security/privilege/builtin/get", + "cluster:admin/xpack/security/privilege/cache/clear", + "cluster:admin/xpack/security/privilege/delete", + "cluster:admin/xpack/security/privilege/get", + "cluster:admin/xpack/security/privilege/put", + "cluster:admin/xpack/security/realm/cache/clear", + "cluster:admin/xpack/security/role/delete", + "cluster:admin/xpack/security/role/get", + "cluster:admin/xpack/security/role/put", + "cluster:admin/xpack/security/role_mapping/delete", + "cluster:admin/xpack/security/role_mapping/get", + "cluster:admin/xpack/security/role_mapping/put", + "cluster:admin/xpack/security/roles/cache/clear", + "cluster:admin/xpack/security/saml/authenticate", + "cluster:admin/xpack/security/saml/complete_logout", + "cluster:admin/xpack/security/saml/invalidate", + "cluster:admin/xpack/security/saml/logout", + "cluster:admin/xpack/security/saml/prepare", + "cluster:admin/xpack/security/token/create", + "cluster:admin/xpack/security/token/invalidate", + "cluster:admin/xpack/security/token/refresh", + "cluster:admin/xpack/security/user/authenticate", + "cluster:admin/xpack/security/user/change_password", + "cluster:admin/xpack/security/user/delete", + "cluster:admin/xpack/security/user/get", + "cluster:admin/xpack/security/user/has_privileges", + "cluster:admin/xpack/security/user/list_privileges", + "cluster:admin/xpack/security/user/put", + "cluster:admin/xpack/security/user/set_enabled", + "cluster:admin/xpack/watcher/service", + "cluster:admin/xpack/watcher/watch/ack", + "cluster:admin/xpack/watcher/watch/activate", + "cluster:admin/xpack/watcher/watch/delete", + "cluster:admin/xpack/watcher/watch/execute", + "cluster:admin/xpack/watcher/watch/put", + "cluster:internal/xpack/ml/datafeed/isolate", + "cluster:internal/xpack/ml/inference/infer", + "cluster:internal/xpack/ml/job/finalize_job_execution", + "cluster:internal/xpack/ml/job/kill/process", + "cluster:internal/xpack/ml/job/update/process", + "cluster:monitor/allocation/explain", + "cluster:monitor/async_search/status", + "cluster:monitor/ccr/follow_info", + "cluster:monitor/ccr/follow_stats", + "cluster:monitor/ccr/stats", + "cluster:monitor/data_frame/get", + "cluster:monitor/data_frame/stats/get", + "cluster:monitor/health", + "cluster:monitor/main", + "cluster:monitor/nodes/hot_threads", + "cluster:monitor/nodes/info", + "cluster:monitor/nodes/stats", + "cluster:monitor/nodes/usage", + "cluster:monitor/remote/info", + "cluster:monitor/state", + "cluster:monitor/stats", + "cluster:monitor/task", + "cluster:monitor/task/get", + "cluster:monitor/tasks/lists", + "cluster:monitor/transform/get", + "cluster:monitor/transform/stats/get", + "cluster:monitor/xpack/analytics/stats", + "cluster:monitor/xpack/enrich/coordinator_stats", + "cluster:monitor/xpack/enrich/stats", + "cluster:monitor/xpack/eql/stats/dist", + "cluster:monitor/xpack/info", + "cluster:monitor/xpack/info/aggregate_metric", + "cluster:monitor/xpack/info/analytics", + "cluster:monitor/xpack/info/ccr", + "cluster:monitor/xpack/info/data_streams", + "cluster:monitor/xpack/info/data_tiers", + "cluster:monitor/xpack/info/enrich", + "cluster:monitor/xpack/info/eql", + "cluster:monitor/xpack/info/frozen_indices", + "cluster:monitor/xpack/info/graph", + "cluster:monitor/xpack/info/ilm", + "cluster:monitor/xpack/info/logstash", + "cluster:monitor/xpack/info/ml", + "cluster:monitor/xpack/info/monitoring", + "cluster:monitor/xpack/info/rollup", + "cluster:monitor/xpack/info/searchable_snapshots", + "cluster:monitor/xpack/info/security", + "cluster:monitor/xpack/info/slm", + "cluster:monitor/xpack/info/spatial", + "cluster:monitor/xpack/info/sql", + "cluster:monitor/xpack/info/transform", + "cluster:monitor/xpack/info/vectors", + "cluster:monitor/xpack/info/voting_only", + "cluster:monitor/xpack/info/watcher", + "cluster:monitor/xpack/license/get", + "cluster:monitor/xpack/ml/calendars/events/get", + "cluster:monitor/xpack/ml/calendars/get", + "cluster:monitor/xpack/ml/data_frame/analytics/get", + "cluster:monitor/xpack/ml/data_frame/analytics/stats/get", + "cluster:monitor/xpack/ml/data_frame/evaluate", + "cluster:monitor/xpack/ml/datafeeds/get", + "cluster:monitor/xpack/ml/datafeeds/stats/get", + "cluster:monitor/xpack/ml/findfilestructure", + "cluster:monitor/xpack/ml/inference/get", + "cluster:monitor/xpack/ml/inference/stats/get", + "cluster:monitor/xpack/ml/info/get", + "cluster:monitor/xpack/ml/job/get", + "cluster:monitor/xpack/ml/job/model_snapshots/get", + "cluster:monitor/xpack/ml/job/results/buckets/get", + "cluster:monitor/xpack/ml/job/results/categories/get", + "cluster:monitor/xpack/ml/job/results/influencers/get", + "cluster:monitor/xpack/ml/job/results/overall_buckets/get", + "cluster:monitor/xpack/ml/job/results/records/get", + "cluster:monitor/xpack/ml/job/stats/get", + "cluster:monitor/xpack/repositories_metering/clear_metering_archive", + "cluster:monitor/xpack/repositories_metering/get_metrics", + "cluster:monitor/xpack/rollup/get", + "cluster:monitor/xpack/rollup/get/caps", + "cluster:monitor/xpack/searchable_snapshots/stats", + "cluster:monitor/xpack/security/saml/metadata", + "cluster:monitor/xpack/spatial/stats", + "cluster:monitor/xpack/sql/stats/dist", + "cluster:monitor/xpack/ssl/certificates/get", + "cluster:monitor/xpack/usage", + "cluster:monitor/xpack/usage/aggregate_metric", + "cluster:monitor/xpack/usage/analytics", + "cluster:monitor/xpack/usage/ccr", + "cluster:monitor/xpack/usage/data_streams", + "cluster:monitor/xpack/usage/data_tiers", + "cluster:monitor/xpack/usage/enrich", + "cluster:monitor/xpack/usage/eql", + "cluster:monitor/xpack/usage/frozen_indices", + "cluster:monitor/xpack/usage/graph", + "cluster:monitor/xpack/usage/ilm", + "cluster:monitor/xpack/usage/logstash", + "cluster:monitor/xpack/usage/ml", + "cluster:monitor/xpack/usage/monitoring", + "cluster:monitor/xpack/usage/rollup", + "cluster:monitor/xpack/usage/searchable_snapshots", + "cluster:monitor/xpack/usage/security", + "cluster:monitor/xpack/usage/slm", + "cluster:monitor/xpack/usage/spatial", + "cluster:monitor/xpack/usage/sql", + "cluster:monitor/xpack/usage/transform", + "cluster:monitor/xpack/usage/vectors", + "cluster:monitor/xpack/usage/voting_only", + "cluster:monitor/xpack/usage/watcher", + "cluster:monitor/xpack/watcher/stats/dist", + "cluster:monitor/xpack/watcher/watch/get", + "indices:admin/aliases", + "indices:admin/aliases/get", + "indices:admin/analyze", + "indices:admin/auto_create", + "indices:admin/block/add", + "indices:admin/block/add[s]", + "indices:admin/cache/clear", + "indices:admin/close", + "indices:admin/close[s]", + "indices:admin/create", + "indices:admin/data_stream/create", + "indices:admin/data_stream/delete", + "indices:admin/data_stream/get", + "indices:admin/data_stream/migrate", + "indices:admin/delete", + "indices:admin/flush", + "indices:admin/flush[s]", + "indices:admin/forcemerge", + "indices:admin/freeze", + "indices:admin/get", + "indices:admin/ilm/explain", + "indices:admin/ilm/remove_policy", + "indices:admin/ilm/retry", + "indices:admin/index_template/delete", + "indices:admin/index_template/get", + "indices:admin/index_template/put", + "indices:admin/index_template/simulate", + "indices:admin/index_template/simulate_index", + "indices:admin/mapping/auto_put", + "indices:admin/mapping/put", + "indices:admin/mappings/fields/get", + "indices:admin/mappings/fields/get[index]", + "indices:admin/mappings/get", + "indices:admin/open", + "indices:admin/refresh", + "indices:admin/refresh[s]", + "indices:admin/reload_analyzers", + "indices:admin/resize", + "indices:admin/resolve/index", + "indices:admin/rollover", + "indices:admin/seq_no/add_retention_lease", + "indices:admin/seq_no/global_checkpoint_sync", + "indices:admin/seq_no/remove_retention_lease", + "indices:admin/seq_no/renew_retention_lease", + "indices:admin/settings/update", + "indices:admin/shards/search_shards", + "indices:admin/template/delete", + "indices:admin/template/get", + "indices:admin/template/put", + "indices:admin/validate/query", + "indices:admin/xpack/ccr/forget_follower", + "indices:admin/xpack/ccr/put_follow", + "indices:admin/xpack/ccr/unfollow", + "indices:data/read/async_search/delete", + "indices:data/read/async_search/get", + "indices:data/read/async_search/submit", + "indices:data/read/close_point_in_time", + "indices:data/read/eql", + "indices:data/read/eql/async/get", + "indices:data/read/explain", + "indices:data/read/field_caps", + "indices:data/read/field_caps[index]", + "indices:data/read/get", + "indices:data/read/mget", + "indices:data/read/mget[shard]", + "indices:data/read/msearch", + "indices:data/read/msearch/template", + "indices:data/read/mtv", + "indices:data/read/mtv[shard]", + "indices:data/read/open_point_in_time", + "indices:data/read/rank_eval", + "indices:data/read/scroll", + "indices:data/read/scroll/clear", + "indices:data/read/search", + "indices:data/read/search/template", + "indices:data/read/shard_multi_search", + "indices:data/read/sql", + "indices:data/read/sql/close_cursor", + "indices:data/read/sql/translate", + "indices:data/read/tv", + "indices:data/read/xpack/ccr/shard_changes", + "indices:data/read/xpack/enrich/coordinate_lookups", + "indices:data/read/xpack/graph/explore", + "indices:data/read/xpack/rollup/get/index/caps", + "indices:data/read/xpack/rollup/search", + "indices:data/write/bulk", + "indices:data/write/bulk[s]", + "indices:data/write/bulk_shard_operations[s]", + "indices:data/write/delete", + "indices:data/write/delete/byquery", + "indices:data/write/index", + "indices:data/write/reindex", + "indices:data/write/update", + "indices:data/write/update/byquery", + "indices:monitor/data_stream/stats", + "indices:monitor/recovery", + "indices:monitor/segments", + "indices:monitor/settings/get", + "indices:monitor/shard_stores", + "indices:monitor/stats", + "internal:admin/ccr/internal_repository/delete", + "internal:admin/ccr/internal_repository/put", + "internal:admin/ccr/restore/file_chunk/get", + "internal:admin/ccr/restore/session/clear", + "internal:admin/ccr/restore/session/put", + "internal:cluster/nodes/indices/shard/store", + "internal:gateway/local/meta_state", + "internal:gateway/local/started_shards" + ); +} diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index 7201e5e750c38..ba7950903a53f 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -11,11 +11,13 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.example.actions.GetActionsAction; import org.elasticsearch.test.rest.ESRestTestCase; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.List; import java.util.Map; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; @@ -84,4 +86,15 @@ public void testOperatorUserCanCallNonOperatorOnlyApi() throws IOException { RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); client().performRequest(mainRequest); } + + @SuppressWarnings("unchecked") + public void testAllActionsAreEitherOperatorOnlyOrNonOperator() throws IOException { + final Request request = new Request("GET", "/_test/get_actions"); + final Map response = responseAsMap(client().performRequest(request)); + List allActions = (List)response.get("actions"); + allActions.remove(GetActionsAction.NAME); + allActions.removeAll(CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS); + allActions.removeAll(Constants.NON_OPERATOR_ACTIONS); + assertTrue(allActions.isEmpty()); + } } diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java new file mode 100644 index 0000000000000..bd5186b448ec4 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.example.actions.GetActionsAction; +import org.elasticsearch.example.actions.RestGetActionsAction; +import org.elasticsearch.example.actions.TransportGetActionsAction; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; + +import java.util.List; +import java.util.function.Supplier; + +public class OpTestPlugin extends Plugin implements ActionPlugin { + + @Override + public List getRestHandlers( + Settings settings, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster) { + + return List.of(new RestGetActionsAction()); + } + + @Override + public List> getActions() { + return List.of(new ActionHandler<>(GetActionsAction.INSTANCE, TransportGetActionsAction.class)); + } +} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java new file mode 100644 index 0000000000000..6d87cfe4a29eb --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example.actions; + +import org.elasticsearch.action.ActionType; + +public class GetActionsAction extends ActionType { + + public static final String NAME = "cluster:monitor/test/get_actions"; + public static final GetActionsAction INSTANCE = new GetActionsAction(); + + public GetActionsAction() { + super(NAME, GetActionsResponse::new); + } +} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java new file mode 100644 index 0000000000000..0f1bf91d909d7 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example.actions; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +public class GetActionsRequest extends ActionRequest { + + public GetActionsRequest() { + } + + public GetActionsRequest(StreamInput in) throws IOException { + super(in); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java new file mode 100644 index 0000000000000..d65aacc787a46 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example.actions; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +public class GetActionsResponse extends ActionResponse implements ToXContentObject { + + private final List actions; + + public GetActionsResponse(List actions) { + this.actions = List.copyOf(Objects.requireNonNull(actions)); + } + + public GetActionsResponse(StreamInput in) throws IOException { + super(in); + actions = in.readStringList(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringCollection(actions); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .field("actions", actions); + return builder.endObject(); + } +} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java new file mode 100644 index 0000000000000..1279a885a3ac2 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example.actions; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestGetActionsAction extends BaseRestHandler { + @Override + public List routes() { + return List.of(new Route(GET, "/_test/get_actions")); + } + + @Override + public String getName() { + return "test_get_actions"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + // It is also possible to use reflection to get NodeClient#actions and save all the transport related classes + return channel -> client.execute(GetActionsAction.INSTANCE, new GetActionsRequest(), new RestToXContentListener<>(channel)); + } +} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java new file mode 100644 index 0000000000000..4af74cb22e3c2 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example.actions; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.TransportAction; +import org.elasticsearch.common.inject.Binding; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.inject.Injector; +import org.elasticsearch.common.inject.TypeLiteral; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; + +import java.util.ArrayList; +import java.util.List; + +public class TransportGetActionsAction extends HandledTransportAction { + + private final Injector injector; + + @Inject + public TransportGetActionsAction(TransportService transportService, ActionFilters actionFilters, Injector injector) { + super(GetActionsAction.NAME, transportService, actionFilters, GetActionsRequest::new); + this.injector = injector; + } + + @SuppressWarnings("rawtypes") + @Override + protected void doExecute(Task task, GetActionsRequest request, ActionListener listener) { + final List> bindings = injector.findBindingsByType(TypeLiteral.get(TransportAction.class)); + + final List allActionNames = new ArrayList<>(bindings.size()); + for (final Binding binding : bindings) { + allActionNames.add(binding.getProvider().get().actionName); + } + listener.onResponse(new GetActionsResponse(allActionNames)); + } +} From be893ec7f85f2080676bedd20b3b97cfbbd8fb26 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 24 Nov 2020 20:01:44 +1100 Subject: [PATCH 06/23] spotless --- .../xpack/security/operator/Constants.java | 16 +++--- .../operator/OperatorPrivilegesIT.java | 51 +++++++++---------- .../elasticsearch/example/OpTestPlugin.java | 4 +- .../example/actions/GetActionsRequest.java | 3 +- .../example/actions/GetActionsResponse.java | 3 +- 5 files changed, 36 insertions(+), 41 deletions(-) diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index c95dd789b9cea..209916a96254c 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -11,10 +11,10 @@ public class Constants { public static final Set NON_OPERATOR_ACTIONS = Set.of( -// "cluster:admin/autoscaling/delete_autoscaling_policy", -// "cluster:admin/autoscaling/get_autoscaling_capacity", -// "cluster:admin/autoscaling/get_autoscaling_policy", -// "cluster:admin/autoscaling/put_autoscaling_policy", + // "cluster:admin/autoscaling/delete_autoscaling_policy", + // "cluster:admin/autoscaling/get_autoscaling_capacity", + // "cluster:admin/autoscaling/get_autoscaling_policy", + // "cluster:admin/autoscaling/put_autoscaling_policy", "cluster:admin/component_template/delete", "cluster:admin/component_template/get", "cluster:admin/component_template/put", @@ -87,8 +87,8 @@ public class Constants { "cluster:admin/transform/start", "cluster:admin/transform/stop", "cluster:admin/transform/update", -// "cluster:admin/voting_config/add_exclusions", -// "cluster:admin/voting_config/clear_exclusions", + // "cluster:admin/voting_config/add_exclusions", + // "cluster:admin/voting_config/clear_exclusions", "cluster:admin/xpack/ccr/auto_follow_pattern/activate", "cluster:admin/xpack/ccr/auto_follow_pattern/delete", "cluster:admin/xpack/ccr/auto_follow_pattern/get", @@ -102,9 +102,9 @@ public class Constants { "cluster:admin/xpack/enrich/get", "cluster:admin/xpack/enrich/put", "cluster:admin/xpack/license/basic_status", -// "cluster:admin/xpack/license/delete", + // "cluster:admin/xpack/license/delete", "cluster:admin/xpack/license/feature_usage", -// "cluster:admin/xpack/license/put", + // "cluster:admin/xpack/license/put", "cluster:admin/xpack/license/start_basic", "cluster:admin/xpack/license/start_trial", "cluster:admin/xpack/license/trial_status", diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index ba7950903a53f..0049302c1da14 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -29,25 +29,25 @@ public class OperatorPrivilegesIT extends ESRestTestCase { @Override protected Settings restClientSettings() { String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); - return Settings.builder() - .put(ThreadContext.PREFIX + ".Authorization", token) - .build(); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } @SuppressWarnings("unchecked") public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApiWhenOperatorPrivilegesIsEnabled() throws IOException { - final Request getClusterSettingsRequest = new Request("GET", - "_cluster/settings?flat_settings&include_defaults&filter_path=defaults.*operator_privileges*"); + final Request getClusterSettingsRequest = new Request( + "GET", + "_cluster/settings?flat_settings&include_defaults&filter_path=defaults.*operator_privileges*" + ); final Map settingsMap = entityAsMap(client().performRequest(getClusterSettingsRequest)); final Map defaults = (Map) settingsMap.get("defaults"); final Object isOperatorPrivilegesEnabled = defaults.get("xpack.security.operator_privileges.enabled"); - final Request postVotingConfigExclusionsRequest = new Request( - "POST", "_cluster/voting_config_exclusions?node_names=foo"); + final Request postVotingConfigExclusionsRequest = new Request("POST", "_cluster/voting_config_exclusions?node_names=foo"); if ("true".equals(isOperatorPrivilegesEnabled)) { final ResponseException responseException = expectThrows( ResponseException.class, - () -> client().performRequest(postVotingConfigExclusionsRequest)); + () -> client().performRequest(postVotingConfigExclusionsRequest) + ); assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); assertThat(responseException.getMessage(), containsString("Operator privileges are required for action")); } else { @@ -56,34 +56,31 @@ public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApiWhenOperatorPri } public void testOperatorUserWillSucceedToCallOperatorOnlyApi() throws IOException { - final Request postVotingConfigExclusionsRequest = new Request( - "POST", "_cluster/voting_config_exclusions?node_names=foo"); - final String authHeader = "Basic " + Base64.getEncoder().encodeToString( - "test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); - postVotingConfigExclusionsRequest.setOptions( - RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); + final Request postVotingConfigExclusionsRequest = new Request("POST", "_cluster/voting_config_exclusions?node_names=foo"); + final String authHeader = "Basic " + + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); + postVotingConfigExclusionsRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); client().performRequest(postVotingConfigExclusionsRequest); } public void testOperatorUserWillFailToCallOperatorOnlyApiIfRbacFails() throws IOException { - final Request deleteVotingConfigExclusionsRequest = new Request( - "DELETE", "_cluster/voting_config_exclusions"); - final String authHeader = "Basic " + Base64.getEncoder().encodeToString( - "test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); - deleteVotingConfigExclusionsRequest.setOptions( - RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); - final ResponseException responseException = expectThrows(ResponseException.class, - () -> client().performRequest(deleteVotingConfigExclusionsRequest)); + final Request deleteVotingConfigExclusionsRequest = new Request("DELETE", "_cluster/voting_config_exclusions"); + final String authHeader = "Basic " + + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); + deleteVotingConfigExclusionsRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); + final ResponseException responseException = expectThrows( + ResponseException.class, + () -> client().performRequest(deleteVotingConfigExclusionsRequest) + ); assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); assertThat(responseException.getMessage(), containsString("is unauthorized for user")); } public void testOperatorUserCanCallNonOperatorOnlyApi() throws IOException { final Request mainRequest = new Request("GET", "/"); - final String authHeader = "Basic " + Base64.getEncoder().encodeToString( - "test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); - mainRequest.setOptions( - RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); + final String authHeader = "Basic " + + Base64.getEncoder().encodeToString("test_operator:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); + mainRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader)); client().performRequest(mainRequest); } @@ -91,7 +88,7 @@ public void testOperatorUserCanCallNonOperatorOnlyApi() throws IOException { public void testAllActionsAreEitherOperatorOnlyOrNonOperator() throws IOException { final Request request = new Request("GET", "/_test/get_actions"); final Map response = responseAsMap(client().performRequest(request)); - List allActions = (List)response.get("actions"); + List allActions = (List) response.get("actions"); allActions.remove(GetActionsAction.NAME); allActions.removeAll(CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS); allActions.removeAll(Constants.NON_OPERATOR_ACTIONS); diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java index bd5186b448ec4..531e14a8aae9d 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java @@ -35,8 +35,8 @@ public List getRestHandlers( IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, IndexNameExpressionResolver indexNameExpressionResolver, - Supplier nodesInCluster) { - + Supplier nodesInCluster + ) { return List.of(new RestGetActionsAction()); } diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java index 0f1bf91d909d7..476a028fd08ba 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java @@ -14,8 +14,7 @@ public class GetActionsRequest extends ActionRequest { - public GetActionsRequest() { - } + public GetActionsRequest() {} public GetActionsRequest(StreamInput in) throws IOException { super(in); diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java index d65aacc787a46..2eb626d046ef6 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java @@ -36,8 +36,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject() - .field("actions", actions); + builder.startObject().field("actions", actions); return builder.endObject(); } } From 2d66a057b26a5eb605e0d0fb7eaa4e04fbcb2016 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 24 Nov 2020 20:36:58 +1100 Subject: [PATCH 07/23] testingConventions test --- .../example/OpTestPluginTests.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java diff --git a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java new file mode 100644 index 0000000000000..10e24d1ac1982 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.example.actions.GetActionsAction; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.test.ESTestCase; + +import java.util.List; + +// This test class is really to pass the testingConventions test +public class OpTestPluginTests extends ESTestCase { + + public void testActionWillBeProvided() { + final OpTestPlugin opTestPlugin = new OpTestPlugin(); + final List> actions = opTestPlugin.getActions(); + assertEquals(1, actions.size()); + assertSame(GetActionsAction.INSTANCE, actions.get(0).getAction()); + } + +} From 1b284760b6abbc4996208c644ae731b1e1b5dd8e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 00:25:33 +1100 Subject: [PATCH 08/23] Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java Co-authored-by: Tim Vernum --- .../xpack/security/operator/OperatorUserDescriptor.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java index 839813d8116a3..b9443b50207e2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java @@ -124,8 +124,10 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Group group = (Group) o; - return usernames.equals(group.usernames) && Objects.equals(realmName, - group.realmName) && realmType.equals(group.realmType) && authenticationType == group.authenticationType; + return usernames.equals(group.usernames) + && Objects.equals(realmName, group.realmName) + && realmType.equals(group.realmType) + && authenticationType == group.authenticationType; } @Override From 892840f780d89465a78a55ca4343e7aaee2a1b38 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 00:23:07 +1100 Subject: [PATCH 09/23] address feedback --- .../OperatorPrivilegesSingleNodeTests.java | 218 +----------------- .../xpack/security/Security.java | 4 +- .../operator/CompositeOperatorOnly.java | 125 ---------- .../xpack/security/operator/OperatorOnly.java | 96 ++++++-- .../security/operator/OperatorPrivileges.java | 19 +- .../authc/AuthenticationServiceTests.java | 4 +- .../support/SecondaryAuthenticatorTests.java | 4 +- .../authz/AuthorizationServiceTests.java | 4 +- .../operator/CompositeOperatorOnlyTests.java | 29 --- .../security/operator/OperatorOnlyTests.java | 30 +++ .../operator/OperatorPrivilegesTests.java | 16 +- .../qa/operator-privileges-tests/build.gradle | 2 - .../operator/OperatorPrivilegesIT.java | 2 +- 13 files changed, 143 insertions(+), 410 deletions(-) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java index 18c14a191ece6..cebf105b9c8c4 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java @@ -9,21 +9,14 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction; import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsRequest; -import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.client.Client; -import org.elasticsearch.common.inject.Binding; -import org.elasticsearch.common.inject.Injector; -import org.elasticsearch.common.inject.TypeLiteral; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.core.security.action.user.GetUsersAction; +import org.elasticsearch.xpack.core.security.action.user.GetUsersRequest; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import static org.elasticsearch.test.SecuritySettingsSource.TEST_PASSWORD_HASHED; import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD; @@ -70,23 +63,6 @@ protected Settings nodeSettings() { return builder.build(); } - // TODO: Not all plugins are available in internal cluster tests. Hence not all action names can be checked. - public void testActionsAreEitherOperatorOnlyOrNot() { - final Injector injector = node().injector(); - final List> bindings = injector.findBindingsByType(TypeLiteral.get(TransportAction.class)); - - final List allActionNames = new ArrayList<>(bindings.size()); - for (final Binding binding : bindings) { - allActionNames.add(binding.getProvider().get().actionName); - } - - final Set nonOperatorActions = Set.of(NON_OPERATOR_ACTIONS); - final Set expectedOperatorOnlyActions = Sets.difference(Set.copyOf(allActionNames), nonOperatorActions); - final Set actualOperatorOnlyActions = new HashSet<>(CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS); - assertTrue(actualOperatorOnlyActions.containsAll(expectedOperatorOnlyActions)); - assertFalse(actualOperatorOnlyActions.removeAll(nonOperatorActions)); - } - public void testSuperuserWillFailToCallOperatorOnlyAction() { final ClearVotingConfigExclusionsRequest clearVotingConfigExclusionsRequest = new ClearVotingConfigExclusionsRequest(); final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -102,185 +78,13 @@ public void testOperatorUserWillSucceedToCallOperatorOnlyAction() { client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet(); } - public static final String[] NON_OPERATOR_ACTIONS = new String[] { - "cluster:admin/component_template/delete", - "cluster:admin/component_template/get", - "cluster:admin/component_template/put", - "cluster:admin/indices/dangling/delete", - "cluster:admin/indices/dangling/find", - "cluster:admin/indices/dangling/import", - "cluster:admin/indices/dangling/list", - "cluster:admin/ingest/pipeline/delete", - "cluster:admin/ingest/pipeline/get", - "cluster:admin/ingest/pipeline/put", - "cluster:admin/ingest/pipeline/simulate", - "cluster:admin/nodes/reload_secure_settings", - "cluster:admin/persistent/completion", - "cluster:admin/persistent/remove", - "cluster:admin/persistent/start", - "cluster:admin/persistent/update_status", - "cluster:admin/reindex/rethrottle", - "cluster:admin/repository/_cleanup", - "cluster:admin/repository/delete", - "cluster:admin/repository/get", - "cluster:admin/repository/put", - "cluster:admin/repository/verify", - "cluster:admin/reroute", - "cluster:admin/script/delete", - "cluster:admin/script/get", - "cluster:admin/script/put", - "cluster:admin/script_context/get", - "cluster:admin/script_language/get", - "cluster:admin/settings/update", - "cluster:admin/snapshot/clone", - "cluster:admin/snapshot/create", - "cluster:admin/snapshot/delete", - "cluster:admin/snapshot/get", - "cluster:admin/snapshot/restore", - "cluster:admin/snapshot/status", - "cluster:admin/snapshot/status[nodes]", - "cluster:admin/tasks/cancel", - "cluster:admin/xpack/license/basic_status", - "cluster:admin/xpack/license/feature_usage", - "cluster:admin/xpack/license/start_basic", - "cluster:admin/xpack/license/start_trial", - "cluster:admin/xpack/license/trial_status", - "cluster:admin/xpack/monitoring/bulk", - "cluster:admin/xpack/security/api_key/create", - "cluster:admin/xpack/security/api_key/get", - "cluster:admin/xpack/security/api_key/grant", - "cluster:admin/xpack/security/api_key/invalidate", - "cluster:admin/xpack/security/cache/clear", - "cluster:admin/xpack/security/delegate_pki", - "cluster:admin/xpack/security/oidc/authenticate", - "cluster:admin/xpack/security/oidc/logout", - "cluster:admin/xpack/security/oidc/prepare", - "cluster:admin/xpack/security/privilege/builtin/get", - "cluster:admin/xpack/security/privilege/cache/clear", - "cluster:admin/xpack/security/privilege/delete", - "cluster:admin/xpack/security/privilege/get", - "cluster:admin/xpack/security/privilege/put", - "cluster:admin/xpack/security/realm/cache/clear", - "cluster:admin/xpack/security/role/delete", - "cluster:admin/xpack/security/role/get", - "cluster:admin/xpack/security/role/put", - "cluster:admin/xpack/security/role_mapping/delete", - "cluster:admin/xpack/security/role_mapping/get", - "cluster:admin/xpack/security/role_mapping/put", - "cluster:admin/xpack/security/roles/cache/clear", - "cluster:admin/xpack/security/saml/authenticate", - "cluster:admin/xpack/security/saml/complete_logout", - "cluster:admin/xpack/security/saml/invalidate", - "cluster:admin/xpack/security/saml/logout", - "cluster:admin/xpack/security/saml/prepare", - "cluster:admin/xpack/security/token/create", - "cluster:admin/xpack/security/token/invalidate", - "cluster:admin/xpack/security/token/refresh", - "cluster:admin/xpack/security/user/authenticate", - "cluster:admin/xpack/security/user/change_password", - "cluster:admin/xpack/security/user/delete", - "cluster:admin/xpack/security/user/get", - "cluster:admin/xpack/security/user/has_privileges", - "cluster:admin/xpack/security/user/list_privileges", - "cluster:admin/xpack/security/user/put", - "cluster:admin/xpack/security/user/set_enabled", - "cluster:monitor/allocation/explain", - "cluster:monitor/health", - "cluster:monitor/main", - "cluster:monitor/nodes/hot_threads", - "cluster:monitor/nodes/info", - "cluster:monitor/nodes/stats", - "cluster:monitor/nodes/usage", - "cluster:monitor/remote/info", - "cluster:monitor/state", - "cluster:monitor/stats", - "cluster:monitor/task", - "cluster:monitor/task/get", - "cluster:monitor/tasks/lists", - "cluster:monitor/xpack/info", - "cluster:monitor/xpack/info/data_tiers", - "cluster:monitor/xpack/info/monitoring", - "cluster:monitor/xpack/info/security", - "cluster:monitor/xpack/license/get", - "cluster:monitor/xpack/security/saml/metadata", - "cluster:monitor/xpack/ssl/certificates/get", - "cluster:monitor/xpack/usage", - "cluster:monitor/xpack/usage/data_tiers", - "cluster:monitor/xpack/usage/monitoring", - "cluster:monitor/xpack/usage/security", - "indices:admin/aliases", - "indices:admin/aliases/get", - "indices:admin/analyze", - "indices:admin/auto_create", - "indices:admin/block/add", - "indices:admin/block/add[s]", - "indices:admin/cache/clear", - "indices:admin/close", - "indices:admin/close[s]", - "indices:admin/create", - "indices:admin/delete", - "indices:admin/flush", - "indices:admin/flush[s]", - "indices:admin/forcemerge", - "indices:admin/get", - "indices:admin/index_template/delete", - "indices:admin/index_template/get", - "indices:admin/index_template/put", - "indices:admin/index_template/simulate", - "indices:admin/index_template/simulate_index", - "indices:admin/mapping/auto_put", - "indices:admin/mapping/put", - "indices:admin/mappings/fields/get", - "indices:admin/mappings/fields/get[index]", - "indices:admin/mappings/get", - "indices:admin/open", - "indices:admin/refresh", - "indices:admin/refresh[s]", - "indices:admin/reload_analyzers", - "indices:admin/resize", - "indices:admin/resolve/index", - "indices:admin/rollover", - "indices:admin/seq_no/add_retention_lease", - "indices:admin/seq_no/global_checkpoint_sync", - "indices:admin/seq_no/remove_retention_lease", - "indices:admin/seq_no/renew_retention_lease", - "indices:admin/settings/update", - "indices:admin/shards/search_shards", - "indices:admin/template/delete", - "indices:admin/template/get", - "indices:admin/template/put", - "indices:admin/validate/query", - "indices:data/read/async_search/delete", - "indices:data/read/close_point_in_time", - "indices:data/read/explain", - "indices:data/read/field_caps", - "indices:data/read/field_caps[index]", - "indices:data/read/get", - "indices:data/read/mget", - "indices:data/read/mget[shard]", - "indices:data/read/msearch", - "indices:data/read/mtv", - "indices:data/read/mtv[shard]", - "indices:data/read/open_point_in_time", - "indices:data/read/scroll", - "indices:data/read/scroll/clear", - "indices:data/read/search", - "indices:data/read/tv", - "indices:data/write/bulk", - "indices:data/write/bulk[s]", - "indices:data/write/delete", - "indices:data/write/delete/byquery", - "indices:data/write/index", - "indices:data/write/reindex", - "indices:data/write/update", - "indices:data/write/update/byquery", - "indices:monitor/recovery", - "indices:monitor/segments", - "indices:monitor/settings/get", - "indices:monitor/shard_stores", - "indices:monitor/stats", - "internal:cluster/nodes/indices/shard/store", - "internal:gateway/local/meta_state", - "internal:gateway/local/started_shards" - }; + public void testOperatorUserIsStillSubjectToRoleLimits() { + final Client client = client().filterWithHeader(Map.of( + "Authorization", + basicAuthHeaderValue(OPERATOR_USER_NAME, new SecureString(TEST_PASSWORD.toCharArray())))); + final GetUsersRequest getUsersRequest = new GetUsersRequest(); + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> client.execute(GetUsersAction.INSTANCE, getUsersRequest).actionGet()); + assertThat(e.getMessage(), containsString("is unauthorized for user")); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 2c32accca422c..1ed1a4b5d6041 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -214,7 +214,7 @@ import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor; -import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; @@ -477,7 +477,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste final AuthenticationFailureHandler failureHandler = createAuthenticationFailureHandler(realms, extensionComponents); final OperatorPrivileges operatorPrivileges = new OperatorPrivileges(settings, getLicenseState(), - new OperatorUserDescriptor(environment, resourceWatcherService), new CompositeOperatorOnly()); + new OperatorUserDescriptor(environment, resourceWatcherService), new OperatorOnly()); authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges)); components.add(authcService.get()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java deleted file mode 100644 index d690f4f4d176a..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnly.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.security.operator; - -import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction; -import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction; -import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryAction; -import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.license.DeleteLicenseAction; -import org.elasticsearch.license.PutLicenseAction; -import org.elasticsearch.transport.TransportRequest; - -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_HTTP_SETTING; -import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_SETTING; - -public class CompositeOperatorOnly implements OperatorOnly { - - private final List checks; - - public CompositeOperatorOnly() { - checks = List.of(new ActionOperatorOnly(), new SettingOperatorOnly()); - } - - @Override - public Result check(String action, TransportRequest request) { - return checks.stream() - .map(c -> c.check(action, request)) - .filter(r -> r.getStatus() != Status.CONTINUE) - .findFirst() - .orElse(OperatorOnly.RESULT_NO); - } - - public static final class ActionOperatorOnly implements OperatorOnly { - - public static final Set SIMPLE_ACTIONS = Set.of( - AddVotingConfigExclusionsAction.NAME, - ClearVotingConfigExclusionsAction.NAME, - PutLicenseAction.NAME, - DeleteLicenseAction.NAME, - // Autoscaling does not publish its actions to core, literal strings are needed. - "cluster:admin/autoscaling/put_autoscaling_policy", - "cluster:admin/autoscaling/delete_autoscaling_policy", - "cluster:admin/autoscaling/get_autoscaling_policy", - "cluster:admin/autoscaling/get_autoscaling_capacity" - ); - - // This map is just to showcase how "partial" operator-only API would work. - // It will not be included in phase 1 delivery. - public static final Map> PARAMETER_SENSITIVE_ACTIONS = Map.of( - DeleteRepositoryAction.NAME, (request) -> { - assert request instanceof DeleteRepositoryRequest; - final DeleteRepositoryRequest deleteRepositoryRequest = (DeleteRepositoryRequest) request; - if ("found-snapshots".equals(deleteRepositoryRequest.name())) { - return OperatorOnly.Result.yes( - () -> "action [" + DeleteRepositoryAction.NAME + "] with repository [" + deleteRepositoryRequest.name()); - } else { - return OperatorOnly.RESULT_NO; - } - } - ); - - @Override - public Result check(String action, TransportRequest request) { - if (SIMPLE_ACTIONS.contains(action)) { - return OperatorOnly.Result.yes(() -> "action [" + action + "]"); - } else if (PARAMETER_SENSITIVE_ACTIONS.containsKey(action)) { - return PARAMETER_SENSITIVE_ACTIONS.get(action).apply(request); - } else { - return OperatorOnly.RESULT_CONTINUE; - } - } - } - - // This class is a prototype to showcase what it would look like for operator only settings - // It may not be included in phase 1 delivery - public static final class SettingOperatorOnly implements OperatorOnly { - - public static final Set SIMPLE_SETTINGS = Set.of( - IP_FILTER_ENABLED_HTTP_SETTING.getKey(), - IP_FILTER_ENABLED_SETTING.getKey(), - // TODO: Use literal strings due to dependency. Alternatively we can let each plugin publish names of operator settings - "xpack.ml.max_machine_memory_percent", - "xpack.ml.max_model_memory_limit" - ); - - @Override - public Result check(String action, TransportRequest request) { - if (false == ClusterUpdateSettingsAction.NAME.equals(action)) { - return OperatorOnly.RESULT_CONTINUE; - } - assert request instanceof ClusterUpdateSettingsRequest; - final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = (ClusterUpdateSettingsRequest) request; - - final boolean hasNoOverlap = - Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.persistentSettings().keySet()) - && Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.transientSettings().keySet()); - - if (hasNoOverlap) { - return OperatorOnly.RESULT_NO; - } else { - final HashSet requestedSettings = new HashSet<>(clusterUpdateSettingsRequest.persistentSettings().keySet()); - requestedSettings.addAll(clusterUpdateSettingsRequest.transientSettings().keySet()); - requestedSettings.retainAll(SIMPLE_SETTINGS); - return OperatorOnly.Result.yes( - () -> requestedSettings.size() > 1 ? "settings" : "setting" - +" [" + Strings.collectionToCommaDelimitedString(requestedSettings) + "]"); - } - } - } -} - diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java index 6d21642500bdc..c884f09dfcbdf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java @@ -6,40 +6,92 @@ package org.elasticsearch.xpack.security.operator; +import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction; +import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction; +import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryAction; +import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.license.DeleteLicenseAction; +import org.elasticsearch.license.PutLicenseAction; import org.elasticsearch.transport.TransportRequest; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; -public interface OperatorOnly { +import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_HTTP_SETTING; +import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_SETTING; - Result check(String action, TransportRequest request); +public class OperatorOnly { - enum Status { - YES, NO, CONTINUE; - } + public static final Set SIMPLE_ACTIONS = Set.of(AddVotingConfigExclusionsAction.NAME, + ClearVotingConfigExclusionsAction.NAME, + PutLicenseAction.NAME, + DeleteLicenseAction.NAME, + // Autoscaling does not publish its actions to core, literal strings are needed. + "cluster:admin/autoscaling/put_autoscaling_policy", + "cluster:admin/autoscaling/delete_autoscaling_policy", + "cluster:admin/autoscaling/get_autoscaling_policy", + "cluster:admin/autoscaling/get_autoscaling_capacity"); - final class Result { - private final Status status; - private final Supplier messageSupplier; + // This class is a prototype to showcase what it would look like for operator only settings + // It may not be included in phase 1 delivery. Also this may end up using Enum Property to + // mark operator only settings instead of using the list here. + public static final Set SIMPLE_SETTINGS = Set.of(IP_FILTER_ENABLED_HTTP_SETTING.getKey(), IP_FILTER_ENABLED_SETTING.getKey(), + // TODO: Use literal strings due to dependency. Alternatively we can let each plugin publish names of operator settings + "xpack.ml.max_machine_memory_percent", "xpack.ml.max_model_memory_limit"); - private Result(Status status, Supplier messageSupplier) { - this.status = status; - this.messageSupplier = messageSupplier; - } + // This map is just to showcase how "partial" operator-only API would work. + // It will not be included in phase 1 delivery. + public static final Map>> PARAMETER_SENSITIVE_ACTIONS = + Map.of(DeleteRepositoryAction.NAME, (request) -> { + assert request instanceof DeleteRepositoryRequest; + final DeleteRepositoryRequest deleteRepositoryRequest = (DeleteRepositoryRequest) request; + if ("found-snapshots".equals(deleteRepositoryRequest.name())) { + return () -> "action [" + DeleteRepositoryAction.NAME + "] with repository [" + deleteRepositoryRequest.name(); + } else { + return null; + } + }); - static Result yes(Supplier messageSupplier) { - return new Result(Status.YES, messageSupplier); + // The return type is a bit weird, but it is a shortcut to avoid having to use either + // a Tuple or a new class to hold true/false and a message/null. + // Since the combination is either true+message or false+null, it is possible to just + // use the existence of the message to also indicate whether the result is true or false. + public Supplier check(String action, TransportRequest request) { + if (SIMPLE_ACTIONS.contains(action)) { + return () -> "action [" + action + "]"; + } else if (PARAMETER_SENSITIVE_ACTIONS.containsKey(action)) { + return PARAMETER_SENSITIVE_ACTIONS.get(action).apply(request); + } else if (ClusterUpdateSettingsAction.NAME.equals(action)) { + assert request instanceof ClusterUpdateSettingsRequest; + final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = (ClusterUpdateSettingsRequest) request; + return checkSettings(clusterUpdateSettingsRequest); + } else { + return null; } + } - public Status getStatus() { - return status; - } + private Supplier checkSettings(ClusterUpdateSettingsRequest clusterUpdateSettingsRequest) { + final boolean hasEmptyIntersection = Sets.haveEmptyIntersection( + SIMPLE_SETTINGS, clusterUpdateSettingsRequest.persistentSettings().keySet()) + && Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.transientSettings().keySet()); - public String getMessage() { - return messageSupplier.get(); + if (hasEmptyIntersection) { + return null; + } else { + final HashSet requestedSettings = new HashSet<>(clusterUpdateSettingsRequest.persistentSettings().keySet()); + requestedSettings.addAll(clusterUpdateSettingsRequest.transientSettings().keySet()); + requestedSettings.retainAll(SIMPLE_SETTINGS); + return () -> requestedSettings.size() > 1 ? + "settings" : + "setting" + " [" + Strings.collectionToCommaDelimitedString(requestedSettings) + "]"; } } - - Result RESULT_NO = new Result(Status.NO, null); - Result RESULT_CONTINUE = new Result(Status.CONTINUE, null); } + diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java index ad17337a06097..23bd0dfd2019f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -15,20 +15,22 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import java.util.function.Supplier; + public class OperatorPrivileges { public static final Setting OPERATOR_PRIVILEGES_ENABLED = Setting.boolSetting("xpack.security.operator_privileges.enabled", false, Setting.Property.NodeScope); private final OperatorUserDescriptor operatorUserDescriptor; - private final CompositeOperatorOnly compositeOperatorOnly; + private final OperatorOnly operatorOnly; private final XPackLicenseState licenseState; private final boolean enabled; public OperatorPrivileges(Settings settings, XPackLicenseState licenseState, - OperatorUserDescriptor operatorUserDescriptor, CompositeOperatorOnly compositeOperatorOnly) { + OperatorUserDescriptor operatorUserDescriptor, OperatorOnly operatorOnly) { this.operatorUserDescriptor = operatorUserDescriptor; - this.compositeOperatorOnly = compositeOperatorOnly; + this.operatorOnly = operatorOnly; this.licenseState = licenseState; this.enabled = OPERATOR_PRIVILEGES_ENABLED.get(settings); } @@ -45,12 +47,13 @@ public ElasticsearchSecurityException check(String action, TransportRequest requ if (false == shouldProcess()) { return null; } - final OperatorOnly.Result operatorOnlyCheckResult = compositeOperatorOnly.check(action, request); - if (operatorOnlyCheckResult.getStatus() == OperatorOnly.Status.YES) { - if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( - threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { + if (shouldProcess() && false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( + threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { + // Only check whether request is operator only if user is not an operator + final Supplier messageSupplier = operatorOnly.check(action, request); + if (messageSupplier != null) { return new ElasticsearchSecurityException( - "Operator privileges are required for " + operatorOnlyCheckResult.getMessage()); + "Operator privileges are required for " + messageSupplier.get()); } } return null; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 93564db4ad81b..9dfb3189bb058 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -85,7 +85,7 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; @@ -261,7 +261,7 @@ public void init() throws Exception { tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new CompositeOperatorOnly()); + operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new OperatorOnly()); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), tokenService, apiKeyService, operatorPrivileges); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 4161ce30085cc..4ea17764225c9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -46,7 +46,7 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; -import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; @@ -128,7 +128,7 @@ public void setupMocks() throws Exception { securityIndex, clusterService, mock(CacheInvalidatorRegistry.class),threadPool); final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new CompositeOperatorOnly()); + operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new OperatorOnly()); authenticationService = new AuthenticationService(settings, realms, auditTrail, failureHandler, threadPool, anonymous, tokenService, apiKeyService, operatorPrivileges); authenticator = new SecondaryAuthenticator(securityContext, authenticationService); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 1b5d7280dcedf..2bf0171d60f17 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -147,7 +147,7 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import org.elasticsearch.xpack.security.operator.CompositeOperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.sql.action.SqlQueryAction; @@ -272,7 +272,7 @@ public void setup() { }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), any(ActionListener.class)); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new CompositeOperatorOnly()); + operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new OperatorOnly()); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), licenseState, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java deleted file mode 100644 index ff49722daf03a..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/CompositeOperatorOnlyTests.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.security.operator; - -import org.elasticsearch.action.main.MainAction; -import org.elasticsearch.test.ESTestCase; - -public class CompositeOperatorOnlyTests extends ESTestCase { - - public void testSimpleOperatorOnlyApi() { - final CompositeOperatorOnly compositeOperatorOnly = new CompositeOperatorOnly(); - for (final String actionName : CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS) { - final OperatorOnly.Result result = compositeOperatorOnly.check(actionName, null); - assertEquals(OperatorOnly.Status.YES, result.getStatus()); - assertNotNull(result.getMessage()); - } - } - - public void testNonOperatorOnlyApi() { - final CompositeOperatorOnly compositeOperatorOnly = new CompositeOperatorOnly(); - final OperatorOnly.Result result = compositeOperatorOnly.check(MainAction.NAME, null); - assertEquals(result, OperatorOnly.RESULT_NO); - } - -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java new file mode 100644 index 0000000000000..eb9d2d030150a --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.action.main.MainAction; +import org.elasticsearch.test.ESTestCase; + +import java.util.function.Supplier; + +public class OperatorOnlyTests extends ESTestCase { + + public void testSimpleOperatorOnlyApi() { + final OperatorOnly operatorOnly = new OperatorOnly(); + for (final String actionName : OperatorOnly.SIMPLE_ACTIONS) { + final Supplier messageSupplier = operatorOnly.check(actionName, null); + assertNotNull(messageSupplier); + assertNotNull(messageSupplier.get()); + } + } + + public void testNonOperatorOnlyApi() { + final OperatorOnly operatorOnly = new OperatorOnly(); + assertNull(operatorOnly.check(MainAction.NAME, null)); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java index 326dc2e7db306..c6b4751e5eaea 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java @@ -27,13 +27,13 @@ public class OperatorPrivilegesTests extends ESTestCase { private XPackLicenseState xPackLicenseState; private OperatorUserDescriptor operatorUserDescriptor; - private CompositeOperatorOnly compositeOperatorOnly; + private OperatorOnly operatorOnly; @Before public void init() { xPackLicenseState = mock(XPackLicenseState.class); operatorUserDescriptor = mock(OperatorUserDescriptor.class); - compositeOperatorOnly = mock(CompositeOperatorOnly.class); + operatorOnly = mock(OperatorOnly.class); } public void testWillNotProcessWhenFeatureIsDisabledOrLicenseDoesNotSupport() { @@ -43,7 +43,7 @@ public void testWillNotProcessWhenFeatureIsDisabledOrLicenseDoesNotSupport() { when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(false); final OperatorPrivileges operatorPrivileges = - new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, compositeOperatorOnly); + new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, operatorOnly); final ThreadContext threadContext = new ThreadContext(settings); operatorPrivileges.maybeMarkOperatorUser(mock(Authentication.class), threadContext); @@ -52,7 +52,7 @@ public void testWillNotProcessWhenFeatureIsDisabledOrLicenseDoesNotSupport() { final ElasticsearchSecurityException e = operatorPrivileges.check("cluster:action", mock(TransportRequest.class), threadContext); assertNull(e); - verifyZeroInteractions(compositeOperatorOnly); + verifyZeroInteractions(operatorOnly); } public void testMarkOperatorUser() { @@ -66,7 +66,7 @@ public void testMarkOperatorUser() { when(operatorUserDescriptor.isOperatorUser(nonOperatorAuth)).thenReturn(false); final OperatorPrivileges operatorPrivileges = - new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, compositeOperatorOnly); + new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, operatorOnly); ThreadContext threadContext = new ThreadContext(settings); operatorPrivileges.maybeMarkOperatorUser(operatorAuth, threadContext); @@ -87,11 +87,11 @@ public void testCheck() { final String operatorAction = "cluster:operator_only/action"; final String nonOperatorAction = "cluster:non_operator/action"; final String message = "[" + operatorAction + "]"; - when(compositeOperatorOnly.check(eq(operatorAction), any())).thenReturn(OperatorOnly.Result.yes(() -> message)); - when(compositeOperatorOnly.check(eq(nonOperatorAction), any())).thenReturn(OperatorOnly.RESULT_NO); + when(operatorOnly.check(eq(operatorAction), any())).thenReturn(() -> message); + when(operatorOnly.check(eq(nonOperatorAction), any())).thenReturn(null); final OperatorPrivileges operatorPrivileges = - new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, compositeOperatorOnly); + new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, operatorOnly); ThreadContext threadContext = new ThreadContext(settings); if (randomBoolean()) { diff --git a/x-pack/qa/operator-privileges-tests/build.gradle b/x-pack/qa/operator-privileges-tests/build.gradle index c2b57c506e72b..01dc0a8713a4d 100644 --- a/x-pack/qa/operator-privileges-tests/build.gradle +++ b/x-pack/qa/operator-privileges-tests/build.gradle @@ -7,8 +7,6 @@ esplugin { name 'op-test' description 'An test plugin for testing hard to get internals' classname 'org.elasticsearch.example.OpTestPlugin' - licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') - noticeFile rootProject.file('NOTICE.txt') } dependencies { diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index 0049302c1da14..9c0b04353d918 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -90,7 +90,7 @@ public void testAllActionsAreEitherOperatorOnlyOrNonOperator() throws IOExceptio final Map response = responseAsMap(client().performRequest(request)); List allActions = (List) response.get("actions"); allActions.remove(GetActionsAction.NAME); - allActions.removeAll(CompositeOperatorOnly.ActionOperatorOnly.SIMPLE_ACTIONS); + allActions.removeAll(OperatorOnly.SIMPLE_ACTIONS); allActions.removeAll(Constants.NON_OPERATOR_ACTIONS); assertTrue(allActions.isEmpty()); } From 9314789eac46196c338749976e6db3a7f745dd2d Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 01:00:27 +1100 Subject: [PATCH 10/23] Simplify the plugin IT test with reflection --- .../operator/OperatorPrivilegesIT.java | 3 +- .../elasticsearch/example/OpTestPlugin.java | 11 +---- .../example/actions/GetActionsAction.java | 19 -------- .../example/actions/GetActionsRequest.java | 27 ------------ .../example/actions/GetActionsResponse.java | 42 ------------------ .../example/actions/RestGetActionsAction.java | 25 ++++++++++- .../actions/TransportGetActionsAction.java | 44 ------------------- .../plugin-metadata/plugin-security.policy | 4 ++ .../example/OpTestPluginTests.java | 11 +---- 9 files changed, 30 insertions(+), 156 deletions(-) delete mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java delete mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java delete mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java delete mode 100644 x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java create mode 100644 x-pack/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index 9c0b04353d918..fdbe078a254c5 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -11,7 +11,6 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.example.actions.GetActionsAction; import org.elasticsearch.test.rest.ESRestTestCase; import java.io.IOException; @@ -89,7 +88,7 @@ public void testAllActionsAreEitherOperatorOnlyOrNonOperator() throws IOExceptio final Request request = new Request("GET", "/_test/get_actions"); final Map response = responseAsMap(client().performRequest(request)); List allActions = (List) response.get("actions"); - allActions.remove(GetActionsAction.NAME); + assertFalse(allActions.isEmpty()); allActions.removeAll(OperatorOnly.SIMPLE_ACTIONS); allActions.removeAll(Constants.NON_OPERATOR_ACTIONS); assertTrue(allActions.isEmpty()); diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java index 531e14a8aae9d..0766d4d302f2e 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java @@ -6,19 +6,15 @@ package org.elasticsearch.example; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionResponse; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; -import org.elasticsearch.example.actions.GetActionsAction; import org.elasticsearch.example.actions.RestGetActionsAction; -import org.elasticsearch.example.actions.TransportGetActionsAction; -import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; @@ -39,9 +35,4 @@ public List getRestHandlers( ) { return List.of(new RestGetActionsAction()); } - - @Override - public List> getActions() { - return List.of(new ActionHandler<>(GetActionsAction.INSTANCE, TransportGetActionsAction.class)); - } } diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java deleted file mode 100644 index 6d87cfe4a29eb..0000000000000 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsAction.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.example.actions; - -import org.elasticsearch.action.ActionType; - -public class GetActionsAction extends ActionType { - - public static final String NAME = "cluster:monitor/test/get_actions"; - public static final GetActionsAction INSTANCE = new GetActionsAction(); - - public GetActionsAction() { - super(NAME, GetActionsResponse::new); - } -} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java deleted file mode 100644 index 476a028fd08ba..0000000000000 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.example.actions; - -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.common.io.stream.StreamInput; - -import java.io.IOException; - -public class GetActionsRequest extends ActionRequest { - - public GetActionsRequest() {} - - public GetActionsRequest(StreamInput in) throws IOException { - super(in); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } -} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java deleted file mode 100644 index 2eb626d046ef6..0000000000000 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/GetActionsResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.example.actions; - -import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; - -public class GetActionsResponse extends ActionResponse implements ToXContentObject { - - private final List actions; - - public GetActionsResponse(List actions) { - this.actions = List.copyOf(Objects.requireNonNull(actions)); - } - - public GetActionsResponse(StreamInput in) throws IOException { - super(in); - actions = in.readStringList(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeStringCollection(actions); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject().field("actions", actions); - return builder.endObject(); - } -} diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java index 1279a885a3ac2..d4ac0a4164ba2 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java @@ -6,13 +6,21 @@ package org.elasticsearch.example.actions; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; import java.io.IOException; +import java.lang.reflect.Field; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static org.elasticsearch.rest.RestRequest.Method.GET; @@ -27,9 +35,22 @@ public String getName() { return "test_get_actions"; } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - // It is also possible to use reflection to get NodeClient#actions and save all the transport related classes - return channel -> client.execute(GetActionsAction.INSTANCE, new GetActionsRequest(), new RestToXContentListener<>(channel)); + final Map actions = + AccessController.doPrivileged((PrivilegedAction>) () -> { + try { + final Field actionsField = client.getClass().getDeclaredField("actions"); + actionsField.setAccessible(true); + return (Map) actionsField.get(client); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new ElasticsearchException(e); + } + }); + + final List actionNames = actions.keySet().stream().map(ActionType::name).collect(Collectors.toList()); + return channel -> new RestToXContentListener<>(channel).onResponse( + (builder, params) -> builder.startObject().field("actions", actionNames).endObject()); } } diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java deleted file mode 100644 index 4af74cb22e3c2..0000000000000 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/TransportGetActionsAction.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.example.actions; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.action.support.TransportAction; -import org.elasticsearch.common.inject.Binding; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.inject.Injector; -import org.elasticsearch.common.inject.TypeLiteral; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.transport.TransportService; - -import java.util.ArrayList; -import java.util.List; - -public class TransportGetActionsAction extends HandledTransportAction { - - private final Injector injector; - - @Inject - public TransportGetActionsAction(TransportService transportService, ActionFilters actionFilters, Injector injector) { - super(GetActionsAction.NAME, transportService, actionFilters, GetActionsRequest::new); - this.injector = injector; - } - - @SuppressWarnings("rawtypes") - @Override - protected void doExecute(Task task, GetActionsRequest request, ActionListener listener) { - final List> bindings = injector.findBindingsByType(TypeLiteral.get(TransportAction.class)); - - final List allActionNames = new ArrayList<>(bindings.size()); - for (final Binding binding : bindings) { - allActionNames.add(binding.getProvider().get().actionName); - } - listener.onResponse(new GetActionsResponse(allActionNames)); - } -} diff --git a/x-pack/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy b/x-pack/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000000000..eb1558fb8e381 --- /dev/null +++ b/x-pack/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,4 @@ +grant { + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; +}; diff --git a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java index 10e24d1ac1982..5b9c33f54dd24 100644 --- a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java +++ b/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java @@ -6,22 +6,13 @@ package org.elasticsearch.example; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.example.actions.GetActionsAction; -import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.test.ESTestCase; -import java.util.List; - // This test class is really to pass the testingConventions test public class OpTestPluginTests extends ESTestCase { - public void testActionWillBeProvided() { + public void testPluginWillInstantiate() { final OpTestPlugin opTestPlugin = new OpTestPlugin(); - final List> actions = opTestPlugin.getActions(); - assertEquals(1, actions.size()); - assertSame(GetActionsAction.INSTANCE, actions.get(0).getAction()); } } From 5dedd54c3ae9e696ef2d12af95afdb1267792536 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 08:41:44 +1100 Subject: [PATCH 11/23] spotless --- .../operator/OperatorUserDescriptor.java | 6 ++--- .../example/actions/RestGetActionsAction.java | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java index b9443b50207e2..1ca314980e095 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java @@ -124,9 +124,9 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Group group = (Group) o; - return usernames.equals(group.usernames) - && Objects.equals(realmName, group.realmName) - && realmType.equals(group.realmType) + return usernames.equals(group.usernames) + && Objects.equals(realmName, group.realmName) + && realmType.equals(group.realmType) && authenticationType == group.authenticationType; } diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java index d4ac0a4164ba2..2b913dbd5a026 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java @@ -38,19 +38,21 @@ public String getName() { @SuppressWarnings({ "rawtypes", "unchecked" }) @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - final Map actions = - AccessController.doPrivileged((PrivilegedAction>) () -> { - try { - final Field actionsField = client.getClass().getDeclaredField("actions"); - actionsField.setAccessible(true); - return (Map) actionsField.get(client); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new ElasticsearchException(e); + final Map actions = AccessController.doPrivileged( + (PrivilegedAction>) () -> { + try { + final Field actionsField = client.getClass().getDeclaredField("actions"); + actionsField.setAccessible(true); + return (Map) actionsField.get(client); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new ElasticsearchException(e); + } } - }); + ); final List actionNames = actions.keySet().stream().map(ActionType::name).collect(Collectors.toList()); return channel -> new RestToXContentListener<>(channel).onResponse( - (builder, params) -> builder.startObject().field("actions", actionNames).endObject()); + (builder, params) -> builder.startObject().field("actions", actionNames).endObject() + ); } } From 901ecd2d907653c956bf0718719908b579dd1602 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 14:57:27 +1100 Subject: [PATCH 12/23] Refactor as suggested --- .../OperatorPrivilegesSingleNodeTests.java | 23 +- .../xpack/security/Security.java | 21 +- .../security/authc/AuthenticationService.java | 10 +- .../security/authz/AuthorizationService.java | 11 +- .../operator/FileOperatorUsersStore.java | 284 +++++++++++++++++ ...torOnly.java => OperatorOnlyRegistry.java} | 2 +- .../security/operator/OperatorPrivileges.java | 89 ++++-- .../operator/OperatorUserDescriptor.java | 276 ----------------- .../authc/AuthenticationServiceTests.java | 27 +- .../support/SecondaryAuthenticatorTests.java | 6 +- .../authz/AuthorizationServiceTests.java | 12 +- .../operator/FileOperatorUsersStoreTests.java | 287 ++++++++++++++++++ ...ts.java => OperatorOnlyRegistryTests.java} | 12 +- .../operator/OperatorPrivilegesTests.java | 49 +-- .../operator/OperatorUserDescriptorTests.java | 144 --------- .../security/operator/operator_users.yml | 1 + .../operator/OperatorPrivilegesIT.java | 2 +- .../example/actions/RestGetActionsAction.java | 2 + 18 files changed, 726 insertions(+), 532 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/{OperatorOnly.java => OperatorOnlyRegistry.java} (99%) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStoreTests.java rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/{OperatorOnlyTests.java => OperatorOnlyRegistryTests.java} (62%) delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java index cebf105b9c8c4..5740fc7251377 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.xpack.core.security.action.user.GetUsersAction; import org.elasticsearch.xpack.core.security.action.user.GetUsersRequest; +import org.junit.BeforeClass; import java.util.Map; @@ -27,6 +28,13 @@ public class OperatorPrivilegesSingleNodeTests extends SecuritySingleNodeTestCas private static final String OPERATOR_USER_NAME = "test_operator"; + private static boolean OPERATOR_PRIVILEGES_ENABLED; + + @BeforeClass + public static void randomOperatorPrivilegesEnabled() { + OPERATOR_PRIVILEGES_ENABLED = randomBoolean(); + } + @Override protected String configUsers() { return super.configUsers() @@ -59,15 +67,20 @@ protected String configOperatorUsers() { protected Settings nodeSettings() { Settings.Builder builder = Settings.builder().put(super.nodeSettings()); // Ensure the new settings can be configured - builder.put("xpack.security.operator_privileges.enabled", "true"); + builder.put("xpack.security.operator_privileges.enabled", OPERATOR_PRIVILEGES_ENABLED); return builder.build(); } - public void testSuperuserWillFailToCallOperatorOnlyAction() { + public void testOutcomeOfSuperuserPerformingOperatorOnlyActionWillDependOnWhetherFeatureIsEnabled() { + final Client client = client(); final ClearVotingConfigExclusionsRequest clearVotingConfigExclusionsRequest = new ClearVotingConfigExclusionsRequest(); - final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> client().execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet()); - assertThat(e.getCause().getMessage(), containsString("Operator privileges are required for action")); + if (OPERATOR_PRIVILEGES_ENABLED) { + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet()); + assertThat(e.getCause().getMessage(), containsString("Operator privileges are required for action")); + } else { + client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet(); + } } public void testOperatorUserWillSucceedToCallOperatorOnlyAction() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 1ed1a4b5d6041..d496756e19ee4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -214,9 +214,10 @@ import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor; -import org.elasticsearch.xpack.security.operator.OperatorOnly; +import org.elasticsearch.xpack.security.operator.OperatorOnlyRegistry; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; -import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; +import org.elasticsearch.xpack.security.operator.FileOperatorUsersStore; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestClearApiKeyCacheAction; @@ -293,6 +294,7 @@ import static org.elasticsearch.xpack.core.XPackSettings.API_KEY_SERVICE_ENABLED_SETTING; import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; +import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_MAIN_TEMPLATE_7; public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin, NetworkPlugin, ClusterPlugin, @@ -476,10 +478,15 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste getLicenseState().addListener(new SecurityStatusChangeListener(getLicenseState())); final AuthenticationFailureHandler failureHandler = createAuthenticationFailureHandler(realms, extensionComponents); - final OperatorPrivileges operatorPrivileges = new OperatorPrivileges(settings, getLicenseState(), - new OperatorUserDescriptor(environment, resourceWatcherService), new OperatorOnly()); + final OperatorPrivilegesService operatorPrivilegesService; + if (OPERATOR_PRIVILEGES_ENABLED.get(settings)) { + operatorPrivilegesService = new OperatorPrivileges.DefaultOperatorPrivilegesService(getLicenseState(), + new FileOperatorUsersStore(environment, resourceWatcherService), new OperatorOnlyRegistry()); + } else { + operatorPrivilegesService = OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE; + } authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool, - anonymousUser, tokenService, apiKeyService, operatorPrivileges)); + anonymousUser, tokenService, apiKeyService, operatorPrivilegesService)); components.add(authcService.get()); securityIndex.get().addIndexStateListener(authcService.get()::onSecurityIndexStateChange); @@ -497,7 +504,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine(), requestInterceptors, - getLicenseState(), expressionResolver, operatorPrivileges); + getLicenseState(), expressionResolver, operatorPrivilegesService); components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions @@ -678,7 +685,7 @@ public static List> getSettings(List securityExten settingsList.add(ApiKeyService.DOC_CACHE_TTL_SETTING); settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING); - settingsList.add(OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED); + settingsList.add(OPERATOR_PRIVILEGES_ENABLED); // hide settings settingsList.add(Setting.listSetting(SecurityField.setting("hide_settings"), Collections.emptyList(), Function.identity(), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 883943c4f4ff4..048d5b869d821 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -46,7 +46,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; -import org.elasticsearch.xpack.security.operator.OperatorPrivileges; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; @@ -87,7 +87,7 @@ public class AuthenticationService { private final Cache lastSuccessfulAuthCache; private final AtomicLong numInvalidation = new AtomicLong(); private final ApiKeyService apiKeyService; - private final OperatorPrivileges operatorPrivileges; + private final OperatorPrivilegesService operatorPrivilegesService; private final boolean runAsEnabled; private final boolean isAnonymousUserEnabled; private final AuthenticationContextSerializer authenticationSerializer; @@ -95,7 +95,7 @@ public class AuthenticationService { public AuthenticationService(Settings settings, Realms realms, AuditTrailService auditTrailService, AuthenticationFailureHandler failureHandler, ThreadPool threadPool, AnonymousUser anonymousUser, TokenService tokenService, ApiKeyService apiKeyService, - OperatorPrivileges operatorPrivileges) { + OperatorPrivilegesService operatorPrivilegesService) { this.nodeName = Node.NODE_NAME_SETTING.get(settings); this.realms = realms; this.auditTrailService = auditTrailService; @@ -114,7 +114,7 @@ public AuthenticationService(Settings settings, Realms realms, AuditTrailService this.lastSuccessfulAuthCache = null; } this.apiKeyService = apiKeyService; - this.operatorPrivileges = operatorPrivileges; + this.operatorPrivilegesService = operatorPrivilegesService; this.authenticationSerializer = new AuthenticationContextSerializer(); } @@ -693,7 +693,7 @@ void writeAuthToContext(Authentication authentication) { try { authenticationSerializer.writeToContext(authentication, threadContext); request.authenticationSuccess(authentication); - operatorPrivileges.maybeMarkOperatorUser(authentication, threadContext); + operatorPrivilegesService.maybeMarkOperatorUser(authentication, threadContext); } catch (Exception e) { action = () -> { logger.debug( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 9651d49aa047b..1830c4279b14c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -73,7 +73,7 @@ import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; -import org.elasticsearch.xpack.security.operator.OperatorPrivileges; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import java.util.ArrayList; import java.util.Collection; @@ -119,7 +119,7 @@ public class AuthorizationService { private final AuthorizationEngine authorizationEngine; private final Set requestInterceptors; private final XPackLicenseState licenseState; - private final OperatorPrivileges operatorPrivileges; + private final OperatorPrivilegesService operatorPrivilegesService; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; @@ -127,7 +127,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C AuditTrailService auditTrailService, AuthenticationFailureHandler authcFailureHandler, ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine, Set requestInterceptors, XPackLicenseState licenseState, - IndexNameExpressionResolver resolver, OperatorPrivileges operatorPrivileges) { + IndexNameExpressionResolver resolver, OperatorPrivilegesService operatorPrivilegesService) { this.clusterService = clusterService; this.auditTrailService = auditTrailService; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver); @@ -141,7 +141,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.requestInterceptors = requestInterceptors; this.settings = settings; this.licenseState = licenseState; - this.operatorPrivileges = operatorPrivileges; + this.operatorPrivilegesService = operatorPrivilegesService; } public void checkPrivileges(Authentication authentication, HasPrivilegesRequest request, @@ -188,7 +188,8 @@ public void authorize(final Authentication authentication, final String action, putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action); // Check operator privileges first if applicable - final ElasticsearchSecurityException operatorException = operatorPrivileges.check(action, originalRequest, threadContext); + final ElasticsearchSecurityException operatorException + = operatorPrivilegesService.check(action, originalRequest, threadContext); if (operatorException != null) { // TODO: audit listener.onFailure(denialException(authentication, action, operatorException)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java new file mode 100644 index 0000000000000..0d9311e6ff047 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.env.Environment; +import org.elasticsearch.watcher.FileChangesListener; +import org.elasticsearch.watcher.FileWatcher; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; +import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; + +public class FileOperatorUsersStore { + private static final Logger logger = LogManager.getLogger(FileOperatorUsersStore.class); + + private final Path file; + private volatile OperatorUsersDescriptor operatorUsersDescriptor; + + public FileOperatorUsersStore(Environment env, ResourceWatcherService watcherService) { + this.file = XPackPlugin.resolveConfigFile(env, "operator_users.yml"); + this.operatorUsersDescriptor = parseFile(this.file, logger); + FileWatcher watcher = new FileWatcher(file.getParent()); + watcher.addListener(new FileOperatorUsersStore.FileListener()); + try { + watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); + } catch (IOException e) { + throw new ElasticsearchException("Failed to start watching the operator users file [" + file.toAbsolutePath() + "]", e); + } + } + + public boolean isOperatorUser(Authentication authentication) { + if (authentication.getUser().isRunAs()) { + return false; + } else if (User.isInternal(authentication.getUser())) { + // Internal user are considered operator users + return true; + } + + // Other than realm name, other criteria must always be an exact match for the user to be an operator. + // Realm name of a descriptor can be null. When it is null, it is ignored for comparison. + // If not null, it will be compared exactly as well. + // The special handling for realm name is because there can only be one file or native realm and it does + // not matter what the name is. + return operatorUsersDescriptor.groups.stream().anyMatch(group -> { + final Authentication.RealmRef realm = authentication.getSourceRealm(); + return group.usernames.contains(authentication.getUser().principal()) + && group.authenticationType == authentication.getAuthenticationType() + && realm.getType().equals(group.realmType) + && (group.realmName == null || group.realmName.equals(realm.getName())); + }); + } + + // Package private for tests + public OperatorUsersDescriptor getOperatorUsersDescriptor() { + return operatorUsersDescriptor; + } + + static final class OperatorUsersDescriptor { + private final List groups; + + private OperatorUsersDescriptor(List groups) { + this.groups = groups; + } + + // Package private for tests + List getGroups() { + return groups; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + OperatorUsersDescriptor that = (OperatorUsersDescriptor) o; + return groups.equals(that.groups); + } + + @Override + public int hashCode() { + return Objects.hash(groups); + } + } + + private static final OperatorUsersDescriptor EMPTY_OPERATOR_USERS_DESCRIPTOR = new OperatorUsersDescriptor(List.of()); + + static final class Group { + private static final Set SINGLETON_REALM_TYPES = Set.of( + FileRealmSettings.TYPE, NativeRealmSettings.TYPE, ReservedRealm.TYPE); + + private final Set usernames; + private final String realmName; + private final String realmType; + private final Authentication.AuthenticationType authenticationType; + + Group(Set usernames) { + this(usernames, null); + } + + Group(Set usernames, @Nullable String realmName) { + this(usernames, realmName, null, null); + } + + Group(Set usernames, @Nullable String realmName, @Nullable String realmType, + @Nullable String authenticationType) { + this.usernames = usernames; + this.realmName = realmName; + this.realmType = realmType == null ? FileRealmSettings.TYPE : realmType; + this.authenticationType = authenticationType == null ? Authentication.AuthenticationType.REALM : + Authentication.AuthenticationType.valueOf(authenticationType.toUpperCase(Locale.ROOT)); + validate(); + } + + private void validate() { + final ValidationException validationException = new ValidationException(); + if (false == FileRealmSettings.TYPE.equals(realmType)) { + validationException.addValidationError("[realm_type] only supports [file]"); + } + if (Authentication.AuthenticationType.REALM != authenticationType) { + validationException.addValidationError("[auth_type] only supports [realm]"); + } + if (realmName == null) { + if (false == SINGLETON_REALM_TYPES.contains(realmType)) { + validationException.addValidationError( + "[realm_name] must be specified for realm types other than [reserved], [file] and [native]"); + } + } + if (false == validationException.validationErrors().isEmpty()) { + throw validationException; + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Group["); + sb.append("usernames=").append(usernames); + if (realmName != null) { + sb.append(", realm_name=").append(realmName); + } + if (realmType != null) { + sb.append(", realm_type=").append(realmType); + } + if (authenticationType != null) { + sb.append(", auth_type=").append(authenticationType.name().toLowerCase(Locale.ROOT)); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Group group = (Group) o; + return usernames.equals(group.usernames) + && Objects.equals(realmName, group.realmName) + && realmType.equals(group.realmType) + && authenticationType == group.authenticationType; + } + + @Override + public int hashCode() { + return Objects.hash(usernames, realmName, realmType, authenticationType); + } + } + + public static OperatorUsersDescriptor parseFile(Path file, Logger logger) { + if (false == Files.exists(file)) { + logger.warn("Operator privileges [{}] is enabled, but operator user file does not exist. " + + "No user will be able to perform operator-only actions.", OPERATOR_PRIVILEGES_ENABLED.getKey()); + return EMPTY_OPERATOR_USERS_DESCRIPTOR; + } else { + logger.debug("Reading operator users file [{}]", file.toAbsolutePath()); + try (InputStream in = Files.newInputStream(file, StandardOpenOption.READ)) { + return parseConfig(in); + } catch (IOException | RuntimeException e) { + logger.error("Failed to parse operator users file [" + file + "].", e); + throw new ElasticsearchParseException("Error parsing operator users file [{}]", e, file.toAbsolutePath()); + } + } + } + + public static OperatorUsersDescriptor parseConfig(InputStream in) throws IOException { + try (XContentParser parser = yamlParser(in)) { + return OPERATOR_USER_PARSER.parse(parser, null); + } + } + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser GROUP_PARSER = new ConstructingObjectParser<>( + "operator_privileges.operator.group", false, + (Object[] arr) -> new Group( + Set.copyOf((List)arr[0]), + (String) arr[1], + (String) arr[2], + (String) arr[3] + ) + ); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser OPERATOR_USER_PARSER = new ConstructingObjectParser<>( + "operator_privileges.operator", false, + (Object[] arr) -> new OperatorUsersDescriptor((List) arr[0]) + ); + + static { + GROUP_PARSER.declareStringArray(constructorArg(), Fields.USERNAMES); + GROUP_PARSER.declareString(optionalConstructorArg(), Fields.REALM_NAME); + GROUP_PARSER.declareString(optionalConstructorArg(), Fields.REALM_TYPE); + GROUP_PARSER.declareString(optionalConstructorArg(), Fields.AUTH_TYPE); + OPERATOR_USER_PARSER.declareObjectArray(constructorArg(), (parser, ignore) -> GROUP_PARSER.parse(parser, null), Fields.OPERATOR); + } + + private static XContentParser yamlParser(InputStream in) throws IOException { + return XContentType.YAML.xContent().createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, in); + } + + public interface Fields { + ParseField OPERATOR = new ParseField("operator"); + ParseField USERNAMES = new ParseField("usernames"); + ParseField REALM_NAME = new ParseField("realm_name"); + ParseField REALM_TYPE = new ParseField("realm_type"); + ParseField AUTH_TYPE = new ParseField("auth_type"); + } + + private class FileListener implements FileChangesListener { + @Override + public void onFileCreated(Path file) { + onFileChanged(file); + } + + @Override + public void onFileDeleted(Path file) { + onFileChanged(file); + } + + @Override + public void onFileChanged(Path file) { + if (file.equals(FileOperatorUsersStore.this.file)) { + final OperatorUsersDescriptor newDescriptor = parseFile(file, logger); + if (operatorUsersDescriptor.equals(newDescriptor) == false) { + logger.info("operator users file [{}] changed. updating operator users...", file.toAbsolutePath()); + operatorUsersDescriptor = newDescriptor; + } + } + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java similarity index 99% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java index c884f09dfcbdf..58e572cdc037b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnly.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java @@ -27,7 +27,7 @@ import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_HTTP_SETTING; import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_SETTING; -public class OperatorOnly { +public class OperatorOnlyRegistry { public static final Set SIMPLE_ACTIONS = Set.of(AddVotingConfigExclusionsAction.NAME, ClearVotingConfigExclusionsAction.NAME, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java index 23bd0dfd2019f..357f88a56a170 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -8,7 +8,6 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.transport.TransportRequest; @@ -22,44 +21,72 @@ public class OperatorPrivileges { public static final Setting OPERATOR_PRIVILEGES_ENABLED = Setting.boolSetting("xpack.security.operator_privileges.enabled", false, Setting.Property.NodeScope); - private final OperatorUserDescriptor operatorUserDescriptor; - private final OperatorOnly operatorOnly; - private final XPackLicenseState licenseState; - private final boolean enabled; + public interface OperatorPrivilegesService { + /** + * Set a ThreadContext Header {@link AuthenticationField#PRIVILEGE_CATEGORY_KEY} if authentication + * is an operator user. + */ + void maybeMarkOperatorUser(Authentication authentication, ThreadContext threadContext); - public OperatorPrivileges(Settings settings, XPackLicenseState licenseState, - OperatorUserDescriptor operatorUserDescriptor, OperatorOnly operatorOnly) { - this.operatorUserDescriptor = operatorUserDescriptor; - this.operatorOnly = operatorOnly; - this.licenseState = licenseState; - this.enabled = OPERATOR_PRIVILEGES_ENABLED.get(settings); + /** + * Check whether the user is an operator and whether the request is an operator-only. + * @return An exception if user is an non-operator and the request is operotor-only. Otherwise return null. + */ + ElasticsearchSecurityException check(String action, TransportRequest request, ThreadContext threadContext); } - public void maybeMarkOperatorUser(Authentication authentication, ThreadContext threadContext) { - if (shouldProcess() && operatorUserDescriptor.isOperatorUser(authentication)) { - threadContext.putHeader( - AuthenticationField.PRIVILEGE_CATEGORY_KEY, - AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR); + public static final class DefaultOperatorPrivilegesService implements OperatorPrivilegesService { + + private final FileOperatorUsersStore fileOperatorUsersStore; + private final OperatorOnlyRegistry operatorOnlyRegistry; + private final XPackLicenseState licenseState; + + public DefaultOperatorPrivilegesService( + XPackLicenseState licenseState, + FileOperatorUsersStore fileOperatorUsersStore, + OperatorOnlyRegistry operatorOnlyRegistry) { + this.fileOperatorUsersStore = fileOperatorUsersStore; + this.operatorOnlyRegistry = operatorOnlyRegistry; + this.licenseState = licenseState; } - } - public ElasticsearchSecurityException check(String action, TransportRequest request, ThreadContext threadContext) { - if (false == shouldProcess()) { - return null; + public void maybeMarkOperatorUser(Authentication authentication, ThreadContext threadContext) { + if (false == shouldProcess()) { + return; + } + if (fileOperatorUsersStore.isOperatorUser(authentication)) { + threadContext.putHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY, AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR); + } } - if (shouldProcess() && false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( - threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { - // Only check whether request is operator only if user is not an operator - final Supplier messageSupplier = operatorOnly.check(action, request); - if (messageSupplier != null) { - return new ElasticsearchSecurityException( - "Operator privileges are required for " + messageSupplier.get()); + + public ElasticsearchSecurityException check(String action, TransportRequest request, ThreadContext threadContext) { + if (false == shouldProcess()) { + return null; + } + if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( + threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { + // Only check whether request is operator only if user is not an operator + final Supplier messageSupplier = operatorOnlyRegistry.check(action, request); + if (messageSupplier != null) { + return new ElasticsearchSecurityException("Operator privileges are required for " + messageSupplier.get()); + } } + return null; } - return null; - } - private boolean shouldProcess() { - return enabled && licenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES); + private boolean shouldProcess() { + return licenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES); + } } + + public static final OperatorPrivilegesService NOOP_OPERATOR_PRIVILEGES_SERVICE = new OperatorPrivilegesService() { + @Override + public void maybeMarkOperatorUser(Authentication authentication, ThreadContext threadContext) { + } + + @Override + public ElasticsearchSecurityException check(String action, TransportRequest request, ThreadContext threadContext) { + return null; + } + }; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java deleted file mode 100644 index 1ca314980e095..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptor.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.security.operator; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.xcontent.DeprecationHandler; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentParserUtils; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.env.Environment; -import org.elasticsearch.watcher.FileChangesListener; -import org.elasticsearch.watcher.FileWatcher; -import org.elasticsearch.watcher.ResourceWatcherService; -import org.elasticsearch.xpack.core.XPackPlugin; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; -import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.Set; - -public class OperatorUserDescriptor { - private static final Logger logger = LogManager.getLogger(OperatorUserDescriptor.class); - - private final Path file; - private volatile List groups; - - public OperatorUserDescriptor(Environment env, ResourceWatcherService watcherService) { - this.file = XPackPlugin.resolveConfigFile(env, "operator_users.yml"); - this.groups = parseFileLenient(file, logger); - FileWatcher watcher = new FileWatcher(file.getParent()); - watcher.addListener(new OperatorUserDescriptor.FileListener()); - try { - watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); - } catch (IOException e) { - throw new ElasticsearchException("failed to start watching the operator users file [" + file.toAbsolutePath() + "]", e); - } - } - - public boolean isOperatorUser(Authentication authentication) { - if (authentication.getUser().isRunAs()) { - return false; - } else if (User.isInternal(authentication.getUser())) { - // Internal user are considered operator users - return true; - } - - return groups.stream().anyMatch(group -> { - final Authentication.RealmRef realm = authentication.getSourceRealm(); - return group.usernames.contains(authentication.getUser().principal()) - && group.authenticationType == authentication.getAuthenticationType() - && realm.getType().equals(group.realmType) - && (realm.getType().equals(FileRealmSettings.TYPE) || realm.getName().equals(group.realmName)); - }); - } - - // Package private for tests - List getGroups() { - return groups; - } - - public static final class Group { - private final Set usernames; - private final String realmName; - private final String realmType; - private final Authentication.AuthenticationType authenticationType; - - public Group(Set usernames) { - this(usernames, null, FileRealmSettings.TYPE, Authentication.AuthenticationType.REALM); - } - - public Group(Set usernames, String realmName) { - this(usernames, realmName, FileRealmSettings.TYPE, Authentication.AuthenticationType.REALM); - } - - public Group( - Set usernames, String realmName, String realmType, Authentication.AuthenticationType authenticationType) { - this.usernames = usernames; - this.realmName = realmName; - this.realmType = realmType; - this.authenticationType = authenticationType; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Group["); - sb.append("usernames=").append(usernames); - if (realmName != null) { - sb.append(", realm_name=").append(realmName); - } - if (realmType != null) { - sb.append(", realm_type=").append(realmType); - } - if (authenticationType != null) { - sb.append(", auth_type=").append(authenticationType.name().toLowerCase(Locale.ROOT)); - } - sb.append("]"); - return sb.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Group group = (Group) o; - return usernames.equals(group.usernames) - && Objects.equals(realmName, group.realmName) - && realmType.equals(group.realmType) - && authenticationType == group.authenticationType; - } - - @Override - public int hashCode() { - return Objects.hash(usernames, realmName, realmType, authenticationType); - } - } - - public static List parseFileLenient(Path path, Logger logger) { - if (false == Files.exists(path)) { - logger.debug("Skip reading operator user file since it does not exist"); - return List.of(); - } - logger.debug("Reading operator users file [{}]", path.toAbsolutePath()); - try { - return parseFile(path); - } catch (IOException | RuntimeException e) { - logger.error("Failed to parse operator_users file [" + path + "].", e); - return List.of(); - } - } - - public static List parseFile(Path path) throws IOException { - try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { - return parseConfig(in); - } - } - - public static List parseConfig(InputStream in) throws IOException { - final List groups = new ArrayList<>(); - try (XContentParser parser = yamlParser(in)) { - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - while (parser.nextToken() != XContentParser.Token.END_OBJECT) { - XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); - final String categoryName = parser.currentName(); - if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals(categoryName)) { - throw new IllegalArgumentException("Operator user config file must begin with operator, got [" + categoryName + "]"); - } - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser); - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - groups.add(parseOneGroup(parser)); - } - } - } - return List.copyOf(groups); - } - - private static Group parseOneGroup(XContentParser parser) throws IOException { - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); - String[] usernames = null; - String realmName = null; - String realmType = FileRealmSettings.TYPE; - Authentication.AuthenticationType authenticationType = Authentication.AuthenticationType.REALM; - String currentFieldName = null; - XContentParser.Token token; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (Fields.USERNAMES.match(currentFieldName, parser.getDeprecationHandler())) { - usernames = XContentUtils.readStringArray(parser, false); - } else if (Fields.REALM_NAME.match(currentFieldName, parser.getDeprecationHandler())) { - if (token == XContentParser.Token.VALUE_STRING) { - realmName = parser.text(); - } else { - throw mismatchedFieldException("realm_name", currentFieldName, "string", token); - } - } else if (Fields.REALM_TYPE.match(currentFieldName, parser.getDeprecationHandler())) { - if (token == XContentParser.Token.VALUE_STRING) { - realmType = parser.text(); - } else { - throw mismatchedFieldException("realm type", currentFieldName, "string", token); - } - } else if (Fields.AUTH_TYPE.match(currentFieldName, parser.getDeprecationHandler())) { - if (token == XContentParser.Token.VALUE_STRING) { - authenticationType = Authentication.AuthenticationType.valueOf(parser.text().toUpperCase(Locale.ROOT)); - } else { - throw mismatchedFieldException("authentication type", currentFieldName, "string", token); - } - } else { - throw unexpectedFieldException("user", currentFieldName); - } - } - if (usernames == null) { - throw missingRequiredFieldException("usernames", Fields.USERNAMES.getPreferredName()); - } - return new Group(Set.of(usernames), realmName, realmType, authenticationType); - } - - private static ElasticsearchParseException mismatchedFieldException(String entityName, - String fieldName, String expectedType, - XContentParser.Token token) { - return new ElasticsearchParseException( - "failed to parse {}. " + - "expected field [{}] value to be {}, but found an element of type [{}]", - entityName, fieldName, expectedType, token); - } - - private static ElasticsearchParseException missingRequiredFieldException(String entityName, - String fieldName) { - return new ElasticsearchParseException( - "failed to parse {}. missing required [{}] field", - entityName, fieldName); - } - - private static ElasticsearchParseException unexpectedFieldException(String entityName, - String fieldName) { - return new ElasticsearchParseException( - "failed to parse {}. unexpected field [{}]", - entityName, fieldName); - } - - private static XContentParser yamlParser(InputStream in) throws IOException { - return XContentType.YAML.xContent().createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, in); - } - - public interface Fields { - ParseField USERNAMES = new ParseField("usernames"); - ParseField REALM_NAME = new ParseField("realm_name"); - ParseField REALM_TYPE = new ParseField("realm_type"); - ParseField AUTH_TYPE = new ParseField("auth_type"); - } - - private class FileListener implements FileChangesListener { - @Override - public void onFileCreated(Path file) { - onFileChanged(file); - } - - @Override - public void onFileDeleted(Path file) { - onFileChanged(file); - } - - @Override - public void onFileChanged(Path file) { - if (file.equals(OperatorUserDescriptor.this.file)) { - List newGroups = parseFileLenient(file, logger); - - if (groups.equals(newGroups) == false) { - logger.info("operator users file [{}] changed. updating operator users...", file.toAbsolutePath()); - groups = newGroups; - } - } - } - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 9dfb3189bb058..996a53e46b39a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -85,9 +85,7 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; -import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; @@ -260,11 +258,10 @@ public void init() throws Exception { mock(CacheInvalidatorRegistry.class), threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); - final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new OperatorOnly()); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, new AnonymousUser(settings), tokenService, apiKeyService, operatorPrivileges); + threadPool, new AnonymousUser(settings), tokenService, apiKeyService, + OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } @After @@ -469,7 +466,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { .build(); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); User user = new User("_username", "r1"); when(firstRealm.supports(token)).thenReturn(true); mockAuthenticate(firstRealm, token, null); @@ -739,7 +736,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { ThreadContext threadContext1 = threadPool1.getThreadContext(); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool1, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); threadContext1.putTransient(AuthenticationField.AUTHENTICATION_KEY, authRef.get()); threadContext1.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); @@ -763,7 +760,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { try (ThreadContext.StoredContext ignore = threadContext2.stashContext()) { service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); threadContext2.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); BytesStreamOutput output = new BytesStreamOutput(); @@ -777,7 +774,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { threadPool2.getThreadContext().putHeader(AuthenticationField.AUTHENTICATION_KEY, header); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); service.authenticate("_action", new InternalRequest(), SystemUser.INSTANCE, ActionListener.wrap(result -> { assertThat(result, notNullValue()); assertThat(result.getUser(), equalTo(user1)); @@ -816,7 +813,7 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); @@ -840,7 +837,7 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); doAnswer(invocationOnMock -> { final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0]; final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; @@ -871,7 +868,7 @@ public void testAnonymousUserRest() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); + threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); RestRequest request = new FakeRestRequest(); Authentication result = authenticateBlocking(request); @@ -897,7 +894,7 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); + threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); RestRequest request = new FakeRestRequest(); PlainActionFuture future = new PlainActionFuture<>(); @@ -920,7 +917,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); + threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); InternalRequest message = new InternalRequest(); Authentication result = authenticateBlocking("_action", message, null); @@ -937,7 +934,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivileges); + threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); InternalRequest message = new InternalRequest(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 4ea17764225c9..8f1425c4b9782 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -46,9 +46,7 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; -import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; -import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.test.SecurityMocks; @@ -127,10 +125,8 @@ public void setupMocks() throws Exception { final ApiKeyService apiKeyService = new ApiKeyService(settings, clock, client, licenseState, securityIndex, clusterService, mock(CacheInvalidatorRegistry.class),threadPool); - final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new OperatorOnly()); authenticationService = new AuthenticationService(settings, realms, auditTrail, failureHandler, threadPool, anonymous, - tokenService, apiKeyService, operatorPrivileges); + tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); authenticator = new SecondaryAuthenticator(securityContext, authenticationService); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 2bf0171d60f17..4e45ecefe62aa 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -147,9 +147,7 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import org.elasticsearch.xpack.security.operator.OperatorOnly; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; -import org.elasticsearch.xpack.security.operator.OperatorUserDescriptor; import org.elasticsearch.xpack.sql.action.SqlQueryAction; import org.elasticsearch.xpack.sql.action.SqlQueryRequest; import org.junit.Before; @@ -271,12 +269,10 @@ public void setup() { return Void.TYPE; }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), any(ActionListener.class)); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); - final OperatorUserDescriptor operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorPrivileges = new OperatorPrivileges(settings, licenseState, operatorUserDescriptor, new OperatorOnly()); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), licenseState, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - operatorPrivileges); + OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -921,7 +917,7 @@ public void testDenialForAnonymousUser() throws IOException { authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet(), new XPackLicenseState(settings, () -> 0), - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivileges); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -950,7 +946,7 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() throws IO authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), new XPackLicenseState(settings, () -> 0), - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivileges); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1692,7 +1688,7 @@ public void getUserPrivileges(Authentication authentication, AuthorizationInfo a authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), engine, Collections.emptySet(), licenseState, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivileges); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); Authentication authentication; try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { authentication = createAuthentication(new User("test user", "a_all")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStoreTests.java new file mode 100644 index 0000000000000..b6989d47c5305 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStoreTests.java @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.Version; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.security.audit.logfile.CapturingLogger; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SystemUser; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; +import org.elasticsearch.xpack.core.security.user.XPackUser; +import org.junit.After; +import org.junit.Before; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.mock; + +public class FileOperatorUsersStoreTests extends ESTestCase { + + private Settings settings; + private Environment env; + private ThreadPool threadPool; + + @Before + public void init() { + settings = Settings.builder() + .put("resource.reload.interval.high", "100ms") + .put("path.home", createTempDir()) + .build(); + env = TestEnvironment.newEnvironment(settings); + threadPool = new TestThreadPool("test"); + } + + @After + public void shutdown() { + terminate(threadPool); + } + + public void testIsOperator() throws IOException { + Path sampleFile = getDataPath("operator_users.yml"); + Path inUseFile = getOperatorUsersPath(); + Files.copy(sampleFile, inUseFile, StandardCopyOption.REPLACE_EXISTING); + final ResourceWatcherService resourceWatcherService = mock(ResourceWatcherService.class); + final FileOperatorUsersStore fileOperatorUsersStore = new FileOperatorUsersStore(env, resourceWatcherService); + + // user operator_1 from file realm is an operator + final Authentication.RealmRef fileRealm = new Authentication.RealmRef("file", "file", randomAlphaOfLength(8)); + final User operator_1 = new User("operator_1", randomRoles()); + assertTrue(fileOperatorUsersStore.isOperatorUser(new Authentication(operator_1, fileRealm, fileRealm))); + + // user operator_3 is an operator and its file realm can have any name + final Authentication.RealmRef anotherFileRealm = new Authentication.RealmRef( + randomAlphaOfLengthBetween(3, 8), "file", randomAlphaOfLength(8)); + assertTrue(fileOperatorUsersStore.isOperatorUser( + new Authentication(new User("operator_3", randomRoles()), anotherFileRealm, anotherFileRealm))); + + // user operator_1 from a different realm is not an operator + final Authentication.RealmRef differentRealm = randomFrom( + new Authentication.RealmRef("file", randomAlphaOfLengthBetween(5, 8), randomAlphaOfLength(8)), + new Authentication.RealmRef(randomAlphaOfLengthBetween(5, 8), "file", randomAlphaOfLength(8)), + new Authentication.RealmRef(randomAlphaOfLengthBetween(5, 8), randomAlphaOfLengthBetween(5, 8), randomAlphaOfLength(8)) + ); + assertFalse(fileOperatorUsersStore.isOperatorUser(new Authentication(operator_1, differentRealm, differentRealm))); + + // user operator_1 with non realm auth type is not an operator + assertFalse(fileOperatorUsersStore.isOperatorUser( + new Authentication(operator_1, fileRealm, fileRealm, Version.CURRENT, Authentication.AuthenticationType.TOKEN, Map.of()))); + + // Run as user operator_1 is not an operator + final User runAsOperator_1 = new User(operator_1, new User(randomAlphaOfLengthBetween(5, 8), randomRoles())); + assertFalse(fileOperatorUsersStore.isOperatorUser(new Authentication(runAsOperator_1, fileRealm, fileRealm))); + + // Internal users are operator + final Authentication.RealmRef realm = + new Authentication.RealmRef(randomAlphaOfLength(8), randomAlphaOfLength(8), randomAlphaOfLength(8)); + final Authentication authentication = new Authentication( + randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE), + realm, realm); + assertTrue(fileOperatorUsersStore.isOperatorUser(authentication)); + } + + public void testFileAutoReload() throws Exception { + Path sampleFile = getDataPath("operator_users.yml"); + Path inUseFile = getOperatorUsersPath(); + Files.copy(sampleFile, inUseFile, StandardCopyOption.REPLACE_EXISTING); + + final Logger logger = LogManager.getLogger(FileOperatorUsersStore.class); + final MockLogAppender appender = new MockLogAppender(); + appender.start(); + appender.addExpectation( + new MockLogAppender.ExceptionSeenEventExpectation( + getTestName(), + logger.getName(), + Level.ERROR, + "Failed to parse operator users file", + XContentParseException.class, + "[10:1] [operator_privileges.operator] failed to parse field [operator]" + ) + ); + Loggers.addAppender(logger, appender); + + try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { + final FileOperatorUsersStore fileOperatorUsersStore = new FileOperatorUsersStore(env, watcherService); + final List groups = fileOperatorUsersStore.getOperatorUsersDescriptor().getGroups(); + + assertEquals(2, groups.size()); + assertEquals(new FileOperatorUsersStore.Group(Set.of("operator_1", "operator_2"), + "file"), groups.get(0)); + assertEquals(new FileOperatorUsersStore.Group(Set.of("operator_3"), null), groups.get(1)); + + // Content does not change, the groups should not be updated + try (BufferedWriter writer = Files.newBufferedWriter(inUseFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append("\n"); + } + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + assertSame(groups, fileOperatorUsersStore.getOperatorUsersDescriptor().getGroups()); + + // Add one more entry + try (BufferedWriter writer = Files.newBufferedWriter(inUseFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append(" - usernames: [ 'operator_4' ]\n"); + } + assertBusy(() -> { + final List newGroups = fileOperatorUsersStore.getOperatorUsersDescriptor().getGroups(); + assertEquals(3, newGroups.size()); + assertEquals(new FileOperatorUsersStore.Group(Set.of("operator_4")), newGroups.get(2)); + }); + + // Add mal-formatted entry + try (BufferedWriter writer = Files.newBufferedWriter(inUseFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append(" - blah\n"); + } + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + assertEquals(3, fileOperatorUsersStore.getOperatorUsersDescriptor().getGroups().size()); + appender.assertAllExpectationsMatched(); + + // Delete the file will remove all the operator users + Files.delete(inUseFile); + assertBusy(() -> assertEquals(0, fileOperatorUsersStore.getOperatorUsersDescriptor().getGroups().size())); + + // Back to original content + Files.copy(sampleFile, inUseFile, StandardCopyOption.REPLACE_EXISTING); + assertBusy(() -> assertEquals(2, fileOperatorUsersStore.getOperatorUsersDescriptor().getGroups().size())); + } finally { + Loggers.removeAppender(logger, appender); + appender.stop(); + } + } + + public void testMalFormattedOrEmptyFile() throws IOException { + // Mal-formatted file is functionally equivalent to an empty file + writeOperatorUsers(randomBoolean() ? "foobar" : ""); + try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { + final ElasticsearchParseException e = + expectThrows(ElasticsearchParseException.class, () -> new FileOperatorUsersStore(env, watcherService)); + assertThat(e.getMessage(), containsString("Error parsing operator users file")); + } + } + + public void testParseFileWhenFileDoesNotExist() throws Exception { + Path file = createTempDir().resolve(randomAlphaOfLength(10)); + Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); + final List groups = FileOperatorUsersStore.parseFile(file, logger).getGroups(); + assertEquals(0, groups.size()); + List events = CapturingLogger.output(logger.getName(), Level.WARN); + assertEquals(1, events.size()); + assertThat(events.get(0), containsString("operator user file does not exist")); + } + + public void testParseConfig() throws IOException { + String config = "" + + "operator:\n" + + " - usernames: [\"operator_1\"]\n"; + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final List groups = FileOperatorUsersStore.parseConfig(in).getGroups(); + assertEquals(1, groups.size()); + assertEquals(new FileOperatorUsersStore.Group(Set.of("operator_1")), groups.get(0)); + } + + config = "" + + "operator:\n" + + " - usernames: [\"operator_1\",\"operator_2\"]\n" + + " realm_name: \"file1\"\n" + + " realm_type: \"file\"\n" + + " auth_type: \"realm\"\n" + + " - usernames: [\"internal_system\"]\n"; + + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final List groups = FileOperatorUsersStore.parseConfig(in).getGroups(); + assertEquals(2, groups.size()); + assertEquals(new FileOperatorUsersStore.Group(Set.of("operator_1", "operator_2"), "file1"), groups.get(0)); + assertEquals(new FileOperatorUsersStore.Group(Set.of("internal_system")), groups.get(1)); + } + + config = "" + + "operator:\n" + + " - realm_name: \"file1\"\n" + + " usernames: [\"internal_system\"]\n" + + " - auth_type: \"realm\"\n" + + " usernames: [\"operator_1\",\"operator_2\"]\n"; + + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final List groups = FileOperatorUsersStore.parseConfig(in).getGroups(); + assertEquals(2, groups.size()); + assertEquals(new FileOperatorUsersStore.Group(Set.of("internal_system"), "file1"), groups.get(0)); + assertEquals(new FileOperatorUsersStore.Group(Set.of("operator_1", "operator_2")), groups.get(1)); + } + } + + public void testParseInvalidConfig() throws IOException { + String config = "" + + "operator:\n" + + " - usernames: [\"operator_1\"]\n" + + " realm_type: \"native\"\n"; + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final XContentParseException e = expectThrows(XContentParseException.class, + () -> FileOperatorUsersStore.parseConfig(in)); + assertThat(e.getCause().getCause().getMessage(), containsString("[realm_type] only supports [file]")); + } + + config = "" + + "operator:\n" + + " - usernames: [\"operator_1\"]\n" + + " auth_type: \"token\"\n"; + + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final XContentParseException e = expectThrows(XContentParseException.class, + () -> FileOperatorUsersStore.parseConfig(in)); + assertThat(e.getCause().getCause().getMessage(), containsString("[auth_type] only supports [realm]")); + } + + config = "" + + "operator:\n" + + " auth_type: \"realm\"\n"; + try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { + final XContentParseException e = expectThrows(XContentParseException.class, + () -> FileOperatorUsersStore.parseConfig(in)); + assertThat(e.getCause().getMessage(), containsString("Required [usernames]")); + } + } + + private Path getOperatorUsersPath() throws IOException { + Path xpackConf = env.configFile(); + Files.createDirectories(xpackConf); + return xpackConf.resolve("operator_users.yml"); + } + + private Path writeOperatorUsers(String input) throws IOException { + Path file = getOperatorUsersPath(); + Files.write(file, input.getBytes(StandardCharsets.UTF_8)); + return file; + } + + private String[] randomRoles() { + return randomArray(0, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java similarity index 62% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java index eb9d2d030150a..4cde194e6b92f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java @@ -11,20 +11,20 @@ import java.util.function.Supplier; -public class OperatorOnlyTests extends ESTestCase { +public class OperatorOnlyRegistryTests extends ESTestCase { public void testSimpleOperatorOnlyApi() { - final OperatorOnly operatorOnly = new OperatorOnly(); - for (final String actionName : OperatorOnly.SIMPLE_ACTIONS) { - final Supplier messageSupplier = operatorOnly.check(actionName, null); + final OperatorOnlyRegistry operatorOnlyRegistry = new OperatorOnlyRegistry(); + for (final String actionName : OperatorOnlyRegistry.SIMPLE_ACTIONS) { + final Supplier messageSupplier = operatorOnlyRegistry.check(actionName, null); assertNotNull(messageSupplier); assertNotNull(messageSupplier.get()); } } public void testNonOperatorOnlyApi() { - final OperatorOnly operatorOnly = new OperatorOnly(); - assertNull(operatorOnly.check(MainAction.NAME, null)); + final OperatorOnlyRegistry operatorOnlyRegistry = new OperatorOnlyRegistry(); + assertNull(operatorOnlyRegistry.check(MainAction.NAME, null)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java index c6b4751e5eaea..75cb7d31d1ecc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java @@ -14,6 +14,8 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges.DefaultOperatorPrivilegesService; +import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.junit.Before; import static org.hamcrest.Matchers.containsString; @@ -26,14 +28,14 @@ public class OperatorPrivilegesTests extends ESTestCase { private XPackLicenseState xPackLicenseState; - private OperatorUserDescriptor operatorUserDescriptor; - private OperatorOnly operatorOnly; + private FileOperatorUsersStore fileOperatorUsersStore; + private OperatorOnlyRegistry operatorOnlyRegistry; @Before public void init() { xPackLicenseState = mock(XPackLicenseState.class); - operatorUserDescriptor = mock(OperatorUserDescriptor.class); - operatorOnly = mock(OperatorOnly.class); + fileOperatorUsersStore = mock(FileOperatorUsersStore.class); + operatorOnlyRegistry = mock(OperatorOnlyRegistry.class); } public void testWillNotProcessWhenFeatureIsDisabledOrLicenseDoesNotSupport() { @@ -42,17 +44,17 @@ public void testWillNotProcessWhenFeatureIsDisabledOrLicenseDoesNotSupport() { .build(); when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(false); - final OperatorPrivileges operatorPrivileges = - new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, operatorOnly); + final OperatorPrivilegesService operatorPrivilegesService = + new DefaultOperatorPrivilegesService(xPackLicenseState, fileOperatorUsersStore, operatorOnlyRegistry); final ThreadContext threadContext = new ThreadContext(settings); - operatorPrivileges.maybeMarkOperatorUser(mock(Authentication.class), threadContext); - verifyZeroInteractions(operatorUserDescriptor); + operatorPrivilegesService.maybeMarkOperatorUser(mock(Authentication.class), threadContext); + verifyZeroInteractions(fileOperatorUsersStore); final ElasticsearchSecurityException e = - operatorPrivileges.check("cluster:action", mock(TransportRequest.class), threadContext); + operatorPrivilegesService.check("cluster:action", mock(TransportRequest.class), threadContext); assertNull(e); - verifyZeroInteractions(operatorOnly); + verifyZeroInteractions(operatorOnlyRegistry); } public void testMarkOperatorUser() { @@ -62,19 +64,19 @@ public void testMarkOperatorUser() { when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(true); final Authentication operatorAuth = mock(Authentication.class); final Authentication nonOperatorAuth = mock(Authentication.class); - when(operatorUserDescriptor.isOperatorUser(operatorAuth)).thenReturn(true); - when(operatorUserDescriptor.isOperatorUser(nonOperatorAuth)).thenReturn(false); + when(fileOperatorUsersStore.isOperatorUser(operatorAuth)).thenReturn(true); + when(fileOperatorUsersStore.isOperatorUser(nonOperatorAuth)).thenReturn(false); - final OperatorPrivileges operatorPrivileges = - new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, operatorOnly); + final OperatorPrivilegesService operatorPrivilegesService = + new DefaultOperatorPrivilegesService(xPackLicenseState, fileOperatorUsersStore, operatorOnlyRegistry); ThreadContext threadContext = new ThreadContext(settings); - operatorPrivileges.maybeMarkOperatorUser(operatorAuth, threadContext); + operatorPrivilegesService.maybeMarkOperatorUser(operatorAuth, threadContext); assertEquals(AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR, threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY)); threadContext = new ThreadContext(settings); - operatorPrivileges.maybeMarkOperatorUser(nonOperatorAuth, threadContext); + operatorPrivilegesService.maybeMarkOperatorUser(nonOperatorAuth, threadContext); assertNull(threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY)); } @@ -87,23 +89,24 @@ public void testCheck() { final String operatorAction = "cluster:operator_only/action"; final String nonOperatorAction = "cluster:non_operator/action"; final String message = "[" + operatorAction + "]"; - when(operatorOnly.check(eq(operatorAction), any())).thenReturn(() -> message); - when(operatorOnly.check(eq(nonOperatorAction), any())).thenReturn(null); + when(operatorOnlyRegistry.check(eq(operatorAction), any())).thenReturn(() -> message); + when(operatorOnlyRegistry.check(eq(nonOperatorAction), any())).thenReturn(null); - final OperatorPrivileges operatorPrivileges = - new OperatorPrivileges(settings, xPackLicenseState, operatorUserDescriptor, operatorOnly); + final OperatorPrivilegesService operatorPrivilegesService = + new DefaultOperatorPrivilegesService(xPackLicenseState, fileOperatorUsersStore, operatorOnlyRegistry); ThreadContext threadContext = new ThreadContext(settings); if (randomBoolean()) { threadContext.putHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY, AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR); - assertNull(operatorPrivileges.check(operatorAction, mock(TransportRequest.class), threadContext)); + assertNull(operatorPrivilegesService.check(operatorAction, mock(TransportRequest.class), threadContext)); } else { - final ElasticsearchSecurityException e = operatorPrivileges.check(operatorAction, mock(TransportRequest.class), threadContext); + final ElasticsearchSecurityException e = operatorPrivilegesService.check( + operatorAction, mock(TransportRequest.class), threadContext); assertNotNull(e); assertThat(e.getMessage(), containsString("Operator privileges are required for " + message)); } - assertNull(operatorPrivileges.check(nonOperatorAction, mock(TransportRequest.class), threadContext)); + assertNull(operatorPrivilegesService.check(nonOperatorAction, mock(TransportRequest.class), threadContext)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java deleted file mode 100644 index ef1946e678722..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorUserDescriptorTests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.security.operator; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.watcher.ResourceWatcherService; -import org.elasticsearch.xpack.core.security.audit.logfile.CapturingLogger; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.junit.After; -import org.junit.Before; - -import java.io.BufferedWriter; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.List; -import java.util.Set; - -public class OperatorUserDescriptorTests extends ESTestCase { - - private Settings settings; - private Environment env; - private ThreadPool threadPool; - - @Before - public void init() { - settings = Settings.builder() - .put("resource.reload.interval.high", "100ms") - .put("path.home", createTempDir()) - .build(); - env = TestEnvironment.newEnvironment(settings); - threadPool = new TestThreadPool("test"); - } - - @After - public void shutdown() throws InterruptedException { - terminate(threadPool); - } - - public void testFileAutoReload() throws Exception { - Path operatorUsers = getDataPath("operator_users.yml"); - Path tmp = getOperatorUsersPath(); - Files.copy(operatorUsers, tmp, StandardCopyOption.REPLACE_EXISTING); - - try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { - final OperatorUserDescriptor operatorUserDescriptor = new OperatorUserDescriptor(env, watcherService); - final List groups = operatorUserDescriptor.getGroups(); - - assertEquals(1, groups.size()); - assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), - "file", "file", Authentication.AuthenticationType.REALM), groups.get(0)); - - // Content does not change, the groups should not be updated - try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { - writer.append("\n"); - } - watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); - assertSame(groups, operatorUserDescriptor.getGroups()); - - // Add one more entry - try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { - writer.append(" - usernames: [ 'operator_3' ]\n"); - } - assertBusy(() -> { - final List newGroups = operatorUserDescriptor.getGroups(); - assertEquals(2, newGroups.size()); - assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), - "file", "file", Authentication.AuthenticationType.REALM), newGroups.get(0)); - assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_3")), newGroups.get(1)); - }); - - // Add mal-formatted entry - try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { - writer.append(" - blah\n"); - } - assertBusy(() -> { - assertEquals(0, operatorUserDescriptor.getGroups().size()); - }); - } - } - - public void testMalFormattedOrEmptyFile() throws IOException { - // Mal-formatted file is functionally equivalent to an empty file - writeOperatorUsers(randomBoolean() ? "foobar" : ""); - try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { - final OperatorUserDescriptor operatorUserDescriptor = new OperatorUserDescriptor(env, watcherService); - assertEquals(0, operatorUserDescriptor.getGroups().size()); - } - } - - public void testParseFileWhenFileDoesNotExist() throws Exception { - Path file = createTempDir().resolve(randomAlphaOfLength(10)); - Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); - final List groups = OperatorUserDescriptor.parseFileLenient(file, logger); - assertEquals(0, groups.size()); - List events = CapturingLogger.output(logger.getName(), Level.DEBUG); - assertEquals(1, events.size()); - assertEquals("Skip reading operator user file since it does not exist", events.get(0)); - } - - public void testParseConfig() throws IOException { - final String config = "" - + "operator:\n" - + " - usernames: [\"operator_1\",\"operator_2\"]\n" - + " realm_name: \"found\"\n" - + " realm_type: \"file\"\n" - + " auth_type: \"REALM\"\n" - + " - usernames: [\"internal_system\"]\n"; - - try (ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))) { - final List groups = OperatorUserDescriptor.parseConfig(in); - assertEquals(2, groups.size()); - assertEquals(new OperatorUserDescriptor.Group(Set.of("operator_1", "operator_2"), "found"), groups.get(0)); - assertEquals(new OperatorUserDescriptor.Group(Set.of("internal_system")), groups.get(1)); - } - } - - private Path getOperatorUsersPath() throws IOException { - Path xpackConf = env.configFile(); - Files.createDirectories(xpackConf); - return xpackConf.resolve("operator_users.yml"); - } - - private Path writeOperatorUsers(String input) throws IOException { - Path file = getOperatorUsersPath(); - Files.write(file, input.getBytes(StandardCharsets.UTF_8)); - return file; - } -} diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml index 2227d1e3ae708..507f4d550e446 100644 --- a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/operator/operator_users.yml @@ -3,3 +3,4 @@ operator: realm_name: file realm_type: file auth_type: realm + - usernames: ['operator_3'] diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index fdbe078a254c5..09ed31f04e68f 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -89,7 +89,7 @@ public void testAllActionsAreEitherOperatorOnlyOrNonOperator() throws IOExceptio final Map response = responseAsMap(client().performRequest(request)); List allActions = (List) response.get("actions"); assertFalse(allActions.isEmpty()); - allActions.removeAll(OperatorOnly.SIMPLE_ACTIONS); + allActions.removeAll(OperatorOnlyRegistry.SIMPLE_ACTIONS); allActions.removeAll(Constants.NON_OPERATOR_ACTIONS); assertTrue(allActions.isEmpty()); } diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java index 2b913dbd5a026..97cb06f7f51f2 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java +++ b/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java @@ -10,6 +10,7 @@ import org.elasticsearch.action.ActionType; import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; @@ -35,6 +36,7 @@ public String getName() { return "test_get_actions"; } + @SuppressForbidden(reason = "Use reflection for testing only") @SuppressWarnings({ "rawtypes", "unchecked" }) @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { From b52c8147e66d4236f6b12c1f001b0592c5a30302 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 19:00:58 +1100 Subject: [PATCH 13/23] Move plugin test package --- .../qa/operator-privileges-tests/build.gradle | 2 +- .../xpack/security/operator/Constants.java | 0 .../operator/OperatorPrivilegesIT.java | 22 ++++++++++++++----- .../javaRestTest/resources/operator_users.yml | 0 .../src/javaRestTest/resources/roles.yml | 0 .../security/operator}/OpTestPlugin.java | 4 ++-- .../actions/RestGetActionsAction.java | 2 +- .../plugin-metadata/plugin-security.policy | 0 .../security/operator}/OpTestPluginTests.java | 2 +- 9 files changed, 21 insertions(+), 11 deletions(-) rename x-pack/{ => plugin/security}/qa/operator-privileges-tests/build.gradle (95%) rename x-pack/{ => plugin/security}/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java (100%) rename x-pack/{ => plugin/security}/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java (82%) rename x-pack/{ => plugin/security}/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml (100%) rename x-pack/{ => plugin/security}/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml (100%) rename x-pack/{qa/operator-privileges-tests/src/main/java/org/elasticsearch/example => plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator}/OpTestPlugin.java (91%) rename x-pack/{qa/operator-privileges-tests/src/main/java/org/elasticsearch/example => plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator}/actions/RestGetActionsAction.java (97%) rename x-pack/{ => plugin/security}/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy (100%) rename x-pack/{qa/operator-privileges-tests/src/test/java/org/elasticsearch/example => plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator}/OpTestPluginTests.java (91%) diff --git a/x-pack/qa/operator-privileges-tests/build.gradle b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle similarity index 95% rename from x-pack/qa/operator-privileges-tests/build.gradle rename to x-pack/plugin/security/qa/operator-privileges-tests/build.gradle index 01dc0a8713a4d..ca9ffb9790625 100644 --- a/x-pack/qa/operator-privileges-tests/build.gradle +++ b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'elasticsearch.java-rest-test' esplugin { name 'op-test' description 'An test plugin for testing hard to get internals' - classname 'org.elasticsearch.example.OpTestPlugin' + classname 'org.elasticsearch.xpack.security.operator.OpTestPlugin' } dependencies { diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java similarity index 82% rename from x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index 09ed31f04e68f..d6aa4be75c495 100644 --- a/x-pack/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -11,13 +11,16 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.rest.ESRestTestCase; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.containsString; @@ -84,13 +87,20 @@ public void testOperatorUserCanCallNonOperatorOnlyApi() throws IOException { } @SuppressWarnings("unchecked") - public void testAllActionsAreEitherOperatorOnlyOrNonOperator() throws IOException { + public void testEveryActionIsEitherOperatorOnlyOrNonOperator() throws IOException { + Set doubleLabelled = Sets.intersection(Constants.NON_OPERATOR_ACTIONS, OperatorOnlyRegistry.SIMPLE_ACTIONS); + assertTrue("Actions are both operator-only and non-operator", doubleLabelled.isEmpty()); + final Request request = new Request("GET", "/_test/get_actions"); final Map response = responseAsMap(client().performRequest(request)); - List allActions = (List) response.get("actions"); - assertFalse(allActions.isEmpty()); - allActions.removeAll(OperatorOnlyRegistry.SIMPLE_ACTIONS); - allActions.removeAll(Constants.NON_OPERATOR_ACTIONS); - assertTrue(allActions.isEmpty()); + Set allActions = Set.copyOf((List) response.get("actions")); + final HashSet labelledActions = new HashSet<>(OperatorOnlyRegistry.SIMPLE_ACTIONS); + labelledActions.addAll(Constants.NON_OPERATOR_ACTIONS); + + final Set unlabelledActions = Sets.difference(allActions, labelledActions); + assertTrue("Actions are neither operator-only nor non-operator", unlabelledActions.isEmpty()); + + final Set redundantLabelledActions = Sets.difference(labelledActions, allActions); + assertTrue("Actions are no longer valid", redundantLabelledActions.isEmpty()); } } diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml rename to x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/resources/operator_users.yml diff --git a/x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml rename to x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/resources/roles.yml diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OpTestPlugin.java similarity index 91% rename from x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OpTestPlugin.java index 0766d4d302f2e..da1fb0d4b9583 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/OpTestPlugin.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OpTestPlugin.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.example; +package org.elasticsearch.xpack.security.operator; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -12,7 +12,7 @@ import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; -import org.elasticsearch.example.actions.RestGetActionsAction; +import org.elasticsearch.xpack.security.operator.actions.RestGetActionsAction; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestController; diff --git a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/actions/RestGetActionsAction.java similarity index 97% rename from x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/actions/RestGetActionsAction.java index 97cb06f7f51f2..ed0c9ca0d6e8c 100644 --- a/x-pack/qa/operator-privileges-tests/src/main/java/org/elasticsearch/example/actions/RestGetActionsAction.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/actions/RestGetActionsAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.example.actions; +package org.elasticsearch.xpack.security.operator.actions; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionType; diff --git a/x-pack/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy similarity index 100% rename from x-pack/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy rename to x-pack/plugin/security/qa/operator-privileges-tests/src/main/plugin-metadata/plugin-security.policy diff --git a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OpTestPluginTests.java similarity index 91% rename from x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OpTestPluginTests.java index 5b9c33f54dd24..7fb3ecc40ad78 100644 --- a/x-pack/qa/operator-privileges-tests/src/test/java/org/elasticsearch/example/OpTestPluginTests.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OpTestPluginTests.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.example; +package org.elasticsearch.xpack.security.operator; import org.elasticsearch.test.ESTestCase; From 5883923520fb9418330ef726d3a8fb460fbca498 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 19:58:50 +1100 Subject: [PATCH 14/23] Fix authenticationService tests --- .../security/authc/AuthenticationService.java | 2 + .../authc/AuthenticationServiceTests.java | 104 +++++++++++++----- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 048d5b869d821..c125c35f728c4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -693,6 +693,8 @@ void writeAuthToContext(Authentication authentication) { try { authenticationSerializer.writeToContext(authentication, threadContext); request.authenticationSuccess(authentication); + // Header for operator privileges will only be written if authentication actually happens, + // i.e. not read from either header or transient header operatorPrivilegesService.maybeMarkOperatorUser(authentication, threadContext); } catch (Exception e) { action = () -> { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 996a53e46b39a..b75c26014ae06 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -168,7 +168,7 @@ public class AuthenticationServiceTests extends ESTestCase { private SecurityIndexManager securityIndex; private Client client; private InetSocketAddress remoteAddress; - private OperatorPrivileges operatorPrivileges; + private OperatorPrivileges.OperatorPrivilegesService operatorPrivilegesService; private String concreteSecurityIndexName; @Before @@ -258,10 +258,12 @@ public void init() throws Exception { mock(CacheInvalidatorRegistry.class), threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); + + operatorPrivilegesService = mock(OperatorPrivileges.OperatorPrivilegesService.class); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), tokenService, apiKeyService, - OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + operatorPrivilegesService); } @After @@ -340,6 +342,7 @@ public void testAuthenticateBothSupportSecondSucceeds() throws Exception { assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); assertThreadContextContainsAuthentication(result); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); assertTrue(completed.get()); verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", transportRequest); @@ -368,6 +371,7 @@ public void testAuthenticateSmartRealmOrdering() { assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); assertTrue(completed.get()); @@ -375,6 +379,7 @@ public void testAuthenticateSmartRealmOrdering() { // Authenticate against the smart chain. // "SecondRealm" will be at the top of the list and will successfully authc. // "FirstRealm" will not be used + Mockito.reset(operatorPrivilegesService); service.authenticate("_action", transportRequest, true, ActionListener.wrap(result -> { assertThat(result, notNullValue()); assertThat(result.getUser(), is(user)); @@ -385,6 +390,7 @@ public void testAuthenticateSmartRealmOrdering() { assertThreadContextContainsAuthentication(result); verify(auditTrail, times(2)).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", transportRequest); @@ -417,6 +423,7 @@ public void testAuthenticateSmartRealmOrdering() { assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); verify(auditTrail).authenticationFailed(reqId, SECOND_REALM_NAME, token, "_action", transportRequest); @@ -466,7 +473,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { .build(); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, operatorPrivilegesService); User user = new User("_username", "r1"); when(firstRealm.supports(token)).thenReturn(true); mockAuthenticate(firstRealm, token, null); @@ -484,10 +491,12 @@ public void testAuthenticateSmartRealmOrderingDisabled() { assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); assertTrue(completed.get()); completed.set(false); + Mockito.reset(operatorPrivilegesService); service.authenticate("_action", transportRequest, true, ActionListener.wrap(result -> { assertThat(result, notNullValue()); assertThat(result.getUser(), is(user)); @@ -496,6 +505,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { assertThreadContextContainsAuthentication(result); verify(auditTrail, times(2)).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); verify(auditTrail, times(2)).authenticationFailed(reqId, firstRealm.name(), token, "_action", transportRequest); verify(firstRealm, times(3)).name(); // used above one time @@ -528,6 +538,7 @@ public void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); verifyNoMoreInteractions(auditTrail); verify(firstRealm, never()).authenticate(eq(token), any(ActionListener.class)); @@ -546,6 +557,7 @@ public void testAuthenticateCached() throws Exception { verifyZeroInteractions(auditTrail); verifyZeroInteractions(firstRealm); verifyZeroInteractions(secondRealm); + verifyZeroInteractions(operatorPrivilegesService); } public void testAuthenticateNonExistentRestRequestUserThrowsAuthenticationException() throws Exception { @@ -556,6 +568,7 @@ public void testAuthenticateNonExistentRestRequestUserThrowsAuthenticationExcept fail("Authentication was successful but should not"); } catch (ElasticsearchSecurityException e) { assertAuthenticationException(e, containsString("unable to authenticate user [idonotexist] for REST request [/]")); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -569,24 +582,23 @@ public void testTokenRestMissing() throws Exception { }); } - public void authenticationInContextAndHeader() throws Exception { + public void testAuthenticationInContextAndHeader() throws Exception { User user = new User("_username", "r1"); when(firstRealm.token(threadContext)).thenReturn(token); when(firstRealm.supports(token)).thenReturn(true); mockAuthenticate(firstRealm, token, user); - Authentication result = authenticateBlocking("_action", transportRequest, null); - - assertThat(result, notNullValue()); - assertThat(result.getUser(), is(user)); - assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); - - String userStr = threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY); - assertThat(userStr, notNullValue()); - assertThat(userStr, equalTo("_signed_auth")); + service.authenticate("_action", transportRequest, true, ActionListener.wrap(result -> { + assertThat(result, notNullValue()); + assertThat(result.getUser(), is(user)); + assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); - Authentication ctxAuth = threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY); - assertThat(ctxAuth, is(result)); + String userStr = threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY); + assertThat(userStr, notNullValue()); + Authentication ctxAuth = threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY); + assertThat(ctxAuth, is(result)); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); + }, this::logAndFail)); } public void testAuthenticateTransportAnonymous() throws Exception { @@ -599,6 +611,7 @@ public void testAuthenticateTransportAnonymous() throws Exception { } catch (ElasticsearchSecurityException e) { // expected assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } verify(auditTrail).anonymousAccessDenied(reqId, "_action", transportRequest); } @@ -612,6 +625,7 @@ public void testAuthenticateRestAnonymous() throws Exception { } catch (ElasticsearchSecurityException e) { // expected assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } String reqId = expectAuditRequestId(); verify(auditTrail).anonymousAccessDenied(reqId, restRequest); @@ -627,6 +641,7 @@ public void testAuthenticateTransportFallback() throws Exception { assertThat(result.getUser(), sameInstance(user1)); assertThat(result.getAuthenticationType(), is(AuthenticationType.INTERNAL)); assertThreadContextContainsAuthentication(result); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); } public void testAuthenticateTransportDisabledUser() throws Exception { @@ -642,6 +657,7 @@ public void testAuthenticateTransportDisabledUser() throws Exception { verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } public void testAuthenticateRestDisabledUser() throws Exception { @@ -656,6 +672,7 @@ public void testAuthenticateRestDisabledUser() throws Exception { verify(auditTrail).authenticationFailed(reqId, token, restRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } public void testAuthenticateTransportSuccess() throws Exception { @@ -680,6 +697,7 @@ public void testAuthenticateTransportSuccess() throws Exception { assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId, result, "_action", transportRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); }, this::logAndFail)); verifyNoMoreInteractions(auditTrail); @@ -702,6 +720,7 @@ public void testAuthenticateRestSuccess() throws Exception { String reqId = expectAuditRequestId(); verify(auditTrail).authenticationSuccess(reqId, authentication, restRequest); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(authentication), eq(threadContext)); }, this::logAndFail)); verifyNoMoreInteractions(auditTrail); assertTrue(completed.get()); @@ -724,6 +743,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { authRef.set(authentication); authHeaderRef.set(threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY)); setCompletedToTrue(completed); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(authentication), eq(threadContext)); }, this::logAndFail)); } assertTrue(completed.compareAndSet(true, false)); @@ -732,11 +752,12 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { // checking authentication from the context InternalRequest message1 = new InternalRequest(); ThreadPool threadPool1 = new TestThreadPool("testAutheticateTransportContextAndHeader1"); + Mockito.reset(operatorPrivilegesService); try { ThreadContext threadContext1 = threadPool1.getThreadContext(); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool1, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, operatorPrivilegesService); threadContext1.putTransient(AuthenticationField.AUTHENTICATION_KEY, authRef.get()); threadContext1.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); @@ -744,6 +765,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { assertThat(ctxAuth, sameInstance(authRef.get())); assertThat(threadContext1.getHeader(AuthenticationField.AUTHENTICATION_KEY), sameInstance(authHeaderRef.get())); setCompletedToTrue(completed); + verifyZeroInteractions(operatorPrivilegesService); }, this::logAndFail)); assertTrue(completed.compareAndSet(true, false)); verifyZeroInteractions(firstRealm); @@ -754,13 +776,14 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { // checking authentication from the user header ThreadPool threadPool2 = new TestThreadPool("testAutheticateTransportContextAndHeader2"); + Mockito.reset(operatorPrivilegesService); try { ThreadContext threadContext2 = threadPool2.getThreadContext(); final String header; try (ThreadContext.StoredContext ignore = threadContext2.stashContext()) { service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, operatorPrivilegesService); threadContext2.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); BytesStreamOutput output = new BytesStreamOutput(); @@ -774,12 +797,13 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { threadPool2.getThreadContext().putHeader(AuthenticationField.AUTHENTICATION_KEY, header); service = new AuthenticationService(Settings.EMPTY, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY), - tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, operatorPrivilegesService); service.authenticate("_action", new InternalRequest(), SystemUser.INSTANCE, ActionListener.wrap(result -> { assertThat(result, notNullValue()); assertThat(result.getUser(), equalTo(user1)); assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); setCompletedToTrue(completed); + verifyZeroInteractions(operatorPrivilegesService); }, this::logAndFail)); assertTrue(completed.get()); verifyZeroInteractions(firstRealm); @@ -799,6 +823,7 @@ public void testAuthenticateTamperedUser() throws Exception { //expected verify(auditTrail).tamperedRequest(reqId, "_action", message); verifyNoMoreInteractions(auditTrail); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -813,7 +838,7 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, - tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, operatorPrivilegesService); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); @@ -823,6 +848,7 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { verify(auditTrail).anonymousAccessDenied(reqId, "_action", transportRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -837,7 +863,7 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings); service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, - tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, operatorPrivilegesService); doAnswer(invocationOnMock -> { final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0]; final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; @@ -854,6 +880,7 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { verify(auditTrail).anonymousAccessDenied(reqId, "_action", transportRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -868,7 +895,7 @@ public void testAnonymousUserRest() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivilegesService); RestRequest request = new FakeRestRequest(); Authentication result = authenticateBlocking(request); @@ -880,6 +907,7 @@ public void testAnonymousUserRest() throws Exception { String reqId = expectAuditRequestId(); verify(auditTrail).authenticationSuccess(reqId, result, request); verifyNoMoreInteractions(auditTrail); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); } public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { @@ -894,7 +922,7 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivilegesService); RestRequest request = new FakeRestRequest(); PlainActionFuture future = new PlainActionFuture<>(); @@ -908,6 +936,7 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { String reqId = expectAuditRequestId(); verify(auditTrail).anonymousAccessDenied(reqId, request); verifyNoMoreInteractions(auditTrail); + verifyZeroInteractions(operatorPrivilegesService); } public void testAnonymousUserTransportNoDefaultUser() throws Exception { @@ -917,7 +946,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivilegesService); InternalRequest message = new InternalRequest(); Authentication result = authenticateBlocking("_action", message, null); @@ -925,6 +954,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { assertThat(result.getUser(), sameInstance(anonymousUser)); assertThat(result.getAuthenticationType(), is(AuthenticationType.ANONYMOUS)); assertThreadContextContainsAuthentication(result); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); } public void testAnonymousUserTransportWithDefaultUser() throws Exception { @@ -934,7 +964,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), - threadPool, anonymousUser, tokenService, apiKeyService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + threadPool, anonymousUser, tokenService, apiKeyService, operatorPrivilegesService); InternalRequest message = new InternalRequest(); @@ -943,6 +973,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { assertThat(result.getUser(), sameInstance(SystemUser.INSTANCE)); assertThat(result.getAuthenticationType(), is(AuthenticationType.INTERNAL)); assertThreadContextContainsAuthentication(result); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); } public void testRealmTokenThrowingException() throws Exception { @@ -954,6 +985,7 @@ public void testRealmTokenThrowingException() throws Exception { } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like tokens")); verify(auditTrail).authenticationFailed(reqId, "_action", transportRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -966,6 +998,7 @@ public void testRealmTokenThrowingExceptionRest() throws Exception { assertThat(e.getMessage(), is("realm doesn't like tokens")); String reqId = expectAuditRequestId(); verify(auditTrail).authenticationFailed(reqId, restRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -981,6 +1014,7 @@ public void testRealmSupportsMethodThrowingException() throws Exception { } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like supports")); verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -996,6 +1030,7 @@ public void testRealmSupportsMethodThrowingExceptionRest() throws Exception { assertThat(e.getMessage(), is("realm doesn't like supports")); String reqId = expectAuditRequestId(); verify(auditTrail).authenticationFailed(reqId, token, restRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1035,6 +1070,7 @@ public void testRealmAuthenticateTerminateAuthenticationProcessWithException() { verify(auditTrail).authenticationFailed(reqId, secondRealm.name(), token, "_action", transportRequest); verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); verifyNoMoreInteractions(auditTrail); + verifyZeroInteractions(operatorPrivilegesService); } public void testRealmAuthenticateGracefulTerminateAuthenticationProcess() { @@ -1054,6 +1090,7 @@ public void testRealmAuthenticateGracefulTerminateAuthenticationProcess() { verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", transportRequest); verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); verifyNoMoreInteractions(auditTrail); + verifyZeroInteractions(operatorPrivilegesService); } public void testRealmAuthenticateThrowingException() throws Exception { @@ -1070,6 +1107,7 @@ public void testRealmAuthenticateThrowingException() throws Exception { } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like authenticate")); verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1087,6 +1125,7 @@ public void testRealmAuthenticateThrowingExceptionRest() throws Exception { assertThat(e.getMessage(), is("realm doesn't like authenticate")); String reqId = expectAuditRequestId(); verify(auditTrail).authenticationFailed(reqId, token, restRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1108,6 +1147,7 @@ public void testRealmLookupThrowingException() throws Exception { } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't want to lookup")); verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1128,6 +1168,7 @@ public void testRealmLookupThrowingExceptionRest() throws Exception { assertThat(e.getMessage(), is("realm doesn't want to lookup")); String reqId = expectAuditRequestId(); verify(auditTrail).authenticationFailed(reqId, token, restRequest); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1166,8 +1207,7 @@ public void testRunAsLookupSameRealm() throws Exception { assertEquals(user.email(), authUser.email()); assertEquals(user.enabled(), authUser.enabled()); assertEquals(user.fullName(), authUser.fullName()); - - + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); setCompletedToTrue(completed); }, this::logAndFail); @@ -1207,6 +1247,7 @@ public void testRunAsLookupDifferentRealm() throws Exception { assertThat(authenticated.principal(), is("looked up user")); assertThat(authenticated.roles(), arrayContaining("some role")); assertThreadContextContainsAuthentication(result); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); setCompletedToTrue(completed); }, this::logAndFail); @@ -1235,6 +1276,7 @@ public void testRunAsWithEmptyRunAsUsernameRest() throws Exception { String reqId = expectAuditRequestId(); verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq(restRequest), eq(EmptyAuthorizationInfo.INSTANCE)); verifyNoMoreInteractions(auditTrail); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1255,6 +1297,7 @@ public void testRunAsWithEmptyRunAsUsername() throws Exception { verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq("_action"), eq(transportRequest), eq(EmptyAuthorizationInfo.INSTANCE)); verifyNoMoreInteractions(auditTrail); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1279,6 +1322,7 @@ public void testAuthenticateTransportDisabledRunAsUser() throws Exception { verify(auditTrail).authenticationFailed(reqId, token, "_action", transportRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } public void testAuthenticateRestDisabledRunAsUser() throws Exception { @@ -1302,6 +1346,7 @@ public void testAuthenticateRestDisabledRunAsUser() throws Exception { verify(auditTrail).authenticationFailed(reqId, token, restRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); + verifyZeroInteractions(operatorPrivilegesService); } public void testAuthenticateWithToken() throws Exception { @@ -1330,6 +1375,7 @@ public void testAuthenticateWithToken() throws Exception { assertThat(result.getAuthenticatedBy(), is(notNullValue())); assertThat(result.getAuthenticatedBy().getName(), is("realm")); // TODO implement equals assertThat(result.getAuthenticationType(), is(AuthenticationType.TOKEN)); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); setCompletedToTrue(completed); verify(auditTrail).authenticationSuccess(anyString(), eq(result), eq("_action"), same(transportRequest)); }, this::logAndFail)); @@ -1358,9 +1404,11 @@ public void testInvalidToken() throws Exception { assertThat(result.getAuthenticatedBy(), is(notNullValue())); assertThreadContextContainsAuthentication(result); assertEquals(expected, result); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext)); success.set(true); latch.countDown(); }, e -> { + verifyZeroInteractions(operatorPrivilegesService); if (e instanceof IllegalStateException) { assertThat(e.getMessage(), containsString("array length must be <= to " + ArrayUtil.MAX_ARRAY_LENGTH + " but was: ")); latch.countDown(); @@ -1416,6 +1464,7 @@ public void testExpiredToken() throws Exception { expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", transportRequest, null)); assertEquals(RestStatus.UNAUTHORIZED, e.status()); assertEquals("token expired", e.getMessage()); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1427,6 +1476,7 @@ public void testApiKeyAuthInvalidHeader() { () -> authenticateBlocking("_action", transportRequest, null)); assertEquals(RestStatus.UNAUTHORIZED, e.status()); assertThat(e.getMessage(), containsString("missing authentication credentials")); + verifyZeroInteractions(operatorPrivilegesService); } } @@ -1475,6 +1525,7 @@ public void testApiKeyAuth() { assertThat(authentication.getUser().fullName(), is("john doe")); assertThat(authentication.getUser().email(), is("john@doe.com")); assertThat(authentication.getAuthenticationType(), is(AuthenticationType.API_KEY)); + verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(authentication), eq(threadContext)); } } @@ -1515,6 +1566,7 @@ public void testExpiredApiKey() { ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", transportRequest, null)); assertEquals(RestStatus.UNAUTHORIZED, e.status()); + verifyZeroInteractions(operatorPrivilegesService); } } From 1b3344f199b70b3db51de6c49d5efe4b289791f1 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Nov 2020 23:48:21 +1100 Subject: [PATCH 15/23] Fix tests and telemetry --- .../elasticsearch/xpack/core/XPackField.java | 2 + .../core/action/XPackInfoFeatureAction.java | 3 +- .../xpack/security/operator/Constants.java | 1 + .../operator/OperatorPrivilegesIT.java | 44 +++++++++++------ .../xpack/security/Security.java | 2 + .../security/authz/AuthorizationService.java | 19 ++++---- .../operator/OperatorOnlyRegistry.java | 4 +- ...OperatorPrivilegesInfoTransportAction.java | 47 +++++++++++++++++++ .../authz/AuthorizationServiceTests.java | 44 ++++++++++++++--- .../operator/OperatorOnlyRegistryTests.java | 21 +++++++-- .../operator/OperatorPrivilegesTests.java | 14 ++++++ 11 files changed, 162 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesInfoTransportAction.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java index 1d95209f7911a..698547f5843f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java @@ -67,6 +67,8 @@ public final class XPackField { public static final String DATA_TIERS = "data_tiers"; /** Name constant for the aggregate_metric plugin. */ public static final String AGGREGATE_METRIC = "aggregate_metric"; + /** Name constant for the operator privileges feature. */ + public static final String OPERATOR_PRIVILEGES = "operator_privileges"; private XPackField() {} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoFeatureAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoFeatureAction.java index 08bfe678bb910..493d991428a7f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoFeatureAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoFeatureAction.java @@ -47,6 +47,7 @@ public class XPackInfoFeatureAction extends ActionType public static final XPackInfoFeatureAction DATA_STREAMS = new XPackInfoFeatureAction(XPackField.DATA_STREAMS); public static final XPackInfoFeatureAction DATA_TIERS = new XPackInfoFeatureAction(XPackField.DATA_TIERS); public static final XPackInfoFeatureAction AGGREGATE_METRIC = new XPackInfoFeatureAction(XPackField.AGGREGATE_METRIC); + public static final XPackInfoFeatureAction OPERATOR_PRIVILEGES = new XPackInfoFeatureAction(XPackField.OPERATOR_PRIVILEGES); public static final List ALL; static { @@ -54,7 +55,7 @@ public class XPackInfoFeatureAction extends ActionType actions.addAll(Arrays.asList( SECURITY, MONITORING, WATCHER, GRAPH, MACHINE_LEARNING, LOGSTASH, EQL, SQL, ROLLUP, INDEX_LIFECYCLE, SNAPSHOT_LIFECYCLE, CCR, TRANSFORM, VECTORS, VOTING_ONLY, FROZEN_INDICES, SPATIAL, ANALYTICS, ENRICH, DATA_STREAMS, SEARCHABLE_SNAPSHOTS, DATA_TIERS, - AGGREGATE_METRIC + AGGREGATE_METRIC, OPERATOR_PRIVILEGES )); ALL = Collections.unmodifiableList(actions); } diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index 209916a96254c..5f26944a1c921 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -244,6 +244,7 @@ public class Constants { "cluster:monitor/xpack/info/logstash", "cluster:monitor/xpack/info/ml", "cluster:monitor/xpack/info/monitoring", + "cluster:monitor/xpack/info/operator_privileges", "cluster:monitor/xpack/info/rollup", "cluster:monitor/xpack/info/searchable_snapshots", "cluster:monitor/xpack/info/security", diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index d6aa4be75c495..b46c97584f397 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -34,18 +34,9 @@ protected Settings restClientSettings() { return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } - @SuppressWarnings("unchecked") public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApiWhenOperatorPrivilegesIsEnabled() throws IOException { - final Request getClusterSettingsRequest = new Request( - "GET", - "_cluster/settings?flat_settings&include_defaults&filter_path=defaults.*operator_privileges*" - ); - final Map settingsMap = entityAsMap(client().performRequest(getClusterSettingsRequest)); - final Map defaults = (Map) settingsMap.get("defaults"); - final Object isOperatorPrivilegesEnabled = defaults.get("xpack.security.operator_privileges.enabled"); - final Request postVotingConfigExclusionsRequest = new Request("POST", "_cluster/voting_config_exclusions?node_names=foo"); - if ("true".equals(isOperatorPrivilegesEnabled)) { + if (isOperatorPrivilegesEnabled()) { final ResponseException responseException = expectThrows( ResponseException.class, () -> client().performRequest(postVotingConfigExclusionsRequest) @@ -89,7 +80,7 @@ public void testOperatorUserCanCallNonOperatorOnlyApi() throws IOException { @SuppressWarnings("unchecked") public void testEveryActionIsEitherOperatorOnlyOrNonOperator() throws IOException { Set doubleLabelled = Sets.intersection(Constants.NON_OPERATOR_ACTIONS, OperatorOnlyRegistry.SIMPLE_ACTIONS); - assertTrue("Actions are both operator-only and non-operator", doubleLabelled.isEmpty()); + assertTrue("Actions are both operator-only and non-operator: " + doubleLabelled, doubleLabelled.isEmpty()); final Request request = new Request("GET", "/_test/get_actions"); final Map response = responseAsMap(client().performRequest(request)); @@ -97,10 +88,33 @@ public void testEveryActionIsEitherOperatorOnlyOrNonOperator() throws IOExceptio final HashSet labelledActions = new HashSet<>(OperatorOnlyRegistry.SIMPLE_ACTIONS); labelledActions.addAll(Constants.NON_OPERATOR_ACTIONS); - final Set unlabelledActions = Sets.difference(allActions, labelledActions); - assertTrue("Actions are neither operator-only nor non-operator", unlabelledActions.isEmpty()); + final Set unlabelled = Sets.difference(allActions, labelledActions); + assertTrue("Actions are neither operator-only nor non-operator: " + unlabelled, unlabelled.isEmpty()); - final Set redundantLabelledActions = Sets.difference(labelledActions, allActions); - assertTrue("Actions are no longer valid", redundantLabelledActions.isEmpty()); + final Set redundant = Sets.difference(labelledActions, allActions); + assertTrue("Actions may no longer be valid: " + redundant, redundant.isEmpty()); + } + + @SuppressWarnings("unchecked") + public void testOperatorPrivilegesXpackInfo() throws IOException { + final Request xpackRequest = new Request("GET", "/_xpack"); + final Map response = entityAsMap(client().performRequest(xpackRequest)); + final Map features = (Map) response.get("features"); + final Map featureInfo = (Map) features.get("operator_privileges"); + assertTrue((boolean) featureInfo.get("available")); + if (isOperatorPrivilegesEnabled()) { + assertTrue((boolean) featureInfo.get("enabled")); + } + } + + @SuppressWarnings("unchecked") + private boolean isOperatorPrivilegesEnabled() throws IOException { + final Request getClusterSettingsRequest = new Request( + "GET", + "_cluster/settings?flat_settings&include_defaults&filter_path=defaults.*operator_privileges*" + ); + final Map settingsMap = entityAsMap(client().performRequest(getClusterSettingsRequest)); + final Map defaults = (Map) settingsMap.get("defaults"); + return Boolean.parseBoolean((String) defaults.get("xpack.security.operator_privileges.enabled")); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index d496756e19ee4..e5e0da103fbfc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -218,6 +218,7 @@ import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.operator.FileOperatorUsersStore; +import org.elasticsearch.xpack.security.operator.OperatorPrivilegesInfoTransportAction; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestClearApiKeyCacheAction; @@ -815,6 +816,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), new ActionHandler<>(DelegatePkiAuthenticationAction.INSTANCE, TransportDelegatePkiAuthenticationAction.class), + new ActionHandler<>(XPackInfoFeatureAction.OPERATOR_PRIVILEGES, OperatorPrivilegesInfoTransportAction.class), usageAction, infoAction); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 1830c4279b14c..472d74744980b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -187,15 +187,6 @@ public void authorize(final Authentication authentication, final String action, // if there is already an original action, that stays put (eg. the current action is a child action) putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action); - // Check operator privileges first if applicable - final ElasticsearchSecurityException operatorException - = operatorPrivilegesService.check(action, originalRequest, threadContext); - if (operatorException != null) { - // TODO: audit - listener.onFailure(denialException(authentication, action, operatorException)); - return; - } - String auditId = AuditUtil.extractRequestId(threadContext); if (auditId == null) { // We would like to assert that there is an existing request-id, but if this is a system action, then that might not be @@ -214,6 +205,16 @@ public void authorize(final Authentication authentication, final String action, // sometimes a request might be wrapped within another, which is the case for proxied // requests and concrete shard requests final TransportRequest unwrappedRequest = maybeUnwrapRequest(authentication, originalRequest, action, auditId); + + // Check operator privileges + // TODO: audit? + final ElasticsearchSecurityException operatorException = + operatorPrivilegesService.check(action, originalRequest, threadContext); + if (operatorException != null) { + listener.onFailure(denialException(authentication, action, operatorException)); + return; + } + if (SystemUser.is(authentication.getUser())) { // this never goes async so no need to wrap the listener authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java index 58e572cdc037b..a9d52c92d0ade 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java @@ -40,14 +40,14 @@ public class OperatorOnlyRegistry { "cluster:admin/autoscaling/get_autoscaling_capacity"); // This class is a prototype to showcase what it would look like for operator only settings - // It may not be included in phase 1 delivery. Also this may end up using Enum Property to + // It may NOT be included in phase 1 delivery. Also this may end up using Enum Property to // mark operator only settings instead of using the list here. public static final Set SIMPLE_SETTINGS = Set.of(IP_FILTER_ENABLED_HTTP_SETTING.getKey(), IP_FILTER_ENABLED_SETTING.getKey(), // TODO: Use literal strings due to dependency. Alternatively we can let each plugin publish names of operator settings "xpack.ml.max_machine_memory_percent", "xpack.ml.max_model_memory_limit"); // This map is just to showcase how "partial" operator-only API would work. - // It will not be included in phase 1 delivery. + // It will be REMOVED before phase 1 delivery. public static final Map>> PARAMETER_SENSITIVE_ACTIONS = Map.of(DeleteRepositoryAction.NAME, (request) -> { assert request instanceof DeleteRepositoryRequest; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesInfoTransportAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesInfoTransportAction.java new file mode 100644 index 0000000000000..013787c4d8066 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesInfoTransportAction.java @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.operator; + +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction; +import org.elasticsearch.xpack.core.action.XPackInfoFeatureTransportAction; + +import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; + +public class OperatorPrivilegesInfoTransportAction extends XPackInfoFeatureTransportAction { + + private final XPackLicenseState licenseState; + private final boolean enabled; + + @Inject + public OperatorPrivilegesInfoTransportAction(TransportService transportService, ActionFilters actionFilters, + Settings settings, XPackLicenseState licenseState) { + super(XPackInfoFeatureAction.OPERATOR_PRIVILEGES.name(), transportService, actionFilters); + this.licenseState = licenseState; + enabled = OPERATOR_PRIVILEGES_ENABLED.get(settings); + } + + @Override + protected String name() { + return XPackField.OPERATOR_PRIVILEGES; + } + + @Override + protected boolean available() { + return licenseState.isAllowed(XPackLicenseState.Feature.OPERATOR_PRIVILEGES); + } + + @Override + protected boolean enabled() { + return enabled; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 4e45ecefe62aa..d1c19bd7a7e66 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -153,6 +153,7 @@ import org.junit.Before; import org.mockito.ArgumentMatcher; import org.mockito.Matchers; +import org.mockito.Mockito; import java.io.IOException; import java.io.UncheckedIOException; @@ -167,6 +168,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Predicate; @@ -201,6 +203,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; public class AuthorizationServiceTests extends ESTestCase { @@ -212,7 +215,8 @@ public class AuthorizationServiceTests extends ESTestCase { private ThreadPool threadPool; private Map roleMap = new HashMap<>(); private CompositeRolesStore rolesStore; - private OperatorPrivileges operatorPrivileges; + private OperatorPrivileges.OperatorPrivilegesService operatorPrivilegesService; + private boolean shouldFailOperatorPrivilegesCheck = false; @SuppressWarnings("unchecked") @Before @@ -269,10 +273,11 @@ public void setup() { return Void.TYPE; }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), any(ActionListener.class)); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); + operatorPrivilegesService = mock(OperatorPrivileges.OperatorPrivilegesService.class); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), licenseState, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + operatorPrivilegesService); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -299,6 +304,16 @@ private void authorize(Authentication authentication, String action, TransportRe authorizationInfoHeader = mock(AuthorizationInfo.class); threadContext.putTransient(AUTHORIZATION_INFO_KEY, authorizationInfoHeader); } + Mockito.reset(operatorPrivilegesService); + final AtomicBoolean operatorPrivilegesChecked = new AtomicBoolean(false); + final ElasticsearchSecurityException operatorPrivilegesException = + new ElasticsearchSecurityException("Operator privileges check failed"); + if (shouldFailOperatorPrivilegesCheck) { + when(operatorPrivilegesService.check(action, request, threadContext)).thenAnswer(invocationOnMock -> { + operatorPrivilegesChecked.set(true); + return operatorPrivilegesException; + }); + } ActionListener listener = ActionListener.wrap(response -> { // extract the authorization transient headers from the thread context of the action // that has been authorized @@ -306,12 +321,16 @@ private void authorize(Authentication authentication, String action, TransportRe authorizationInfo.onResponse(threadContext.getTransient(AUTHORIZATION_INFO_KEY)); indicesPermissions.onResponse(threadContext.getTransient(INDICES_PERMISSIONS_KEY)); done.onResponse(threadContext.getTransient(someRandomHeader)); + assertNull(verify(operatorPrivilegesService).check(action, request, threadContext)); }, e -> { + if (shouldFailOperatorPrivilegesCheck && operatorPrivilegesChecked.get()) { + assertSame(operatorPrivilegesException, e.getCause()); + } done.onFailure(e); }); authorizationService.authorize(authentication, action, request, listener); - Object someRandonHeaderValueInListener = done.actionGet(); - assertThat(someRandonHeaderValueInListener, sameInstance(someRandomHeaderValue)); + Object someRandomHeaderValueInListener = done.actionGet(); + assertThat(someRandomHeaderValueInListener, sameInstance(someRandomHeaderValue)); assertThat(threadContext.getTransient(someRandomHeader), sameInstance(someRandomHeaderValue)); // authorization restores any previously existing transient headers if (mockAccessControlHeader != null) { @@ -917,7 +936,7 @@ public void testDenialForAnonymousUser() throws IOException { authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet(), new XPackLicenseState(settings, () -> 0), - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivilegesService); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -946,7 +965,7 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() throws IO authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, Collections.emptySet(), new XPackLicenseState(settings, () -> 0), - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivilegesService); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1688,7 +1707,7 @@ public void getUserPrivileges(Authentication authentication, AuthorizationInfo a authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), engine, Collections.emptySet(), licenseState, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), operatorPrivilegesService); Authentication authentication; try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { authentication = createAuthentication(new User("test user", "a_all")); @@ -1753,6 +1772,17 @@ auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap( } } + public void testOperatorPrivileges() { + shouldFailOperatorPrivilegesCheck = true; + AuditUtil.getOrGenerateRequestId(threadContext); + final Authentication authentication = createAuthentication(new User("user1", "role1")); + assertThrowsAuthorizationException( + () -> authorize(authentication, "cluster:admin/whatever", mock(TransportRequest.class)), + "cluster:admin/whatever", "user1"); + // The operator related exception is verified in the authorize(...) call + verifyZeroInteractions(auditTrail); + } + static AuthorizationInfo authzInfoRoles(String[] expectedRoles) { return Matchers.argThat(new RBACAuthorizationInfoRoleMatcher(expectedRoles)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java index 4cde194e6b92f..298571379c550 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java @@ -6,25 +6,36 @@ package org.elasticsearch.xpack.security.operator; -import org.elasticsearch.action.main.MainAction; import org.elasticsearch.test.ESTestCase; +import org.junit.Before; import java.util.function.Supplier; +import static org.hamcrest.Matchers.containsString; + public class OperatorOnlyRegistryTests extends ESTestCase { + private OperatorOnlyRegistry operatorOnlyRegistry; + + @Before + public void init() { + operatorOnlyRegistry = new OperatorOnlyRegistry(); + } + public void testSimpleOperatorOnlyApi() { - final OperatorOnlyRegistry operatorOnlyRegistry = new OperatorOnlyRegistry(); for (final String actionName : OperatorOnlyRegistry.SIMPLE_ACTIONS) { final Supplier messageSupplier = operatorOnlyRegistry.check(actionName, null); assertNotNull(messageSupplier); - assertNotNull(messageSupplier.get()); + assertThat(messageSupplier.get(), containsString("action [" + actionName + "]")); } } public void testNonOperatorOnlyApi() { - final OperatorOnlyRegistry operatorOnlyRegistry = new OperatorOnlyRegistry(); - assertNull(operatorOnlyRegistry.check(MainAction.NAME, null)); + final String actionName = randomValueOtherThanMany( + OperatorOnlyRegistry.SIMPLE_ACTIONS::contains, () -> randomAlphaOfLengthBetween(10, 40)); + assertNull(operatorOnlyRegistry.check(actionName, null)); } + // TODO: not tests for settings yet since it's not settled whether it will be part of phase 1 + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java index 75cb7d31d1ecc..9fd21f090fb86 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.junit.Before; +import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE; import static org.hamcrest.Matchers.containsString; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -109,4 +110,17 @@ public void testCheck() { assertNull(operatorPrivilegesService.check(nonOperatorAction, mock(TransportRequest.class), threadContext)); } + public void testNoOpService() { + final Authentication authentication = mock(Authentication.class); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + NOOP_OPERATOR_PRIVILEGES_SERVICE.maybeMarkOperatorUser(authentication, threadContext); + verifyZeroInteractions(authentication); + assertNull(threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY)); + + final TransportRequest request = mock(TransportRequest.class); + assertNull(NOOP_OPERATOR_PRIVILEGES_SERVICE.check( + randomAlphaOfLengthBetween(10, 20), request, threadContext)); + verifyZeroInteractions(request); + } + } From 1082b139f548520b4b790fbca6dace8c26410093 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 27 Nov 2020 09:09:56 +1100 Subject: [PATCH 16/23] fix test failures --- docs/reference/rest-api/info.asciidoc | 4 ++++ .../security/qa/operator-privileges-tests/build.gradle | 1 + .../xpack/security/operator/OperatorPrivilegesIT.java | 8 +++++--- .../java/org/elasticsearch/xpack/security/Security.java | 7 ++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/reference/rest-api/info.asciidoc b/docs/reference/rest-api/info.asciidoc index 5f4176b44069c..ff0abac56d0bb 100644 --- a/docs/reference/rest-api/info.asciidoc +++ b/docs/reference/rest-api/info.asciidoc @@ -103,6 +103,10 @@ Example response: "available" : true, "enabled" : true }, + "operator_privileges": { + "available": true, + "enabled": false + }, "rollup": { "available": true, "enabled": true diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle index ca9ffb9790625..3dfa64b57ef3c 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle +++ b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle @@ -22,6 +22,7 @@ boolean enableOperatorPrivileges = (new Random(Long.parseUnsignedLong(BuildParam testClusters.all { testDistribution = 'DEFAULT' + numberOfNodes = 3 extraConfigFile 'operator_users.yml', file('src/javaRestTest/resources/operator_users.yml') extraConfigFile 'roles.yml', file('src/javaRestTest/resources/roles.yml') diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index b46c97584f397..80460382dfed1 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -100,10 +100,12 @@ public void testOperatorPrivilegesXpackInfo() throws IOException { final Request xpackRequest = new Request("GET", "/_xpack"); final Map response = entityAsMap(client().performRequest(xpackRequest)); final Map features = (Map) response.get("features"); - final Map featureInfo = (Map) features.get("operator_privileges"); - assertTrue((boolean) featureInfo.get("available")); + final Map operatorPrivileges = (Map) features.get("operator_privileges"); + assertTrue((boolean) operatorPrivileges.get("available")); if (isOperatorPrivilegesEnabled()) { - assertTrue((boolean) featureInfo.get("enabled")); + assertTrue((boolean) operatorPrivileges.get("enabled")); + } else { + assertFalse((boolean) operatorPrivileges.get("enabled")); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index e5e0da103fbfc..8a6fbdfc58b08 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -771,8 +771,9 @@ public void onIndexModule(IndexModule module) { public List> getActions() { var usageAction = new ActionHandler<>(XPackUsageFeatureAction.SECURITY, SecurityUsageTransportAction.class); var infoAction = new ActionHandler<>(XPackInfoFeatureAction.SECURITY, SecurityInfoTransportAction.class); + var opInfoAction = new ActionHandler<>(XPackInfoFeatureAction.OPERATOR_PRIVILEGES, OperatorPrivilegesInfoTransportAction.class); if (enabled == false) { - return Arrays.asList(usageAction, infoAction); + return Arrays.asList(usageAction, infoAction, opInfoAction); } return Arrays.asList( new ActionHandler<>(ClearRealmCacheAction.INSTANCE, TransportClearRealmCacheAction.class), @@ -816,9 +817,9 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), new ActionHandler<>(DelegatePkiAuthenticationAction.INSTANCE, TransportDelegatePkiAuthenticationAction.class), - new ActionHandler<>(XPackInfoFeatureAction.OPERATOR_PRIVILEGES, OperatorPrivilegesInfoTransportAction.class), usageAction, - infoAction); + infoAction, + opInfoAction); } @Override From c3fa2bae4dd76fc02dbbd19564d5d10e27c76190 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 30 Nov 2020 22:54:42 +1100 Subject: [PATCH 17/23] Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java Co-authored-by: Tim Vernum --- .../xpack/security/operator/FileOperatorUsersStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java index 0d9311e6ff047..9a2cedcb7a44a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -210,7 +210,7 @@ public static OperatorUsersDescriptor parseFile(Path file, Logger logger) { try (InputStream in = Files.newInputStream(file, StandardOpenOption.READ)) { return parseConfig(in); } catch (IOException | RuntimeException e) { - logger.error("Failed to parse operator users file [" + file + "].", e); + logger.error(new ParameterizedMessage("Failed to parse operator users file [{}].", file), e); throw new ElasticsearchParseException("Error parsing operator users file [{}]", e, file.toAbsolutePath()); } } From 4cf5b0873ebe60edc70d6c263774bcf0ad78e73a Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 30 Nov 2020 08:46:38 +1100 Subject: [PATCH 18/23] wip --- .../xpack/security/operator/OperatorPrivileges.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java index 357f88a56a170..8a56df5ef2552 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -30,7 +30,7 @@ public interface OperatorPrivilegesService { /** * Check whether the user is an operator and whether the request is an operator-only. - * @return An exception if user is an non-operator and the request is operotor-only. Otherwise return null. + * @return An exception if user is an non-operator and the request is operator-only. Otherwise returns null. */ ElasticsearchSecurityException check(String action, TransportRequest request, ThreadContext threadContext); } From 973e7a5da693e247dbc61ba6b7c67daf91d0203a Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 30 Nov 2020 22:54:56 +1100 Subject: [PATCH 19/23] Address feedback --- .../qa/operator-privileges-tests/build.gradle | 10 +--- .../operator/OperatorPrivilegesIT.java | 33 +++-------- ...java => OperatorPrivilegesTestPlugin.java} | 2 +- ...=> OperatorPrivilegesTestPluginTests.java} | 4 +- .../OperatorPrivilegesSingleNodeTests.java | 20 ++----- .../operator/FileOperatorUsersStore.java | 4 +- .../operator/OperatorOnlyRegistry.java | 58 +------------------ .../security/operator/OperatorPrivileges.java | 8 +-- .../operator/OperatorOnlyRegistryTests.java | 8 +-- 9 files changed, 29 insertions(+), 118 deletions(-) rename x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/{OpTestPlugin.java => OperatorPrivilegesTestPlugin.java} (94%) rename x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/{OpTestPluginTests.java => OperatorPrivilegesTestPluginTests.java} (72%) diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle index 3dfa64b57ef3c..f4afcb9d30c58 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle +++ b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle @@ -1,12 +1,10 @@ -import org.elasticsearch.gradle.info.BuildParams - apply plugin: 'elasticsearch.esplugin' apply plugin: 'elasticsearch.java-rest-test' esplugin { - name 'op-test' + name 'operator-privileges-test' description 'An test plugin for testing hard to get internals' - classname 'org.elasticsearch.xpack.security.operator.OpTestPlugin' + classname 'org.elasticsearch.xpack.security.operator.OperatorPrivilegesTestPlugin' } dependencies { @@ -18,8 +16,6 @@ dependencies { javaRestTestImplementation project.sourceSets.main.runtimeClasspath } -boolean enableOperatorPrivileges = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() - testClusters.all { testDistribution = 'DEFAULT' numberOfNodes = 3 @@ -30,7 +26,7 @@ testClusters.all { setting 'xpack.license.self_generated.type', 'trial' setting 'xpack.security.enabled', 'true' setting 'xpack.security.http.ssl.enabled', 'false' - setting 'xpack.security.operator_privileges.enabled', enableOperatorPrivileges.toString() + setting 'xpack.security.operator_privileges.enabled', "true" user username: "test_admin", password: 'x-pack-test-password', role: "superuser" user username: "test_operator", password: 'x-pack-test-password', role: "limited_operator" diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java index 80460382dfed1..e4389a4f60721 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesIT.java @@ -36,16 +36,12 @@ protected Settings restClientSettings() { public void testNonOperatorSuperuserWillFailToCallOperatorOnlyApiWhenOperatorPrivilegesIsEnabled() throws IOException { final Request postVotingConfigExclusionsRequest = new Request("POST", "_cluster/voting_config_exclusions?node_names=foo"); - if (isOperatorPrivilegesEnabled()) { - final ResponseException responseException = expectThrows( - ResponseException.class, - () -> client().performRequest(postVotingConfigExclusionsRequest) - ); - assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat(responseException.getMessage(), containsString("Operator privileges are required for action")); - } else { - client().performRequest(postVotingConfigExclusionsRequest); - } + final ResponseException responseException = expectThrows( + ResponseException.class, + () -> client().performRequest(postVotingConfigExclusionsRequest) + ); + assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(responseException.getMessage(), containsString("Operator privileges are required for action")); } public void testOperatorUserWillSucceedToCallOperatorOnlyApi() throws IOException { @@ -102,21 +98,6 @@ public void testOperatorPrivilegesXpackInfo() throws IOException { final Map features = (Map) response.get("features"); final Map operatorPrivileges = (Map) features.get("operator_privileges"); assertTrue((boolean) operatorPrivileges.get("available")); - if (isOperatorPrivilegesEnabled()) { - assertTrue((boolean) operatorPrivileges.get("enabled")); - } else { - assertFalse((boolean) operatorPrivileges.get("enabled")); - } - } - - @SuppressWarnings("unchecked") - private boolean isOperatorPrivilegesEnabled() throws IOException { - final Request getClusterSettingsRequest = new Request( - "GET", - "_cluster/settings?flat_settings&include_defaults&filter_path=defaults.*operator_privileges*" - ); - final Map settingsMap = entityAsMap(client().performRequest(getClusterSettingsRequest)); - final Map defaults = (Map) settingsMap.get("defaults"); - return Boolean.parseBoolean((String) defaults.get("xpack.security.operator_privileges.enabled")); + assertTrue((boolean) operatorPrivileges.get("enabled")); } } diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OpTestPlugin.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTestPlugin.java similarity index 94% rename from x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OpTestPlugin.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTestPlugin.java index da1fb0d4b9583..49e273fd33031 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OpTestPlugin.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTestPlugin.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.function.Supplier; -public class OpTestPlugin extends Plugin implements ActionPlugin { +public class OperatorPrivilegesTestPlugin extends Plugin implements ActionPlugin { @Override public List getRestHandlers( diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OpTestPluginTests.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTestPluginTests.java similarity index 72% rename from x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OpTestPluginTests.java rename to x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTestPluginTests.java index 7fb3ecc40ad78..509dd4757f659 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OpTestPluginTests.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTestPluginTests.java @@ -9,10 +9,10 @@ import org.elasticsearch.test.ESTestCase; // This test class is really to pass the testingConventions test -public class OpTestPluginTests extends ESTestCase { +public class OperatorPrivilegesTestPluginTests extends ESTestCase { public void testPluginWillInstantiate() { - final OpTestPlugin opTestPlugin = new OpTestPlugin(); + final OperatorPrivilegesTestPlugin operatorPrivilegesTestPlugin = new OperatorPrivilegesTestPlugin(); } } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java index 5740fc7251377..5aada3954e96a 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesSingleNodeTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.xpack.core.security.action.user.GetUsersAction; import org.elasticsearch.xpack.core.security.action.user.GetUsersRequest; -import org.junit.BeforeClass; import java.util.Map; @@ -28,13 +27,6 @@ public class OperatorPrivilegesSingleNodeTests extends SecuritySingleNodeTestCas private static final String OPERATOR_USER_NAME = "test_operator"; - private static boolean OPERATOR_PRIVILEGES_ENABLED; - - @BeforeClass - public static void randomOperatorPrivilegesEnabled() { - OPERATOR_PRIVILEGES_ENABLED = randomBoolean(); - } - @Override protected String configUsers() { return super.configUsers() @@ -67,20 +59,16 @@ protected String configOperatorUsers() { protected Settings nodeSettings() { Settings.Builder builder = Settings.builder().put(super.nodeSettings()); // Ensure the new settings can be configured - builder.put("xpack.security.operator_privileges.enabled", OPERATOR_PRIVILEGES_ENABLED); + builder.put("xpack.security.operator_privileges.enabled", "true"); return builder.build(); } public void testOutcomeOfSuperuserPerformingOperatorOnlyActionWillDependOnWhetherFeatureIsEnabled() { final Client client = client(); final ClearVotingConfigExclusionsRequest clearVotingConfigExclusionsRequest = new ClearVotingConfigExclusionsRequest(); - if (OPERATOR_PRIVILEGES_ENABLED) { - final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet()); - assertThat(e.getCause().getMessage(), containsString("Operator privileges are required for action")); - } else { - client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet(); - } + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> client.execute(ClearVotingConfigExclusionsAction.INSTANCE, clearVotingConfigExclusionsRequest).actionGet()); + assertThat(e.getCause().getMessage(), containsString("Operator privileges are required for action")); } public void testOperatorUserWillSucceedToCallOperatorOnlyAction() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java index 9a2cedcb7a44a..f6a10775d5eb1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -12,6 +12,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.DeprecationHandler; @@ -156,7 +157,8 @@ private void validate() { if (realmName == null) { if (false == SINGLETON_REALM_TYPES.contains(realmType)) { validationException.addValidationError( - "[realm_name] must be specified for realm types other than [reserved], [file] and [native]"); + "[realm_name] must be specified for realm types other than [" + + Strings.collectionToCommaDelimitedString(SINGLETON_REALM_TYPES) + "]"); } } if (false == validationException.validationErrors().isEmpty()) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java index a9d52c92d0ade..ab1370008977a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java @@ -8,24 +8,11 @@ import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction; import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction; -import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryAction; -import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.license.DeleteLicenseAction; import org.elasticsearch.license.PutLicenseAction; import org.elasticsearch.transport.TransportRequest; -import java.util.HashSet; -import java.util.Map; import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; - -import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_HTTP_SETTING; -import static org.elasticsearch.xpack.security.transport.filter.IPFilter.IP_FILTER_ENABLED_SETTING; public class OperatorOnlyRegistry { @@ -39,59 +26,20 @@ public class OperatorOnlyRegistry { "cluster:admin/autoscaling/get_autoscaling_policy", "cluster:admin/autoscaling/get_autoscaling_capacity"); - // This class is a prototype to showcase what it would look like for operator only settings - // It may NOT be included in phase 1 delivery. Also this may end up using Enum Property to - // mark operator only settings instead of using the list here. - public static final Set SIMPLE_SETTINGS = Set.of(IP_FILTER_ENABLED_HTTP_SETTING.getKey(), IP_FILTER_ENABLED_SETTING.getKey(), - // TODO: Use literal strings due to dependency. Alternatively we can let each plugin publish names of operator settings - "xpack.ml.max_machine_memory_percent", "xpack.ml.max_model_memory_limit"); - - // This map is just to showcase how "partial" operator-only API would work. - // It will be REMOVED before phase 1 delivery. - public static final Map>> PARAMETER_SENSITIVE_ACTIONS = - Map.of(DeleteRepositoryAction.NAME, (request) -> { - assert request instanceof DeleteRepositoryRequest; - final DeleteRepositoryRequest deleteRepositoryRequest = (DeleteRepositoryRequest) request; - if ("found-snapshots".equals(deleteRepositoryRequest.name())) { - return () -> "action [" + DeleteRepositoryAction.NAME + "] with repository [" + deleteRepositoryRequest.name(); - } else { - return null; - } - }); - // The return type is a bit weird, but it is a shortcut to avoid having to use either // a Tuple or a new class to hold true/false and a message/null. // Since the combination is either true+message or false+null, it is possible to just // use the existence of the message to also indicate whether the result is true or false. - public Supplier check(String action, TransportRequest request) { + public OperatorPrivilegesViolation check(String action, TransportRequest request) { if (SIMPLE_ACTIONS.contains(action)) { return () -> "action [" + action + "]"; - } else if (PARAMETER_SENSITIVE_ACTIONS.containsKey(action)) { - return PARAMETER_SENSITIVE_ACTIONS.get(action).apply(request); - } else if (ClusterUpdateSettingsAction.NAME.equals(action)) { - assert request instanceof ClusterUpdateSettingsRequest; - final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = (ClusterUpdateSettingsRequest) request; - return checkSettings(clusterUpdateSettingsRequest); } else { return null; } } - private Supplier checkSettings(ClusterUpdateSettingsRequest clusterUpdateSettingsRequest) { - final boolean hasEmptyIntersection = Sets.haveEmptyIntersection( - SIMPLE_SETTINGS, clusterUpdateSettingsRequest.persistentSettings().keySet()) - && Sets.haveEmptyIntersection(SIMPLE_SETTINGS, clusterUpdateSettingsRequest.transientSettings().keySet()); - - if (hasEmptyIntersection) { - return null; - } else { - final HashSet requestedSettings = new HashSet<>(clusterUpdateSettingsRequest.persistentSettings().keySet()); - requestedSettings.addAll(clusterUpdateSettingsRequest.transientSettings().keySet()); - requestedSettings.retainAll(SIMPLE_SETTINGS); - return () -> requestedSettings.size() > 1 ? - "settings" : - "setting" + " [" + Strings.collectionToCommaDelimitedString(requestedSettings) + "]"; - } + public interface OperatorPrivilegesViolation { + String message(); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java index 8a56df5ef2552..7cb7a7c5ef944 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -14,8 +14,6 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import java.util.function.Supplier; - public class OperatorPrivileges { public static final Setting OPERATOR_PRIVILEGES_ENABLED = @@ -66,9 +64,9 @@ public ElasticsearchSecurityException check(String action, TransportRequest requ if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { // Only check whether request is operator only if user is not an operator - final Supplier messageSupplier = operatorOnlyRegistry.check(action, request); - if (messageSupplier != null) { - return new ElasticsearchSecurityException("Operator privileges are required for " + messageSupplier.get()); + final OperatorOnlyRegistry.OperatorPrivilegesViolation violation = operatorOnlyRegistry.check(action, request); + if (violation != null) { + return new ElasticsearchSecurityException("Operator privileges are required for " + violation.message()); } } return null; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java index 298571379c550..71ce0aa488c2a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistryTests.java @@ -9,8 +9,6 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.function.Supplier; - import static org.hamcrest.Matchers.containsString; public class OperatorOnlyRegistryTests extends ESTestCase { @@ -24,9 +22,9 @@ public void init() { public void testSimpleOperatorOnlyApi() { for (final String actionName : OperatorOnlyRegistry.SIMPLE_ACTIONS) { - final Supplier messageSupplier = operatorOnlyRegistry.check(actionName, null); - assertNotNull(messageSupplier); - assertThat(messageSupplier.get(), containsString("action [" + actionName + "]")); + final OperatorOnlyRegistry.OperatorPrivilegesViolation violation = operatorOnlyRegistry.check(actionName, null); + assertNotNull(violation); + assertThat(violation.message(), containsString("action [" + actionName + "]")); } } From 2ae1bef7a0e39fe51d2d9c5e0cc150f4acb00a1d Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 30 Nov 2020 22:59:32 +1100 Subject: [PATCH 20/23] Fix import --- .../xpack/security/operator/FileOperatorUsersStore.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java index f6a10775d5eb1..ce86728d787b9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Nullable; From 91d2ef0b150150e69cc4b8b7a9c3c352e3663fa6 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 1 Dec 2020 09:10:39 +1100 Subject: [PATCH 21/23] Tweak --- .../security/operator/FileOperatorUsersStore.java | 9 ++++++++- .../xpack/security/operator/OperatorOnlyRegistry.java | 10 +++++----- .../xpack/security/operator/OperatorPrivileges.java | 8 +++++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java index ce86728d787b9..2b4ae50b106fe 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -116,6 +116,11 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(groups); } + + @Override + public String toString() { + return "OperatorUsersDescriptor{" + "groups=" + groups + '}'; + } } private static final OperatorUsersDescriptor EMPTY_OPERATOR_USERS_DESCRIPTOR = new OperatorUsersDescriptor(List.of()); @@ -221,7 +226,9 @@ public static OperatorUsersDescriptor parseFile(Path file, Logger logger) { public static OperatorUsersDescriptor parseConfig(InputStream in) throws IOException { try (XContentParser parser = yamlParser(in)) { - return OPERATOR_USER_PARSER.parse(parser, null); + final OperatorUsersDescriptor operatorUsersDescriptor = OPERATOR_USER_PARSER.parse(parser, null); + logger.trace("Parsed: [{}]", operatorUsersDescriptor); + return operatorUsersDescriptor; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java index ab1370008977a..5f449d3632d3f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java @@ -26,10 +26,11 @@ public class OperatorOnlyRegistry { "cluster:admin/autoscaling/get_autoscaling_policy", "cluster:admin/autoscaling/get_autoscaling_capacity"); - // The return type is a bit weird, but it is a shortcut to avoid having to use either - // a Tuple or a new class to hold true/false and a message/null. - // Since the combination is either true+message or false+null, it is possible to just - // use the existence of the message to also indicate whether the result is true or false. + /** + * Check whether the given action and request qualify as operator-only. The method returns + * null if the action+request is NOT operator-only. Other it returns a violation object + * that contains the message for details. + */ public OperatorPrivilegesViolation check(String action, TransportRequest request) { if (SIMPLE_ACTIONS.contains(action)) { return () -> "action [" + action + "]"; @@ -42,4 +43,3 @@ public interface OperatorPrivilegesViolation { String message(); } } - diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java index 7cb7a7c5ef944..98083b20ad54e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorPrivileges.java @@ -6,6 +6,8 @@ package org.elasticsearch.xpack.security.operator; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -16,6 +18,8 @@ public class OperatorPrivileges { + private static final Logger logger = LogManager.getLogger(OperatorPrivileges.class); + public static final Setting OPERATOR_PRIVILEGES_ENABLED = Setting.boolSetting("xpack.security.operator_privileges.enabled", false, Setting.Property.NodeScope); @@ -53,6 +57,7 @@ public void maybeMarkOperatorUser(Authentication authentication, ThreadContext t return; } if (fileOperatorUsersStore.isOperatorUser(authentication)) { + logger.trace("User [{}] is an operator", authentication.getUser().principal()); threadContext.putHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY, AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR); } } @@ -63,7 +68,8 @@ public ElasticsearchSecurityException check(String action, TransportRequest requ } if (false == AuthenticationField.PRIVILEGE_CATEGORY_VALUE_OPERATOR.equals( threadContext.getHeader(AuthenticationField.PRIVILEGE_CATEGORY_KEY))) { - // Only check whether request is operator only if user is not an operator + // Only check whether request is operator-only when user is NOT an operator + logger.trace("Checking operator-only violation for: action [{}]", action); final OperatorOnlyRegistry.OperatorPrivilegesViolation violation = operatorOnlyRegistry.check(action, request); if (violation != null) { return new ElasticsearchSecurityException("Operator privileges are required for " + violation.message()); From 6bddcff16232142d958ca17ac50644f21fdccdf1 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 1 Dec 2020 10:24:37 +1100 Subject: [PATCH 22/23] fix test --- .../xpack/security/operator/OperatorPrivilegesTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java index 9fd21f090fb86..d51f18377cd7b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/operator/OperatorPrivilegesTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.DefaultOperatorPrivilegesService; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.junit.Before; @@ -65,6 +66,7 @@ public void testMarkOperatorUser() { when(xPackLicenseState.checkFeature(XPackLicenseState.Feature.OPERATOR_PRIVILEGES)).thenReturn(true); final Authentication operatorAuth = mock(Authentication.class); final Authentication nonOperatorAuth = mock(Authentication.class); + when(operatorAuth.getUser()).thenReturn(new User("operator_user")); when(fileOperatorUsersStore.isOperatorUser(operatorAuth)).thenReturn(true); when(fileOperatorUsersStore.isOperatorUser(nonOperatorAuth)).thenReturn(false); From e7042f90b146b48791cd07fc3f2711f918ad7197 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 3 Dec 2020 15:31:08 +1100 Subject: [PATCH 23/23] Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java Co-authored-by: Tim Vernum --- .../xpack/security/operator/OperatorOnlyRegistry.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java index 5f449d3632d3f..b3b633199d8f8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/OperatorOnlyRegistry.java @@ -39,6 +39,7 @@ public OperatorPrivilegesViolation check(String action, TransportRequest request } } + @FunctionalInterface public interface OperatorPrivilegesViolation { String message(); }