From 1e6a924e7412c1a54119359ddec60336c5c3d5a7 Mon Sep 17 00:00:00 2001 From: jaymode Date: Tue, 23 Aug 2016 06:41:15 -0400 Subject: [PATCH] security: add support for disabling users This change adds support for disabling users. Users can be disabled by setting the enabled property to false and the AuthenticationService will check to make sure that the user is enabled. If the user is not enabled, this will be audited as an authentication failure. Also as part of this work, the AnonymousUser was cleaned up to remove having a static instance that caused issues with tests. Finally, the poller of users was removed to simplify the code in the NativeUsersStore. In our other realms we rely on the clear cache APIs and the timeout of the user cache. We should have the same semantics for the native realm. Closes elastic/elasticsearch#2172 Original commit: elastic/x-pack-elasticsearch@0820e401832ca2ee9de436446d825e08967b6e1e --- .../xpack/security/Security.java | 20 +- .../xpack/security/SecurityFeatureSet.java | 2 +- .../action/role/DeleteRoleRequest.java | 28 +- .../action/role/DeleteRoleRequestBuilder.java | 9 +- .../action/user/DeleteUserRequest.java | 28 +- .../action/user/DeleteUserRequestBuilder.java | 9 +- .../security/action/user/PutUserRequest.java | 5 + .../action/user/SetEnabledAction.java | 32 ++ .../action/user/SetEnabledRequest.java | 106 ++++ .../action/user/SetEnabledRequestBuilder.java | 37 ++ .../action/user/SetEnabledResponse.java | 14 + .../user/TransportAuthenticateAction.java | 3 +- .../user/TransportChangePasswordAction.java | 5 +- .../user/TransportDeleteUserAction.java | 7 +- .../action/user/TransportGetUsersAction.java | 19 +- .../action/user/TransportPutUserAction.java | 7 +- .../user/TransportSetEnabledAction.java | 65 +++ .../security/authc/AuthenticationService.java | 18 +- .../esnative/ESNativeRealmMigrateTool.java | 2 +- .../security/authc/esnative/NativeRealm.java | 15 +- .../authc/esnative/NativeUsersStore.java | 501 +++++++++--------- .../authc/esnative/ReservedRealm.java | 104 ++-- .../authc/file/FileUserPasswdStore.java | 17 +- .../security/authc/file/tool/UsersTool.java | 28 +- .../support/CachingUsernamePasswordRealm.java | 12 +- .../security/authz/AuthorizationService.java | 12 +- .../authz/store/NativeRolesStore.java | 5 +- .../authz/store/ReservedRolesStore.java | 11 +- .../xpack/security/client/SecurityClient.java | 12 + .../action/role/RestDeleteRoleAction.java | 25 +- .../action/user/RestChangePasswordAction.java | 2 +- .../action/user/RestDeleteUserAction.java | 27 +- .../rest/action/user/RestPutUserAction.java | 4 +- .../action/user/RestSetEnabledAction.java | 53 ++ .../xpack/security/support/MetadataUtils.java | 2 +- .../xpack/security/support/Validation.java | 12 +- .../xpack/security/user/AnonymousUser.java | 66 +-- .../xpack/security/user/ElasticUser.java | 29 +- .../xpack/security/user/KibanaUser.java | 25 +- .../xpack/security/user/User.java | 79 +-- .../xpack/security/user/XPackUser.java | 3 +- .../resources/security-index-template.json | 6 + .../security/SecurityFeatureSetTests.java | 8 +- .../role/TransportGetRolesActionTests.java | 12 +- .../TransportAuthenticateActionTests.java | 7 +- .../TransportChangePasswordActionTests.java | 21 +- .../user/TransportDeleteUserActionTests.java | 21 +- .../user/TransportGetUsersActionTests.java | 60 ++- .../user/TransportPutUserActionTests.java | 24 +- .../user/TransportSetEnabledActionTests.java | 259 +++++++++ .../authc/AuthenticationServiceTests.java | 122 +++-- .../authc/esnative/NativeRealmIntegTests.java | 23 + .../esnative/ReservedRealmIntegTests.java | 31 ++ .../authc/esnative/ReservedRealmTests.java | 87 +-- .../authc/file/FileUserPasswdStoreTests.java | 15 +- .../authc/file/tool/UsersToolTests.java | 6 +- .../authz/AuthorizationServiceTests.java | 32 +- ...SecurityIndexSearcherWrapperUnitTests.java | 2 +- .../SetSecurityUserProcessorTests.java | 8 +- .../DefaultIndicesResolverTests.java | 10 +- .../authz/store/ReservedRolesStoreTests.java | 4 +- .../security/support/ValidationTests.java | 13 +- .../security/user/AnonymousUserTests.java | 32 +- .../xpack/security/user/UserTests.java | 27 +- .../org/elasticsearch/transport/actions | 1 + .../org/elasticsearch/transport/handlers | 1 + .../api/xpack.security.change_password.json | 5 +- .../api/xpack.security.delete_role.json | 5 +- .../api/xpack.security.delete_user.json | 5 +- .../api/xpack.security.disable_user.json | 25 + .../api/xpack.security.enable_user.json | 25 + .../api/xpack.security.put_role.json | 5 +- .../api/xpack.security.put_user.json | 5 +- .../test/users/30_enable_disable.yaml | 124 +++++ .../action/TransportXPackInfoActionTests.java | 19 - 75 files changed, 1647 insertions(+), 863 deletions(-) create mode 100644 elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledAction.java create mode 100644 elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequest.java create mode 100644 elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequestBuilder.java create mode 100644 elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledResponse.java create mode 100644 elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledAction.java create mode 100644 elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestSetEnabledAction.java create mode 100644 elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java create mode 100644 elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json create mode 100644 elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json create mode 100644 elasticsearch/x-pack/security/src/test/resources/rest-api-spec/test/users/30_enable_disable.yaml diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 88a7e17df60..17974ffcd8d 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -51,11 +51,13 @@ import org.elasticsearch.xpack.security.action.user.ChangePasswordAction; import org.elasticsearch.xpack.security.action.user.DeleteUserAction; import org.elasticsearch.xpack.security.action.user.GetUsersAction; import org.elasticsearch.xpack.security.action.user.PutUserAction; +import org.elasticsearch.xpack.security.action.user.SetEnabledAction; import org.elasticsearch.xpack.security.action.user.TransportAuthenticateAction; import org.elasticsearch.xpack.security.action.user.TransportChangePasswordAction; import org.elasticsearch.xpack.security.action.user.TransportDeleteUserAction; import org.elasticsearch.xpack.security.action.user.TransportGetUsersAction; import org.elasticsearch.xpack.security.action.user.TransportPutUserAction; +import org.elasticsearch.xpack.security.action.user.TransportSetEnabledAction; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail; @@ -96,6 +98,7 @@ import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordActio import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestGetUsersAction; import org.elasticsearch.xpack.security.rest.action.user.RestPutUserAction; +import org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction; import org.elasticsearch.xpack.security.transport.SecurityServerTransportService; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.netty3.SecurityNetty3HttpServerTransport; @@ -219,15 +222,15 @@ public class Security implements ActionPlugin, IngestPlugin { if (enabled == false) { return Collections.emptyList(); } - AnonymousUser.initialize(settings); // TODO: this is sketchy...testing is difficult b/c it is static.... List components = new ArrayList<>(); final SecurityContext securityContext = new SecurityContext(settings, threadPool, cryptoService); components.add(securityContext); // realms construction - final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, threadPool); - final ReservedRealm reservedRealm = new ReservedRealm(env, settings, nativeUsersStore); + final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client); + final AnonymousUser anonymousUser = new AnonymousUser(settings); + final ReservedRealm reservedRealm = new ReservedRealm(env, settings, nativeUsersStore, anonymousUser); Map realmFactories = new HashMap<>(); realmFactories.put(FileRealm.TYPE, config -> new FileRealm(config, resourceWatcherService)); realmFactories.put(NativeRealm.TYPE, config -> new NativeRealm(config, nativeUsersStore)); @@ -246,6 +249,7 @@ public class Security implements ActionPlugin, IngestPlugin { final Realms realms = new Realms(settings, env, realmFactories, licenseState, reservedRealm); components.add(nativeUsersStore); components.add(realms); + components.add(reservedRealm); // audit trails construction IndexAuditTrail indexAuditTrail = null; @@ -294,7 +298,7 @@ public class Security implements ActionPlugin, IngestPlugin { } final AuthenticationService authcService = new AuthenticationService(settings, realms, auditTrailService, - cryptoService, failureHandler, threadPool); + cryptoService, failureHandler, threadPool, anonymousUser); components.add(authcService); final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService); @@ -302,7 +306,7 @@ public class Security implements ActionPlugin, IngestPlugin { final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(securityContext); final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore); final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, - auditTrailService, failureHandler, threadPool); + auditTrailService, failureHandler, threadPool, anonymousUser); components.add(fileRolesStore); // has lifecycle components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions @@ -458,7 +462,8 @@ public class Security implements ActionPlugin, IngestPlugin { new ActionHandler<>(PutRoleAction.INSTANCE, TransportPutRoleAction.class), new ActionHandler<>(DeleteRoleAction.INSTANCE, TransportDeleteRoleAction.class), new ActionHandler<>(ChangePasswordAction.INSTANCE, TransportChangePasswordAction.class), - new ActionHandler<>(AuthenticateAction.INSTANCE, TransportAuthenticateAction.class)); + new ActionHandler<>(AuthenticateAction.INSTANCE, TransportAuthenticateAction.class), + new ActionHandler<>(SetEnabledAction.INSTANCE, TransportSetEnabledAction.class)); } @Override @@ -487,7 +492,8 @@ public class Security implements ActionPlugin, IngestPlugin { RestGetRolesAction.class, RestPutRoleAction.class, RestDeleteRoleAction.class, - RestChangePasswordAction.class); + RestChangePasswordAction.class, + RestSetEnabledAction.class); } @Override diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java index 8c636cc8e00..61a21d2f84f 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java @@ -94,7 +94,7 @@ public class SecurityFeatureSet implements XPackFeatureSet { Map auditUsage = auditUsage(auditTrailService); Map ipFilterUsage = ipFilterUsage(ipFilter); Map systemKeyUsage = systemKeyUsage(cryptoService); - Map anonymousUsage = Collections.singletonMap("enabled", AnonymousUser.enabled()); + Map anonymousUsage = Collections.singletonMap("enabled", AnonymousUser.isAnonymousEnabled(settings)); return new Usage(available(), enabled(), realmsUsage, rolesStoreUsage, sslUsage, auditUsage, ipFilterUsage, systemKeyUsage, anonymousUsage); } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequest.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequest.java index 08237d1e476..6a7147eaa06 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequest.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequest.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.action.role; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -17,14 +18,25 @@ import static org.elasticsearch.action.ValidateActions.addValidationError; /** * A request delete a role from the security index */ -public class DeleteRoleRequest extends ActionRequest { +public class DeleteRoleRequest extends ActionRequest implements WriteRequest { private String name; - private boolean refresh = true; + private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; public DeleteRoleRequest() { } + @Override + public DeleteRoleRequest setRefreshPolicy(RefreshPolicy refreshPolicy) { + this.refreshPolicy = refreshPolicy; + return this; + } + + @Override + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; @@ -42,25 +54,17 @@ public class DeleteRoleRequest extends ActionRequest { return name; } - public void refresh(boolean refresh) { - this.refresh = refresh; - } - - public boolean refresh() { - return refresh; - } - @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); name = in.readString(); - refresh = in.readBoolean(); + refreshPolicy = RefreshPolicy.readFrom(in); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(name); - out.writeBoolean(refresh); + refreshPolicy.writeTo(out); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequestBuilder.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequestBuilder.java index 646be72ca4f..77fe219c463 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequestBuilder.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/role/DeleteRoleRequestBuilder.java @@ -6,12 +6,14 @@ package org.elasticsearch.xpack.security.action.role; import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; /** * A builder for requests to delete a role from the security index */ -public class DeleteRoleRequestBuilder extends ActionRequestBuilder { +public class DeleteRoleRequestBuilder extends ActionRequestBuilder + implements WriteRequestBuilder { public DeleteRoleRequestBuilder(ElasticsearchClient client) { this(client, DeleteRoleAction.INSTANCE); @@ -25,9 +27,4 @@ public class DeleteRoleRequestBuilder extends ActionRequestBuilder implements UserRequest { +public class DeleteUserRequest extends ActionRequest implements UserRequest, WriteRequest { private String username; - private boolean refresh = true; + private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; public DeleteUserRequest() { } @@ -29,6 +30,17 @@ public class DeleteUserRequest extends ActionRequest implemen this.username = username; } + @Override + public DeleteUserRequest setRefreshPolicy(RefreshPolicy refreshPolicy) { + this.refreshPolicy = refreshPolicy; + return this; + } + + @Override + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; @@ -42,18 +54,10 @@ public class DeleteUserRequest extends ActionRequest implemen return this.username; } - public boolean refresh() { - return refresh; - } - public void username(String username) { this.username = username; } - public void refresh(boolean refresh) { - this.refresh = refresh; - } - @Override public String[] usernames() { return new String[] { username }; @@ -63,14 +67,14 @@ public class DeleteUserRequest extends ActionRequest implemen public void readFrom(StreamInput in) throws IOException { super.readFrom(in); username = in.readString(); - refresh = in.readBoolean(); + refreshPolicy = RefreshPolicy.readFrom(in); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(username); - out.writeBoolean(refresh); + refreshPolicy.writeTo(out); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/DeleteUserRequestBuilder.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/DeleteUserRequestBuilder.java index b228e979eb4..5d44cdb3418 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/DeleteUserRequestBuilder.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/DeleteUserRequestBuilder.java @@ -6,9 +6,11 @@ package org.elasticsearch.xpack.security.action.user; import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; -public class DeleteUserRequestBuilder extends ActionRequestBuilder { +public class DeleteUserRequestBuilder extends ActionRequestBuilder + implements WriteRequestBuilder { public DeleteUserRequestBuilder(ElasticsearchClient client) { this(client, DeleteUserAction.INSTANCE); @@ -22,9 +24,4 @@ public class DeleteUserRequestBuilder extends ActionRequestBuilder implements Use if (roles == null) { validationException = addValidationError("roles are missing", validationException); } + if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) { + validationException = addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", + validationException); + } // we do not check for a password hash here since it is possible that the user exists and we don't want to update the password return validationException; } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledAction.java new file mode 100644 index 00000000000..c579b769e85 --- /dev/null +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledAction.java @@ -0,0 +1,32 @@ +/* + * 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.action.user; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * This action is for setting the enabled flag on a native or reserved user + */ +public class SetEnabledAction extends Action { + + public static final SetEnabledAction INSTANCE = new SetEnabledAction(); + public static final String NAME = "cluster:admin/xpack/security/user/set_enabled"; + + private SetEnabledAction() { + super(NAME); + } + + @Override + public SetEnabledRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new SetEnabledRequestBuilder(client); + } + + @Override + public SetEnabledResponse newResponse() { + return new SetEnabledResponse(); + } +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequest.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequest.java new file mode 100644 index 00000000000..195fcd37c22 --- /dev/null +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequest.java @@ -0,0 +1,106 @@ +/* + * 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.action.user; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.security.support.Validation.Error; +import org.elasticsearch.xpack.security.support.Validation.Users; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * The request that allows to set a user as enabled or disabled + */ +public class SetEnabledRequest extends ActionRequest implements UserRequest, WriteRequest { + + private Boolean enabled; + private String username; + private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + Error error = Users.validateUsername(username, true, Settings.EMPTY); + if (error != null) { + validationException = addValidationError(error.toString(), validationException); + } + if (enabled == null) { + validationException = addValidationError("enabled must be set", validationException); + } + return validationException; + } + + /** + * @return whether the user should be set to enabled or not + */ + public Boolean enabled() { + return enabled; + } + + /** + * Set whether the user should be enabled or not. + */ + public void enabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * @return the username that this request applies to. + */ + public String username() { + return username; + } + + /** + * Set the username that the request applies to. Must not be {@code null} + */ + public void username(String username) { + this.username = username; + } + + @Override + public String[] usernames() { + return new String[] { username }; + } + + /** + * Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default), wait for a refresh ( + * {@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes entirely ({@linkplain RefreshPolicy#NONE}). + */ + @Override + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + @Override + public SetEnabledRequest setRefreshPolicy(RefreshPolicy refreshPolicy) { + this.refreshPolicy = refreshPolicy; + return this; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.enabled = in.readBoolean(); + this.username = in.readString(); + this.refreshPolicy = RefreshPolicy.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeBoolean(enabled); + out.writeString(username); + refreshPolicy.writeTo(out); + } +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequestBuilder.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequestBuilder.java new file mode 100644 index 00000000000..133069e2b31 --- /dev/null +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledRequestBuilder.java @@ -0,0 +1,37 @@ +/* + * 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.action.user; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for setting a user as enabled or disabled + */ +public class SetEnabledRequestBuilder extends ActionRequestBuilder + implements WriteRequestBuilder { + + public SetEnabledRequestBuilder(ElasticsearchClient client) { + super(client, SetEnabledAction.INSTANCE, new SetEnabledRequest()); + } + + /** + * Set the username of the user that should enabled or disabled. Must not be {@code null} + */ + public SetEnabledRequestBuilder username(String username) { + request.username(username); + return this; + } + + /** + * Set whether the user should be enabled or not + */ + public SetEnabledRequestBuilder enabled(boolean enabled) { + request.enabled(enabled); + return this; + } +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledResponse.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledResponse.java new file mode 100644 index 00000000000..fe44f5f5197 --- /dev/null +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/SetEnabledResponse.java @@ -0,0 +1,14 @@ +/* + * 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.action.user; + +import org.elasticsearch.action.ActionResponse; + +/** + * Empty response for a {@link SetEnabledRequest} + */ +public class SetEnabledResponse extends ActionResponse { +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java index b05959caec3..5f368564be9 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.security.user.XPackUser; /** */ @@ -36,7 +37,7 @@ public class TransportAuthenticateAction extends HandledTransportAction listener) { final User user = securityContext.getUser(); - if (SystemUser.is(user)) { + if (SystemUser.is(user) || XPackUser.is(user)) { listener.onFailure(new IllegalArgumentException("user [" + user.principal() + "] is internal")); return; } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java index 17d0356e5a0..b91206d24b3 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.security.user.XPackUser; /** */ @@ -35,10 +36,10 @@ public class TransportChangePasswordAction extends HandledTransportAction listener) { final String username = request.username(); - if (AnonymousUser.isAnonymousUsername(username)) { + if (AnonymousUser.isAnonymousUsername(username, settings)) { listener.onFailure(new IllegalArgumentException("user [" + username + "] is anonymous and cannot be modified via the API")); return; - } else if (SystemUser.NAME.equals(username)) { + } else if (SystemUser.NAME.equals(username) || XPackUser.NAME.equals(username)) { listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); return; } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserAction.java index 207bac9ba17..736f8301498 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.security.user.XPackUser; public class TransportDeleteUserAction extends HandledTransportAction { @@ -34,15 +35,15 @@ public class TransportDeleteUserAction extends HandledTransportAction listener) { final String username = request.username(); - if (ReservedRealm.isReserved(username)) { - if (AnonymousUser.isAnonymousUsername(username)) { + if (ReservedRealm.isReserved(username, settings)) { + if (AnonymousUser.isAnonymousUsername(username, settings)) { listener.onFailure(new IllegalArgumentException("user [" + username + "] is anonymous and cannot be deleted")); return; } else { listener.onFailure(new IllegalArgumentException("user [" + username + "] is reserved and cannot be deleted")); return; } - } else if (SystemUser.NAME.equals(username)) { + } else if (SystemUser.NAME.equals(username) || XPackUser.NAME.equals(username)) { listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); return; } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java index 192828be9b4..82220a32cb7 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java @@ -17,9 +17,9 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.xpack.security.user.User; +import org.elasticsearch.xpack.security.user.XPackUser; import java.util.ArrayList; import java.util.List; @@ -29,14 +29,16 @@ import static org.elasticsearch.common.Strings.arrayToDelimitedString; public class TransportGetUsersAction extends HandledTransportAction { private final NativeUsersStore usersStore; + private final ReservedRealm reservedRealm; @Inject public TransportGetUsersAction(Settings settings, ThreadPool threadPool, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, NativeUsersStore usersStore, - TransportService transportService) { + TransportService transportService, ReservedRealm reservedRealm) { super(settings, GetUsersAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, GetUsersRequest::new); this.usersStore = usersStore; + this.reservedRealm = reservedRealm; } @Override @@ -48,16 +50,13 @@ public class TransportGetUsersAction extends HandledTransportAction { @@ -35,8 +36,8 @@ public class TransportPutUserAction extends HandledTransportAction listener) { final String username = request.username(); - if (ReservedRealm.isReserved(username)) { - if (AnonymousUser.isAnonymousUsername(username)) { + if (ReservedRealm.isReserved(username, settings)) { + if (AnonymousUser.isAnonymousUsername(username, settings)) { listener.onFailure(new IllegalArgumentException("user [" + username + "] is anonymous and cannot be modified via the API")); return; } else { @@ -44,7 +45,7 @@ public class TransportPutUserAction extends HandledTransportAction { + + private final NativeUsersStore usersStore; + + @Inject + public TransportSetEnabledAction(Settings settings, ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, + NativeUsersStore usersStore) { + super(settings, SetEnabledAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, + SetEnabledRequest::new); + this.usersStore = usersStore; + } + + @Override + protected void doExecute(SetEnabledRequest request, ActionListener listener) { + final String username = request.username(); + // make sure the user is not disabling themselves + if (Authentication.getAuthentication(threadPool.getThreadContext()).getRunAsUser().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)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); + return; + } else if (AnonymousUser.isAnonymousUsername(username, settings)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is anonymous and cannot be modified using the api")); + return; + } + + usersStore.setEnabled(username, request.enabled(), request.getRefreshPolicy(), new ActionListener() { + @Override + public void onResponse(Void v) { + listener.onResponse(new SetEnabledResponse()); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 08f86226729..b884b9f7699 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -50,11 +50,13 @@ public class AuthenticationService extends AbstractComponent { private final AuthenticationFailureHandler failureHandler; private final ThreadContext threadContext; private final String nodeName; + private final AnonymousUser anonymousUser; private final boolean signUserHeader; private final boolean runAsEnabled; + private final boolean isAnonymousUserEnabled; public AuthenticationService(Settings settings, Realms realms, AuditTrailService auditTrail, CryptoService cryptoService, - AuthenticationFailureHandler failureHandler, ThreadPool threadPool) { + AuthenticationFailureHandler failureHandler, ThreadPool threadPool, AnonymousUser anonymousUser) { super(settings); this.nodeName = Node.NODE_NAME_SETTING.get(settings); this.realms = realms; @@ -62,8 +64,10 @@ public class AuthenticationService extends AbstractComponent { this.cryptoService = cryptoService; this.failureHandler = failureHandler; this.threadContext = threadPool.getThreadContext(); + this.anonymousUser = anonymousUser; this.signUserHeader = SIGN_USER_HEADER.get(settings); this.runAsEnabled = RUN_AS_ENABLED.get(settings); + this.isAnonymousUserEnabled = AnonymousUser.isAnonymousEnabled(settings); } /** @@ -157,6 +161,7 @@ public class AuthenticationService extends AbstractComponent { throw handleNullUser(token); } user = lookupRunAsUserIfNecessary(user, token); + checkIfUserIsDisabled(user, token); final Authentication authentication = new Authentication(user, authenticatedBy, lookedupBy); authentication.writeToContext(threadContext, cryptoService, signUserHeader); @@ -204,9 +209,9 @@ public class AuthenticationService extends AbstractComponent { if (fallbackUser != null) { RealmRef authenticatedBy = new RealmRef("__fallback", "__fallback", nodeName); authentication = new Authentication(fallbackUser, authenticatedBy, null); - } else if (AnonymousUser.enabled()) { + } else if (isAnonymousUserEnabled) { RealmRef authenticatedBy = new RealmRef("__anonymous", "__anonymous", nodeName); - authentication = new Authentication(AnonymousUser.INSTANCE, authenticatedBy, null); + authentication = new Authentication(anonymousUser, authenticatedBy, null); } if (authentication != null) { @@ -297,6 +302,13 @@ public class AuthenticationService extends AbstractComponent { return user; } + void checkIfUserIsDisabled(User user, AuthenticationToken token) { + if (user.enabled() == false || (user.runAs() != null && user.runAs().enabled() == false)) { + logger.debug("user [{}] is disabled. failing authentication", user); + throw request.authenticationFailed(token); + } + } + abstract class AuditableRequest { abstract void realmAuthenticationFailed(AuthenticationToken token, String realm); diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateTool.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateTool.java index 0d896971b23..df502a83d94 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateTool.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateTool.java @@ -229,7 +229,7 @@ public class ESNativeRealmMigrateTool extends MultiCommand { Path usersFile = FileUserPasswdStore.resolveFile(env); Path usersRolesFile = FileUserRolesStore.resolveFile(env); terminal.println("importing users from [" + usersFile + "]..."); - Map userToHashedPW = FileUserPasswdStore.parseFile(usersFile, null); + Map userToHashedPW = FileUserPasswdStore.parseFile(usersFile, null, settings); Map userToRoles = FileUserRolesStore.parseFile(usersRolesFile, null); Set existingUsers; try { diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java index e018c5e3ac8..172e5c9a9a3 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java @@ -5,8 +5,6 @@ */ package org.elasticsearch.xpack.security.authc.esnative; -import java.util.List; - import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; @@ -19,12 +17,11 @@ public class NativeRealm extends CachingUsernamePasswordRealm { public static final String TYPE = "native"; - final NativeUsersStore userStore; + private final NativeUsersStore userStore; public NativeRealm(RealmConfig config, NativeUsersStore usersStore) { super(TYPE, config); this.userStore = usersStore; - usersStore.addListener(new Listener()); } @Override @@ -41,14 +38,4 @@ public class NativeRealm extends CachingUsernamePasswordRealm { protected User doAuthenticate(UsernamePasswordToken token) { return userStore.verifyPassword(token.principal(), token.credentials()); } - - class Listener implements NativeUsersStore.ChangeListener { - - @Override - public void onUsersChanged(List usernames) { - for (String username : usernames) { - expire(username); - } - } - } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index 1eabb7937fa..682b5d6d9ef 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -5,16 +5,13 @@ */ package org.elasticsearch.xpack.security.authc.esnative; -import com.carrotsearch.hppc.ObjectHashSet; -import com.carrotsearch.hppc.ObjectLongHashMap; -import com.carrotsearch.hppc.ObjectLongMap; -import com.carrotsearch.hppc.cursors.ObjectCursor; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.DocWriteResponse; +import org.elasticsearch.action.DocWriteResponse.Result; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteResponse; @@ -28,7 +25,6 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.update.UpdateResponse; -import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateListener; @@ -41,16 +37,12 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.gateway.GatewayService; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.engine.DocumentMissingException; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.threadpool.ThreadPool.Cancellable; -import org.elasticsearch.threadpool.ThreadPool.Names; import org.elasticsearch.xpack.security.InternalClient; import org.elasticsearch.xpack.security.SecurityTemplateService; import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheRequest; @@ -64,14 +56,14 @@ import org.elasticsearch.xpack.security.client.SecurityClient; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.xpack.security.user.User.Fields; +import org.elasticsearch.xpack.security.user.XPackUser; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -81,25 +73,20 @@ import static org.elasticsearch.xpack.security.Security.setting; import static org.elasticsearch.xpack.security.SecurityTemplateService.securityIndexMappingAndTemplateUpToDate; /** - * ESNativeUsersStore is a {@code UserStore} that, instead of reading from a - * file, reads from an Elasticsearch index instead. This {@code UserStore} in - * particular implements both a User store and a UserRoles store, which means it - * is responsible for fetching not only {@code User} objects, but also - * retrieving the roles for a given username. + * NativeUsersStore is a store for users that reads from an Elasticsearch index. This store is responsible for fetching the full + * {@link User} object, which includes the names of the roles assigned to the user. *

- * No caching is done by this class, it is handled at a higher level + * No caching is done by this class, it is handled at a higher level and no polling for changes is done by this class. Modification + * operations make a best effort attempt to clear the cache on all nodes for the user that was modified. */ public class NativeUsersStore extends AbstractComponent implements ClusterStateListener { - public static final Setting SCROLL_SIZE_SETTING = + private static final Setting SCROLL_SIZE_SETTING = Setting.intSetting(setting("authc.native.scroll.size"), 1000, Property.NodeScope); - public static final Setting SCROLL_KEEP_ALIVE_SETTING = + private static final Setting SCROLL_KEEP_ALIVE_SETTING = Setting.timeSetting(setting("authc.native.scroll.keep_alive"), TimeValue.timeValueSeconds(10L), Property.NodeScope); - public static final Setting POLL_INTERVAL_SETTING = - Setting.timeSetting(setting("authc.native.reload.interval"), TimeValue.timeValueSeconds(30L), Property.NodeScope); - public enum State { INITIALIZED, STARTING, @@ -109,25 +96,20 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL FAILED } - public static final String USER_DOC_TYPE = "user"; - static final String RESERVED_USER_DOC_TYPE = "reserved-user"; + private static final String USER_DOC_TYPE = "user"; + private static final String RESERVED_USER_DOC_TYPE = "reserved-user"; private final Hasher hasher = Hasher.BCRYPT; - private final List listeners = new CopyOnWriteArrayList<>(); private final AtomicReference state = new AtomicReference<>(State.INITIALIZED); private final InternalClient client; - private final ThreadPool threadPool; - - private Cancellable pollerCancellable; private int scrollSize; private TimeValue scrollKeepAlive; private volatile boolean securityIndexExists = false; - public NativeUsersStore(Settings settings, InternalClient client, ThreadPool threadPool) { + public NativeUsersStore(Settings settings, InternalClient client) { super(settings); this.client = client; - this.threadPool = threadPool; } /** @@ -249,6 +231,9 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL } } + /** + * Blocking method to get the user and their password hash + */ private UserAndPassword getUserAndPassword(final String username) { final AtomicReference userRef = new AtomicReference<>(null); final CountDownLatch latch = new CountDownLatch(1); @@ -278,6 +263,9 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL return userRef.get(); } + /** + * Async method to retrieve a user and their password + */ private void getUserAndPassword(final String user, final ActionListener listener) { try { GetRequest request = client.prepareGet(SecurityTemplateService.SECURITY_INDEX_NAME, USER_DOC_TYPE, user).request(); @@ -310,17 +298,16 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL } } + /** + * Async method to change the password of a native or reserved user. If a reserved user does not exist, the document will be created + * with a hash of the provided password. + */ public void changePassword(final ChangePasswordRequest request, final ActionListener listener) { final String username = request.username(); - if (SystemUser.NAME.equals(username)) { - ValidationException validationException = new ValidationException(); - validationException.addValidationError("changing the password for [" + username + "] is not allowed"); - listener.onFailure(validationException); - return; - } + assert SystemUser.NAME.equals(username) == false && XPackUser.NAME.equals(username) == false : username + "is internal!"; final String docType; - if (ReservedRealm.isReserved(username)) { + if (ReservedRealm.isReserved(username, settings)) { docType = RESERVED_USER_DOC_TYPE; } else { docType = USER_DOC_TYPE; @@ -338,33 +325,30 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL @Override public void onFailure(Exception e) { - Throwable cause = e; - if (e instanceof ElasticsearchException) { - cause = ExceptionsHelper.unwrapCause(e); - if ((cause instanceof IndexNotFoundException) == false - && (cause instanceof DocumentMissingException) == false) { - listener.onFailure(e); - return; + if (isIndexNotFoundOrDocumentMissing(e)) { + if (docType.equals(RESERVED_USER_DOC_TYPE)) { + createReservedUser(username, request.passwordHash(), request.getRefreshPolicy(), listener); + } else { + logger.debug((Supplier) () -> + new ParameterizedMessage("failed to change password for user [{}]", request.username()), e); + ValidationException validationException = new ValidationException(); + validationException.addValidationError("user must exist in order to change password"); + listener.onFailure(validationException); } - } - - if (docType.equals(RESERVED_USER_DOC_TYPE)) { - createReservedUser(username, request.passwordHash(), request.getRefreshPolicy(), listener); } else { - logger.debug( - (Supplier) () -> new ParameterizedMessage( - "failed to change password for user [{}]", request.username()), cause); - ValidationException validationException = new ValidationException(); - validationException.addValidationError("user must exist in order to change password"); - listener.onFailure(validationException); + listener.onFailure(e); } } }); } + /** + * Asynchronous method to create a reserved user with the given password hash. The cache for the user will be cleared after the document + * has been indexed + */ private void createReservedUser(String username, char[] passwordHash, RefreshPolicy refresh, ActionListener listener) { client.prepareIndex(SecurityTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username) - .setSource(Fields.PASSWORD.getPreferredName(), String.valueOf(passwordHash)) + .setSource(Fields.PASSWORD.getPreferredName(), String.valueOf(passwordHash), Fields.ENABLED.getPreferredName(), true) .setRefreshPolicy(refresh) .execute(new ActionListener() { @Override @@ -379,6 +363,12 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL }); } + /** + * Asynchronous method to put a user. A put user request without a password hash is treated as an update and will fail with a + * {@link ValidationException} if the user does not exist. If a password hash is provided, then we issue a update request with an + * upsert document as well; the upsert document sets the enabled flag of the user to true but if the document already exists, this + * method will not modify the enabled value. + */ public void putUser(final PutUserRequest request, final ActionListener listener) { if (state() != State.STARTED) { listener.onFailure(new IllegalStateException("user cannot be added as native user service has not been started")); @@ -389,7 +379,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL if (request.passwordHash() == null) { updateUserWithoutPassword(request, listener); } else { - indexUser(request, listener); + upsertUser(request, listener); } } catch (Exception e) { logger.error((Supplier) () -> new ParameterizedMessage("unable to put user [{}]", request.username()), e); @@ -397,6 +387,9 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL } } + /** + * Handles updating a user that should already exist where their password should not change + */ private void updateUserWithoutPassword(final PutUserRequest putUserRequest, final ActionListener listener) { assert putUserRequest.passwordHash() == null; // We must have an existing document @@ -416,52 +409,43 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL @Override public void onFailure(Exception e) { - Throwable cause = e; - if (e instanceof ElasticsearchException) { - cause = ExceptionsHelper.unwrapCause(e); - if ((cause instanceof IndexNotFoundException) == false - && (cause instanceof DocumentMissingException) == false) { - listener.onFailure(e); - return; - } + Exception failure = e; + if (isIndexNotFoundOrDocumentMissing(e)) { + // if the index doesn't exist we can never update a user + // if the document doesn't exist, then this update is not valid + logger.debug((Supplier) () -> new ParameterizedMessage("failed to update user document with username [{}]", + putUserRequest.username()), e); + ValidationException validationException = new ValidationException(); + validationException.addValidationError("password must be specified unless you are updating an existing user"); + failure = validationException; } - - // if the index doesn't exist we can never update a user - // if the document doesn't exist, then this update is not valid - logger.debug( - (Supplier) () -> new ParameterizedMessage( - "failed to update user document with username [{}]", - putUserRequest.username()), - cause); - ValidationException validationException = new ValidationException(); - validationException.addValidationError("password must be specified unless you are updating an existing user"); - listener.onFailure(validationException); + listener.onFailure(failure); } }); } - private void indexUser(final PutUserRequest putUserRequest, final ActionListener listener) { + private void upsertUser(final PutUserRequest putUserRequest, final ActionListener listener) { assert putUserRequest.passwordHash() != null; - client.prepareIndex(SecurityTemplateService.SECURITY_INDEX_NAME, + client.prepareUpdate(SecurityTemplateService.SECURITY_INDEX_NAME, USER_DOC_TYPE, putUserRequest.username()) - .setSource(User.Fields.USERNAME.getPreferredName(), putUserRequest.username(), + .setDoc(User.Fields.USERNAME.getPreferredName(), putUserRequest.username(), User.Fields.PASSWORD.getPreferredName(), String.valueOf(putUserRequest.passwordHash()), User.Fields.ROLES.getPreferredName(), putUserRequest.roles(), User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(), User.Fields.EMAIL.getPreferredName(), putUserRequest.email(), User.Fields.METADATA.getPreferredName(), putUserRequest.metadata()) + .setUpsert(User.Fields.USERNAME.getPreferredName(), putUserRequest.username(), + User.Fields.PASSWORD.getPreferredName(), String.valueOf(putUserRequest.passwordHash()), + User.Fields.ROLES.getPreferredName(), putUserRequest.roles(), + User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(), + User.Fields.EMAIL.getPreferredName(), putUserRequest.email(), + User.Fields.METADATA.getPreferredName(), putUserRequest.metadata(), + User.Fields.ENABLED.getPreferredName(), true) .setRefreshPolicy(putUserRequest.getRefreshPolicy()) - .execute(new ActionListener() { + .execute(new ActionListener() { @Override - public void onResponse(IndexResponse indexResponse) { - // if the document was just created, then we don't need to clear cache - boolean created = indexResponse.getResult() == DocWriteResponse.Result.CREATED; - if (created) { - listener.onResponse(true); - return; - } - - clearRealmCache(putUserRequest.username(), listener, created); + public void onResponse(UpdateResponse updateResponse) { + clearRealmCache(putUserRequest.username(), listener, updateResponse.getResult() == DocWriteResponse.Result.CREATED); } @Override @@ -471,6 +455,82 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL }); } + /** + * Asynchronous method that will update the enabled flag of a user. If the user is reserved and the document does not exist, a document + * will be created. If the user is not reserved, the user must exist otherwise the operation will fail. + */ + public void setEnabled(final String username, final boolean enabled, final RefreshPolicy refreshPolicy, + final ActionListener listener) { + if (state() != State.STARTED) { + listener.onFailure(new IllegalStateException("enabled status cannot be changed as native user service has not been started")); + return; + } + + if (ReservedRealm.isReserved(username, settings)) { + setReservedUserEnabled(username, enabled, refreshPolicy, listener); + } else { + setRegularUserEnabled(username, enabled, refreshPolicy, listener); + } + } + + private void setRegularUserEnabled(final String username, final boolean enabled, final RefreshPolicy refreshPolicy, + final ActionListener listener) { + try { + client.prepareUpdate(SecurityTemplateService.SECURITY_INDEX_NAME, USER_DOC_TYPE, username) + .setDoc(User.Fields.ENABLED.getPreferredName(), enabled) + .setRefreshPolicy(refreshPolicy) + .execute(new ActionListener() { + @Override + public void onResponse(UpdateResponse updateResponse) { + assert updateResponse.getResult() == Result.UPDATED; + clearRealmCache(username, listener, null); + } + + @Override + public void onFailure(Exception e) { + Exception failure = e; + if (isIndexNotFoundOrDocumentMissing(e)) { + // if the index doesn't exist we can never update a user + // if the document doesn't exist, then this update is not valid + logger.debug((Supplier) () -> + new ParameterizedMessage("failed to {} user [{}]", enabled ? "enable" : "disable", username), e); + ValidationException validationException = new ValidationException(); + validationException.addValidationError("only existing users can be " + (enabled ? "enabled" : "disabled")); + failure = validationException; + } + listener.onFailure(failure); + } + }); + } catch (Exception e) { + listener.onFailure(e); + } + } + + private void setReservedUserEnabled(final String username, final boolean enabled, final RefreshPolicy refreshPolicy, + final ActionListener listener) { + try { + client.prepareUpdate(SecurityTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username) + .setDoc(User.Fields.ENABLED.getPreferredName(), enabled) + .setUpsert(User.Fields.PASSWORD.getPreferredName(), String.valueOf(ReservedRealm.DEFAULT_PASSWORD_HASH), + User.Fields.ENABLED.getPreferredName(), enabled) + .setRefreshPolicy(refreshPolicy) + .execute(new ActionListener() { + @Override + public void onResponse(UpdateResponse updateResponse) { + assert updateResponse.getResult() == Result.UPDATED || updateResponse.getResult() == Result.CREATED; + clearRealmCache(username, listener, null); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } catch (Exception e) { + listener.onFailure(e); + } + } + public void deleteUser(final DeleteUserRequest deleteUserRequest, final ActionListener listener) { if (state() != State.STARTED) { listener.onFailure(new IllegalStateException("user cannot be deleted as native user service has not been started")); @@ -481,7 +541,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL DeleteRequest request = client.prepareDelete(SecurityTemplateService.SECURITY_INDEX_NAME, USER_DOC_TYPE, deleteUserRequest.username()).request(); request.indicesOptions().ignoreUnavailable(); - request.setRefreshPolicy(deleteUserRequest.refresh() ? RefreshPolicy.IMMEDIATE : RefreshPolicy.WAIT_UNTIL); + request.setRefreshPolicy(deleteUserRequest.getRefreshPolicy()); client.delete(request, new ActionListener() { @Override public void onResponse(DeleteResponse deleteResponse) { @@ -537,15 +597,6 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL if (state.compareAndSet(State.INITIALIZED, State.STARTING)) { this.scrollSize = SCROLL_SIZE_SETTING.get(settings); this.scrollKeepAlive = SCROLL_KEEP_ALIVE_SETTING.get(settings); - - UserStorePoller poller = new UserStorePoller(); - try { - poller.doRun(); - } catch (Exception e) { - logger.warn("failed to do initial poll of users", e); - } - TimeValue interval = settings.getAsTime("shield.authc.native.reload.interval", TimeValue.timeValueSeconds(30L)); - pollerCancellable = threadPool.scheduleWithFixedDelay(poller, interval, Names.GENERIC); state.set(State.STARTED); } } catch (Exception e) { @@ -556,14 +607,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL public void stop() { if (state.compareAndSet(State.STARTED, State.STOPPING)) { - try { - pollerCancellable.cancel(); - } catch (Exception e) { - state.set(State.FAILED); - throw e; - } finally { - state.set(State.STOPPED); - } + state.set(State.STOPPED); } } @@ -574,7 +618,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL * @param password the plaintext password to verify * @return {@link} User object if successful or {@code null} if verification fails */ - public User verifyPassword(String username, final SecuredString password) { + User verifyPassword(String username, final SecuredString password) { if (state() != State.STARTED) { logger.trace("attempted to verify user credentials for [{}] but service was not started", username); return null; @@ -590,11 +634,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL return null; } - public void addListener(ChangeListener listener) { - listeners.add(listener); - } - - boolean started() { + public boolean started() { return state() == State.STARTED; } @@ -602,9 +642,9 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL return securityIndexExists; } - char[] reservedUserPassword(String username) throws Exception { + ReservedUserInfo getReservedUserInfo(String username) throws Exception { assert started(); - final AtomicReference passwordHash = new AtomicReference<>(); + final AtomicReference userInfoRef = new AtomicReference<>(); final AtomicReference failure = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); client.prepareGet(SecurityTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username) @@ -614,26 +654,26 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL if (getResponse.isExists()) { Map sourceMap = getResponse.getSourceAsMap(); String password = (String) sourceMap.get(User.Fields.PASSWORD.getPreferredName()); + Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName()); if (password == null || password.isEmpty()) { failure.set(new IllegalStateException("password hash must not be empty!")); - return; + } else if (enabled == null) { + failure.set(new IllegalStateException("enabled must not be null!")); + } else { + userInfoRef.set(new ReservedUserInfo(password.toCharArray(), enabled)); } - passwordHash.set(password.toCharArray()); } } @Override public void onFailure(Exception e) { if (e instanceof IndexNotFoundException) { - logger.trace( - (Supplier) () -> new ParameterizedMessage( - "could not retrieve built in user [{}] password since security index does not exist", - username), - e); + logger.trace((Supplier) () -> new ParameterizedMessage( + "could not retrieve built in user [{}] info since security index does not exist", username), e); } else { logger.error( (Supplier) () -> new ParameterizedMessage( - "failed to retrieve built in user [{}] password", username), e); + "failed to retrieve built in user [{}] info", username), e); failure.set(e); } } @@ -653,7 +693,65 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL // if there is any sort of failure we need to throw an exception to prevent the fallback to the default password... throw failureCause; } - return passwordHash.get(); + return userInfoRef.get(); + } + + Map getAllReservedUserInfo() throws Exception { + assert started(); + final Map userInfos = new HashMap<>(); + final AtomicReference failure = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + client.prepareSearch(SecurityTemplateService.SECURITY_INDEX_NAME) + .setTypes(RESERVED_USER_DOC_TYPE) + .setQuery(QueryBuilders.matchAllQuery()) + .setFetchSource(true) + .execute(new LatchedActionListener<>(new ActionListener() { + @Override + public void onResponse(SearchResponse searchResponse) { + assert searchResponse.getHits().getTotalHits() <= 10 : "there are more than 10 reserved users we need to change " + + "this to retrieve them all!"; + for (SearchHit searchHit : searchResponse.getHits().getHits()) { + Map sourceMap = searchHit.getSource(); + String password = (String) sourceMap.get(User.Fields.PASSWORD.getPreferredName()); + Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName()); + if (password == null || password.isEmpty()) { + failure.set(new IllegalStateException("password hash must not be empty!")); + break; + } else if (enabled == null) { + failure.set(new IllegalStateException("enabled must not be null!")); + break; + } else { + userInfos.put(searchHit.getId(), new ReservedUserInfo(password.toCharArray(), enabled)); + } + } + } + + @Override + public void onFailure(Exception e) { + if (e instanceof IndexNotFoundException) { + logger.trace("could not retrieve built in users since security index does not exist", e); + } else { + logger.error("failed to retrieve built in users", e); + failure.set(e); + } + } + }, latch)); + + try { + final boolean responseReceived = latch.await(30, TimeUnit.SECONDS); + if (responseReceived == false) { + failure.set(new TimeoutException("timed out trying to get built in users")); + } + } catch (InterruptedException e) { + failure.set(e); + } + + Exception failureCause = failure.get(); + if (failureCause != null) { + // if there is any sort of failure we need to throw an exception to prevent the fallback to the default password... + throw failureCause; + } + return userInfos; } private void clearScrollResponse(String scrollId) { @@ -716,7 +814,6 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL if (state != State.STOPPED && state != State.FAILED) { throw new IllegalStateException("can only reset if stopped!!!"); } - this.listeners.clear(); this.securityIndexExists = false; this.state.set(State.INITIALIZED); } @@ -731,158 +828,42 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL String[] roles = ((List) sourceMap.get(User.Fields.ROLES.getPreferredName())).toArray(Strings.EMPTY_ARRAY); String fullName = (String) sourceMap.get(User.Fields.FULL_NAME.getPreferredName()); String email = (String) sourceMap.get(User.Fields.EMAIL.getPreferredName()); + Boolean enabled = (Boolean) sourceMap.get(User.Fields.ENABLED.getPreferredName()); + if (enabled == null) { + // fallback mechanism as a user from 2.x may not have the enabled field + enabled = Boolean.TRUE; + } Map metadata = (Map) sourceMap.get(User.Fields.METADATA.getPreferredName()); - return new UserAndPassword(new User(username, roles, fullName, email, metadata), password.toCharArray()); + return new UserAndPassword(new User(username, roles, fullName, email, metadata, enabled), password.toCharArray()); } catch (Exception e) { logger.error((Supplier) () -> new ParameterizedMessage("error in the format of data for user [{}]", username), e); return null; } } - private class UserStorePoller extends AbstractRunnable { - - // this map contains the mapping for username -> version, which is used when polling the index to easily detect of - // any changes that may have been missed since the last update. - private final ObjectLongHashMap userVersionMap = new ObjectLongHashMap<>(); - private final ObjectLongHashMap reservedUserVersionMap = new ObjectLongHashMap<>(); - - @Override - public void doRun() { - // hold a reference to the client since the poller may run after the class is stopped (we don't interrupt it running) and - // we reset when we test which sets the client to null... - final Client client = NativeUsersStore.this.client; - if (isStopped()) { - return; + private static boolean isIndexNotFoundOrDocumentMissing(Exception e) { + if (e instanceof ElasticsearchException) { + Throwable cause = ExceptionsHelper.unwrapCause(e); + if (cause instanceof IndexNotFoundException || cause instanceof DocumentMissingException) { + return true; } - if (securityIndexExists == false) { - logger.trace("cannot poll for user changes since security index [{}] does not exist", SecurityTemplateService - .SECURITY_INDEX_NAME); - return; - } - - logger.trace("starting polling of user index to check for changes"); - List changedUsers = scrollForModifiedUsers(client, USER_DOC_TYPE, userVersionMap); - if (isStopped()) { - return; - } - - changedUsers.addAll(scrollForModifiedUsers(client, RESERVED_USER_DOC_TYPE, reservedUserVersionMap)); - if (isStopped()) { - return; - } - - notifyListeners(changedUsers); - logger.trace("finished polling of user index"); - } - - private List scrollForModifiedUsers(Client client, String docType, ObjectLongMap usersMap) { - // create a copy of all known users - ObjectHashSet knownUsers = new ObjectHashSet<>(usersMap.keys()); - List changedUsers = new ArrayList<>(); - - SearchResponse response = null; - try { - client.admin().indices().prepareRefresh(SecurityTemplateService.SECURITY_INDEX_NAME).get(); - response = client.prepareSearch(SecurityTemplateService.SECURITY_INDEX_NAME) - .setScroll(scrollKeepAlive) - .setQuery(QueryBuilders.typeQuery(docType)) - .setSize(scrollSize) - .setVersion(true) - .setFetchSource(false) // we only need id and version - .get(); - - boolean keepScrolling = response.getHits().getHits().length > 0; - while (keepScrolling) { - for (SearchHit hit : response.getHits().getHits()) { - final String username = hit.id(); - final long version = hit.version(); - if (knownUsers.contains(username)) { - final long lastKnownVersion = usersMap.get(username); - if (version != lastKnownVersion) { - // version is only changed by this method - assert version > lastKnownVersion; - usersMap.put(username, version); - // there is a chance that the user's cache has already been cleared and we'll clear it again but - // this should be ok in most cases as user changes should not be that frequent - changedUsers.add(username); - } - knownUsers.remove(username); - } else { - usersMap.put(username, version); - } - } - - if (isStopped()) { - // bail here - return Collections.emptyList(); - } - response = client.prepareSearchScroll(response.getScrollId()).setScroll(scrollKeepAlive).get(); - keepScrolling = response.getHits().getHits().length > 0; - } - } catch (IndexNotFoundException e) { - logger.trace("security index does not exist", e); - } finally { - if (response != null && response.getScrollId() != null) { - ClearScrollRequest clearScrollRequest = client.prepareClearScroll().addScrollId(response.getScrollId()).request(); - client.clearScroll(clearScrollRequest).actionGet(); - } - } - - // we now have a list of users that were in our version map and have been deleted - Iterator> userIter = knownUsers.iterator(); - while (userIter.hasNext()) { - String user = userIter.next().value; - usersMap.remove(user); - changedUsers.add(user); - } - - return changedUsers; - } - - private void notifyListeners(List changedUsers) { - if (changedUsers.isEmpty()) { - return; - } - - // make the list unmodifiable to prevent modifications by any listeners - changedUsers = Collections.unmodifiableList(changedUsers); - if (logger.isDebugEnabled()) { - logger.debug("changes detected for users [{}]", changedUsers); - } - - // call listeners - RuntimeException ex = null; - for (ChangeListener listener : listeners) { - try { - listener.onUsersChanged(changedUsers); - } catch (Exception e) { - if (ex == null) ex = new RuntimeException("exception while notifying listeners"); - ex.addSuppressed(e); - } - } - - if (ex != null) throw ex; - } - - @Override - public void onFailure(Exception e) { - logger.error("error occurred while checking the native users for changes", e); - } - - private boolean isStopped() { - State state = state(); - return state == State.STOPPED || state == State.STOPPING; } + return false; } - interface ChangeListener { + static class ReservedUserInfo { - void onUsersChanged(List username); + final char[] passwordHash; + final boolean enabled; + + ReservedUserInfo(char[] passwordHash, boolean enabled) { + this.passwordHash = passwordHash; + this.enabled = enabled; + } } public static void addSettings(List> settings) { settings.add(SCROLL_SIZE_SETTING); settings.add(SCROLL_KEEP_ALIVE_SETTING); - settings.add(POLL_INTERVAL_SETTING); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 1fbe87bdf8a..acb93901917 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -10,7 +10,7 @@ import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.security.authc.RealmConfig; -import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ChangeListener; +import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.xpack.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.support.SecuredString; @@ -21,9 +21,12 @@ import org.elasticsearch.xpack.security.user.ElasticUser; import org.elasticsearch.xpack.security.user.KibanaUser; import org.elasticsearch.xpack.security.user.User; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; /** * A realm for predefined users. These users can only be modified in terms of changing their passwords; no other modifications are allowed. @@ -32,40 +35,35 @@ import java.util.List; public class ReservedRealm extends CachingUsernamePasswordRealm { public static final String TYPE = "reserved"; - private static final char[] DEFAULT_PASSWORD_HASH = Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray())); + static final char[] DEFAULT_PASSWORD_HASH = Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray())); + private static final ReservedUserInfo DEFAULT_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, true); private final NativeUsersStore nativeUsersStore; + private final AnonymousUser anonymousUser; + private final boolean anonymousEnabled; - public ReservedRealm(Environment env, Settings settings, NativeUsersStore nativeUsersStore) { + public ReservedRealm(Environment env, Settings settings, NativeUsersStore nativeUsersStore, AnonymousUser anonymousUser) { super(TYPE, new RealmConfig(TYPE, Settings.EMPTY, settings, env)); this.nativeUsersStore = nativeUsersStore; - nativeUsersStore.addListener(new ChangeListener() { - @Override - public void onUsersChanged(List changedUsers) { - changedUsers.stream() - .filter(ReservedRealm::isReserved) - .forEach(ReservedRealm.this::expire); - } - }); - + this.anonymousUser = anonymousUser; + this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); } @Override protected User doAuthenticate(UsernamePasswordToken token) { - final User user = getUser(token.principal()); - if (user == null) { + if (isReserved(token.principal(), config.globalSettings()) == false) { return null; } - final char[] passwordHash = getPasswordHash(user.principal()); - if (passwordHash != null) { + final ReservedUserInfo userInfo = getUserInfo(token.principal()); + if (userInfo != null) { try { - if (Hasher.BCRYPT.verify(token.credentials(), passwordHash)) { - return user; + if (Hasher.BCRYPT.verify(token.credentials(), userInfo.passwordHash)) { + return getUser(token.principal(), userInfo); } } finally { - if (passwordHash != DEFAULT_PASSWORD_HASH) { - Arrays.fill(passwordHash, (char) 0); + if (userInfo.passwordHash != DEFAULT_PASSWORD_HASH) { + Arrays.fill(userInfo.passwordHash, (char) 0); } } } @@ -75,7 +73,20 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { @Override protected User doLookupUser(String username) { - return getUser(username); + if (isReserved(username, config.globalSettings()) == false) { + return null; + } + + if (AnonymousUser.isAnonymousUsername(username, config.globalSettings())) { + return anonymousEnabled ? anonymousUser : null; + } + + final ReservedUserInfo userInfo = getUserInfo(username); + if (userInfo != null) { + return getUser(username, userInfo); + } + // this was a reserved username - don't allow this to go to another realm... + throw Exceptions.authenticationError("failed to lookup user [{}]", username); } @Override @@ -83,54 +94,71 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { return true; } - public static boolean isReserved(String username) { + public static boolean isReserved(String username, Settings settings) { assert username != null; switch (username) { case ElasticUser.NAME: case KibanaUser.NAME: return true; default: - return AnonymousUser.isAnonymousUsername(username); + return AnonymousUser.isAnonymousUsername(username, settings); } } - public static User getUser(String username) { + User getUser(String username, ReservedUserInfo userInfo) { assert username != null; switch (username) { case ElasticUser.NAME: - return ElasticUser.INSTANCE; + return new ElasticUser(userInfo.enabled); case KibanaUser.NAME: - return KibanaUser.INSTANCE; + return new KibanaUser(userInfo.enabled); default: - if (AnonymousUser.enabled() && AnonymousUser.isAnonymousUsername(username)) { - return AnonymousUser.INSTANCE; + if (anonymousEnabled && anonymousUser.principal().equals(username)) { + return anonymousUser; } return null; } } - public static Collection users() { - if (AnonymousUser.enabled()) { - return Arrays.asList(ElasticUser.INSTANCE, KibanaUser.INSTANCE, AnonymousUser.INSTANCE); + public Collection users() { + if (nativeUsersStore.started() == false) { + return anonymousEnabled ? Collections.singletonList(anonymousUser) : Collections.emptyList(); } - return Arrays.asList(ElasticUser.INSTANCE, KibanaUser.INSTANCE); + + List users = new ArrayList<>(3); + try { + Map reservedUserInfos = nativeUsersStore.getAllReservedUserInfo(); + ReservedUserInfo userInfo = reservedUserInfos.get(ElasticUser.NAME); + users.add(new ElasticUser(userInfo == null || userInfo.enabled)); + userInfo = reservedUserInfos.get(KibanaUser.NAME); + users.add(new KibanaUser(userInfo == null || userInfo.enabled)); + if (anonymousEnabled) { + users.add(anonymousUser); + } + } catch (Exception e) { + logger.error("failed to retrieve reserved users", e); + return anonymousEnabled ? Collections.singletonList(anonymousUser) : Collections.emptyList(); + } + + return users; } - private char[] getPasswordHash(final String username) { + private ReservedUserInfo getUserInfo(final String username) { if (nativeUsersStore.started() == false) { // we need to be able to check for the user store being started... return null; } if (nativeUsersStore.securityIndexExists() == false) { - return DEFAULT_PASSWORD_HASH; + return DEFAULT_USER_INFO; } + try { - char[] passwordHash = nativeUsersStore.reservedUserPassword(username); - if (passwordHash == null) { - return DEFAULT_PASSWORD_HASH; + ReservedUserInfo userInfo = nativeUsersStore.getReservedUserInfo(username); + if (userInfo == null) { + return DEFAULT_USER_INFO; } - return passwordHash; + return userInfo; } catch (Exception e) { logger.error( (Supplier) () -> new ParameterizedMessage("failed to retrieve password hash for reserved user [{}]", username), e); diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java index 4ae317a2a22..9cff9389a84 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.inject.internal.Nullable; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; @@ -43,7 +44,8 @@ public class FileUserPasswdStore { private final Logger logger; private final Path file; - final Hasher hasher = Hasher.BCRYPT; + private final Hasher hasher = Hasher.BCRYPT; + private final Settings settings; private volatile Map users; @@ -56,7 +58,8 @@ public class FileUserPasswdStore { FileUserPasswdStore(RealmConfig config, ResourceWatcherService watcherService, RefreshListener listener) { logger = config.logger(FileUserPasswdStore.class); file = resolveFile(config.env()); - users = parseFileLenient(file, logger); + settings = config.globalSettings(); + users = parseFileLenient(file, logger, settings); FileWatcher watcher = new FileWatcher(file.getParent()); watcher.addListener(new FileListener()); try { @@ -96,9 +99,9 @@ public class FileUserPasswdStore { * Internally in this class, we try to load the file, but if for some reason we can't, we're being more lenient by * logging the error and skipping all users. This is aligned with how we handle other auto-loaded files in security. */ - static Map parseFileLenient(Path path, Logger logger) { + static Map parseFileLenient(Path path, Logger logger, Settings settings) { try { - return parseFile(path, logger); + return parseFile(path, logger, settings); } catch (Exception e) { logger.error( (Supplier) () -> new ParameterizedMessage( @@ -111,7 +114,7 @@ public class FileUserPasswdStore { * parses the users file. Should never return {@code null}, if the file doesn't exist an * empty map is returned */ - public static Map parseFile(Path path, @Nullable Logger logger) { + public static Map parseFile(Path path, @Nullable Logger logger, Settings settings) { if (logger == null) { logger = NoOpLogger.INSTANCE; } @@ -146,7 +149,7 @@ public class FileUserPasswdStore { continue; } String username = line.substring(0, i); - Validation.Error validationError = Users.validateUsername(username); + Validation.Error validationError = Users.validateUsername(username, false, settings); if (validationError != null) { logger.error("invalid username [{}] in users file [{}], skipping... ({})", username, path.toAbsolutePath(), validationError); @@ -191,7 +194,7 @@ public class FileUserPasswdStore { public void onFileChanged(Path file) { if (file.equals(FileUserPasswdStore.this.file)) { logger.info("users file [{}] changed. updating users... )", file.toAbsolutePath()); - users = parseFileLenient(file, logger); + users = parseFileLenient(file, logger, settings); notifyRefresh(); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/tool/UsersTool.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/tool/UsersTool.java index d09199b3017..496a2767149 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/tool/UsersTool.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/file/tool/UsersTool.java @@ -84,21 +84,21 @@ public class UsersTool extends MultiCommand { @Override protected void execute(Terminal terminal, OptionSet options, Map settings) throws Exception { - String username = parseUsername(arguments.values(options)); - Validation.Error validationError = Users.validateUsername(username); + Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, terminal, settings); + String username = parseUsername(arguments.values(options), env.settings()); + Validation.Error validationError = Users.validateUsername(username, false, Settings.EMPTY); if (validationError != null) { throw new UserException(ExitCodes.DATA_ERROR, "Invalid username [" + username + "]... " + validationError); } char[] password = parsePassword(terminal, passwordOption.value(options)); - Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, terminal, settings); String[] roles = parseRoles(terminal, env, rolesOption.value(options)); Path passwordFile = FileUserPasswdStore.resolveFile(env); Path rolesFile = FileUserRolesStore.resolveFile(env); FileAttributesChecker attributesChecker = new FileAttributesChecker(passwordFile, rolesFile); - Map users = new HashMap<>(FileUserPasswdStore.parseFile(passwordFile, null)); + Map users = new HashMap<>(FileUserPasswdStore.parseFile(passwordFile, null, env.settings())); if (users.containsKey(username)) { throw new UserException(ExitCodes.CODE_ERROR, "User [" + username + "] already exists"); } @@ -138,13 +138,13 @@ public class UsersTool extends MultiCommand { @Override protected void execute(Terminal terminal, OptionSet options, Map settings) throws Exception { - String username = parseUsername(arguments.values(options)); Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, terminal, settings); + String username = parseUsername(arguments.values(options), env.settings()); Path passwordFile = FileUserPasswdStore.resolveFile(env); Path rolesFile = FileUserRolesStore.resolveFile(env); FileAttributesChecker attributesChecker = new FileAttributesChecker(passwordFile, rolesFile); - Map users = new HashMap<>(FileUserPasswdStore.parseFile(passwordFile, null)); + Map users = new HashMap<>(FileUserPasswdStore.parseFile(passwordFile, null, env.settings())); if (users.containsKey(username) == false) { throw new UserException(ExitCodes.NO_USER, "User [" + username + "] doesn't exist"); } @@ -193,13 +193,13 @@ public class UsersTool extends MultiCommand { @Override protected void execute(Terminal terminal, OptionSet options, Map settings) throws Exception { - String username = parseUsername(arguments.values(options)); + Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, terminal, settings); + String username = parseUsername(arguments.values(options), env.settings()); char[] password = parsePassword(terminal, passwordOption.value(options)); - Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, terminal, settings); Path file = FileUserPasswdStore.resolveFile(env); FileAttributesChecker attributesChecker = new FileAttributesChecker(file); - Map users = new HashMap<>(FileUserPasswdStore.parseFile(file, null)); + Map users = new HashMap<>(FileUserPasswdStore.parseFile(file, null, env.settings())); if (users.containsKey(username) == false) { throw new UserException(ExitCodes.NO_USER, "User [" + username + "] doesn't exist"); } @@ -237,8 +237,8 @@ public class UsersTool extends MultiCommand { @Override protected void execute(Terminal terminal, OptionSet options, Map settings) throws Exception { - String username = parseUsername(arguments.values(options)); Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, terminal, settings); + String username = parseUsername(arguments.values(options), env.settings()); String[] addRoles = parseRoles(terminal, env, addOption.value(options)); String[] removeRoles = parseRoles(terminal, env, removeOption.value(options)); @@ -254,7 +254,7 @@ public class UsersTool extends MultiCommand { Path rolesFile = FileUserRolesStore.resolveFile(env); FileAttributesChecker attributesChecker = new FileAttributesChecker(usersFile, rolesFile); - Map usersMap = FileUserPasswdStore.parseFile(usersFile, null); + Map usersMap = FileUserPasswdStore.parseFile(usersFile, null, env.settings()); if (!usersMap.containsKey(username)) { throw new UserException(ExitCodes.NO_USER, "User [" + username + "] doesn't exist"); } @@ -312,7 +312,7 @@ public class UsersTool extends MultiCommand { Map userRoles = FileUserRolesStore.parseFile(userRolesFilePath, null); Path userFilePath = FileUserPasswdStore.resolveFile(env); - Set users = FileUserPasswdStore.parseFile(userFilePath, null).keySet(); + Set users = FileUserPasswdStore.parseFile(userFilePath, null, env.settings()).keySet(); Path rolesFilePath = FileRolesStore.resolveFile(env); Set knownRoles = Sets.union(FileRolesStore.parseFileForRoleNames(rolesFilePath, null), ReservedRolesStore.names()); @@ -388,14 +388,14 @@ public class UsersTool extends MultiCommand { } // pkg private for testing - static String parseUsername(List args) throws UserException { + static String parseUsername(List args, Settings settings) throws UserException { if (args.isEmpty()) { throw new UserException(ExitCodes.USAGE, "Missing username argument"); } else if (args.size() > 1) { throw new UserException(ExitCodes.USAGE, "Expected a single username argument, found extra: " + args.toString()); } String username = args.get(0); - Validation.Error validationError = Users.validateUsername(username); + Validation.Error validationError = Users.validateUsername(username, false, settings); if (validationError != null) { throw new UserException(ExitCodes.DATA_ERROR, "Invalid username [" + username + "]... " + validationError); } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java index 6926f7c2bbd..142276af267 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.cache.CacheLoader; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.security.authc.AuthenticationToken; import org.elasticsearch.xpack.security.authc.RealmConfig; -import org.elasticsearch.xpack.security.support.Exceptions; import org.elasticsearch.xpack.security.user.User; import java.util.Map; @@ -149,11 +148,11 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm CacheLoader callback = key -> { if (logger.isDebugEnabled()) { - logger.debug("user not found in cache, proceeding with normal lookup"); + logger.debug("user [{}] not found in cache, proceeding with normal lookup", username); } User user = doLookupUser(username); if (user == null) { - throw Exceptions.authenticationError("could not lookup [{}]", username); + return null; } return new UserWithHash(user, null, null); }; @@ -162,10 +161,15 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm UserWithHash userWithHash = cache.computeIfAbsent(username, callback); return userWithHash.user; } catch (ExecutionException ee) { + if (ee.getCause() instanceof ElasticsearchSecurityException) { + // this should bubble out + throw (ElasticsearchSecurityException) ee.getCause(); + } + if (logger.isTraceEnabled()) { logger.trace((Supplier) () -> new ParameterizedMessage("realm [{}] could not lookup [{}]", name(), username), ee); } else if (logger.isDebugEnabled()) { - logger.debug("realm [{}] could not authenticate [{}]", name(), username); + logger.debug("realm [{}] could not lookup [{}]", name(), username); } return null; } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index ad2c9b1fc06..2692cd9bec3 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -77,11 +77,13 @@ public class AuthorizationService extends AbstractComponent { private final IndicesAndAliasesResolver[] indicesAndAliasesResolvers; private final AuthenticationFailureHandler authcFailureHandler; private final ThreadContext threadContext; + private final AnonymousUser anonymousUser; + private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, ClusterService clusterService, AuditTrailService auditTrail, AuthenticationFailureHandler authcFailureHandler, - ThreadPool threadPool) { + ThreadPool threadPool, AnonymousUser anonymousUser) { super(settings); this.rolesStore = rolesStore; this.clusterService = clusterService; @@ -91,6 +93,8 @@ public class AuthorizationService extends AbstractComponent { }; this.authcFailureHandler = authcFailureHandler; this.threadContext = threadPool.getThreadContext(); + this.anonymousUser = anonymousUser; + this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings); } @@ -101,7 +105,7 @@ public class AuthorizationService extends AbstractComponent { * @param action The action */ public List authorizedIndicesAndAliases(User user, String action) { - final String[] anonymousRoles = AnonymousUser.enabled() ? AnonymousUser.getRoles() : Strings.EMPTY_ARRAY; + final String[] anonymousRoles = isAnonymousEnabled ? anonymousUser.roles() : Strings.EMPTY_ARRAY; String[] rolesNames = user.roles(); if (rolesNames.length == 0 && anonymousRoles.length == 0) { return Collections.emptyList(); @@ -114,7 +118,7 @@ public class AuthorizationService extends AbstractComponent { predicates.add(role.indices().allowedIndicesMatcher(action)); } } - if (AnonymousUser.is(user) == false) { + if (anonymousUser.equals(user) == false) { for (String roleName : anonymousRoles) { Role role = rolesStore.role(roleName); if (role != null) { @@ -360,7 +364,7 @@ public class AuthorizationService extends AbstractComponent { private ElasticsearchSecurityException denialException(Authentication authentication, String action) { final User user = authentication.getUser(); // Special case for anonymous user - if (AnonymousUser.enabled() && AnonymousUser.is(user)) { + if (isAnonymousEnabled && anonymousUser.equals(user)) { if (anonymousAuthzExceptionEnabled == false) { throw authcFailureHandler.authenticationRequired(action, threadContext); } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java index 3e11e946663..49e9c64e314 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java @@ -24,7 +24,6 @@ import org.elasticsearch.action.search.MultiSearchResponse.Item; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchScrollRequest; -import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateListener; @@ -74,7 +73,7 @@ import static org.elasticsearch.xpack.security.Security.setting; import static org.elasticsearch.xpack.security.SecurityTemplateService.securityIndexMappingAndTemplateUpToDate; /** - * ESNativeRolesStore is a {@code RolesStore} that, instead of reading from a + * NativeRolesStore is a {@code RolesStore} that, instead of reading from a * file, reads from an Elasticsearch index instead. Unlike the file-based roles * store, ESNativeRolesStore can be used to add a role to the store by inserting * the document into the administrative index. @@ -264,7 +263,7 @@ public class NativeRolesStore extends AbstractComponent implements RolesStore, C try { DeleteRequest request = client.prepareDelete(SecurityTemplateService.SECURITY_INDEX_NAME, ROLE_DOC_TYPE, deleteRoleRequest.name()).request(); - request.setRefreshPolicy(deleteRoleRequest.refresh() ? RefreshPolicy.IMMEDIATE : RefreshPolicy.WAIT_UNTIL); + request.setRefreshPolicy(deleteRoleRequest.getRefreshPolicy()); client.delete(request, new ActionListener() { @Override public void onResponse(DeleteResponse deleteResponse) { diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStore.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStore.java index d6ed72968a7..d1a56cdbfc7 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStore.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStore.java @@ -24,12 +24,14 @@ import org.elasticsearch.xpack.security.authz.permission.SuperuserRole; import org.elasticsearch.xpack.security.authz.permission.TransportClientRole; import org.elasticsearch.xpack.security.user.KibanaUser; import org.elasticsearch.xpack.security.user.SystemUser; +import org.elasticsearch.xpack.security.user.User; /** * */ public class ReservedRolesStore implements RolesStore { + private static final User DEFAULT_ENABLED_KIBANA_USER = new KibanaUser(true); private final SecurityContext securityContext; public ReservedRolesStore(SecurityContext securityContext) { @@ -54,8 +56,9 @@ public class ReservedRolesStore implements RolesStore { case KibanaRole.NAME: // The only user that should know about this role is the kibana user itself (who has this role). The reason we want to hide // this role is that it was created specifically for kibana, with all the permissions that the kibana user needs. - // We don't want it to be assigned to other users. - if (KibanaUser.is(securityContext.getUser())) { + // We don't want it to be assigned to other users. The Kibana user here must always be enabled if it is in the + // security context + if (DEFAULT_ENABLED_KIBANA_USER.equals(securityContext.getUser())) { return KibanaRole.INSTANCE; } return null; @@ -87,7 +90,7 @@ public class ReservedRolesStore implements RolesStore { // The only user that should know about this role is the kibana user itself (who has this role). The reason we want to hide // this role is that it was created specifically for kibana, with all the permissions that the kibana user needs. // We don't want it to be assigned to other users. - if (KibanaUser.is(securityContext.getUser())) { + if (DEFAULT_ENABLED_KIBANA_USER.equals(securityContext.getUser())) { return KibanaRole.DESCRIPTOR; } return null; @@ -97,7 +100,7 @@ public class ReservedRolesStore implements RolesStore { } public Collection roleDescriptors() { - if (KibanaUser.is(securityContext.getUser())) { + if (DEFAULT_ENABLED_KIBANA_USER.equals(securityContext.getUser())) { return Arrays.asList(SuperuserRole.DESCRIPTOR, TransportClientRole.DESCRIPTOR, KibanaUserRole.DESCRIPTOR, KibanaRole.DESCRIPTOR, MonitoringUserRole.DESCRIPTOR, RemoteMonitoringAgentRole.DESCRIPTOR, IngestAdminRole.DESCRIPTOR); diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/client/SecurityClient.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/client/SecurityClient.java index 9463e89f417..e20ff763f3f 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/client/SecurityClient.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/client/SecurityClient.java @@ -45,6 +45,10 @@ import org.elasticsearch.xpack.security.action.user.PutUserAction; import org.elasticsearch.xpack.security.action.user.PutUserRequest; import org.elasticsearch.xpack.security.action.user.PutUserRequestBuilder; import org.elasticsearch.xpack.security.action.user.PutUserResponse; +import org.elasticsearch.xpack.security.action.user.SetEnabledAction; +import org.elasticsearch.xpack.security.action.user.SetEnabledRequest; +import org.elasticsearch.xpack.security.action.user.SetEnabledRequestBuilder; +import org.elasticsearch.xpack.security.action.user.SetEnabledResponse; import java.io.IOException; @@ -163,6 +167,14 @@ public class SecurityClient { client.execute(ChangePasswordAction.INSTANCE, request, listener); } + public SetEnabledRequestBuilder prepareSetEnabled(String username, boolean enabled) { + return new SetEnabledRequestBuilder(client).username(username).enabled(enabled); + } + + public void setEnabled(SetEnabledRequest request, ActionListener listener) { + client.execute(SetEnabledAction.INSTANCE, request, listener); + } + /** Role Management */ public GetRolesRequestBuilder prepareGetRoles(String... names) { diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java index fac5f081ff0..fa1a8108572 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java @@ -17,7 +17,6 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestBuilderListener; -import org.elasticsearch.xpack.security.action.role.DeleteRoleRequestBuilder; import org.elasticsearch.xpack.security.action.role.DeleteRoleResponse; import org.elasticsearch.xpack.security.client.SecurityClient; @@ -42,18 +41,16 @@ public class RestDeleteRoleAction extends BaseRestHandler { @Override public void handleRequest(RestRequest request, final RestChannel channel, NodeClient client) throws Exception { - DeleteRoleRequestBuilder requestBuilder = new SecurityClient(client).prepareDeleteRole(request.param("name")); - if (request.hasParam("refresh")) { - requestBuilder.refresh(request.paramAsBoolean("refresh", true)); - } - requestBuilder.execute(new RestBuilderListener(channel) { - @Override - public RestResponse buildResponse(DeleteRoleResponse response, XContentBuilder builder) throws Exception { - return new BytesRestResponse(response.found() ? RestStatus.OK : RestStatus.NOT_FOUND, - builder.startObject() - .field("found", response.found()) - .endObject()); - } - }); + new SecurityClient(client).prepareDeleteRole(request.param("name")) + .setRefreshPolicy(request.param("refresh")) + .execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(DeleteRoleResponse response, XContentBuilder builder) throws Exception { + return new BytesRestResponse(response.found() ? RestStatus.OK : RestStatus.NOT_FOUND, + builder.startObject() + .field("found", response.found()) + .endObject()); + } + }); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java index 844f98d8497..7b04fc6f011 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java @@ -46,7 +46,7 @@ public class RestChangePasswordAction extends BaseRestHandler { final User user = securityContext.getUser(); String username = request.param("username"); if (username == null) { - username = user.runAs() == null ? user.principal() : user.runAs().principal();; + username = user.runAs() == null ? user.principal() : user.runAs().principal(); } new SecurityClient(client).prepareChangePassword(username, request.content()) diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestDeleteUserAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestDeleteUserAction.java index cf900911559..4198aecebfc 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestDeleteUserAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestDeleteUserAction.java @@ -17,7 +17,6 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestBuilderListener; -import org.elasticsearch.xpack.security.action.user.DeleteUserRequestBuilder; import org.elasticsearch.xpack.security.action.user.DeleteUserResponse; import org.elasticsearch.xpack.security.client.SecurityClient; @@ -42,20 +41,16 @@ public class RestDeleteUserAction extends BaseRestHandler { @Override public void handleRequest(RestRequest request, final RestChannel channel, NodeClient client) throws Exception { - String username = request.param("username"); - - DeleteUserRequestBuilder requestBuilder = new SecurityClient(client).prepareDeleteUser(username); - if (request.hasParam("refresh")) { - requestBuilder.refresh(request.paramAsBoolean("refresh", true)); - } - requestBuilder.execute(new RestBuilderListener(channel) { - @Override - public RestResponse buildResponse(DeleteUserResponse response, XContentBuilder builder) throws Exception { - return new BytesRestResponse(response.found() ? RestStatus.OK : RestStatus.NOT_FOUND, - builder.startObject() - .field("found", response.found()) - .endObject()); - } - }); + new SecurityClient(client).prepareDeleteUser(request.param("username")) + .setRefreshPolicy(request.param("refresh")) + .execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(DeleteUserResponse response, XContentBuilder builder) throws Exception { + return new BytesRestResponse(response.found() ? RestStatus.OK : RestStatus.NOT_FOUND, + builder.startObject() + .field("found", response.found()) + .endObject()); + } + }); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java index cb6bee30f65..6a9dc220a0e 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java @@ -49,9 +49,7 @@ public class RestPutUserAction extends BaseRestHandler { @Override public void handleRequest(RestRequest request, final RestChannel channel, NodeClient client) throws Exception { PutUserRequestBuilder requestBuilder = new SecurityClient(client).preparePutUser(request.param("username"), request.content()); - if (request.hasParam("refresh")) { - requestBuilder.setRefreshPolicy(request.param("refresh")); - } + requestBuilder.setRefreshPolicy(request.param("refresh")); requestBuilder.execute(new RestBuilderListener(channel) { @Override public RestResponse buildResponse(PutUserResponse putUserResponse, XContentBuilder builder) throws Exception { diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestSetEnabledAction.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestSetEnabledAction.java new file mode 100644 index 00000000000..e7e8cc96628 --- /dev/null +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestSetEnabledAction.java @@ -0,0 +1,53 @@ +/* + * 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.rest.action.user; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.security.action.user.SetEnabledResponse; +import org.elasticsearch.xpack.security.client.SecurityClient; + +import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestRequest.Method.PUT; + +/** + * REST handler for enabling and disabling users. The username is required and we use the path to determine if the user is being + * enabled or disabled. + */ +public class RestSetEnabledAction extends BaseRestHandler { + + @Inject + public RestSetEnabledAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(POST, "/_xpack/security/user/{username}/_enable", this); + controller.registerHandler(PUT, "/_xpack/security/user/{username}/_enable", this); + controller.registerHandler(POST, "/_xpack/security/user/{username}/_disable", this); + controller.registerHandler(PUT, "/_xpack/security/user/{username}/_disable", this); + } + + @Override + public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { + final boolean enabled = request.path().endsWith("_enable"); + assert enabled || request.path().endsWith("_disable"); + new SecurityClient(client).prepareSetEnabled(request.param("username"), enabled) + .execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(SetEnabledResponse setEnabledResponse, XContentBuilder builder) throws Exception { + return new BytesRestResponse(RestStatus.OK, channel.newBuilder().startObject().endObject()); + } + }); + } +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/MetadataUtils.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/MetadataUtils.java index 03715aec196..b80fd0aa252 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/MetadataUtils.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/MetadataUtils.java @@ -55,7 +55,7 @@ public class MetadataUtils { public static void verifyNoReservedMetadata(Map metadata) { for (String key : metadata.keySet()) { if (key.startsWith(RESERVED_PREFIX)) { - throw new IllegalArgumentException("invalid user metadata. [" + key + "] is a reserved for internal uses"); + throw new IllegalArgumentException("invalid user metadata. [" + key + "] is a reserved for internal use"); } } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/Validation.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/Validation.java index 18a53481236..54ebbf9a119 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/Validation.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/support/Validation.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.support; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.ReservedRolesStore; @@ -21,14 +22,21 @@ public final class Validation { private static final int MIN_PASSWD_LENGTH = 6; - public static Error validateUsername(String username) { + /** + * Validate the username + * @param username the username to validate + * @param allowReserved whether or not to allow reserved user names + * @param settings the settings which may contain information about reserved users + * @return {@code null} if valid + */ + public static Error validateUsername(String username, boolean allowReserved, Settings settings) { if (COMMON_NAME_PATTERN.matcher(username).matches() == false) { return new Error("A valid username must be at least 1 character and no longer than 30 characters. " + "It must begin with a letter (`a-z` or `A-Z`) or an underscore (`_`). Subsequent " + "characters can be letters, underscores (`_`), digits (`0-9`) or any of the following " + "symbols `@`, `-`, `.` or `$`"); } - if (ReservedRealm.isReserved(username)) { + if (allowReserved == false && ReservedRealm.isReserved(username, settings)) { return new Error("Username [" + username + "] is reserved and may not be used."); } return null; diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/AnonymousUser.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/AnonymousUser.java index 505f76161db..1b6545fcf4d 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/AnonymousUser.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/AnonymousUser.java @@ -9,22 +9,17 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.settings.SettingsModule; -import org.elasticsearch.xpack.security.user.User.ReservedUser; +import org.elasticsearch.xpack.security.support.MetadataUtils; -import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.elasticsearch.xpack.security.Security.setting; /** - * The user object for the anonymous user. This class needs to be instantiated with the initialize method since the values - * of the user depends on the settings. However, this is still a singleton instance. Ideally we would assert that an instance of this class - * is only initialized once, but with the way our tests work the same class will be initialized multiple times (one for each node in a - * integration test). + * The user object for the anonymous user. */ -public class AnonymousUser extends ReservedUser { +public class AnonymousUser extends User { public static final String DEFAULT_ANONYMOUS_USERNAME = "_anonymous"; public static final Setting USERNAME_SETTING = @@ -32,57 +27,18 @@ public class AnonymousUser extends ReservedUser { public static final Setting> ROLES_SETTING = Setting.listSetting(setting("authc.anonymous.roles"), Collections.emptyList(), s -> s, Property.NodeScope); - private static String username = DEFAULT_ANONYMOUS_USERNAME; - private static String[] roles = null; - - public static final AnonymousUser INSTANCE = new AnonymousUser(); - - private AnonymousUser() { - super(DEFAULT_ANONYMOUS_USERNAME); + public AnonymousUser(Settings settings) { + super(USERNAME_SETTING.get(settings), ROLES_SETTING.get(settings).toArray(Strings.EMPTY_ARRAY), null, null, + MetadataUtils.DEFAULT_RESERVED_METADATA, isAnonymousEnabled(settings)); } - @Override - public String principal() { - return username; + public static boolean isAnonymousEnabled(Settings settings) { + return ROLES_SETTING.exists(settings) && ROLES_SETTING.get(settings).isEmpty() == false; } - @Override - public String[] roles() { - return roles; - } - - public static boolean enabled() { - return roles != null; - } - - public static boolean is(User user) { - return INSTANCE == user; - } - - public static boolean isAnonymousUsername(String username) { - return AnonymousUser.username.equals(username); - } - - /** - * This method should be used to initialize the AnonymousUser instance with the correct username and password - * @param settings the settings to initialize the anonymous user with - */ - public static synchronized void initialize(Settings settings) { - username = USERNAME_SETTING.get(settings); - List rolesList = ROLES_SETTING.get(settings); - if (rolesList.isEmpty()) { - roles = null; - } else { - roles = rolesList.toArray(Strings.EMPTY_ARRAY); - } - } - - public static String[] getRoles() { - return roles; - } - - public static List> getSettings() { - return Arrays.asList(); + public static boolean isAnonymousUsername(String username, Settings settings) { + // this is possibly the same check but we should not let anything use the default name either + return USERNAME_SETTING.get(settings).equals(username) || DEFAULT_ANONYMOUS_USERNAME.equals(username); } public static void addSettings(List> settingsList) { diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/ElasticUser.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/ElasticUser.java index 6f99a7cb493..ff740d44ef8 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/ElasticUser.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/ElasticUser.java @@ -6,37 +6,18 @@ package org.elasticsearch.xpack.security.user; import org.elasticsearch.xpack.security.authz.permission.SuperuserRole; -import org.elasticsearch.xpack.security.user.User.ReservedUser; +import org.elasticsearch.xpack.security.support.MetadataUtils; /** - * The reserved {@code elastic} superuser. As full permission/access to the cluster/indices and can + * The reserved {@code elastic} superuser. Has full permission/access to the cluster/indices and can * run as any other user. */ -public class ElasticUser extends ReservedUser { +public class ElasticUser extends User { public static final String NAME = "elastic"; public static final String ROLE_NAME = SuperuserRole.NAME; - public static final ElasticUser INSTANCE = new ElasticUser(); - private ElasticUser() { - super(NAME, ROLE_NAME); - } - - @Override - public boolean equals(Object o) { - return INSTANCE == o; - } - - @Override - public int hashCode() { - return System.identityHashCode(this); - } - - public static boolean is(User user) { - return INSTANCE.equals(user); - } - - public static boolean is(String principal) { - return NAME.equals(principal); + public ElasticUser(boolean enabled) { + super(NAME, new String[] { ROLE_NAME }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA, enabled); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/KibanaUser.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/KibanaUser.java index 4a462e7acee..614a0d0abd6 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/KibanaUser.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/KibanaUser.java @@ -6,32 +6,17 @@ package org.elasticsearch.xpack.security.user; import org.elasticsearch.xpack.security.authz.permission.KibanaRole; -import org.elasticsearch.xpack.security.user.User.ReservedUser; +import org.elasticsearch.xpack.security.support.MetadataUtils; /** - * + * Built in user for the kibana server */ -public class KibanaUser extends ReservedUser { +public class KibanaUser extends User { public static final String NAME = "kibana"; public static final String ROLE_NAME = KibanaRole.NAME; - public static final KibanaUser INSTANCE = new KibanaUser(); - KibanaUser() { - super(NAME, ROLE_NAME); - } - - @Override - public boolean equals(Object o) { - return INSTANCE == o; - } - - @Override - public int hashCode() { - return System.identityHashCode(this); - } - - public static boolean is(User user) { - return INSTANCE.equals(user); + public KibanaUser(boolean enabled) { + super(NAME, new String[]{ ROLE_NAME }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA, enabled); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/User.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/User.java index 861a84609ce..143997f12f9 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/User.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/User.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.support.MetadataUtils; @@ -31,40 +30,41 @@ public class User implements ToXContent { private final String[] roles; private final User runAs; private final Map metadata; + private final boolean enabled; @Nullable private final String fullName; @Nullable private final String email; public User(String username, String... roles) { - this(username, roles, null, null, null); + this(username, roles, null, null, null, true); } public User(String username, String[] roles, User runAs) { - this(username, roles, null, null, null, runAs); + this(username, roles, null, null, null, true, runAs); } - public User(String username, String[] roles, String fullName, String email, Map metadata) { + 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; - verifyNoReservedMetadata(this.metadata); } - public User(String username, String[] roles, String fullName, String email, Map metadata, User runAs) { + public User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled, User runAs) { 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; - verifyNoReservedMetadata(this.metadata); } /** @@ -105,6 +105,13 @@ public class User implements ToXContent { return email; } + /** + * @return whether the user is enabled or not + */ + public boolean enabled() { + return enabled; + } + /** * @return The user that will be used for run as functionality. If run as * functionality is not being used, then null will be @@ -133,7 +140,7 @@ public class User implements ToXContent { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o instanceof User == false) return false; User user = (User) o; @@ -166,46 +173,28 @@ public class User implements ToXContent { builder.field(Fields.FULL_NAME.getPreferredName(), fullName()); builder.field(Fields.EMAIL.getPreferredName(), email()); builder.field(Fields.METADATA.getPreferredName(), metadata()); + builder.field(Fields.ENABLED.getPreferredName(), enabled()); return builder.endObject(); } - void verifyNoReservedMetadata(Map metadata) { - if (this instanceof ReservedUser) { - return; - } - - MetadataUtils.verifyNoReservedMetadata(metadata); - } - public static User readFrom(StreamInput input) throws IOException { - if (input.readBoolean()) { - String name = input.readString(); - if (SystemUser.is(name)) { + final boolean isInternalUser = input.readBoolean(); + final String username = input.readString(); + if (isInternalUser) { + if (SystemUser.is(username)) { return SystemUser.INSTANCE; - } else if (XPackUser.is(name)) { + } else if (XPackUser.is(username)) { return XPackUser.INSTANCE; } - User user = ReservedRealm.getUser(name); - if (user == null) { - throw new IllegalStateException("invalid reserved user"); - } - return user; + throw new IllegalStateException("user [" + username + "] is not an internal user"); } - String username = input.readString(); String[] roles = input.readStringArray(); Map metadata = input.readMap(); String fullName = input.readOptionalString(); String email = input.readOptionalString(); - if (input.readBoolean()) { - String runAsUsername = input.readString(); - String[] runAsRoles = input.readStringArray(); - Map runAsMetadata = input.readMap(); - String runAsFullName = input.readOptionalString(); - String runAsEmail = input.readOptionalString(); - User runAs = new User(runAsUsername, runAsRoles, runAsFullName, runAsEmail, runAsMetadata); - return new User(username, roles, fullName, email, metadata, runAs); - } - return new User(username, roles, fullName, email, metadata); + boolean enabled = input.readBoolean(); + User runAs = input.readBoolean() ? readFrom(input) : null; + return new User(username, roles, fullName, email, metadata, enabled, runAs); } public static void writeTo(User user, StreamOutput output) throws IOException { @@ -215,9 +204,6 @@ public class User implements ToXContent { } else if (XPackUser.is(user)) { output.writeBoolean(true); output.writeString(XPackUser.NAME); - } else if (ReservedRealm.isReserved(user.principal())) { - output.writeBoolean(true); - output.writeString(user.principal()); } else { output.writeBoolean(false); output.writeString(user.username); @@ -225,26 +211,16 @@ public class User implements ToXContent { output.writeMap(user.metadata); output.writeOptionalString(user.fullName); output.writeOptionalString(user.email); + output.writeBoolean(user.enabled); if (user.runAs == null) { output.writeBoolean(false); } else { output.writeBoolean(true); - output.writeString(user.runAs.username); - output.writeStringArray(user.runAs.roles); - output.writeMap(user.runAs.metadata); - output.writeOptionalString(user.runAs.fullName); - output.writeOptionalString(user.runAs.email); + writeTo(user.runAs, output); } } } - abstract static class ReservedUser extends User { - - ReservedUser(String username, String... roles) { - super(username, roles, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA); - } - } - public interface Fields { ParseField USERNAME = new ParseField("username"); ParseField PASSWORD = new ParseField("password"); @@ -253,5 +229,6 @@ public class User implements ToXContent { ParseField FULL_NAME = new ParseField("full_name"); ParseField EMAIL = new ParseField("email"); ParseField METADATA = new ParseField("metadata"); + ParseField ENABLED = new ParseField("enabled"); } } diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/XPackUser.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/XPackUser.java index 42006c30828..c96bd9b3c4d 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/XPackUser.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/user/XPackUser.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.security.user; import org.elasticsearch.xpack.security.authz.permission.SuperuserRole; -import org.elasticsearch.xpack.security.user.User.ReservedUser; /** * XPack internal user that manages xpack. Has all cluster/indices permissions for x-pack to operate. @@ -17,7 +16,7 @@ public class XPackUser extends User { public static final String ROLE_NAME = SuperuserRole.NAME; public static final XPackUser INSTANCE = new XPackUser(); - XPackUser() { + private XPackUser() { super(NAME, ROLE_NAME); } diff --git a/elasticsearch/x-pack/security/src/main/resources/security-index-template.json b/elasticsearch/x-pack/security/src/main/resources/security-index-template.json index 2a44a1b1aa6..a9e72dd3877 100644 --- a/elasticsearch/x-pack/security/src/main/resources/security-index-template.json +++ b/elasticsearch/x-pack/security/src/main/resources/security-index-template.json @@ -58,6 +58,9 @@ "metadata" : { "type" : "object", "dynamic" : true + }, + "enabled": { + "type": "boolean" } } }, @@ -109,6 +112,9 @@ "type" : "keyword", "index" : false, "doc_values" : false + }, + "enabled": { + "type": "boolean" } } } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java index 38df2751180..cb7e0438a52 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java @@ -24,7 +24,6 @@ import org.elasticsearch.xpack.security.crypto.CryptoService; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.watcher.support.xcontent.XContentSource; -import org.junit.After; import org.junit.Before; import static org.hamcrest.CoreMatchers.nullValue; @@ -56,11 +55,6 @@ public class SecurityFeatureSetTests extends ESTestCase { cryptoService = mock(CryptoService.class); } - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testAvailable() throws Exception { SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore, ipFilter, auditTrail, cryptoService); @@ -150,7 +144,7 @@ public class SecurityFeatureSetTests extends ESTestCase { final boolean anonymousEnabled = randomBoolean(); if (anonymousEnabled) { - AnonymousUser.initialize(Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "foo").build()); + settings.put(AnonymousUser.ROLES_SETTING.getKey(), "foo"); } SecurityFeatureSet featureSet = new SecurityFeatureSet(settings.build(), licenseState, realms, rolesStore, diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java index 2205f0cbcf7..054a5037994 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.security.authz.RoleDescriptor; import org.elasticsearch.xpack.security.authz.permission.KibanaRole; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.security.user.ElasticUser; import org.elasticsearch.xpack.security.user.KibanaUser; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; @@ -56,7 +57,9 @@ public class TransportGetRolesActionTests extends ESTestCase { final boolean isKibanaUser = randomBoolean(); if (isKibanaUser) { - when(context.getUser()).thenReturn(KibanaUser.INSTANCE); + when(context.getUser()).thenReturn(new KibanaUser(true)); + } else { + when(context.getUser()).thenReturn(new ElasticUser(true)); } final int size = randomIntBetween(1, ReservedRolesStore.names().size()); final List names = randomSubsetOf(size, ReservedRolesStore.names()); @@ -116,7 +119,9 @@ public class TransportGetRolesActionTests extends ESTestCase { final boolean isKibanaUser = randomBoolean(); if (isKibanaUser) { - when(context.getUser()).thenReturn(KibanaUser.INSTANCE); + when(context.getUser()).thenReturn(new KibanaUser(true)); + } else { + when(context.getUser()).thenReturn(new ElasticUser(true)); } GetRolesRequest request = new GetRolesRequest(); @@ -199,9 +204,10 @@ public class TransportGetRolesActionTests extends ESTestCase { } if (isKibanaUser) { - when(context.getUser()).thenReturn(KibanaUser.INSTANCE); + when(context.getUser()).thenReturn(new KibanaUser(true)); } else { expectedNames.remove(KibanaRole.NAME); + when(context.getUser()).thenReturn(new ElasticUser(true)); } GetRolesRequest request = new GetRolesRequest(); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java index 236dce76b97..ca83e386469 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.security.user.XPackUser; import java.util.concurrent.atomic.AtomicReference; @@ -31,9 +32,9 @@ import static org.mockito.Mockito.when; public class TransportAuthenticateActionTests extends ESTestCase { - public void testSystemUser() { + public void testInternalUser() { SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.getUser()).thenReturn(SystemUser.INSTANCE); + when(securityContext.getUser()).thenReturn(randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE)); TransportAuthenticateAction action = new TransportAuthenticateAction(Settings.EMPTY, mock(ThreadPool.class), mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), securityContext); @@ -83,7 +84,7 @@ public class TransportAuthenticateActionTests extends ESTestCase { } public void testValidUser() { - final User user = randomFrom(ElasticUser.INSTANCE, KibanaUser.INSTANCE, new User("joe")); + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); SecurityContext securityContext = mock(SecurityContext.class); when(securityContext.getUser()).thenReturn(user); TransportAuthenticateAction action = new TransportAuthenticateAction(Settings.EMPTY, mock(ThreadPool.class), diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java index 35344ba2394..d3a8f452e35 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import org.junit.After; +import org.elasticsearch.xpack.security.user.XPackUser; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -43,20 +43,15 @@ import static org.mockito.Mockito.verifyZeroInteractions; public class TransportChangePasswordActionTests extends ESTestCase { - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testAnonymousUser() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); - AnonymousUser.initialize(settings); + AnonymousUser anonymousUser = new AnonymousUser(settings); NativeUsersStore usersStore = mock(NativeUsersStore.class); - TransportChangePasswordAction action = new TransportChangePasswordAction(Settings.EMPTY, mock(ThreadPool.class), + TransportChangePasswordAction action = new TransportChangePasswordAction(settings, mock(ThreadPool.class), mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); ChangePasswordRequest request = new ChangePasswordRequest(); - request.username(AnonymousUser.INSTANCE.principal()); + request.username(anonymousUser.principal()); request.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); final AtomicReference throwableRef = new AtomicReference<>(); @@ -79,13 +74,13 @@ public class TransportChangePasswordActionTests extends ESTestCase { verifyZeroInteractions(usersStore); } - public void testSystemUser() { + public void testInternalUsers() { NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportChangePasswordAction action = new TransportChangePasswordAction(Settings.EMPTY, mock(ThreadPool.class), mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); ChangePasswordRequest request = new ChangePasswordRequest(); - request.username(SystemUser.INSTANCE.principal()); + request.username(randomFrom(SystemUser.INSTANCE.principal(), XPackUser.INSTANCE.principal())); request.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); final AtomicReference throwableRef = new AtomicReference<>(); @@ -109,7 +104,7 @@ public class TransportChangePasswordActionTests extends ESTestCase { } public void testValidUser() { - final User user = randomFrom(ElasticUser.INSTANCE, KibanaUser.INSTANCE, new User("joe")); + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); NativeUsersStore usersStore = mock(NativeUsersStore.class); ChangePasswordRequest request = new ChangePasswordRequest(); request.username(user.principal()); @@ -147,7 +142,7 @@ public class TransportChangePasswordActionTests extends ESTestCase { } public void testException() { - final User user = randomFrom(ElasticUser.INSTANCE, KibanaUser.INSTANCE, new User("joe")); + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); NativeUsersStore usersStore = mock(NativeUsersStore.class); ChangePasswordRequest request = new ChangePasswordRequest(); request.username(user.principal()); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java index 614681491df..6b647461fc9 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java @@ -11,14 +11,15 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; -import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.user.AnonymousUser; +import org.elasticsearch.xpack.security.user.ElasticUser; +import org.elasticsearch.xpack.security.user.KibanaUser; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import org.junit.After; +import org.elasticsearch.xpack.security.user.XPackUser; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -40,19 +41,13 @@ import static org.mockito.Mockito.verifyZeroInteractions; public class TransportDeleteUserActionTests extends ESTestCase { - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testAnonymousUser() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); - AnonymousUser.initialize(settings); NativeUsersStore usersStore = mock(NativeUsersStore.class); - TransportDeleteUserAction action = new TransportDeleteUserAction(Settings.EMPTY, mock(ThreadPool.class), + TransportDeleteUserAction action = new TransportDeleteUserAction(settings, mock(ThreadPool.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); - DeleteUserRequest request = new DeleteUserRequest(AnonymousUser.INSTANCE.principal()); + DeleteUserRequest request = new DeleteUserRequest(new AnonymousUser(settings).principal()); final AtomicReference throwableRef = new AtomicReference<>(); final AtomicReference responseRef = new AtomicReference<>(); @@ -74,12 +69,12 @@ public class TransportDeleteUserActionTests extends ESTestCase { verifyZeroInteractions(usersStore); } - public void testSystemUser() { + public void testInternalUser() { NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportDeleteUserAction action = new TransportDeleteUserAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); - DeleteUserRequest request = new DeleteUserRequest(SystemUser.INSTANCE.principal()); + DeleteUserRequest request = new DeleteUserRequest(randomFrom(SystemUser.INSTANCE.principal(), XPackUser.INSTANCE.principal())); final AtomicReference throwableRef = new AtomicReference<>(); final AtomicReference responseRef = new AtomicReference<>(); @@ -102,7 +97,7 @@ public class TransportDeleteUserActionTests extends ESTestCase { } public void testReservedUser() { - final User reserved = randomFrom(ReservedRealm.users().toArray(new User[0])); + final User reserved = randomFrom(new ElasticUser(true), new KibanaUser(true)); NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportDeleteUserAction action = new TransportDeleteUserAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java index 603e54e7dc3..a5d57683bcd 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.Strings; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.user.AnonymousUser; @@ -20,13 +21,14 @@ import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import org.junit.After; +import org.elasticsearch.xpack.security.user.XPackUser; import org.junit.Before; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -48,32 +50,34 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; public class TransportGetUsersActionTests extends ESTestCase { private boolean anonymousEnabled; + private Settings settings; @Before public void maybeEnableAnonymous() { anonymousEnabled = randomBoolean(); if (anonymousEnabled) { - Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); - AnonymousUser.initialize(settings); + settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); + } else { + settings = Settings.EMPTY; } } - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testAnonymousUser() { NativeUsersStore usersStore = mock(NativeUsersStore.class); + when(usersStore.started()).thenReturn(true); + AnonymousUser anonymousUser = new AnonymousUser(settings); + ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, anonymousUser); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class), + reservedRealm); GetUsersRequest request = new GetUsersRequest(); - request.usernames(AnonymousUser.INSTANCE.principal()); + request.usernames(anonymousUser.principal()); final AtomicReference throwableRef = new AtomicReference<>(); final AtomicReference responseRef = new AtomicReference<>(); @@ -93,20 +97,21 @@ public class TransportGetUsersActionTests extends ESTestCase { assertThat(responseRef.get(), is(notNullValue())); final User[] users = responseRef.get().users(); if (anonymousEnabled) { - assertThat("expected array with anonymous but got: " + Arrays.toString(users), users, arrayContaining(AnonymousUser.INSTANCE)); + assertThat("expected array with anonymous but got: " + Arrays.toString(users), users, arrayContaining(anonymousUser)); } else { assertThat("expected an empty array but got: " + Arrays.toString(users), users, emptyArray()); } verifyZeroInteractions(usersStore); } - public void testSystemUser() { + public void testInternalUser() { NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, + mock(TransportService.class), mock(ReservedRealm.class)); GetUsersRequest request = new GetUsersRequest(); - request.usernames(SystemUser.INSTANCE.principal()); + request.usernames(randomFrom(SystemUser.INSTANCE.principal(), XPackUser.INSTANCE.principal())); final AtomicReference throwableRef = new AtomicReference<>(); final AtomicReference responseRef = new AtomicReference<>(); @@ -129,12 +134,16 @@ public class TransportGetUsersActionTests extends ESTestCase { } public void testReservedUsersOnly() { - final int size = randomIntBetween(1, ReservedRealm.users().size()); - final List reservedUsers = randomSubsetOf(size, ReservedRealm.users()); - final List names = reservedUsers.stream().map(User::principal).collect(Collectors.toList()); NativeUsersStore usersStore = mock(NativeUsersStore.class); + when(usersStore.started()).thenReturn(true); + ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings)); + final Collection allReservedUsers = reservedRealm.users(); + final int size = randomIntBetween(1, allReservedUsers.size()); + final List reservedUsers = randomSubsetOf(size, allReservedUsers); + final List names = reservedUsers.stream().map(User::principal).collect(Collectors.toList()); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class), + reservedRealm); GetUsersRequest request = new GetUsersRequest(); request.usernames(names.toArray(new String[names.size()])); @@ -156,15 +165,18 @@ public class TransportGetUsersActionTests extends ESTestCase { assertThat(throwableRef.get(), is(nullValue())); assertThat(responseRef.get(), is(notNullValue())); assertThat(responseRef.get().users(), arrayContaining(reservedUsers.toArray(new User[reservedUsers.size()]))); - verifyZeroInteractions(usersStore); + verify(usersStore, times(1 + names.size())).started(); } public void testGetAllUsers() { final List storeUsers = randomFrom(Collections.emptyList(), Collections.singletonList(new User("joe")), Arrays.asList(new User("jane"), new User("fred")), randomUsers()); NativeUsersStore usersStore = mock(NativeUsersStore.class); + when(usersStore.started()).thenReturn(true); + ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings)); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class), + reservedRealm); GetUsersRequest request = new GetUsersRequest(); doAnswer(new Answer() { @@ -192,7 +204,7 @@ public class TransportGetUsersActionTests extends ESTestCase { }); final List expectedList = new ArrayList<>(); - expectedList.addAll(ReservedRealm.users()); + expectedList.addAll(reservedRealm.users()); expectedList.addAll(storeUsers); assertThat(throwableRef.get(), is(nullValue())); @@ -207,7 +219,8 @@ public class TransportGetUsersActionTests extends ESTestCase { final String[] storeUsernames = storeUsers.stream().map(User::principal).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY); NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class), + mock(ReservedRealm.class)); GetUsersRequest request = new GetUsersRequest(); request.usernames(storeUsernames); @@ -268,7 +281,8 @@ public class TransportGetUsersActionTests extends ESTestCase { final String[] storeUsernames = storeUsers.stream().map(User::principal).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY); NativeUsersStore usersStore = mock(NativeUsersStore.class); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class), + mock(ReservedRealm.class)); GetUsersRequest request = new GetUsersRequest(); request.usernames(storeUsernames); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java index 52b7a2e247f..d4f386bfe6f 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.support.Hasher; @@ -21,7 +22,7 @@ import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import org.junit.After; +import org.elasticsearch.xpack.security.user.XPackUser; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -40,23 +41,19 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; public class TransportPutUserActionTests extends ESTestCase { - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testAnonymousUser() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); - AnonymousUser.initialize(settings); + final AnonymousUser anonymousUser = new AnonymousUser(settings); NativeUsersStore usersStore = mock(NativeUsersStore.class); - TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), + TransportPutUserAction action = new TransportPutUserAction(settings, mock(ThreadPool.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); PutUserRequest request = new PutUserRequest(); - request.username(AnonymousUser.INSTANCE.principal()); + request.username(anonymousUser.principal()); final AtomicReference throwableRef = new AtomicReference<>(); final AtomicReference responseRef = new AtomicReference<>(); @@ -84,7 +81,7 @@ public class TransportPutUserActionTests extends ESTestCase { mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); PutUserRequest request = new PutUserRequest(); - request.username(SystemUser.INSTANCE.principal()); + request.username(randomFrom(SystemUser.INSTANCE.principal(), XPackUser.INSTANCE.principal())); final AtomicReference throwableRef = new AtomicReference<>(); final AtomicReference responseRef = new AtomicReference<>(); @@ -107,8 +104,11 @@ public class TransportPutUserActionTests extends ESTestCase { } public void testReservedUser() { - final User reserved = randomFrom(ReservedRealm.users().toArray(new User[0])); NativeUsersStore usersStore = mock(NativeUsersStore.class); + when(usersStore.started()).thenReturn(true); + Settings settings = Settings.builder().put("path.home", createTempDir()).build(); + ReservedRealm reservedRealm = new ReservedRealm(new Environment(settings), settings, usersStore, new AnonymousUser(settings)); + final User reserved = randomFrom(reservedRealm.users().toArray(new User[0])); TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); @@ -132,7 +132,7 @@ public class TransportPutUserActionTests extends ESTestCase { assertThat(responseRef.get(), is(nullValue())); assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); assertThat(throwableRef.get().getMessage(), containsString("is reserved and only the password")); - verifyZeroInteractions(usersStore); + verify(usersStore).started(); } public void testValidUser() { diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java new file mode 100644 index 00000000000..bef25e4c3bc --- /dev/null +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java @@ -0,0 +1,259 @@ +/* + * 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.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.security.authc.Authentication; +import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; +import org.elasticsearch.xpack.security.user.AnonymousUser; +import org.elasticsearch.xpack.security.user.ElasticUser; +import org.elasticsearch.xpack.security.user.KibanaUser; +import org.elasticsearch.xpack.security.user.SystemUser; +import org.elasticsearch.xpack.security.user.User; +import org.elasticsearch.xpack.security.user.XPackUser; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the {@link TransportSetEnabledAction} + */ +public class TransportSetEnabledActionTests extends ESTestCase { + + public void testAnonymousUser() { + Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); + ThreadPool threadPool = mock(ThreadPool.class); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Authentication authentication = mock(Authentication.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); + when(authentication.getRunAsUser()).thenReturn(user); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportSetEnabledAction action = new TransportSetEnabledAction(settings, threadPool, + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); + + SetEnabledRequest request = new SetEnabledRequest(); + request.username(new AnonymousUser(settings).principal()); + request.enabled(randomBoolean()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(SetEnabledResponse setEnabledResponse) { + responseRef.set(setEnabledResponse); + } + + @Override + public void onFailure(Exception e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is anonymous")); + verifyZeroInteractions(usersStore); + } + + public void testInternalUser() { + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); + ThreadPool threadPool = mock(ThreadPool.class); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Authentication authentication = mock(Authentication.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); + when(authentication.getRunAsUser()).thenReturn(user); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportSetEnabledAction action = new TransportSetEnabledAction(Settings.EMPTY, threadPool, + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); + + SetEnabledRequest request = new SetEnabledRequest(); + request.username(randomFrom(SystemUser.INSTANCE.principal(), XPackUser.INSTANCE.principal())); + request.enabled(randomBoolean()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(SetEnabledResponse setEnabledResponse) { + responseRef.set(setEnabledResponse); + } + + @Override + public void onFailure(Exception e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is internal")); + verifyZeroInteractions(usersStore); + } + + public void testValidUser() { + ThreadPool threadPool = mock(ThreadPool.class); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Authentication authentication = mock(Authentication.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); + when(authentication.getRunAsUser()).thenReturn(new User("the runner")); + + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + SetEnabledRequest request = new SetEnabledRequest(); + request.username(user.principal()); + request.enabled(randomBoolean()); + request.setRefreshPolicy(randomFrom(RefreshPolicy.values())); + // mock the setEnabled call on the native users store so that it will invoke the action listener with a response + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 4; + ActionListener listener = (ActionListener) args[3]; + listener.onResponse(null); + return null; + } + }).when(usersStore) + .setEnabled(eq(user.principal()), eq(request.enabled()), eq(request.getRefreshPolicy()), any(ActionListener.class)); + TransportSetEnabledAction action = new TransportSetEnabledAction(Settings.EMPTY, threadPool, + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(SetEnabledResponse setEnabledResponse) { + responseRef.set(setEnabledResponse); + } + + @Override + public void onFailure(Exception e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get(), instanceOf(SetEnabledResponse.class)); + assertThat(throwableRef.get(), is(nullValue())); + verify(usersStore, times(1)) + .setEnabled(eq(user.principal()), eq(request.enabled()), eq(request.getRefreshPolicy()), any(ActionListener.class)); + } + + public void testException() { + ThreadPool threadPool = mock(ThreadPool.class); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Authentication authentication = mock(Authentication.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); + when(authentication.getRunAsUser()).thenReturn(new User("the runner")); + + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + SetEnabledRequest request = new SetEnabledRequest(); + request.username(user.principal()); + request.enabled(randomBoolean()); + request.setRefreshPolicy(randomFrom(RefreshPolicy.values())); + final Exception e = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException(), new RuntimeException()); + // we're mocking the setEnabled call on the native users store so that it will invoke the action listener with an exception + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 4; + ActionListener listener = (ActionListener) args[3]; + listener.onFailure(e); + return null; + } + }).when(usersStore) + .setEnabled(eq(user.principal()), eq(request.enabled()), eq(request.getRefreshPolicy()), any(ActionListener.class)); + TransportSetEnabledAction action = new TransportSetEnabledAction(Settings.EMPTY, threadPool, + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(SetEnabledResponse setEnabledResponse) { + responseRef.set(setEnabledResponse); + } + + @Override + public void onFailure(Exception e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), sameInstance(e)); + verify(usersStore, times(1)) + .setEnabled(eq(user.principal()), eq(request.enabled()), eq(request.getRefreshPolicy()), any(ActionListener.class)); + } + + public void testUserModifyingThemselves() { + final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); + ThreadPool threadPool = mock(ThreadPool.class); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Authentication authentication = mock(Authentication.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication); + when(authentication.getRunAsUser()).thenReturn(user); + + NativeUsersStore usersStore = mock(NativeUsersStore.class); + SetEnabledRequest request = new SetEnabledRequest(); + request.username(user.principal()); + request.enabled(randomBoolean()); + request.setRefreshPolicy(randomFrom(RefreshPolicy.values())); + TransportSetEnabledAction action = new TransportSetEnabledAction(Settings.EMPTY, threadPool, + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(SetEnabledResponse setEnabledResponse) { + responseRef.set(setEnabledResponse); + } + + @Override + public void onFailure(Exception e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("own account")); + verifyZeroInteractions(usersStore); + } +} diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 64161fc7607..c536ae36b2b 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -32,7 +32,6 @@ import org.elasticsearch.xpack.security.crypto.CryptoService; import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.xpack.security.user.User; -import org.junit.After; import org.junit.Before; import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException; @@ -56,21 +55,21 @@ import static org.mockito.Mockito.when; /** - * + * Unit tests for the {@link AuthenticationService} */ public class AuthenticationServiceTests extends ESTestCase { - AuthenticationService service; - TransportMessage message; - RestRequest restRequest; - Realms realms; - Realm firstRealm; - Realm secondRealm; - AuditTrailService auditTrail; - AuthenticationToken token; - CryptoService cryptoService; - ThreadPool threadPool; - ThreadContext threadContext; + private AuthenticationService service; + private TransportMessage message; + private RestRequest restRequest; + private Realms realms; + private Realm firstRealm; + private Realm secondRealm; + private AuditTrailService auditTrail; + private AuthenticationToken token; + private CryptoService cryptoService; + private ThreadPool threadPool; + private ThreadContext threadContext; @Before public void init() throws Exception { @@ -109,12 +108,7 @@ public class AuthenticationServiceTests extends ESTestCase { when(threadPool.getThreadContext()).thenReturn(threadContext); when(cryptoService.sign(any(String.class))).thenReturn("_signed_auth"); service = new AuthenticationService(settings, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); - } - - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(settings)); } @SuppressWarnings("unchecked") @@ -268,6 +262,33 @@ public class AuthenticationServiceTests extends ESTestCase { assertThreadContextContainsAuthentication(result); } + public void testAuthenticateTransportDisabledUser() throws Exception { + User user = new User("username", new String[] { "r1", "r2" }, null, null, null, false); + User fallback = randomBoolean() ? SystemUser.INSTANCE : null; + when(firstRealm.token(threadContext)).thenReturn(token); + when(firstRealm.supports(token)).thenReturn(true); + when(firstRealm.authenticate(token)).thenReturn(user); + + ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> service.authenticate("_action", message, fallback)); + verify(auditTrail).authenticationFailed(token, "_action", message); + verifyNoMoreInteractions(auditTrail); + assertAuthenticationException(e); + } + + public void testAuthenticateRestDisabledUser() throws Exception { + User user = new User("username", new String[] { "r1", "r2" }, null, null, null, false); + when(firstRealm.token(threadContext)).thenReturn(token); + when(firstRealm.supports(token)).thenReturn(true); + when(firstRealm.authenticate(token)).thenReturn(user); + + ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> service.authenticate(restRequest)); + verify(auditTrail).authenticationFailed(token, restRequest); + verifyNoMoreInteractions(auditTrail); + assertAuthenticationException(e); + } + public void testAuthenticateTransportSuccess() throws Exception { User user = new User("username", "r1", "r2"); User fallback = randomBoolean() ? SystemUser.INSTANCE : null; @@ -308,7 +329,7 @@ public class AuthenticationServiceTests extends ESTestCase { ThreadContext threadContext1 = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext1); service = new AuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); threadContext1.putTransient(Authentication.AUTHENTICATION_KEY, threadContext.getTransient(Authentication.AUTHENTICATION_KEY)); threadContext1.putHeader(Authentication.AUTHENTICATION_KEY, threadContext.getHeader(Authentication.AUTHENTICATION_KEY)); @@ -317,12 +338,11 @@ public class AuthenticationServiceTests extends ESTestCase { verifyZeroInteractions(firstRealm); reset(firstRealm); - // checking authentication from the user header threadContext1 = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext1); service = new AuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); threadContext1.putHeader(Authentication.AUTHENTICATION_KEY, threadContext.getHeader(Authentication.AUTHENTICATION_KEY)); when(cryptoService.unsignAndVerify("_signed_auth")).thenReturn(authentication.encode()); @@ -334,7 +354,7 @@ public class AuthenticationServiceTests extends ESTestCase { when(threadPool.getThreadContext()).thenReturn(threadContext1); service = new AuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); Authentication result = service.authenticate("_action", new InternalMessage(), SystemUser.INSTANCE); assertThat(result, notNullValue()); assertThat(result.getUser(), equalTo(user1)); @@ -344,7 +364,7 @@ public class AuthenticationServiceTests extends ESTestCase { public void testAuthenticateTransportContextAndHeaderNoSigning() throws Exception { Settings settings = Settings.builder().put(AuthenticationService.SIGN_USER_HEADER.getKey(), false).build(); service = new AuthenticationService(settings, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); User user1 = new User("username", "r1", "r2"); when(firstRealm.supports(token)).thenReturn(true); @@ -361,7 +381,7 @@ public class AuthenticationServiceTests extends ESTestCase { ThreadContext threadContext1 = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext1); service = new AuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); threadContext1.putTransient(Authentication.AUTHENTICATION_KEY, threadContext.getTransient(Authentication.AUTHENTICATION_KEY)); threadContext1.putHeader(Authentication.AUTHENTICATION_KEY, threadContext.getHeader(Authentication.AUTHENTICATION_KEY)); Authentication ctxAuth = service.authenticate("_action", message1, SystemUser.INSTANCE); @@ -381,7 +401,7 @@ public class AuthenticationServiceTests extends ESTestCase { when(threadPool.getThreadContext()).thenReturn(threadContext1); service = new AuthenticationService(settings, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); Authentication result = service.authenticate("_action", new InternalMessage(), SystemUser.INSTANCE); assertThat(result, notNullValue()); assertThat(result.getUser(), equalTo(user1)); @@ -442,15 +462,15 @@ public class AuthenticationServiceTests extends ESTestCase { builder.put(AnonymousUser.USERNAME_SETTING.getKey(), username); } Settings settings = builder.build(); - AnonymousUser.initialize(settings); + final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), - threadPool); + threadPool, anonymousUser); RestRequest request = new FakeRestRequest(); Authentication result = service.authenticate(request); assertThat(result, notNullValue()); - assertThat(result.getUser(), sameInstance((Object) AnonymousUser.INSTANCE)); + assertThat(result.getUser(), sameInstance((Object) anonymousUser)); assertThreadContextContainsAuthentication(result); } @@ -458,14 +478,14 @@ public class AuthenticationServiceTests extends ESTestCase { Settings settings = Settings.builder() .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3") .build(); - AnonymousUser.initialize(settings); + final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); InternalMessage message = new InternalMessage(); Authentication result = service.authenticate("_action", message, null); assertThat(result, notNullValue()); - assertThat(result.getUser(), sameInstance(AnonymousUser.INSTANCE)); + assertThat(result.getUser(), sameInstance(anonymousUser)); assertThreadContextContainsAuthentication(result); } @@ -473,9 +493,9 @@ public class AuthenticationServiceTests extends ESTestCase { Settings settings = Settings.builder() .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3") .build(); - AnonymousUser.initialize(settings); + final AnonymousUser anonymousUser = new AnonymousUser(settings); service = new AuthenticationService(settings, realms, auditTrail, cryptoService, - new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); InternalMessage message = new InternalMessage(); @@ -688,6 +708,40 @@ public class AuthenticationServiceTests extends ESTestCase { } } + public void testAuthenticateTransportDisabledRunAsUser() throws Exception { + AuthenticationToken token = mock(AuthenticationToken.class); + threadContext.putHeader(AuthenticationService.RUN_AS_USER_HEADER, "run_as"); + when(secondRealm.token(threadContext)).thenReturn(token); + when(secondRealm.supports(token)).thenReturn(true); + when(secondRealm.authenticate(token)).thenReturn(new User("lookup user", new String[]{"user"})); + when(secondRealm.lookupUser("run_as")) + .thenReturn(new User("looked up user", new String[]{"some role"}, null, null, null, false)); + when(secondRealm.userLookupSupported()).thenReturn(true); + User fallback = randomBoolean() ? SystemUser.INSTANCE : null; + ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> service.authenticate("_action", message, fallback)); + verify(auditTrail).authenticationFailed(token, "_action", message); + verifyNoMoreInteractions(auditTrail); + assertAuthenticationException(e); + } + + public void testAuthenticateRestDisabledRunAsUser() throws Exception { + AuthenticationToken token = mock(AuthenticationToken.class); + threadContext.putHeader(AuthenticationService.RUN_AS_USER_HEADER, "run_as"); + when(secondRealm.token(threadContext)).thenReturn(token); + when(secondRealm.supports(token)).thenReturn(true); + when(secondRealm.authenticate(token)).thenReturn(new User("lookup user", new String[]{"user"})); + when(secondRealm.lookupUser("run_as")) + .thenReturn(new User("looked up user", new String[]{"some role"}, null, null, null, false)); + when(secondRealm.userLookupSupported()).thenReturn(true); + + ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> service.authenticate(restRequest)); + verify(auditTrail).authenticationFailed(token, restRequest); + verifyNoMoreInteractions(auditTrail); + assertAuthenticationException(e); + } + private static class InternalMessage extends TransportMessage { } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java index 3d162918cfa..5f142a40165 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java @@ -599,4 +599,27 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { assertThat(usage.get("fls"), is(fls)); assertThat(usage.get("dls"), is(dls)); } + + public void testSetEnabled() throws Exception { + securityClient().preparePutUser("joe", "s3krit".toCharArray(), SecuritySettingsSource.DEFAULT_ROLE).get(); + final String token = basicAuthHeaderValue("joe", new SecuredString("s3krit".toCharArray())); + ClusterHealthResponse response = client().filterWithHeader(Collections.singletonMap("Authorization", token)) + .admin().cluster().prepareHealth().get(); + assertThat(response.isTimedOut(), is(false)); + + securityClient(client()).prepareSetEnabled("joe", false).get(); + + ElasticsearchSecurityException expected = expectThrows(ElasticsearchSecurityException.class, + () -> client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get()); + assertThat(expected.status(), is(RestStatus.UNAUTHORIZED)); + + securityClient(client()).prepareSetEnabled("joe", true).get(); + + response = client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get(); + assertThat(response.isTimedOut(), is(false)); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> securityClient(client()).prepareSetEnabled("not_a_real_user", false).get()); + assertThat(e.getMessage(), containsString("only existing users can be disabled")); + } } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java index 0c842875f34..14b91156f12 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java @@ -74,4 +74,35 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase { .get(); assertThat(healthResponse.getClusterName(), is(cluster().getClusterName())); } + + public void testDisablingUser() throws Exception { + // validate the user works + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get(); + assertThat(response.getClusterName(), is(cluster().getClusterName())); + + // disable user + securityClient().prepareSetEnabled(ElasticUser.NAME, false).get(); + ElasticsearchSecurityException elasticsearchSecurityException = expectThrows(ElasticsearchSecurityException.class, () -> client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get()); + assertThat(elasticsearchSecurityException.getMessage(), containsString("authenticate")); + + //enable + securityClient().prepareSetEnabled(ElasticUser.NAME, true).get(); + response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get(); + assertThat(response.getClusterName(), is(cluster().getClusterName())); + } } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index b84b3cb444f..780aa8742af 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -8,7 +8,7 @@ package org.elasticsearch.xpack.security.authc.esnative; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; -import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ChangeListener; +import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.elasticsearch.xpack.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.support.SecuredString; import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; @@ -23,8 +23,6 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -41,20 +39,19 @@ public class ReservedRealmTests extends ESTestCase { @Before public void setupMocks() { - AnonymousUser.initialize(Settings.EMPTY); usersStore = mock(NativeUsersStore.class); when(usersStore.started()).thenReturn(true); } public void testUserStoreNotStarted() { when(usersStore.started()).thenReturn(false); - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); final String principal = randomFrom(ElasticUser.NAME, KibanaUser.NAME); ElasticsearchSecurityException expected = expectThrows(ElasticsearchSecurityException.class, () -> reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, DEFAULT_PASSWORD))); assertThat(expected.getMessage(), containsString("failed to authenticate user [" + principal)); - verify(usersStore).addListener(any(ChangeListener.class)); verify(usersStore).started(); verifyNoMoreInteractions(usersStore); } @@ -64,28 +61,29 @@ public class ReservedRealmTests extends ESTestCase { if (securityIndexExists) { when(usersStore.securityIndexExists()).thenReturn(true); } - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); - final User expected = randomFrom((User) ElasticUser.INSTANCE, KibanaUser.INSTANCE); + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); + final User expected = randomFrom(new ElasticUser(true), new KibanaUser(true)); final String principal = expected.principal(); final User authenticated = reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, DEFAULT_PASSWORD)); - assertThat(authenticated, sameInstance(expected)); - verify(usersStore).addListener(any(ChangeListener.class)); + assertEquals(expected, authenticated); verify(usersStore).started(); verify(usersStore).securityIndexExists(); if (securityIndexExists) { - verify(usersStore).reservedUserPassword(principal); + verify(usersStore).getReservedUserInfo(principal); } verifyNoMoreInteractions(usersStore); } public void testAuthenticationWithStoredPassword() throws Throwable { - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); - final User expectedUser = randomFrom((User) ElasticUser.INSTANCE, KibanaUser.INSTANCE); + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); + final User expectedUser = randomFrom(new ElasticUser(true), new KibanaUser(true)); final String principal = expectedUser.principal(); final SecuredString newPassword = new SecuredString("foobar".toCharArray()); when(usersStore.securityIndexExists()).thenReturn(true); - when(usersStore.reservedUserPassword(principal)).thenReturn(Hasher.BCRYPT.hash(newPassword)); + when(usersStore.getReservedUserInfo(principal)).thenReturn(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), true)); // test default password ElasticsearchSecurityException expected = expectThrows(ElasticsearchSecurityException.class, @@ -93,52 +91,75 @@ public class ReservedRealmTests extends ESTestCase { assertThat(expected.getMessage(), containsString("failed to authenticate user [" + principal)); // the realm assumes it owns the hashed password so it fills it with 0's - when(usersStore.reservedUserPassword(principal)).thenReturn(Hasher.BCRYPT.hash(newPassword)); + when(usersStore.getReservedUserInfo(principal)).thenReturn(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), true)); // test new password final User authenticated = reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, newPassword)); - assertThat(authenticated, sameInstance(expectedUser)); - verify(usersStore).addListener(any(ChangeListener.class)); + assertEquals(expectedUser, authenticated); verify(usersStore, times(2)).started(); verify(usersStore, times(2)).securityIndexExists(); - verify(usersStore, times(2)).reservedUserPassword(principal); + verify(usersStore, times(2)).getReservedUserInfo(principal); verifyNoMoreInteractions(usersStore); } - public void testLookup() { - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); - final User expectedUser = randomFrom((User) ElasticUser.INSTANCE, KibanaUser.INSTANCE); + public void testLookup() throws Exception { + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); + final User expectedUser = randomFrom(new ElasticUser(true), new KibanaUser(true)); final String principal = expectedUser.principal(); final User user = reservedRealm.doLookupUser(principal); - assertThat(user, sameInstance(expectedUser)); - verify(usersStore).addListener(any(ChangeListener.class)); - verifyNoMoreInteractions(usersStore); + assertEquals(expectedUser, user); + verify(usersStore).started(); + verify(usersStore).securityIndexExists(); final User doesntExist = reservedRealm.doLookupUser("foobar"); assertThat(doesntExist, nullValue()); + verifyNoMoreInteractions(usersStore); } - public void testHelperMethods() { - final User expectedUser = randomFrom((User) ElasticUser.INSTANCE, KibanaUser.INSTANCE); + public void testLookupThrows() throws Exception { + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); + final User expectedUser = randomFrom(new ElasticUser(true), new KibanaUser(true)); final String principal = expectedUser.principal(); - assertThat(ReservedRealm.isReserved(principal), is(true)); - assertThat(ReservedRealm.getUser(principal), sameInstance(expectedUser)); + when(usersStore.securityIndexExists()).thenReturn(true); + final RuntimeException e = new RuntimeException("store threw"); + when(usersStore.getReservedUserInfo(principal)).thenThrow(e); + + ElasticsearchSecurityException securityException = + expectThrows(ElasticsearchSecurityException.class, () -> reservedRealm.lookupUser(principal)); + assertThat(securityException.getMessage(), containsString("failed to lookup")); + + verify(usersStore).started(); + verify(usersStore).securityIndexExists(); + verify(usersStore).getReservedUserInfo(principal); + verifyNoMoreInteractions(usersStore); + } + + public void testIsReserved() { + final User expectedUser = randomFrom(new ElasticUser(true), new KibanaUser(true)); + final String principal = expectedUser.principal(); + assertThat(ReservedRealm.isReserved(principal, Settings.EMPTY), is(true)); final String notExpected = randomFrom("foobar", "", randomAsciiOfLengthBetween(1, 30)); - assertThat(ReservedRealm.isReserved(notExpected), is(false)); - assertThat(ReservedRealm.getUser(notExpected), nullValue()); + assertThat(ReservedRealm.isReserved(notExpected, Settings.EMPTY), is(false)); + } - assertThat(ReservedRealm.users(), containsInAnyOrder((User) ElasticUser.INSTANCE, KibanaUser.INSTANCE)); + public void testGetUsers() { + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); + assertThat(reservedRealm.users(), containsInAnyOrder(new ElasticUser(true), new KibanaUser(true))); } public void testFailedAuthentication() { - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); // maybe cache a successful auth if (randomBoolean()) { User user = reservedRealm.authenticate( new UsernamePasswordToken(ElasticUser.NAME, new SecuredString("changeme".toCharArray()))); - assertThat(user, sameInstance(ElasticUser.INSTANCE)); + assertEquals(new ElasticUser(true), user); } try { diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java index fbd7ed13548..8885588405f 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.authc.file; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.LogEvent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; @@ -154,7 +153,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFile() throws Exception { Path path = getDataPath("users"); - Map users = FileUserPasswdStore.parseFile(path, null); + Map users = FileUserPasswdStore.parseFile(path, null, Settings.EMPTY); assertThat(users, notNullValue()); assertThat(users.size(), is(6)); assertThat(users.get("bcrypt"), notNullValue()); @@ -174,7 +173,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFile_Empty() throws Exception { Path empty = createTempFile(); Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG); - Map users = FileUserPasswdStore.parseFile(empty, logger); + Map users = FileUserPasswdStore.parseFile(empty, logger, Settings.EMPTY); assertThat(users.isEmpty(), is(true)); List events = CapturingLogger.output(logger.getName(), Level.DEBUG); assertThat(events.size(), is(1)); @@ -184,7 +183,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFile_WhenFileDoesNotExist() throws Exception { Path file = createTempDir().resolve(randomAsciiOfLength(10)); Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - Map users = FileUserPasswdStore.parseFile(file, logger); + Map users = FileUserPasswdStore.parseFile(file, logger, Settings.EMPTY); assertThat(users, notNullValue()); assertThat(users.isEmpty(), is(true)); } @@ -195,7 +194,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); try { - FileUserPasswdStore.parseFile(file, logger); + FileUserPasswdStore.parseFile(file, logger, Settings.EMPTY); fail("expected a parse failure"); } catch (IllegalStateException se) { this.logger.info("expected", se); @@ -205,7 +204,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFile_InvalidLineDoesNotResultInLoggerNPE() throws Exception { Path file = createTempFile(); Files.write(file, Arrays.asList("NotValidUsername=Password", "user:pass"), StandardCharsets.UTF_8); - Map users = FileUserPasswdStore.parseFile(file, null); + Map users = FileUserPasswdStore.parseFile(file, null, Settings.EMPTY); assertThat(users, notNullValue()); assertThat(users.keySet(), hasSize(1)); } @@ -215,7 +214,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - Map users = FileUserPasswdStore.parseFileLenient(file, logger); + Map users = FileUserPasswdStore.parseFileLenient(file, logger, Settings.EMPTY); assertThat(users, notNullValue()); assertThat(users.isEmpty(), is(true)); List events = CapturingLogger.output(logger.getName(), Level.ERROR); @@ -226,7 +225,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFileWithLineWithEmptyPasswordAndWhitespace() throws Exception { Path file = createTempFile(); Files.write(file, Collections.singletonList("user: "), StandardCharsets.UTF_8); - Map users = FileUserPasswdStore.parseFile(file, null); + Map users = FileUserPasswdStore.parseFile(file, null, Settings.EMPTY); assertThat(users, notNullValue()); assertThat(users.keySet(), is(empty())); } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/tool/UsersToolTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/tool/UsersToolTests.java index 1ba3dd7c220..7859030831d 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/tool/UsersToolTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authc/file/tool/UsersToolTests.java @@ -175,7 +175,7 @@ public class UsersToolTests extends CommandTestCase { public void testParseInvalidUsername() throws Exception { UserException e = expectThrows(UserException.class, () -> { - UsersTool.parseUsername(Collections.singletonList("$34dkl")); + UsersTool.parseUsername(Collections.singletonList("$34dkl"), Settings.EMPTY); }); assertEquals(ExitCodes.DATA_ERROR, e.exitCode); assertTrue(e.getMessage(), e.getMessage().contains("Invalid username")); @@ -183,7 +183,7 @@ public class UsersToolTests extends CommandTestCase { public void testParseUsernameMissing() throws Exception { UserException e = expectThrows(UserException.class, () -> { - UsersTool.parseUsername(Collections.emptyList()); + UsersTool.parseUsername(Collections.emptyList(), Settings.EMPTY); }); assertEquals(ExitCodes.USAGE, e.exitCode); assertTrue(e.getMessage(), e.getMessage().contains("Missing username argument")); @@ -191,7 +191,7 @@ public class UsersToolTests extends CommandTestCase { public void testParseUsernameExtraArgs() throws Exception { UserException e = expectThrows(UserException.class, () -> { - UsersTool.parseUsername(Arrays.asList("username", "extra")); + UsersTool.parseUsername(Arrays.asList("username", "extra"), Settings.EMPTY); }); assertEquals(ExitCodes.USAGE, e.exitCode); assertTrue(e.getMessage(), e.getMessage().contains("Expected a single username argument")); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index a4c4d57b842..1f0cc5d145c 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -73,7 +73,6 @@ import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.security.user.SystemUser; import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.xpack.security.user.XPackUser; -import org.junit.After; import org.junit.Before; import java.util.ArrayList; @@ -109,12 +108,7 @@ public class AuthorizationServiceTests extends ESTestCase { when(threadPool.getThreadContext()).thenReturn(threadContext); authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, - auditTrail, new DefaultAuthenticationFailureHandler(), threadPool); - } - - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); + auditTrail, new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(Settings.EMPTY)); } public void testActionsSystemUserIsAuthorized() { @@ -352,21 +346,22 @@ public class AuthorizationServiceTests extends ESTestCase { public void testDenialForAnonymousUser() { TransportRequest request = new IndicesExistsRequest("b"); ClusterState state = mock(ClusterState.class); - AnonymousUser.initialize(Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "a_all").build()); - authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(), threadPool); + Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "a_all").build(); + final AnonymousUser anonymousUser = new AnonymousUser(settings); + authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, + new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); when(rolesStore.role("a_all")).thenReturn(Role.builder("a_all").add(IndexPrivilege.ALL, "a").build()); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.EMPTY_META_DATA); try { - authorizationService.authorize(createAuthentication(AnonymousUser.INSTANCE), "indices:a", request); + authorizationService.authorize(createAuthentication(anonymousUser), "indices:a", request); fail("indices request for b should be denied since there is no such index"); } catch (ElasticsearchSecurityException e) { assertAuthorizationException(e, - containsString("action [indices:a] is unauthorized for user [" + AnonymousUser.INSTANCE.principal() + "]")); - verify(auditTrail).accessDenied(AnonymousUser.INSTANCE, "indices:a", request); + containsString("action [indices:a] is unauthorized for user [" + anonymousUser.principal() + "]")); + verify(auditTrail).accessDenied(anonymousUser, "indices:a", request); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(2)).state(); verify(state, times(3)).metaData(); @@ -376,14 +371,13 @@ public class AuthorizationServiceTests extends ESTestCase { public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { TransportRequest request = new IndicesExistsRequest("b"); ClusterState state = mock(ClusterState.class); - AnonymousUser.initialize(Settings.builder() + Settings settings = Settings.builder() .put(AnonymousUser.ROLES_SETTING.getKey(), "a_all") .put(AuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false) - .build()); - User anonymousUser = AnonymousUser.INSTANCE; - authorizationService = new AuthorizationService( - Settings.builder().put(AuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false).build(), - rolesStore, clusterService, auditTrail, new DefaultAuthenticationFailureHandler(), threadPool); + .build(); + final AnonymousUser anonymousUser = new AnonymousUser(settings); + authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(settings)); when(rolesStore.role("a_all")).thenReturn(Role.builder("a_all").add(IndexPrivilege.ALL, "a").build()); when(clusterService.state()).thenReturn(state); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java index edfce32992a..29c355e2a02 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java @@ -440,7 +440,7 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase { public void testTemplating() throws Exception { User user = new User("_username", new String[]{"role1", "role2"}, "_full_name", "_email", - Collections.singletonMap("key", "value")); + Collections.singletonMap("key", "value"), true); securityIndexSearcherWrapper = new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService) { diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SetSecurityUserProcessorTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SetSecurityUserProcessorTests.java index f56c4fbe35f..eb821597b0d 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SetSecurityUserProcessorTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/SetSecurityUserProcessorTests.java @@ -25,7 +25,7 @@ public class SetSecurityUserProcessorTests extends ESTestCase { public void testProcessor() throws Exception { User user = new User("_username", new String[]{"role1", "role2"}, "firstname lastname", "_email", - Collections.singletonMap("key", "value")); + Collections.singletonMap("key", "value"), true); Authentication.RealmRef realmRef = new Authentication.RealmRef("_name", "_type", "_node_name"); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, new Authentication(user, realmRef, null)); @@ -100,7 +100,7 @@ public class SetSecurityUserProcessorTests extends ESTestCase { public void testFullNameProperties() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - User user = new User(null, null, "_full_name", null, null); + User user = new User(null, null, "_full_name", null, null, true); Authentication.RealmRef realmRef = new Authentication.RealmRef("_name", "_type", "_node_name"); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, new Authentication(user, realmRef, null)); @@ -116,7 +116,7 @@ public class SetSecurityUserProcessorTests extends ESTestCase { public void testEmailProperties() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - User user = new User(null, null, null, "_email", null); + User user = new User(null, null, null, "_email", null, true); Authentication.RealmRef realmRef = new Authentication.RealmRef("_name", "_type", "_node_name"); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, new Authentication(user, realmRef, null)); @@ -132,7 +132,7 @@ public class SetSecurityUserProcessorTests extends ESTestCase { public void testMetadataProperties() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - User user = new User(null, null, null, null, Collections.singletonMap("key", "value")); + User user = new User(null, null, null, null, Collections.singletonMap("key", "value"), true); Authentication.RealmRef realmRef = new Authentication.RealmRef("_name", "_type", "_node_name"); threadContext.putTransient(Authentication.AUTHENTICATION_KEY, new Authentication(user, realmRef, null)); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/indicesresolver/DefaultIndicesResolverTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/indicesresolver/DefaultIndicesResolverTests.java index ae66de55286..87ce3146a0d 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/indicesresolver/DefaultIndicesResolverTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/indicesresolver/DefaultIndicesResolverTests.java @@ -32,6 +32,10 @@ import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.security.SecurityTemplateService; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.user.AnonymousUser; +import org.elasticsearch.xpack.security.user.User; +import org.elasticsearch.xpack.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.xpack.security.authz.AuthorizationService; @@ -39,9 +43,6 @@ import org.elasticsearch.xpack.security.authz.permission.Role; import org.elasticsearch.xpack.security.authz.permission.SuperuserRole; import org.elasticsearch.xpack.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; -import org.elasticsearch.xpack.security.user.User; -import org.elasticsearch.xpack.security.user.XPackUser; import org.junit.Before; import java.util.Set; @@ -102,7 +103,8 @@ public class DefaultIndicesResolverTests extends ESTestCase { when(state.metaData()).thenReturn(metaData); AuthorizationService authzService = new AuthorizationService(settings, rolesStore, clusterService, - mock(AuditTrailService.class), new DefaultAuthenticationFailureHandler(), mock(ThreadPool.class)); + mock(AuditTrailService.class), new DefaultAuthenticationFailureHandler(), mock(ThreadPool.class), + new AnonymousUser(settings)); defaultIndicesResolver = new DefaultIndicesAndAliasesResolver(authzService, indexNameExpressionResolver); } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStoreTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStoreTests.java index bffce016af9..0036f141872 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStoreTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/store/ReservedRolesStoreTests.java @@ -45,7 +45,7 @@ public class ReservedRolesStoreTests extends ESTestCase { public void testRetrievingReservedRolesNonKibanaUser() { if (randomBoolean()) { - when(securityContext.getUser()).thenReturn(ElasticUser.INSTANCE); + when(securityContext.getUser()).thenReturn(new ElasticUser(true)); } assertThat(reservedRolesStore.role(SuperuserRole.NAME), sameInstance(SuperuserRole.INSTANCE)); @@ -77,7 +77,7 @@ public class ReservedRolesStoreTests extends ESTestCase { } public void testRetrievingReservedRoleKibanaUser() { - when(securityContext.getUser()).thenReturn(KibanaUser.INSTANCE); + when(securityContext.getUser()).thenReturn(new KibanaUser(true)); assertThat(reservedRolesStore.role(SuperuserRole.NAME), sameInstance(SuperuserRole.INSTANCE)); assertThat(reservedRolesStore.roleDescriptor(SuperuserRole.NAME), sameInstance(SuperuserRole.DESCRIPTOR)); diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/support/ValidationTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/support/ValidationTests.java index ba0365e2ad2..05a1128b7c6 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/support/ValidationTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/support/ValidationTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.support; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.security.support.Validation.Error; import org.elasticsearch.xpack.security.support.Validation.Users; @@ -55,12 +56,12 @@ public class ValidationTests extends ESTestCase { public void testUsersValidateUsername() throws Exception { int length = randomIntBetween(1, 30); String name = new String(generateValidName(length)); - assertThat(Users.validateUsername(name), nullValue()); + assertThat(Users.validateUsername(name, false, Settings.EMPTY), nullValue()); } public void testReservedUsernames() { final String username = randomFrom(ElasticUser.NAME, KibanaUser.NAME); - final Error error = Users.validateUsername(username); + final Error error = Users.validateUsername(username, false, Settings.EMPTY); assertNotNull(error); assertThat(error.toString(), containsString("is reserved")); } @@ -71,13 +72,13 @@ public class ValidationTests extends ESTestCase { if (length > 0) { name = generateValidName(length); } - assertThat(Users.validateUsername(new String(name)), notNullValue()); + assertThat(Users.validateUsername(new String(name), false, Settings.EMPTY), notNullValue()); } public void testUsersValidateUsernameInvalidCharacters() throws Exception { int length = randomIntBetween(1, 30); // valid length String name = new String(generateInvalidName(length)); - assertThat(Users.validateUsername(name), notNullValue()); + assertThat(Users.validateUsername(name, false, Settings.EMPTY), notNullValue()); } public void testUsersValidatePassword() throws Exception { @@ -112,13 +113,13 @@ public class ValidationTests extends ESTestCase { if (length > 0) { name = generateValidName(length); } - assertThat(Users.validateUsername(new String(name)), notNullValue()); + assertThat(Users.validateUsername(new String(name), false, Settings.EMPTY), notNullValue()); } public void testRolesValidateRoleNameInvalidCharacters() throws Exception { int length = randomIntBetween(1, 30); // valid length String name = new String(generateInvalidName(length)); - assertThat(Users.validateUsername(name), notNullValue()); + assertThat(Users.validateUsername(name, false, Settings.EMPTY), notNullValue()); } private static char[] generateValidName(int length) { diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java index f4b85ad83da..f2dd7298d2d 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.security.user; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; -import org.junit.After; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.equalTo; @@ -16,26 +15,19 @@ import static org.hamcrest.Matchers.is; public class AnonymousUserTests extends ESTestCase { - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testResolveAnonymousUser() throws Exception { Settings settings = Settings.builder() .put(AnonymousUser.USERNAME_SETTING.getKey(), "anonym1") .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3") .build(); - AnonymousUser.initialize(settings); - User user = AnonymousUser.INSTANCE; + AnonymousUser user = new AnonymousUser(settings); assertThat(user.principal(), equalTo("anonym1")); assertThat(user.roles(), arrayContainingInAnyOrder("r1", "r2", "r3")); settings = Settings.builder() .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3") .build(); - AnonymousUser.initialize(settings); - user = AnonymousUser.INSTANCE; + user = new AnonymousUser(settings); assertThat(user.principal(), equalTo(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME)); assertThat(user.roles(), arrayContainingInAnyOrder("r1", "r2", "r3")); } @@ -44,8 +36,7 @@ public class AnonymousUserTests extends ESTestCase { Settings settings = randomBoolean() ? Settings.EMPTY : Settings.builder().put(AnonymousUser.USERNAME_SETTING.getKey(), "user1").build(); - AnonymousUser.initialize(settings); - assertThat(AnonymousUser.enabled(), is(false)); + assertThat(AnonymousUser.isAnonymousEnabled(settings), is(false)); } public void testAnonymous() throws Exception { @@ -54,24 +45,21 @@ public class AnonymousUserTests extends ESTestCase { settings = Settings.builder().put(settings).put(AnonymousUser.USERNAME_SETTING.getKey(), "anon").build(); } - AnonymousUser.initialize(settings); - User user = AnonymousUser.INSTANCE; - assertThat(AnonymousUser.is(user), is(true)); - assertThat(AnonymousUser.isAnonymousUsername(user.principal()), is(true)); + AnonymousUser user = new AnonymousUser(settings); + assertEquals(user, new AnonymousUser(settings)); + assertThat(AnonymousUser.isAnonymousUsername(user.principal(), settings), is(true)); // make sure check works with serialization BytesStreamOutput output = new BytesStreamOutput(); User.writeTo(user, output); User anonymousSerialized = User.readFrom(output.bytes().streamInput()); - assertThat(AnonymousUser.is(anonymousSerialized), is(true)); + assertEquals(user, anonymousSerialized); - // test with null anonymous - AnonymousUser.initialize(Settings.EMPTY); - assertThat(AnonymousUser.is(null), is(false)); + // test with anonymous disabled if (user.principal().equals(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME)) { - assertThat(AnonymousUser.isAnonymousUsername(user.principal()), is(true)); + assertThat(AnonymousUser.isAnonymousUsername(user.principal(), Settings.EMPTY), is(true)); } else { - assertThat(AnonymousUser.isAnonymousUsername(user.principal()), is(false)); + assertThat(AnonymousUser.isAnonymousUsername(user.principal(), Settings.EMPTY), is(false)); } } } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java index 08f42022328..dbfbef84490 100644 --- a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/user/UserTests.java @@ -6,14 +6,11 @@ package org.elasticsearch.xpack.security.user; import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.xpack.security.support.MetadataUtils; import org.elasticsearch.test.ESTestCase; import java.util.Arrays; import java.util.Collections; -import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -103,7 +100,7 @@ public class UserTests extends ESTestCase { 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")); + 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," + @@ -112,27 +109,17 @@ public class UserTests extends ESTestCase { public void testReservedUserSerialization() throws Exception { BytesStreamOutput output = new BytesStreamOutput(); - User.writeTo(ElasticUser.INSTANCE, output); + final ElasticUser elasticUser = new ElasticUser(true); + User.writeTo(elasticUser, output); User readFrom = User.readFrom(output.bytes().streamInput()); - assertThat(readFrom, is(sameInstance(ElasticUser.INSTANCE))); + assertEquals(elasticUser, readFrom); + final KibanaUser kibanaUser = new KibanaUser(true); output = new BytesStreamOutput(); - User.writeTo(KibanaUser.INSTANCE, output); + User.writeTo(kibanaUser, output); readFrom = User.readFrom(output.bytes().streamInput()); - assertThat(readFrom, is(sameInstance(KibanaUser.INSTANCE))); - } - - public void testReservedMetadata() throws Exception { - Map validMetadata = Collections.singletonMap("foo", "bar"); - Map invalidMetadata = Collections.singletonMap(MetadataUtils.RESERVED_PREFIX + "foo", "bar"); - - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> - new User("john", Strings.EMPTY_ARRAY, "John Doe", "john@doe.com", invalidMetadata)); - assertThat(exception.getMessage(), containsString("reserved")); - - User user = new User("john", Strings.EMPTY_ARRAY, "John Doe", "john@doe.com", validMetadata); - assertNotNull(user); + assertEquals(kibanaUser, readFrom); } } diff --git a/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/actions b/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/actions index f680b281f94..73ffb3b4dfa 100644 --- a/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/actions +++ b/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/actions @@ -87,6 +87,7 @@ cluster:admin/xpack/security/user/change_password cluster:admin/xpack/security/user/put cluster:admin/xpack/security/user/delete cluster:admin/xpack/security/user/get +cluster:admin/xpack/security/user/set_enabled cluster:admin/xpack/security/role/put cluster:admin/xpack/security/role/delete cluster:admin/xpack/security/role/get diff --git a/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/handlers b/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/handlers index 0de48418d6b..bb3374ee88c 100644 --- a/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/handlers +++ b/elasticsearch/x-pack/security/src/test/resources/org/elasticsearch/transport/handlers @@ -21,6 +21,7 @@ cluster:admin/xpack/security/user/change_password cluster:admin/xpack/security/user/put cluster:admin/xpack/security/user/delete cluster:admin/xpack/security/user/get +cluster:admin/xpack/security/user/set_enabled indices:admin/analyze[s] indices:admin/cache/clear[n] indices:admin/forcemerge[n] diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.change_password.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.change_password.json index 9b8b4100663..b193284c1e3 100644 --- a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.change_password.json +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.change_password.json @@ -14,8 +14,9 @@ }, "params": { "refresh": { - "type" : "boolean", - "description" : "Refresh the index after performing the operation" + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." } } }, diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json index 3a04be73dc2..365d3ba4a5c 100644 --- a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json @@ -14,8 +14,9 @@ }, "params": { "refresh": { - "type" : "boolean", - "description" : "Refresh the index after performing the operation" + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." } } }, diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json index 70d3cad0759..4e6c1cc5370 100644 --- a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json @@ -14,8 +14,9 @@ }, "params": { "refresh": { - "type" : "boolean", - "description" : "Refresh the index after performing the operation" + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." } } }, diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json new file mode 100644 index 00000000000..75c1d26cd8a --- /dev/null +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json @@ -0,0 +1,25 @@ +{ + "xpack.security.disable_user": { + "documentation": "https://www.elastic.co/guide/en/x-pack/master/security-api-disable-user.html", + "methods": [ "PUT", "POST" ], + "url": { + "path": "/_xpack/security/user/{username}/_disable", + "paths": [ "/_xpack/security/user/{username}/_disable" ], + "parts": { + "username": { + "type" : "string", + "description" : "The username of the user to disable", + "required" : false + } + }, + "params": { + "refresh": { + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." + } + } + }, + "body": null + } +} diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json new file mode 100644 index 00000000000..eaf40c09275 --- /dev/null +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json @@ -0,0 +1,25 @@ +{ + "xpack.security.enable_user": { + "documentation": "https://www.elastic.co/guide/en/x-pack/master/security-api-enable-user.html", + "methods": [ "PUT", "POST" ], + "url": { + "path": "/_xpack/security/user/{username}/_enable", + "paths": [ "/_xpack/security/user/{username}/_enable" ], + "parts": { + "username": { + "type" : "string", + "description" : "The username of the user to enable", + "required" : false + } + }, + "params": { + "refresh": { + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." + } + } + }, + "body": null + } +} diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_role.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_role.json index 93af66619a4..c2d51dc016a 100644 --- a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_role.json +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_role.json @@ -14,8 +14,9 @@ }, "params": { "refresh": { - "type" : "boolean", - "description" : "Refresh the index after performing the operation" + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." } } }, diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_user.json b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_user.json index c6aa13727f2..a589dd1e61d 100644 --- a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_user.json +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/api/xpack.security.put_user.json @@ -14,8 +14,9 @@ }, "params": { "refresh": { - "type" : "boolean", - "description" : "Refresh the index after performing the operation" + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." } } }, diff --git a/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/test/users/30_enable_disable.yaml b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/test/users/30_enable_disable.yaml new file mode 100644 index 00000000000..1d906f5d003 --- /dev/null +++ b/elasticsearch/x-pack/security/src/test/resources/rest-api-spec/test/users/30_enable_disable.yaml @@ -0,0 +1,124 @@ +--- +setup: + - skip: + features: [headers, catch_unauthorized] + - do: + cluster.health: + wait_for_status: yellow + + - do: + xpack.security.put_user: + username: "joe" + body: > + { + "password": "s3krit", + "roles" : [ "superuser" ] + } + +--- +teardown: + - do: + xpack.security.delete_user: + username: "joe" + ignore: 404 + +--- +"Test disable then enable user": +# check that the user works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# disable the user + - do: + xpack.security.disable_user: + username: "joe" + +# validate user cannot login + - do: + catch: unauthorized + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + +# enable the user + - do: + xpack.security.enable_user: + username: "joe" + +# validate that the user can login again + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +--- +"Test enabling already enabled user": +# check that the user works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# enable the user + - do: + xpack.security.enable_user: + username: "joe" + +# validate that the user still works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +--- +"Test disabling already disabled user": +# check that the user works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# disable the user + - do: + xpack.security.disable_user: + username: "joe" + +# validate user cannot login + - do: + catch: unauthorized + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + +# disable again + - do: + xpack.security.disable_user: + username: "joe" + + - do: + xpack.security.enable_user: + username: "joe" + +--- +"Test disabling yourself": +# check that the user works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# try to disable yourself + - do: + catch: '/users may not update the enabled status of their own account/' + headers: + Authorization: "Basic am9lOnMza3JpdA==" + xpack.security.disable_user: + username: "joe" diff --git a/elasticsearch/x-pack/src/test/java/org/elasticsearch/xpack/action/TransportXPackInfoActionTests.java b/elasticsearch/x-pack/src/test/java/org/elasticsearch/xpack/action/TransportXPackInfoActionTests.java index 7efd1695ee7..40d01a4d0c6 100644 --- a/elasticsearch/x-pack/src/test/java/org/elasticsearch/xpack/action/TransportXPackInfoActionTests.java +++ b/elasticsearch/x-pack/src/test/java/org/elasticsearch/xpack/action/TransportXPackInfoActionTests.java @@ -12,14 +12,11 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackInfoResponse; import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseService; -import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.XPackFeatureSet; import org.elasticsearch.license.XPackInfoResponse.FeatureSetsInfo.FeatureSet; -import org.junit.After; -import org.junit.Before; import java.util.EnumSet; import java.util.HashSet; @@ -40,22 +37,6 @@ import static org.mockito.Mockito.when; public class TransportXPackInfoActionTests extends ESTestCase { - private boolean anonymousEnabled; - - @Before - public void maybeEnableAnonymous() { - anonymousEnabled = randomBoolean(); - if (anonymousEnabled) { - Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "superuser").build(); - AnonymousUser.initialize(settings); - } - } - - @After - public void resetAnonymous() { - AnonymousUser.initialize(Settings.EMPTY); - } - public void testDoExecute() throws Exception { LicenseService licenseService = mock(LicenseService.class);