diff --git a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutDatafeedAction.java b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutDatafeedAction.java index fdf78b6d294..bd59da03baa 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutDatafeedAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/ml/action/PutDatafeedAction.java @@ -42,6 +42,7 @@ import org.elasticsearch.xpack.XPackPlugin; import org.elasticsearch.xpack.XPackSettings; import org.elasticsearch.xpack.ml.MlMetadata; import org.elasticsearch.xpack.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.security.SecurityContext; import org.elasticsearch.xpack.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.security.action.user.HasPrivilegesRequest; import org.elasticsearch.xpack.security.action.user.HasPrivilegesResponse; @@ -193,6 +194,7 @@ public class PutDatafeedAction extends Action privResponseListener = ActionListener.wrap( - r -> handlePrivsResponse(runAsUser, request, r, listener), + r -> handlePrivsResponse(username, request, r, listener), listener::onFailure); HasPrivilegesRequest privRequest = new HasPrivilegesRequest(); - privRequest.username(runAsUser); + privRequest.username(username); privRequest.clusterPrivileges(Strings.EMPTY_ARRAY); // We just check for permission to use the search action. In reality we'll also // use the scroll action, but that's considered an implementation detail. diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/SecurityContext.java b/plugin/src/main/java/org/elasticsearch/xpack/security/SecurityContext.java index 677ac78bbb8..1313fee7b38 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/SecurityContext.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/SecurityContext.java @@ -63,16 +63,19 @@ public class SecurityContext { */ void setUser(User user, Version version) { Objects.requireNonNull(user); + final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef("__attach", "__attach", nodeName); final Authentication.RealmRef lookedUpBy; - if (user.runAs() == null) { - lookedUpBy = null; + if (user.isRunAs()) { + lookedUpBy = authenticatedBy; } else { - lookedUpBy = new Authentication.RealmRef("__attach", "__attach", nodeName); + lookedUpBy = null; } + setAuthentication(new Authentication(user, authenticatedBy, lookedUpBy, version)); + } + /** Writes the authentication to the thread context */ + private void setAuthentication(Authentication authentication) { try { - Authentication authentication = - new Authentication(user, new Authentication.RealmRef("__attach", "__attach", nodeName), lookedUpBy, version); authentication.writeToContext(threadContext); } catch (IOException e) { throw new AssertionError("how can we have a IOException with a user we set", e); @@ -80,7 +83,7 @@ public class SecurityContext { } /** - * Runs the consumer in a new context as the provided user. The original constext is provided to the consumer. When this method + * Runs the consumer in a new context as the provided user. The original context is provided to the consumer. When this method * returns, the original context is restored. */ public void executeAsUser(User user, Consumer consumer, Version version) { @@ -90,4 +93,18 @@ public class SecurityContext { consumer.accept(original); } } + + /** + * Runs the consumer in a new context after setting a new version of the authentication that is compatible with the version provided. + * The original context is provided to the consumer. When this method returns, the original context is restored. + */ + public void executeAfterRewritingAuthentication(Consumer consumer, Version version) { + final StoredContext original = threadContext.newStoredContext(true); + final Authentication authentication = Objects.requireNonNull(getAuthentication()); + try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { + setAuthentication(new Authentication(authentication.getUser().authenticatedUser(), authentication.getAuthenticatedBy(), + authentication.getLookedUpBy(), version)); + consumer.accept(original); + } + } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java index 57c06f9404a..c4bd170e031 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java @@ -34,16 +34,16 @@ public class TransportAuthenticateAction extends HandledTransportAction listener) { - final User user = securityContext.getUser(); - if (SystemUser.is(user) || XPackUser.is(user)) { - listener.onFailure(new IllegalArgumentException("user [" + user.principal() + "] is internal")); - return; - } - - if (user == null) { + final User runAsUser = securityContext.getUser(); + final User authUser = runAsUser == null ? null : runAsUser.authenticatedUser(); + if (authUser == null) { listener.onFailure(new ElasticsearchSecurityException("did not find an authenticated user")); - return; + } else if (SystemUser.is(authUser) || XPackUser.is(authUser)) { + listener.onFailure(new IllegalArgumentException("user [" + authUser.principal() + "] is internal")); + } else if (SystemUser.is(runAsUser) || XPackUser.is(runAsUser)) { + listener.onFailure(new IllegalArgumentException("user [" + runAsUser.principal() + "] is internal")); + } else { + listener.onResponse(new AuthenticateResponse(runAsUser)); } - listener.onResponse(new AuthenticateResponse(user)); } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index 08c7ca7cbbe..78bd0ffec31 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -56,13 +56,13 @@ public class TransportHasPrivilegesAction extends HandledTransportAction listener) { final String username = request.username(); - final User runAsUser = Authentication.getAuthentication(threadPool.getThreadContext()).getRunAsUser(); - if (runAsUser.principal().equals(username) == false) { + final User user = Authentication.getAuthentication(threadPool.getThreadContext()).getUser(); + if (user.principal().equals(username) == false) { listener.onFailure(new IllegalArgumentException("users may only check the privileges of their own account")); return; } - authorizationService.roles(runAsUser, ActionListener.wrap( + authorizationService.roles(user, ActionListener.wrap( role -> checkPrivileges(request, role, listener), listener::onFailure)); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledAction.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledAction.java index 0519a308d51..3bed8c63cc2 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledAction.java @@ -39,7 +39,7 @@ public class TransportSetEnabledAction extends HandledTransportAction listener) { final String username = request.username(); // make sure the user is not disabling themselves - if (Authentication.getAuthentication(threadPool.getThreadContext()).getRunAsUser().principal().equals(request.username())) { + if (Authentication.getAuthentication(threadPool.getThreadContext()).getUser().principal().equals(request.username())) { listener.onFailure(new IllegalArgumentException("users may not update the enabled status of their own account")); return; } else if (SystemUser.NAME.equals(username) || XPackUser.NAME.equals(username)) { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java b/plugin/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java index ffb934b77a2..464d78a6cac 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java @@ -602,13 +602,14 @@ public class IndexAuditTrail extends AbstractComponent implements AuditTrail, Cl msg.builder.field(Field.ACTION, action); } if (user != null) { - if (user.runAs() != null) { + if (user.isRunAs()) { if ("run_as_granted".equals(type) || "run_as_denied".equals(type)) { - msg.builder.field(Field.PRINCIPAL, user.principal()); - msg.builder.field(Field.RUN_AS_PRINCIPAL, user.runAs().principal()); + msg.builder.field(Field.PRINCIPAL, user.authenticatedUser().principal()); + msg.builder.field(Field.RUN_AS_PRINCIPAL, user.principal()); } else { - msg.builder.field(Field.PRINCIPAL, user.runAs().principal()); - msg.builder.field(Field.RUN_BY_PRINCIPAL, user.principal()); + // TODO: this doesn't make sense... + msg.builder.field(Field.PRINCIPAL, user.principal()); + msg.builder.field(Field.RUN_BY_PRINCIPAL, user.authenticatedUser().principal()); } } else { msg.builder.field(Field.PRINCIPAL, user.principal()); @@ -691,9 +692,9 @@ public class IndexAuditTrail extends AbstractComponent implements AuditTrail, Cl common("rest", type, msg.builder); if (user != null) { - if (user.runAs() != null) { - msg.builder.field(Field.PRINCIPAL, user.runAs().principal()); - msg.builder.field(Field.RUN_BY_PRINCIPAL, user.principal()); + if (user.isRunAs()) { + msg.builder.field(Field.PRINCIPAL, user.principal()); + msg.builder.field(Field.RUN_BY_PRINCIPAL, user.authenticatedUser().principal()); } else { msg.builder.field(Field.PRINCIPAL, user.principal()); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/plugin/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index 2a89f000b1d..762519ffe19 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -350,8 +350,8 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail { public void runAsGranted(User user, String action, TransportMessage message) { if (events.contains(RUN_AS_GRANTED)) { logger.info("{}[transport] [run_as_granted]\t{}, principal=[{}], run_as_principal=[{}], action=[{}], request=[{}]", - getPrefix(), originAttributes(message, clusterService.localNode(), threadContext), user.principal(), - user.runAs().principal(), action, message.getClass().getSimpleName()); + getPrefix(), originAttributes(message, clusterService.localNode(), threadContext), user.authenticatedUser().principal(), + user.principal(), action, message.getClass().getSimpleName()); } } @@ -359,8 +359,8 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail { public void runAsDenied(User user, String action, TransportMessage message) { if (events.contains(RUN_AS_DENIED)) { logger.info("{}[transport] [run_as_denied]\t{}, principal=[{}], run_as_principal=[{}], action=[{}], request=[{}]", - getPrefix(), originAttributes(message, clusterService.localNode(), threadContext), user.principal(), - user.runAs().principal(), action, message.getClass().getSimpleName()); + getPrefix(), originAttributes(message, clusterService.localNode(), threadContext), user.authenticatedUser().principal(), + user.principal(), action, message.getClass().getSimpleName()); } } @@ -447,10 +447,11 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail { static String principal(User user) { StringBuilder builder = new StringBuilder("principal=["); - if (user.runAs() != null) { - builder.append(user.runAs().principal()).append("], run_by_principal=["); + builder.append(user.principal()); + if (user.isRunAs()) { + builder.append("], run_by_principal=[").append(user.authenticatedUser().principal()); } - return builder.append(user.principal()).append("]").toString(); + return builder.append("]").toString(); } public static void registerSettings(List> settings) { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Authentication.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Authentication.java index d92c8b8617b..cfec9e644bb 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Authentication.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Authentication.java @@ -52,22 +52,6 @@ public class Authentication { return user; } - // TODO remove run as from the User object... - public User getRunAsUser() { - if (user.runAs() != null) { - return user.runAs(); - } - return user; - } - - /** - * returns true if this authentication represents a authentication object with a authenticated user that is different than the user the - * request should be run as - */ - public boolean isRunAs() { - return getUser().equals(getRunAsUser()) == false; - } - public RealmRef getAuthenticatedBy() { return authenticatedBy; } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 0385678f42f..cb80396eef0 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -324,13 +324,7 @@ public class AuthenticationService extends AbstractComponent { Runnable action; if (authentication != null) { - try { - authentication.writeToContext(threadContext); - request.authenticationSuccess(authentication.getAuthenticatedBy().getName(), authentication.getUser()); - action = () -> listener.onResponse(authentication); - } catch (Exception e) { - action = () -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken)); - } + action = () -> writeAuthToContext(authentication); } else { action = () -> listener.onFailure(request.anonymousAccessDenied()); } @@ -365,8 +359,7 @@ public class AuthenticationService extends AbstractComponent { } else { assert runAsUsername.isEmpty() : "the run as username may not be empty"; logger.debug("user [{}] attempted to runAs with an empty username", user.principal()); - listener.onFailure(request.runAsDenied(new User(user.principal(), user.roles(), - new User(runAsUsername, Strings.EMPTY_ARRAY)), authenticationToken)); + listener.onFailure(request.runAsDenied(new User(runAsUsername, null, user), authenticationToken)); } } else { finishAuthentication(user); @@ -392,7 +385,14 @@ public class AuthenticationService extends AbstractComponent { }, lookupUserListener::onFailure)); final IteratingActionListener userLookupListener = - new IteratingActionListener<>(ActionListener.wrap((lookupUser) -> userConsumer.accept(new User(user, lookupUser)), + new IteratingActionListener<>(ActionListener.wrap((lookupUser) -> { + if (lookupUser == null) { + // the user does not exist, but we still create a User object, which will later be rejected by authz + userConsumer.accept(new User(runAsUsername, null, user)); + } else { + userConsumer.accept(new User(lookupUser, user)); + } + }, (e) -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken))), realmLookupConsumer, realmsList, threadContext); try { @@ -407,7 +407,8 @@ public class AuthenticationService extends AbstractComponent { * one. If authentication is successful, this method also ensures that the authentication is written to the ThreadContext */ void finishAuthentication(User finalUser) { - if (finalUser.enabled() == false || (finalUser.runAs() != null && finalUser.runAs().enabled() == false)) { + if (finalUser.enabled() == false || finalUser.authenticatedUser().enabled() == false) { + // TODO: these should be different log messages if the runas vs auth user is disabled? logger.debug("user [{}] is disabled. failing authentication", finalUser); listener.onFailure(request.authenticationFailed(authenticationToken)); } else { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 4cdbd9da6f9..6afe343528d 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -133,7 +133,7 @@ public class AuthorizationService extends AbstractComponent { setOriginatingAction(action); // first we need to check if the user is the system. If it is, we'll just authorize the system access - if (SystemUser.is(authentication.getRunAsUser())) { + if (SystemUser.is(authentication.getUser())) { if (SystemUser.isAuthorized(action) && SystemUser.is(authentication.getUser())) { setIndicesAccessControl(IndicesAccessControl.ALLOW_ALL); grant(authentication, action, request); @@ -146,13 +146,13 @@ public class AuthorizationService extends AbstractComponent { Role permission = userRole; // check if the request is a run as request - final boolean isRunAs = authentication.isRunAs(); + final boolean isRunAs = authentication.getUser().isRunAs(); if (isRunAs) { // if we are running as a user we looked up then the authentication must contain a lookedUpBy. If it doesn't then this user // doesn't really exist but the authc service allowed it through to avoid leaking users that exist in the system if (authentication.getLookedUpBy() == null) { throw denyRunAs(authentication, action, request); - } else if (permission.runAs().check(authentication.getRunAsUser().principal())) { + } else if (permission.runAs().check(authentication.getUser().principal())) { grantRunAs(authentication, action, request); permission = runAsRole; } else { @@ -203,7 +203,7 @@ public class AuthorizationService extends AbstractComponent { // we only want the xpack user to use the xpack delete by query action if (XPackDeleteByQueryAction.NAME.equals(action) - && XPackUser.is(authentication.getRunAsUser()) == false) { + && XPackUser.is(authentication.getUser()) == false) { throw denial(authentication, action, request); } @@ -230,7 +230,7 @@ public class AuthorizationService extends AbstractComponent { } MetaData metaData = clusterService.state().metaData(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices(authentication.getRunAsUser(), permission, action, metaData); + AuthorizedIndices authorizedIndices = new AuthorizedIndices(authentication.getUser(), permission, action, metaData); Set indexNames = resolveIndexNames(authentication, action, request, metaData, authorizedIndices); assert !indexNames.isEmpty() : "every indices request needs to have its indices set thus the resolved indices must not be empty"; @@ -247,13 +247,13 @@ public class AuthorizationService extends AbstractComponent { throw denial(authentication, action, request); } else if (indicesAccessControl.getIndexPermissions(SecurityLifecycleService.SECURITY_INDEX_NAME) != null && indicesAccessControl.getIndexPermissions(SecurityLifecycleService.SECURITY_INDEX_NAME).isGranted() - && XPackUser.is(authentication.getRunAsUser()) == false + && XPackUser.is(authentication.getUser()) == false && MONITOR_INDEX_PREDICATE.test(action) == false - && isSuperuser(authentication.getRunAsUser()) == false) { + && isSuperuser(authentication.getUser()) == false) { // only the XPackUser is allowed to work with this index, but we should allow indices monitoring actions through for debugging // purposes. These monitor requests also sometimes resolve indices concretely and then requests them logger.debug("user [{}] attempted to directly perform [{}] against the security index [{}]", - authentication.getRunAsUser().principal(), action, SecurityLifecycleService.SECURITY_INDEX_NAME); + authentication.getUser().principal(), action, SecurityLifecycleService.SECURITY_INDEX_NAME); throw denial(authentication, action, request); } else { setIndicesAccessControl(indicesAccessControl); @@ -380,7 +380,7 @@ public class AuthorizationService extends AbstractComponent { return false; } final String username = usernames[0]; - final boolean sameUsername = authentication.getRunAsUser().principal().equals(username); + final boolean sameUsername = authentication.getUser().principal().equals(username); if (sameUsername && ChangePasswordAction.NAME.equals(action)) { return checkChangePasswordAction(authentication); } @@ -396,7 +396,7 @@ public class AuthorizationService extends AbstractComponent { // we need to verify that this user was authenticated by or looked up by a realm type that support password changes // otherwise we open ourselves up to issues where a user in a different realm could be created with the same username // and do malicious things - final boolean isRunAs = authentication.isRunAs(); + final boolean isRunAs = authentication.getUser().isRunAs(); final String realmType; if (isRunAs) { realmType = authentication.getLookedUpBy().getType(); @@ -429,19 +429,19 @@ public class AuthorizationService extends AbstractComponent { } private ElasticsearchSecurityException denialException(Authentication authentication, String action) { - final User user = authentication.getUser(); + final User authUser = authentication.getUser().authenticatedUser(); // Special case for anonymous user - if (isAnonymousEnabled && anonymousUser.equals(user)) { + if (isAnonymousEnabled && anonymousUser.equals(authUser)) { if (anonymousAuthzExceptionEnabled == false) { throw authcFailureHandler.authenticationRequired(action, threadContext); } } // check for run as - if (user != authentication.getRunAsUser()) { - return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", action, user.principal(), - authentication.getRunAsUser().principal()); + if (authentication.getUser().isRunAs()) { + return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", action, authUser.principal(), + authentication.getUser().principal()); } - return authorizationError("action [{}] is unauthorized for user [{}]", action, user.principal()); + return authorizationError("action [{}] is unauthorized for user [{}]", action, authUser.principal()); } static boolean isSuperuser(User user) { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java index b72d4dc2c9e..ef43da20eb0 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java @@ -82,14 +82,14 @@ public final class AuthorizationUtils { } public void authorize(AuthorizationService service) { - if (SystemUser.is(authentication.getUser())) { + if (SystemUser.is(authentication.getUser().authenticatedUser())) { + assert authentication.getUser().isRunAs() == false; setUserRoles(null); // we can inform the listener immediately - nothing to fetch for us on system user setRunAsRoles(null); } else { - service.roles(authentication.getUser(), ActionListener.wrap(this::setUserRoles, listener::onFailure)); - if (authentication.isRunAs()) { - assert authentication.getRunAsUser() != null : "runAs user is null but shouldn't"; - service.roles(authentication.getRunAsUser(), ActionListener.wrap(this::setRunAsRoles, listener::onFailure)); + service.roles(authentication.getUser().authenticatedUser(), ActionListener.wrap(this::setUserRoles, listener::onFailure)); + if (authentication.getUser().isRunAs()) { + service.roles(authentication.getUser(), ActionListener.wrap(this::setRunAsRoles, listener::onFailure)); } else { setRunAsRoles(null); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java b/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java index 4fa2e5b1b17..379ffd3c835 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java @@ -49,7 +49,7 @@ public class RestAuthenticateAction extends SecurityBaseRestHandler { if (user == null) { return restChannel -> { throw new IllegalStateException("we should never have a null user and invoke this consumer"); }; } - final String username = user.runAs() == null ? user.principal() : user.runAs().principal(); + final String username = user.principal(); return channel -> client.execute(AuthenticateAction.INSTANCE, new AuthenticateRequest(username), new RestBuilderListener(channel) { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java b/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java index c8ac73afcf2..b3bfb02802b 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java @@ -48,7 +48,7 @@ public class RestChangePasswordAction extends SecurityBaseRestHandler implements final User user = securityContext.getUser(); final String username; if (request.param("username") == null) { - username = user.runAs() == null ? user.principal() : user.runAs().principal(); + username = user.principal(); } else { username = request.param("username"); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java b/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java index 25be18e398e..ab36fd9544c 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java @@ -59,12 +59,7 @@ public class RestHasPrivilegesAction extends SecurityBaseRestHandler { if (username != null) { return username; } - final User user = securityContext.getUser(); - if (user.runAs() != null) { - return user.runAs().principal(); - } else { - return user.principal(); - } + return securityContext.getUser().principal(); } static class HasPrivilegesRestResponseBuilder extends RestBuilderListener { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index 8c6ee31c6fc..258ac1fa1f9 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -27,6 +27,7 @@ import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportResponse; import org.elasticsearch.transport.TransportResponseHandler; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.TransportService.ContextRestoreResponseHandler; import org.elasticsearch.transport.TransportSettings; import org.elasticsearch.xpack.XPackSettings; import org.elasticsearch.xpack.security.SecurityContext; @@ -91,7 +92,7 @@ public class SecurityServerTransportInterceptor extends AbstractComponent implem // which means that the user is copied over to system actions so we need to change the user if (AuthorizationUtils.shouldReplaceUserWithSystem(threadPool.getThreadContext(), action)) { securityContext.executeAsUser(SystemUser.INSTANCE, (original) -> sendWithUser(connection, action, request, options, - new TransportService.ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original) + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original) , handler), sender), connection.getVersion()); } else if (reservedRealmEnabled && connection.getVersion().before(Version.V_5_2_0_UNRELEASED) && KibanaUser.NAME.equals(securityContext.getUser().principal())) { @@ -99,16 +100,14 @@ public class SecurityServerTransportInterceptor extends AbstractComponent implem final User bwcKibanaUser = new User(kibanaUser.principal(), new String[] { "kibana" }, kibanaUser.fullName(), kibanaUser.email(), kibanaUser.metadata(), kibanaUser.enabled()); securityContext.executeAsUser(bwcKibanaUser, (original) -> sendWithUser(connection, action, request, options, - new TransportService.ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), sender), connection.getVersion()); } else if (securityContext.getAuthentication() != null && securityContext.getAuthentication().getVersion().equals(connection.getVersion()) == false) { // re-write the authentication since we want the authentication version to match the version of the connection - securityContext.executeAsUser(securityContext.getUser(), - (original) -> sendWithUser(connection, action, request, options, - new TransportService.ContextRestoreResponseHandler<>( - threadPool.getThreadContext().wrapRestorable(original), handler), sender), - connection.getVersion()); + securityContext.executeAfterRewritingAuthentication(original -> sendWithUser(connection, action, request, options, + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), sender), + connection.getVersion()); } else { sendWithUser(connection, action, request, options, handler, sender); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/plugin/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index 7b402c1e65b..3dd0cc56c68 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -124,25 +124,11 @@ public interface ServerTransportFilter { } authcService.authenticate(securityAction, request, null, ActionListener.wrap((authentication) -> { - if (reservedRealmEnabled && authentication.getVersion().before(Version.V_5_2_0_UNRELEASED) - && KibanaUser.NAME.equals(authentication.getUser().principal())) { - // the authentication came from an older node - so let's replace the user with our version - final User kibanaUser = new KibanaUser(authentication.getUser().enabled()); - if (kibanaUser.enabled()) { - securityContext.executeAsUser(kibanaUser, (original) -> { - final Authentication replacedUserAuth = Authentication.getAuthentication(threadContext); - final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = - new AuthorizationUtils.AsyncAuthorizer(replacedUserAuth, listener, (userRoles, runAsRoles) -> { - authzService.authorize(replacedUserAuth, securityAction, request, userRoles, runAsRoles); - listener.onResponse(null); - }); - asyncAuthorizer.authorize(authzService); - }, transportChannel.getVersion()); - } else { - throw new IllegalStateException("a disabled user should never be sent. " + kibanaUser); - } + if (reservedRealmEnabled && authentication.getVersion().before(Version.V_5_2_0_UNRELEASED) && + KibanaUser.NAME.equals(authentication.getUser().authenticatedUser().principal())) { + executeAsCurrentVersionKibanaUser(securityAction, request, transportChannel, listener, authentication); } else if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME) && - SystemUser.is(authentication.getUser()) == false) { + SystemUser.is(authentication.getUser()) == false) { securityContext.executeAsUser(SystemUser.INSTANCE, (ctx) -> { final Authentication replaced = Authentication.getAuthentication(threadContext); final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = @@ -162,6 +148,25 @@ public interface ServerTransportFilter { } }, listener::onFailure)); } + + private void executeAsCurrentVersionKibanaUser(String securityAction, TransportRequest request, TransportChannel transportChannel, + ActionListener listener, Authentication authentication) { + // the authentication came from an older node - so let's replace the user with our version + final User kibanaUser = new KibanaUser(authentication.getUser().enabled()); + if (kibanaUser.enabled()) { + securityContext.executeAsUser(kibanaUser, (original) -> { + final Authentication replacedUserAuth = securityContext.getAuthentication(); + final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = + new AuthorizationUtils.AsyncAuthorizer(replacedUserAuth, listener, (userRoles, runAsRoles) -> { + authzService.authorize(replacedUserAuth, securityAction, request, userRoles, runAsRoles); + listener.onResponse(null); + }); + asyncAuthorizer.authorize(authzService); + }, transportChannel.getVersion()); + } else { + throw new IllegalStateException("a disabled user should never be sent. " + kibanaUser); + } + } } static void extactClientCertificates(Logger logger, ThreadContext threadContext, SSLEngine sslEngine, Object channel) { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/user/User.java b/plugin/src/main/java/org/elasticsearch/xpack/security/user/User.java index 10e2e7d4c31..846c38e1c45 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/user/User.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/user/User.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.security.user; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.internal.Nullable; @@ -27,7 +28,7 @@ public class User implements ToXContentObject { private final String username; private final String[] roles; - private final User runAs; + private final User authenticatedUser; private final Map metadata; private final boolean enabled; @@ -38,36 +39,28 @@ public class User implements ToXContentObject { this(username, roles, null, null, null, true); } - public User(String username, String[] roles, User runAs) { - this(username, roles, null, null, null, true, runAs); + public User(String username, String[] roles, User authenticatedUser) { + this(username, roles, null, null, null, true, authenticatedUser); } - public User(User user, User runAs) { - this(user.principal(), user.roles(), user.fullName(), user.email(), user.metadata(), user.enabled(), runAs); + public User(User user, User authenticatedUser) { + this(user.principal(), user.roles(), user.fullName(), user.email(), user.metadata(), user.enabled(), authenticatedUser); } public User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled) { - this.username = username; - this.roles = roles == null ? Strings.EMPTY_ARRAY : roles; - this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - this.fullName = fullName; - this.email = email; - this.enabled = enabled; - this.runAs = null; + this(username, roles, fullName, email, metadata, enabled, null); } - public User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled, User runAs) { + private User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled, + User authenticatedUser) { this.username = username; this.roles = roles == null ? Strings.EMPTY_ARRAY : roles; this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); this.fullName = fullName; this.email = email; this.enabled = enabled; - assert (runAs == null || runAs.runAs() == null) : "the run_as user should not be a user that can run as"; - if (runAs == SystemUser.INSTANCE) { - throw new ElasticsearchSecurityException("invalid run_as user"); - } - this.runAs = runAs; + assert (authenticatedUser == null || authenticatedUser.isRunAs() == false) : "the authenticated user should not be a run_as user"; + this.authenticatedUser = authenticatedUser; } /** @@ -116,12 +109,16 @@ public class User implements ToXContentObject { } /** - * @return The user that will be used for run as functionality. If run as - * functionality is not being used, then null will be - * returned + * @return The user that was originally authenticated. + * This may be the user itself, or a different user which used runAs. */ - public User runAs() { - return runAs; + public User authenticatedUser() { + return authenticatedUser == null ? this : authenticatedUser; + } + + /** Return true if this user was not the originally authenticated user, false otherwise. */ + public boolean isRunAs() { + return authenticatedUser != null; } @Override @@ -133,8 +130,8 @@ public class User implements ToXContentObject { sb.append(",email=").append(email); sb.append(",metadata="); MetadataUtils.writeValue(sb, metadata); - if (runAs != null) { - sb.append(",runAs=[").append(runAs.toString()).append("]"); + if (authenticatedUser != null) { + sb.append(",authenticatedUser=[").append(authenticatedUser.toString()).append("]"); } sb.append("]"); return sb.toString(); @@ -150,7 +147,7 @@ public class User implements ToXContentObject { if (!username.equals(user.username)) return false; // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(roles, user.roles)) return false; - if (runAs != null ? !runAs.equals(user.runAs) : user.runAs != null) return false; + if (authenticatedUser != null ? !authenticatedUser.equals(user.authenticatedUser) : user.authenticatedUser != null) return false; if (!metadata.equals(user.metadata)) return false; if (fullName != null ? !fullName.equals(user.fullName) : user.fullName != null) return false; return !(email != null ? !email.equals(user.email) : user.email != null); @@ -161,7 +158,7 @@ public class User implements ToXContentObject { public int hashCode() { int result = username.hashCode(); result = 31 * result + Arrays.hashCode(roles); - result = 31 * result + (runAs != null ? runAs.hashCode() : 0); + result = 31 * result + (authenticatedUser != null ? authenticatedUser.hashCode() : 0); result = 31 * result + metadata.hashCode(); result = 31 * result + (fullName != null ? fullName.hashCode() : 0); result = 31 * result + (email != null ? email.hashCode() : 0); @@ -196,8 +193,19 @@ public class User implements ToXContentObject { String fullName = input.readOptionalString(); String email = input.readOptionalString(); boolean enabled = input.readBoolean(); - User runAs = input.readBoolean() ? readFrom(input) : null; - return new User(username, roles, fullName, email, metadata, enabled, runAs); + User outerUser = new User(username, roles, fullName, email, metadata, enabled, null); + boolean hasInnerUser = input.readBoolean(); + if (hasInnerUser) { + User innerUser = readFrom(input); + if (input.getVersion().onOrBefore(Version.V_5_4_0_UNRELEASED)) { + // backcompat: runas user was read first, so reverse outer and inner + return new User(innerUser, outerUser); + } else { + return new User(outerUser, innerUser); + } + } else { + return outerUser; + } } public static void writeTo(User user, StreamOutput output) throws IOException { @@ -208,22 +216,34 @@ public class User implements ToXContentObject { output.writeBoolean(true); output.writeString(XPackUser.NAME); } else { - output.writeBoolean(false); - output.writeString(user.username); - output.writeStringArray(user.roles); - output.writeMap(user.metadata); - output.writeOptionalString(user.fullName); - output.writeOptionalString(user.email); - output.writeBoolean(user.enabled); - if (user.runAs == null) { - output.writeBoolean(false); - } else { + if (user.authenticatedUser == null) { + // no backcompat necessary, since there is no inner user + writeUser(user, output); + } else if (output.getVersion().onOrBefore(Version.V_5_4_0_UNRELEASED)) { + // backcompat: write runas user as the "inner" user + writeUser(user.authenticatedUser, output); output.writeBoolean(true); - writeTo(user.runAs, output); + writeUser(user, output); + } else { + writeUser(user, output); + output.writeBoolean(true); + writeUser(user.authenticatedUser, output); } + output.writeBoolean(false); // last user written, regardless of bwc, does not have an inner user } } + /** Write just the given {@link User}, but not the inner {@link #authenticatedUser}. */ + private static void writeUser(User user, StreamOutput output) throws IOException { + output.writeBoolean(false); // not a system user + output.writeString(user.username); + output.writeStringArray(user.roles); + output.writeMap(user.metadata); + output.writeOptionalString(user.fullName); + output.writeOptionalString(user.email); + output.writeBoolean(user.enabled); + } + public interface Fields { ParseField USERNAME = new ParseField("username"); ParseField PASSWORD = new ParseField("password"); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java index 6d782f8245e..2a1acbcbfa5 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java @@ -63,7 +63,7 @@ public class TransportHasPrivilegesActionTests extends ESTestCase { threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); when(threadPool.getThreadContext()).thenReturn(threadContext); - when(authentication.getRunAsUser()).thenReturn(user); + when(authentication.getUser()).thenReturn(user); AuthorizationService authorizationService = mock(AuthorizationService.class); Mockito.doAnswer(invocationOnMock -> { diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java index c0aab55b1c5..e0e2e0ec869 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java @@ -56,7 +56,7 @@ public class TransportSetEnabledActionTests extends ESTestCase { Authentication authentication = mock(Authentication.class); when(threadPool.getThreadContext()).thenReturn(threadContext); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); - when(authentication.getRunAsUser()).thenReturn(user); + when(authentication.getUser()).thenReturn(user); NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportService transportService = new TransportService(Settings.EMPTY, null, null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null); @@ -94,7 +94,7 @@ public class TransportSetEnabledActionTests extends ESTestCase { Authentication authentication = mock(Authentication.class); when(threadPool.getThreadContext()).thenReturn(threadContext); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); - when(authentication.getRunAsUser()).thenReturn(user); + when(authentication.getUser()).thenReturn(user); NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportService transportService = new TransportService(Settings.EMPTY, null, null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null); @@ -131,7 +131,7 @@ public class TransportSetEnabledActionTests extends ESTestCase { Authentication authentication = mock(Authentication.class); when(threadPool.getThreadContext()).thenReturn(threadContext); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); - when(authentication.getRunAsUser()).thenReturn(new User("the runner")); + when(authentication.getUser()).thenReturn(new User("the runner")); final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); NativeUsersStore usersStore = mock(NativeUsersStore.class); @@ -182,7 +182,7 @@ public class TransportSetEnabledActionTests extends ESTestCase { Authentication authentication = mock(Authentication.class); when(threadPool.getThreadContext()).thenReturn(threadContext); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); - when(authentication.getRunAsUser()).thenReturn(new User("the runner")); + when(authentication.getUser()).thenReturn(new User("the runner")); final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); NativeUsersStore usersStore = mock(NativeUsersStore.class); @@ -235,7 +235,7 @@ public class TransportSetEnabledActionTests extends ESTestCase { Authentication authentication = mock(Authentication.class); when(threadPool.getThreadContext()).thenReturn(threadContext); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); - when(authentication.getRunAsUser()).thenReturn(user); + when(authentication.getUser()).thenReturn(user); NativeUsersStore usersStore = mock(NativeUsersStore.class); SetEnabledRequest request = new SetEnabledRequest(); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java new file mode 100644 index 00000000000..b9e4e4e5c5c --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java @@ -0,0 +1,185 @@ +/* + * 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.audit.index; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.http.message.BasicHeader; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse; +import org.elasticsearch.action.admin.indices.recovery.RecoveryAction; +import org.elasticsearch.action.admin.indices.recovery.RecoveryRequestBuilder; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.Requests; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.test.SecuritySettingsSource; +import org.elasticsearch.xpack.security.InternalClient; +import org.elasticsearch.xpack.security.audit.AuditTrail; +import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.authc.AuthenticationService; +import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import static org.elasticsearch.test.SecuritySettingsSource.DEFAULT_PASSWORD_SECURE_STRING; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; + +public class AuditTrailTests extends SecurityIntegTestCase { + + private static final String AUTHENTICATE_USER = "http_user"; + private static final String EXECUTE_USER = "exec_user"; + private static final String ROLE_CAN_RUN_AS = "can_run_as"; + private static final String ROLES = ROLE_CAN_RUN_AS + ":\n" + " run_as: [ '" + EXECUTE_USER + "' ]\n"; + + @Override + public Settings nodeSettings(int nodeOrdinal) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .put("xpack.security.audit.enabled", true) + .put("xpack.security.audit.outputs", "index") + .putArray("xpack.security.audit.index.events.include", "access_denied", "authentication_failed", "run_as_denied") + .build(); + } + + @Override + public String configRoles() { + return ROLES + super.configRoles(); + } + + @Override + public String configUsers() { + return super.configUsers() + + AUTHENTICATE_USER + ":" + SecuritySettingsSource.DEFAULT_PASSWORD_HASHED + "\n" + + EXECUTE_USER + ":xx_no_password_xx\n"; + } + + @Override + public String configUsersRoles() { + return super.configUsersRoles() + + ROLE_CAN_RUN_AS + ":" + AUTHENTICATE_USER + "\n" + + "kibana_user:" + EXECUTE_USER; + } + + @Override + public boolean useGeneratedSSLConfig() { + return true; + } + + public void testAuditAccessDeniedWithRunAsUser() throws Exception { + try { + getRestClient().performRequest("GET", "/.security/_search", + new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + UsernamePasswordToken.basicAuthHeaderValue(AUTHENTICATE_USER, DEFAULT_PASSWORD_SECURE_STRING)), + new BasicHeader(AuthenticationService.RUN_AS_USER_HEADER, EXECUTE_USER)); + fail("request should have failed"); + } catch (ResponseException e) { + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + + final Collection> events = waitForAuditEvents(); + + assertThat(events, iterableWithSize(1)); + final Map event = events.iterator().next(); + assertThat(event.get(IndexAuditTrail.Field.TYPE), equalTo("access_denied")); + assertThat((List) event.get(IndexAuditTrail.Field.INDICES), containsInAnyOrder(".security")); + assertThat(event.get(IndexAuditTrail.Field.PRINCIPAL), equalTo(EXECUTE_USER)); + assertThat(event.get(IndexAuditTrail.Field.RUN_BY_PRINCIPAL), equalTo(AUTHENTICATE_USER)); + } + + + public void testAuditRunAsDeniedEmptyUser() throws Exception { + try { + getRestClient().performRequest("GET", "/.security/_search", + new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + UsernamePasswordToken.basicAuthHeaderValue(AUTHENTICATE_USER, DEFAULT_PASSWORD_SECURE_STRING)), + new BasicHeader(AuthenticationService.RUN_AS_USER_HEADER, "")); + fail("request should have failed"); + } catch (ResponseException e) { + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(401)); + } + + final Collection> events = waitForAuditEvents(); + + assertThat(events, iterableWithSize(1)); + final Map event = events.iterator().next(); + assertThat(event.get(IndexAuditTrail.Field.TYPE), equalTo("run_as_denied")); + assertThat(event.get(IndexAuditTrail.Field.PRINCIPAL), equalTo("")); + assertThat(event.get(IndexAuditTrail.Field.RUN_BY_PRINCIPAL), equalTo(AUTHENTICATE_USER)); + } + + private Collection> waitForAuditEvents() throws InterruptedException { + waitForAuditTrailToBeWritten(); + AtomicReference>> eventsRef = new AtomicReference<>(); + awaitBusy(() -> { + try { + final Collection> events = getAuditEvents(); + eventsRef.set(events); + return events.size() > 0; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + return eventsRef.get(); + } + private Collection> getAuditEvents() throws Exception { + final InternalClient client = internalClient(); + DateTime now = new DateTime(DateTimeZone.UTC); + String indexName = IndexNameResolver.resolve(IndexAuditTrail.INDEX_NAME_PREFIX, now, IndexNameResolver.Rollover.DAILY); + + assertTrue(awaitBusy(() -> indexExists(client, indexName), 5, TimeUnit.SECONDS)); + + client.admin().indices().refresh(Requests.refreshRequest(indexName)).get(); + + SearchRequest request = client.prepareSearch(indexName) + .setTypes(IndexAuditTrail.DOC_TYPE) + .setQuery(QueryBuilders.matchAllQuery()) + .setSize(1000) + .setFetchSource(true) + .request(); + request.indicesOptions().ignoreUnavailable(); + + PlainActionFuture>> listener = new PlainActionFuture(); + InternalClient.fetchAllByEntity(client, request, listener, SearchHit::getSourceAsMap); + + return listener.get(); + } + + private boolean indexExists(Client client, String indexName) { + try { + final ActionFuture future = client.admin().indices().exists(Requests.indicesExistsRequest(indexName)); + return future.get().isExists(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to check if " + indexName + " exists", e); + } + } + + private void waitForAuditTrailToBeWritten() throws InterruptedException { + final AuditTrailService auditTrailService = (AuditTrailService) internalCluster().getInstance(AuditTrail.class); + assertThat(auditTrailService.getAuditTrails(), iterableWithSize(1)); + + final IndexAuditTrail indexAuditTrail = (IndexAuditTrail) auditTrailService.getAuditTrails().get(0); + assertTrue(awaitBusy(() -> indexAuditTrail.peek() == null, 5, TimeUnit.SECONDS)); + } +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java index 1c51bee93c6..7a3d34fd1c6 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java @@ -473,8 +473,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { final boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, - new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -518,8 +517,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { final boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, - new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -578,8 +576,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { final boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, - new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -631,7 +628,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { public void testRunAsGranted() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); - User user = new User("_username", new String[]{"r1"}, new User("running as", new String[] {"r2"})); + User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); auditor.runAsGranted(user, "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); @@ -647,7 +644,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { public void testRunAsDenied() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); - User user = new User("_username", new String[]{"r1"}, new User("running as", new String[] {"r2"})); + User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); auditor.runAsDenied(user, "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); @@ -666,7 +663,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { final boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[] { "r1" }, new User("running as", new String[] { "r2" })); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[] { "r1" }); } @@ -693,7 +690,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase { final boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[] { "r1" }, new User("running as", new String[] { "r2" })); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[] { "r1" }); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index 0da162d2244..22798e3b736 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -326,8 +326,7 @@ public class LoggingAuditTrailTests extends ESTestCase { boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, - new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -379,8 +378,7 @@ public class LoggingAuditTrailTests extends ESTestCase { boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, - new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -410,8 +408,7 @@ public class LoggingAuditTrailTests extends ESTestCase { boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, - new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -481,7 +478,7 @@ public class LoggingAuditTrailTests extends ESTestCase { final boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[]{"r1"}, new User("running as", new String[] {"r2"})); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[]{"r1"}); } @@ -547,7 +544,7 @@ public class LoggingAuditTrailTests extends ESTestCase { LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); TransportMessage message = new MockMessage(threadContext); String origins = LoggingAuditTrail.originAttributes(message, localNode, threadContext); - User user = new User("_username", new String[]{"r1"}, new User("running as", new String[] {"r2"})); + User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); auditTrail.runAsGranted(user, "_action", message); assertMsg(logger, Level.INFO, prefix + "[transport] [run_as_granted]\t" + origins + ", principal=[_username], run_as_principal=[running as], action=[_action], request=[MockMessage]"); @@ -565,7 +562,7 @@ public class LoggingAuditTrailTests extends ESTestCase { LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); TransportMessage message = new MockMessage(threadContext); String origins = LoggingAuditTrail.originAttributes(message, localNode, threadContext); - User user = new User("_username", new String[]{"r1"}, new User("running as", new String[] {"r2"})); + User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); auditTrail.runAsDenied(user, "_action", message); assertMsg(logger, Level.INFO, prefix + "[transport] [run_as_denied]\t" + origins + ", principal=[_username], run_as_principal=[running as], action=[_action], request=[MockMessage]"); @@ -608,7 +605,7 @@ public class LoggingAuditTrailTests extends ESTestCase { boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[] { "r1" }, new User("running as", new String[] { "r2" })); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[] { "r1" }); } @@ -649,7 +646,7 @@ public class LoggingAuditTrailTests extends ESTestCase { boolean runAs = randomBoolean(); User user; if (runAs) { - user = new User("_username", new String[] { "r1" }, new User("running as", new String[] { "r2" })); + user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); } else { user = new User("_username", new String[] { "r1" }); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 086a8278c41..dc3d11a216e 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -645,18 +645,21 @@ public class AuthenticationServiceTests extends ESTestCase { assertThat(result, notNullValue()); User authenticated = result.getUser(); - assertThat(SystemUser.is(authenticated), is(false)); - assertThat(authenticated.runAs(), is(notNullValue())); - assertThat(authenticated.principal(), is("lookup user")); - assertThat(authenticated.roles(), arrayContaining("user")); - assertEquals(user.metadata(), authenticated.metadata()); - assertEquals(user.email(), authenticated.email()); - assertEquals(user.enabled(), authenticated.enabled()); - assertEquals(user.fullName(), authenticated.fullName()); - - assertThat(authenticated.runAs().principal(), is("looked up user")); - assertThat(authenticated.runAs().roles(), arrayContaining("some role")); + assertThat(authenticated.principal(), is("looked up user")); + assertThat(authenticated.roles(), arrayContaining("some role")); assertThreadContextContainsAuthentication(result); + + assertThat(SystemUser.is(authenticated), is(false)); + assertThat(authenticated.isRunAs(), is(true)); + User authUser = authenticated.authenticatedUser(); + assertThat(authUser.principal(), is("lookup user")); + assertThat(authUser.roles(), arrayContaining("user")); + assertEquals(user.metadata(), authUser.metadata()); + assertEquals(user.email(), authUser.email()); + assertEquals(user.enabled(), authUser.enabled()); + assertEquals(user.fullName(), authUser.fullName()); + + setCompletedToTrue(completed); }, this::logAndFail); @@ -687,11 +690,11 @@ public class AuthenticationServiceTests extends ESTestCase { User authenticated = result.getUser(); assertThat(SystemUser.is(authenticated), is(false)); - assertThat(authenticated.runAs(), is(notNullValue())); - assertThat(authenticated.principal(), is("lookup user")); - assertThat(authenticated.roles(), arrayContaining("user")); - assertThat(authenticated.runAs().principal(), is("looked up user")); - assertThat(authenticated.runAs().roles(), arrayContaining("some role")); + assertThat(authenticated.isRunAs(), is(true)); + assertThat(authenticated.authenticatedUser().principal(), is("lookup user")); + assertThat(authenticated.authenticatedUser().roles(), arrayContaining("user")); + assertThat(authenticated.principal(), is("looked up user")); + assertThat(authenticated.roles(), arrayContaining("some role")); assertThreadContextContainsAuthentication(result); setCompletedToTrue(completed); }, this::logAndFail); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index e25a26e15bb..3935006bddc 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -456,8 +456,8 @@ public class AuthorizationServiceTests extends ESTestCase { public void testRunAsRequestWithNoRolesUser() { TransportRequest request = mock(TransportRequest.class); - User user = new User("test user", null, new User("run as me", new String[] { "admin" })); - assertThat(user.runAs(), is(notNullValue())); + User user = new User("run as me", null, new User("test user", "admin")); + assertNotEquals(user.authenticatedUser(), user); assertThrowsAuthorizationExceptionRunAs( () -> authorize(createAuthentication(user), "indices:a", request), "indices:a", "test user", "run as me"); // run as [run as me] @@ -468,9 +468,9 @@ public class AuthorizationServiceTests extends ESTestCase { public void testRunAsRequestWithoutLookedUpBy() { AuthenticateRequest request = new AuthenticateRequest("run as me"); roleMap.put("can run as", ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); - User user = new User("test user", new String[] { "can run as" }, new User("run as me", Strings.EMPTY_ARRAY)); + User user = new User("run as me", Strings.EMPTY_ARRAY, new User("test user", new String[] { "can run as" })); Authentication authentication = new Authentication(user, new RealmRef("foo", "bar", "baz"), null); - assertThat(user.runAs(), is(notNullValue())); + assertNotEquals(user.authenticatedUser(), user); assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, AuthenticateAction.NAME, request), AuthenticateAction.NAME, "test user", "run as me"); // run as [run as me] @@ -480,8 +480,8 @@ public class AuthorizationServiceTests extends ESTestCase { public void testRunAsRequestRunningAsUnAllowedUser() { TransportRequest request = mock(TransportRequest.class); - User user = new User("test user", new String[] { "can run as" }, new User("run as me", "doesn't exist")); - assertThat(user.runAs(), is(notNullValue())); + User user = new User("run as me", new String[] {"doesn't exist"}, new User("test user", "can run as")); + assertNotEquals(user.authenticatedUser(), user); roleMap.put("can run as", new RoleDescriptor("can run as",null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, new String[] { "not the right user" })); @@ -495,8 +495,8 @@ public class AuthorizationServiceTests extends ESTestCase { public void testRunAsRequestWithRunAsUserWithoutPermission() { TransportRequest request = new GetIndexRequest().indices("a"); - User user = new User("test user", new String[] { "can run as" }, new User("run as me", "b")); - assertThat(user.runAs(), is(notNullValue())); + User user = new User("run as me", new String[] {"b"}, new User("test user", "can run as")); + assertNotEquals(user.authenticatedUser(), user); roleMap.put("can run as", new RoleDescriptor("can run as",null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, new String[] { "run as me" })); @@ -525,8 +525,8 @@ public class AuthorizationServiceTests extends ESTestCase { public void testRunAsRequestWithValidPermissions() { TransportRequest request = new GetIndexRequest().indices("b"); - User user = new User("test user", new String[] { "can run as" }, new User("run as me", "b")); - assertThat(user.runAs(), is(notNullValue())); + User user = new User("run as me", new String[] {"b"}, new User("test user", new String[] { "can run as" })); + assertNotEquals(user.authenticatedUser(), user); roleMap.put("can run as", new RoleDescriptor("can run as",null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, new String[] { "run as me" })); @@ -845,7 +845,6 @@ public class AuthorizationServiceTests extends ESTestCase { final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(user); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()) .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealm.TYPE) : randomAlphaOfLengthBetween(4, 12)); @@ -855,7 +854,8 @@ public class AuthorizationServiceTests extends ESTestCase { } public void testSameUserPermissionDoesNotAllowNonMatchingUsername() { - final User user = new User("joe"); + final User authUser = new User("admin", new String[] { "bar" }); + final User user = new User("joe", null, authUser); final boolean changePasswordRequest = randomBoolean(); final String username = randomFrom("", "joe" + randomAlphaOfLengthBetween(1, 5), randomAlphaOfLengthBetween(3, 10)); final TransportRequest request = changePasswordRequest ? @@ -865,7 +865,6 @@ public class AuthorizationServiceTests extends ESTestCase { final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(user); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()) .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealm.TYPE) : randomAlphaOfLengthBetween(4, 12)); @@ -873,9 +872,7 @@ public class AuthorizationServiceTests extends ESTestCase { assertThat(request, instanceOf(UserRequest.class)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); - final User user2 = new User("admin", new String[] { "bar" }, user); - when(authentication.getUser()).thenReturn(user2); - when(authentication.getRunAsUser()).thenReturn(user); + when(authentication.getUser()).thenReturn(user); final RealmRef lookedUpBy = mock(RealmRef.class); when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); when(lookedUpBy.getType()) @@ -898,8 +895,10 @@ public class AuthorizationServiceTests extends ESTestCase { ClusterStatsAction.NAME, GetLicenseAction.NAME); final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); + final boolean runAs = randomBoolean(); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(randomBoolean() ? user : new User("runAs")); + when(user.authenticatedUser()).thenReturn(runAs ? new User("authUser") : user); + when(user.isRunAs()).thenReturn(runAs); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()) .thenReturn(randomAlphaOfLengthBetween(4, 12)); @@ -909,9 +908,9 @@ public class AuthorizationServiceTests extends ESTestCase { } public void testSameUserPermissionRunAsChecksAuthenticatedBy() { + final User authUser = new User("admin", new String[] { "bar" }); final String username = "joe"; - final User runAs = new User(username); - final User user = new User("admin", new String[] { "bar" }, runAs); + final User user = new User(username, null, authUser); final boolean changePasswordRequest = randomBoolean(); final TransportRequest request = changePasswordRequest ? new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : @@ -921,15 +920,13 @@ public class AuthorizationServiceTests extends ESTestCase { final RealmRef authenticatedBy = mock(RealmRef.class); final RealmRef lookedUpBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(runAs); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); - when(authentication.isRunAs()).thenReturn(true); when(lookedUpBy.getType()) .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealm.TYPE) : randomAlphaOfLengthBetween(4, 12)); assertTrue(AuthorizationService.checkSameUserPermissions(action, request, authentication)); - when(authentication.getRunAsUser()).thenReturn(user); + when(authentication.getUser()).thenReturn(authUser); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); } @@ -940,8 +937,6 @@ public class AuthorizationServiceTests extends ESTestCase { final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(user); - when(authentication.isRunAs()).thenReturn(false); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()).thenReturn(randomFrom(LdapRealm.LDAP_TYPE, FileRealm.TYPE, LdapRealm.AD_TYPE, PkiRealm.TYPE, randomAlphaOfLengthBetween(4, 12))); @@ -949,23 +944,20 @@ public class AuthorizationServiceTests extends ESTestCase { assertThat(request, instanceOf(UserRequest.class)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); verify(authenticatedBy).getType(); - verify(authentication).getRunAsUser(); verify(authentication).getAuthenticatedBy(); - verify(authentication).isRunAs(); + verify(authentication, times(2)).getUser(); verifyNoMoreInteractions(authenticatedBy, authentication); } public void testSameUserPermissionDoesNotAllowChangePasswordForLookedUpByOtherRealms() { - final User runAs = new User("joe"); - final User user = new User("admin", new String[] { "bar" }, runAs); - final ChangePasswordRequest request = new ChangePasswordRequestBuilder(mock(Client.class)).username(runAs.principal()).request(); + final User authUser = new User("admin", new String[] { "bar" }); + final User user = new User("joe", null, authUser); + final ChangePasswordRequest request = new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request(); final String action = ChangePasswordAction.NAME; final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); final RealmRef lookedUpBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(runAs); - when(authentication.isRunAs()).thenReturn(true); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); when(lookedUpBy.getType()).thenReturn(randomFrom(LdapRealm.LDAP_TYPE, FileRealm.TYPE, LdapRealm.AD_TYPE, PkiRealm.TYPE, @@ -974,8 +966,7 @@ public class AuthorizationServiceTests extends ESTestCase { assertThat(request, instanceOf(UserRequest.class)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); verify(authentication).getLookedUpBy(); - verify(authentication).getRunAsUser(); - verify(authentication).isRunAs(); + verify(authentication, times(2)).getUser(); verify(lookedUpBy).getType(); verifyNoMoreInteractions(authentication, lookedUpBy, authenticatedBy); } @@ -1021,7 +1012,7 @@ public class AuthorizationServiceTests extends ESTestCase { } private static Authentication createAuthentication(User user) { - RealmRef lookedUpBy = user.runAs() == null ? null : new RealmRef("looked", "up", "by"); + RealmRef lookedUpBy = user.authenticatedUser() == user ? null : new RealmRef("looked", "up", "by"); return new Authentication(user, new RealmRef("test", "test", "foo"), lookedUpBy); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java index feed6db5a83..7647d1deb03 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java @@ -81,7 +81,6 @@ public class ServerTransportFilterTests extends ESTestCase { Authentication authentication = mock(Authentication.class); when(authentication.getVersion()).thenReturn(Version.CURRENT); when(authentication.getUser()).thenReturn(SystemUser.INSTANCE); - when(authentication.getRunAsUser()).thenReturn(SystemUser.INSTANCE); doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[3]; @@ -160,7 +159,6 @@ public class ServerTransportFilterTests extends ESTestCase { }).when(authzService).roles(any(User.class), any(ActionListener.class)); when(authentication.getVersion()).thenReturn(Version.CURRENT); when(authentication.getUser()).thenReturn(XPackUser.INSTANCE); - when(authentication.getRunAsUser()).thenReturn(XPackUser.INSTANCE); PlainActionFuture future = new PlainActionFuture<>(); doThrow(authorizationError("authz failed")).when(authzService).authorize(authentication, "_action", request, empty, null); @@ -228,7 +226,6 @@ public class ServerTransportFilterTests extends ESTestCase { final Version version = Version.fromId(randomIntBetween(Version.V_5_0_0_ID, Version.V_5_2_0_ID_UNRELEASED - 100)); when(authentication.getVersion()).thenReturn(version); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(user); doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[3]; @@ -256,7 +253,6 @@ public class ServerTransportFilterTests extends ESTestCase { rolesRef.set(null); user = new KibanaUser(true); when(authentication.getUser()).thenReturn(user); - when(authentication.getRunAsUser()).thenReturn(user); when(authentication.getVersion()).thenReturn(Version.V_5_2_0_UNRELEASED); future = new PlainActionFuture<>(); filter.inbound("_action", request, channel, future); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java index b594872ea8d..5f38ed80e9e 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java @@ -5,14 +5,15 @@ */ package org.elasticsearch.xpack.security.user; -import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.test.ESTestCase; - import java.util.Arrays; import java.util.Collections; -import static org.hamcrest.Matchers.containsString; +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; + import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -32,14 +33,15 @@ public class UserTests extends ESTestCase { assertThat(readFrom, not(sameInstance(user))); assertThat(readFrom.principal(), is(user.principal())); assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); - assertThat(readFrom.runAs(), is(nullValue())); + assertThat(readFrom.authenticatedUser(), is(user)); } public void testWriteToAndReadFromWithRunAs() throws Exception { - User runAs = new User(randomAlphaOfLengthBetween(4, 30), - randomBoolean() ? generateRandomStringArray(20, 30, false) : null); + User authUser = new User(randomAlphaOfLengthBetween(4, 30), generateRandomStringArray(20, 30, false)); User user = new User(randomAlphaOfLengthBetween(4, 30), - generateRandomStringArray(20, 30, false), runAs); + randomBoolean() ? generateRandomStringArray(20, 30, false) : null, + authUser); + BytesStreamOutput output = new BytesStreamOutput(); User.writeTo(user, output); @@ -48,11 +50,51 @@ public class UserTests extends ESTestCase { assertThat(readFrom, not(sameInstance(user))); assertThat(readFrom.principal(), is(user.principal())); assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); - assertThat(readFrom.runAs(), is(notNullValue())); - User readFromRunAs = readFrom.runAs(); - assertThat(readFromRunAs.principal(), is(runAs.principal())); - assertThat(Arrays.equals(readFromRunAs.roles(), runAs.roles()), is(true)); - assertThat(readFromRunAs.runAs(), is(nullValue())); + User readFromAuthUser = readFrom.authenticatedUser(); + assertThat(authUser, is(notNullValue())); + assertThat(readFromAuthUser.principal(), is(authUser.principal())); + assertThat(Arrays.equals(readFromAuthUser.roles(), authUser.roles()), is(true)); + assertThat(readFromAuthUser.authenticatedUser(), is(authUser)); + } + + public void testRunAsBackcompatRead() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 30), + randomBoolean() ? generateRandomStringArray(20, 30, false) : null); + // store the runAs user as the "authenticationUser" here to mimic old format for writing + User authUser = new User(randomAlphaOfLengthBetween(4, 30), generateRandomStringArray(20, 30, false), user); + + BytesStreamOutput output = new BytesStreamOutput(); + User.writeTo(authUser, output); + StreamInput input = output.bytes().streamInput(); + input.setVersion(randomFrom(Version.V_5_0_0, Version.V_5_4_0_UNRELEASED)); + User readFrom = User.readFrom(input); + + assertThat(readFrom.principal(), is(user.principal())); + assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); + User readFromAuthUser = readFrom.authenticatedUser(); + assertThat(authUser, is(notNullValue())); + assertThat(readFromAuthUser.principal(), is(authUser.principal())); + assertThat(Arrays.equals(readFromAuthUser.roles(), authUser.roles()), is(true)); + } + + public void testRunAsBackcompatWrite() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 30), + randomBoolean() ? generateRandomStringArray(20, 30, false) : null); + // store the runAs user as the "authenticationUser" here to mimic old format for writing + User authUser = new User(randomAlphaOfLengthBetween(4, 30), generateRandomStringArray(20, 30, false), user); + + BytesStreamOutput output = new BytesStreamOutput(); + output.setVersion(randomFrom(Version.V_5_0_0, Version.V_5_4_0_UNRELEASED)); + User.writeTo(authUser, output); + StreamInput input = output.bytes().streamInput(); + User readFrom = User.readFrom(input); + + assertThat(readFrom.principal(), is(user.principal())); + assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); + User readFromAuthUser = readFrom.authenticatedUser(); + assertThat(authUser, is(notNullValue())); + assertThat(readFromAuthUser.principal(), is(authUser.principal())); + assertThat(Arrays.equals(readFromAuthUser.roles(), authUser.roles()), is(true)); } public void testSystemUserReadAndWrite() throws Exception { @@ -62,7 +104,7 @@ public class UserTests extends ESTestCase { User readFrom = User.readFrom(output.bytes().streamInput()); assertThat(readFrom, is(sameInstance(SystemUser.INSTANCE))); - assertThat(readFrom.runAs(), is(nullValue())); + assertThat(readFrom.authenticatedUser(), is(SystemUser.INSTANCE)); } public void testXPackUserReadAndWrite() throws Exception { @@ -72,7 +114,7 @@ public class UserTests extends ESTestCase { User readFrom = User.readFrom(output.bytes().streamInput()); assertThat(readFrom, is(sameInstance(XPackUser.INSTANCE))); - assertThat(readFrom.runAs(), is(nullValue())); + assertThat(readFrom.authenticatedUser(), is(XPackUser.INSTANCE)); } public void testFakeInternalUserSerialization() throws Exception { @@ -87,24 +129,14 @@ public class UserTests extends ESTestCase { } } - public void testCreateUserRunningAsSystemUser() throws Exception { - try { - new User(randomAlphaOfLengthBetween(3, 10), - generateRandomStringArray(16, 30, false), SystemUser.INSTANCE); - fail("should not be able to create a runAs user with the system user"); - } catch (ElasticsearchSecurityException e) { - assertThat(e.getMessage(), containsString("invalid run_as user")); - } - } - public void testUserToString() throws Exception { User user = new User("u1", "r1"); assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}]")); user = new User("u1", new String[] { "r1", "r2" }, "user1", "user1@domain.com", Collections.singletonMap("key", "val"), true); assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=user1,email=user1@domain.com,metadata={key=val}]")); - user = new User("u1", new String[] {"r1", "r2"}, new User("u2", "r3")); - assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=null,email=null,metadata={},runAs=[User[username=u2," + - "roles=[r3],fullName=null,email=null,metadata={}]]]")); + user = new User("u1", new String[] {"r1"}, new User("u2", "r2", "r3")); + assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}," + + "authenticatedUser=[User[username=u2,roles=[r2,r3],fullName=null,email=null,metadata={}]]]")); } public void testReservedUserSerialization() throws Exception { diff --git a/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/11_templated_role_query_runas.yaml b/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/11_templated_role_query_runas.yaml new file mode 100644 index 00000000000..f0e90cecd15 --- /dev/null +++ b/qa/smoke-test-security-with-mustache/src/test/resources/rest-api-spec/test/11_templated_role_query_runas.yaml @@ -0,0 +1,239 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + - do: + xpack.security.put_user: + username: "inline_template_user" + body: > + { + "password": "changeme", + "roles" : [ "inline_template_role" ] + } + - do: + xpack.security.put_user: + username: "stored_template_user" + body: > + { + "password": "changeme", + "roles" : [ "stored_template_role" ] + } + + - do: + xpack.security.put_user: + username: "file_template_user" + body: > + { + "password": "changeme", + "roles" : [ "file_template_role" ] + } + - do: + xpack.security.put_user: + username: "terms_template_user" + body: > + { + "password": "changeme", + "roles" : [ "terms_template_role" ], + "metadata": { + "groups": [ "inline_template_user", "file_template_user" ] + } + } + + - do: + xpack.security.put_role: + name: "inline_template_role" + body: > + { + "indices": [ + { + "names": "foobar", + "privileges": ["all"], + "query" : { + "template" : { + "inline" : { + "term" : { "username" : "{{_user.username}}" } + } + } + } + } + ] + } + + - do: + xpack.security.put_role: + name: "terms_template_role" + body: > + { + "indices": [ + { + "names": "foobar", + "privileges": ["all"], + "query" : { + "template" : { + "inline" : "{\"terms\" : { \"username\" : {{#toJson}}_user.metadata.groups{{/toJson}} } }" + } + } + } + ] + } + + - do: + xpack.security.put_role: + name: "stored_template_role" + body: > + { + "indices": [ + { + "names": "foobar", + "privileges": ["all"], + "query" : { + "template" : { + "stored" : "1" + } + } + } + ] + } + + - do: + xpack.security.put_role: + name: "file_template_role" + body: > + { + "indices": [ + { + "names": "foobar", + "privileges": ["all"], + "query" : { + "template" : { + "file" : "query" + } + } + } + ] + } + + - do: + put_template: + id: "1" + body: > + { + "term" : { + "username" : "{{_user.username}}" + } + } + + - do: + index: + index: foobar + type: type + id: 1 + body: > + { + "username": "inline_template_user" + } + - do: + index: + index: foobar + type: type + id: 2 + body: > + { + "username": "stored_template_user" + } + - do: + index: + index: foobar + type: type + id: 3 + body: > + { + "username": "file_template_user" + } + + - do: + indices.refresh: {} + +--- +teardown: + - do: + xpack.security.delete_user: + username: "inline_template_user" + ignore: 404 + - do: + xpack.security.delete_user: + username: "stored_template_user" + ignore: 404 + - do: + xpack.security.delete_user: + username: "file_template_user" + ignore: 404 + - do: + xpack.security.delete_user: + username: "terms_template_user" + ignore: 404 + - do: + xpack.security.delete_role: + name: "inline_template_role" + ignore: 404 + - do: + xpack.security.delete_role: + name: "stored_template_role" + ignore: 404 + - do: + xpack.security.delete_role: + name: "file_template_role" + ignore: 404 + - do: + xpack.security.delete_role: + name: "terms_template_role" + ignore: 404 +--- +"Test inline template with run as": + - do: + headers: + es-security-runas-user: "inline_template_user" + search: + index: foobar + body: { "query" : { "match_all" : {} } } + - match: { hits.total: 1} + - match: { hits.hits.0._source.username: inline_template_user} + +--- +"Test stored template with run as": + - do: + headers: + es-security-runas-user: "stored_template_user" + search: + index: foobar + body: { "query" : { "match_all" : {} } } + - match: { hits.total: 1} + - match: { hits.hits.0._source.username: stored_template_user} + +--- +"Test file template with run as": + - do: + headers: + es-security-runas-user: "file_template_user" + search: + index: foobar + body: { "query" : { "match_all" : {} } } + - match: { hits.total: 1} + - match: { hits.hits.0._source.username: file_template_user} + +--- +"Test terms template with run as": + - do: + headers: + es-security-runas-user: "terms_template_user" + search: + index: foobar + body: { "query" : { "match_all" : {} } } + - match: { hits.total: 2} + - match: { hits.hits.0._source.username: inline_template_user} + - match: { hits.hits.1._source.username: file_template_user}