diff --git a/elasticsearch/qa/shield-audit-tests/build.gradle b/elasticsearch/qa/shield-audit-tests/build.gradle index 228b9ecdce2..35a0e3d2531 100644 --- a/elasticsearch/qa/shield-audit-tests/build.gradle +++ b/elasticsearch/qa/shield-audit-tests/build.gradle @@ -10,7 +10,7 @@ integTest { setting 'xpack.security.audit.enabled', 'true' setting 'xpack.security.audit.outputs', 'index' setupCommand 'setupDummyUser', - 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' waitCondition = { node, ant -> File tmpFile = new File(node.cwd, 'wait.success') ant.get(src: "http://${node.httpUri()}", diff --git a/elasticsearch/qa/shield-client-tests/build.gradle b/elasticsearch/qa/shield-client-tests/build.gradle index 1e4099fee20..eabdbad56e8 100644 --- a/elasticsearch/qa/shield-client-tests/build.gradle +++ b/elasticsearch/qa/shield-client-tests/build.gradle @@ -8,7 +8,7 @@ integTest { cluster { plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack') setupCommand 'setupDummyUser', - 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' setupCommand 'setupTransportClientUser', 'bin/x-pack/users', 'useradd', 'transport', '-p', 'changeme', '-r', 'transport_client' waitCondition = { node, ant -> diff --git a/elasticsearch/qa/shield-core-rest-tests/build.gradle b/elasticsearch/qa/shield-core-rest-tests/build.gradle index ad48d2e98dd..3b0a5bc69c3 100644 --- a/elasticsearch/qa/shield-core-rest-tests/build.gradle +++ b/elasticsearch/qa/shield-core-rest-tests/build.gradle @@ -37,7 +37,7 @@ integTest { setting 'xpack.watcher.enabled', 'false' setting 'xpack.monitoring.enabled', 'false' setupCommand 'setupDummyUser', - 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' waitCondition = { node, ant -> File tmpFile = new File(node.cwd, 'wait.success') ant.get(src: "http://${node.httpUri()}", diff --git a/elasticsearch/qa/shield-example-realm/build.gradle b/elasticsearch/qa/shield-example-realm/build.gradle index 3d46493c8ed..2518371bbb6 100644 --- a/elasticsearch/qa/shield-example-realm/build.gradle +++ b/elasticsearch/qa/shield-example-realm/build.gradle @@ -41,7 +41,7 @@ task integTest(type: org.elasticsearch.gradle.test.RestIntegTestTask, dependsOn: setting 'xpack.security.authc.realms.esusers.type', 'file' setupCommand 'setupDummyUser', - 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' setupCommand 'installExtension', 'bin/x-pack/extension', 'install', 'file:' + buildZip.archivePath waitCondition = { node, ant -> diff --git a/elasticsearch/qa/shield-example-realm/src/main/java/org/elasticsearch/example/realm/CustomRealm.java b/elasticsearch/qa/shield-example-realm/src/main/java/org/elasticsearch/example/realm/CustomRealm.java index 0bd6898c772..edfa85cec1a 100644 --- a/elasticsearch/qa/shield-example-realm/src/main/java/org/elasticsearch/example/realm/CustomRealm.java +++ b/elasticsearch/qa/shield-example-realm/src/main/java/org/elasticsearch/example/realm/CustomRealm.java @@ -6,7 +6,7 @@ package org.elasticsearch.example.realm; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authc.Realm; import org.elasticsearch.shield.authc.RealmConfig; @@ -22,7 +22,7 @@ public class CustomRealm extends Realm { static final String KNOWN_USER = "custom_user"; static final String KNOWN_PW = "changeme"; - static final String[] ROLES = new String[] { "admin" }; + static final String[] ROLES = new String[] { "superuser" }; public CustomRealm(RealmConfig config) { super(TYPE, config); diff --git a/elasticsearch/qa/shield-example-realm/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java b/elasticsearch/qa/shield-example-realm/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java index 8017096fc60..53df916d0c9 100644 --- a/elasticsearch/qa/shield-example-realm/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java +++ b/elasticsearch/qa/shield-example-realm/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java @@ -6,7 +6,7 @@ package org.elasticsearch.example.realm; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.UsernamePasswordToken; diff --git a/elasticsearch/qa/shield-reindex-tests/build.gradle b/elasticsearch/qa/shield-reindex-tests/build.gradle index ef409b366df..be28f4d6e46 100644 --- a/elasticsearch/qa/shield-reindex-tests/build.gradle +++ b/elasticsearch/qa/shield-reindex-tests/build.gradle @@ -10,8 +10,8 @@ integTest { plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack') extraConfigFile 'x-pack/roles.yml', 'roles.yml' [ - test_admin: 'admin', - powerful_user: 'admin', + test_admin: 'superuser', + powerful_user: 'superuser', minimal_user: 'minimal', readonly_user: 'readonly', dest_only_user: 'dest_only', diff --git a/elasticsearch/qa/smoke-test-graph-with-shield/build.gradle b/elasticsearch/qa/smoke-test-graph-with-shield/build.gradle index 166cb2c821b..820f387a853 100644 --- a/elasticsearch/qa/smoke-test-graph-with-shield/build.gradle +++ b/elasticsearch/qa/smoke-test-graph-with-shield/build.gradle @@ -18,7 +18,7 @@ integTest { plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack') extraConfigFile 'x-pack/roles.yml', 'roles.yml' setupCommand 'setupTestAdminUser', - 'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'superuser' setupCommand 'setupGraphExplorerUser', 'bin/x-pack/users', 'useradd', 'graph_explorer', '-p', 'changeme', '-r', 'graph_explorer' setupCommand 'setupPowerlessUser', diff --git a/elasticsearch/qa/smoke-test-monitoring-with-shield/build.gradle b/elasticsearch/qa/smoke-test-monitoring-with-shield/build.gradle index ea8536bd10f..1eb840cb977 100644 --- a/elasticsearch/qa/smoke-test-monitoring-with-shield/build.gradle +++ b/elasticsearch/qa/smoke-test-monitoring-with-shield/build.gradle @@ -19,7 +19,7 @@ integTest { setting 'xpack.monitoring.agent.interval', '3s' extraConfigFile 'x-pack/roles.yml', 'roles.yml' setupCommand 'setupTestAdminUser', - 'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'superuser' setupCommand 'setupMonitoredSystemUser', 'bin/x-pack/users', 'useradd', 'monitored_system', '-p', 'changeme', '-r', 'monitored_system,required_for_test' setupCommand 'setupPowerlessUser', diff --git a/elasticsearch/qa/smoke-test-plugins-ssl/build.gradle b/elasticsearch/qa/smoke-test-plugins-ssl/build.gradle index ede834b2ed7..6b2a49fc326 100644 --- a/elasticsearch/qa/smoke-test-plugins-ssl/build.gradle +++ b/elasticsearch/qa/smoke-test-plugins-ssl/build.gradle @@ -165,7 +165,7 @@ integTest { extraConfigFile clientKeyStore.name, clientKeyStore setupCommand 'setupTestUser', - 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' setupCommand 'setupMarvelUser', 'bin/x-pack/users', 'useradd', 'monitoring_agent', '-p', 'changeme', '-r', 'remote_monitoring_agent' diff --git a/elasticsearch/qa/smoke-test-plugins/build.gradle b/elasticsearch/qa/smoke-test-plugins/build.gradle index 02f763f90ec..4cb7890e84b 100644 --- a/elasticsearch/qa/smoke-test-plugins/build.gradle +++ b/elasticsearch/qa/smoke-test-plugins/build.gradle @@ -18,7 +18,7 @@ integTest { plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack') setupCommand 'setupDummyUser', - 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' waitCondition = { node, ant -> File tmpFile = new File(node.cwd, 'wait.success') ant.get(src: "http://${node.httpUri()}", diff --git a/elasticsearch/qa/smoke-test-watcher-with-shield/build.gradle b/elasticsearch/qa/smoke-test-watcher-with-shield/build.gradle index 789db6ea5c3..235cee460df 100644 --- a/elasticsearch/qa/smoke-test-watcher-with-shield/build.gradle +++ b/elasticsearch/qa/smoke-test-watcher-with-shield/build.gradle @@ -22,7 +22,7 @@ integTest { plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack') extraConfigFile 'x-pack/roles.yml', 'roles.yml' setupCommand 'setupTestAdminUser', - 'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'admin' + 'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'superuser' setupCommand 'setupWatcherManagerUser', 'bin/x-pack/users', 'useradd', 'watcher_manager', '-p', 'changeme', '-r', 'watcher_manager' setupCommand 'setupPowerlessUser', diff --git a/elasticsearch/x-pack/build.gradle b/elasticsearch/x-pack/build.gradle index f731993f4d4..67a433ef8a1 100644 --- a/elasticsearch/x-pack/build.gradle +++ b/elasticsearch/x-pack/build.gradle @@ -131,7 +131,7 @@ integTest { systemProperty 'tests.rest.blacklist', 'getting_started/10_monitor_cluster_health/*' cluster { setting 'xpack.monitoring.agent.interval', '3s' - setupCommand 'setupDummyUser', 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + setupCommand 'setupDummyUser', 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' waitCondition = { NodeInfo node, AntBuilder ant -> File tmpFile = new File(node.cwd, 'wait.success') ant.get(src: "http://${node.httpUri()}", @@ -159,7 +159,7 @@ artifacts { } run { - setupCommand 'setupDummyUser', 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin' + setupCommand 'setupDummyUser', 'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser' } // classes are missing, e.g. com.ibm.icu.lang.UCharacter diff --git a/elasticsearch/x-pack/license-plugin/src/test/java/org/elasticsearch/shield/authz/AuthorizationUtilsTests.java b/elasticsearch/x-pack/license-plugin/src/test/java/org/elasticsearch/shield/authz/AuthorizationUtilsTests.java index 648cb0d2d7e..add099da47a 100644 --- a/elasticsearch/x-pack/license-plugin/src/test/java/org/elasticsearch/shield/authz/AuthorizationUtilsTests.java +++ b/elasticsearch/x-pack/license-plugin/src/test/java/org/elasticsearch/shield/authz/AuthorizationUtilsTests.java @@ -7,8 +7,8 @@ package org.elasticsearch.shield.authz; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.InternalAuthenticationService; import org.elasticsearch.test.ESTestCase; import org.junit.Before; diff --git a/elasticsearch/x-pack/marvel/src/test/java/org/elasticsearch/marvel/shield/MarvelInternalClientTests.java b/elasticsearch/x-pack/marvel/src/test/java/org/elasticsearch/marvel/shield/MarvelInternalClientTests.java index bdfdcf17abe..375a8241ccf 100644 --- a/elasticsearch/x-pack/marvel/src/test/java/org/elasticsearch/marvel/shield/MarvelInternalClientTests.java +++ b/elasticsearch/x-pack/marvel/src/test/java/org/elasticsearch/marvel/shield/MarvelInternalClientTests.java @@ -53,18 +53,13 @@ public class MarvelInternalClientTests extends MarvelIntegTestCase { assertAccessIsAllowed(internalClient.admin().indices().prepareGetTemplates("foo")); } - public void testDeniedAccess() { + public void testAllowAllAccess() { InternalClient internalClient = internalCluster().getInstance(InternalClient.class); assertAcked(internalClient.admin().indices().preparePutTemplate("foo") .setSource(MarvelTemplateUtils.loadDataIndexTemplate()).get()); - if (shieldEnabled) { - assertAccessIsDenied(internalClient.admin().indices().prepareDeleteTemplate("foo")); - assertAccessIsDenied(internalClient.admin().cluster().prepareGetRepositories()); - } else { - assertAccessIsAllowed(internalClient.admin().indices().prepareDeleteTemplate("foo")); - assertAccessIsAllowed(internalClient.admin().cluster().prepareGetRepositories()); - } + assertAccessIsAllowed(internalClient.admin().indices().prepareDeleteTemplate("foo")); + assertAccessIsAllowed(internalClient.admin().cluster().prepareGetRepositories()); } public void assertAccessIsAllowed(ActionRequestBuilder request) { diff --git a/elasticsearch/x-pack/shield/config/x-pack/roles.yml b/elasticsearch/x-pack/shield/config/x-pack/roles.yml index f998b18b427..8a15b4afcd7 100644 --- a/elasticsearch/x-pack/shield/config/x-pack/roles.yml +++ b/elasticsearch/x-pack/shield/config/x-pack/roles.yml @@ -1,43 +1,3 @@ -admin: - cluster: - - all - indices: - - names: '*' - privileges: - - all - -# monitoring cluster privileges -# All operations on all indices -power_user: - cluster: - - monitor - indices: - - names: '*' - privileges: - - all - -# Read-only operations on indices -user: - indices: - - names: '*' - privileges: - - read - -# Defines the required permissions for transport clients -transport_client: - cluster: - - transport_client - -# The required permissions for the kibana 4 server -kibana4_server: - cluster: - - monitor - indices: - - names: '.kibana' - privileges: - - all - -# The required role for logstash users logstash: cluster: - manage_index_templates diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/InternalClient.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/InternalClient.java index f5352254d29..f939ceb022f 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/InternalClient.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/InternalClient.java @@ -16,6 +16,7 @@ import org.elasticsearch.client.FilterClient; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.shield.authc.AuthenticationService; +import org.elasticsearch.shield.user.XPackUser; import java.io.IOException; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/Security.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/Security.java index 23e0026b189..2658e4f97b9 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/Security.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/Security.java @@ -21,7 +21,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.IndexModule; -import org.elasticsearch.shield.action.ShieldActionFilter; +import org.elasticsearch.shield.action.filter.ShieldActionFilter; import org.elasticsearch.shield.action.ShieldActionModule; import org.elasticsearch.shield.action.realm.ClearRealmCacheAction; import org.elasticsearch.shield.action.realm.TransportClearRealmCacheAction; @@ -33,9 +33,13 @@ import org.elasticsearch.shield.action.role.TransportPutRoleAction; import org.elasticsearch.shield.action.role.TransportClearRolesCacheAction; import org.elasticsearch.shield.action.role.TransportDeleteRoleAction; import org.elasticsearch.shield.action.role.TransportGetRolesAction; +import org.elasticsearch.shield.action.user.AuthenticateAction; +import org.elasticsearch.shield.action.user.ChangePasswordAction; import org.elasticsearch.shield.action.user.PutUserAction; import org.elasticsearch.shield.action.user.DeleteUserAction; import org.elasticsearch.shield.action.user.GetUsersAction; +import org.elasticsearch.shield.action.user.TransportAuthenticateAction; +import org.elasticsearch.shield.action.user.TransportChangePasswordAction; import org.elasticsearch.shield.action.user.TransportPutUserAction; import org.elasticsearch.shield.action.user.TransportDeleteUserAction; import org.elasticsearch.shield.action.user.TransportGetUsersAction; @@ -43,7 +47,6 @@ import org.elasticsearch.shield.audit.AuditTrailModule; import org.elasticsearch.shield.audit.index.IndexAuditTrail; import org.elasticsearch.shield.audit.index.IndexNameResolver; import org.elasticsearch.shield.audit.logfile.LoggingAuditTrail; -import org.elasticsearch.shield.authc.AnonymousService; import org.elasticsearch.shield.authc.AuthenticationModule; import org.elasticsearch.shield.authc.InternalAuthenticationService; import org.elasticsearch.shield.authc.Realms; @@ -52,6 +55,7 @@ import org.elasticsearch.shield.authc.ldap.support.SessionFactory; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.authz.AuthorizationModule; +import org.elasticsearch.shield.authz.InternalAuthorizationService; import org.elasticsearch.shield.authz.accesscontrol.OptOutQueryCache; import org.elasticsearch.shield.authz.accesscontrol.ShieldIndexSearcherWrapper; import org.elasticsearch.shield.authz.store.FileRolesStore; @@ -69,6 +73,7 @@ import org.elasticsearch.shield.rest.action.role.RestPutRoleAction; import org.elasticsearch.shield.rest.action.role.RestClearRolesCacheAction; import org.elasticsearch.shield.rest.action.role.RestDeleteRoleAction; import org.elasticsearch.shield.rest.action.role.RestGetRolesAction; +import org.elasticsearch.shield.rest.action.user.RestChangePasswordAction; import org.elasticsearch.shield.rest.action.user.RestPutUserAction; import org.elasticsearch.shield.rest.action.user.RestDeleteUserAction; import org.elasticsearch.shield.rest.action.user.RestGetUsersAction; @@ -81,6 +86,7 @@ import org.elasticsearch.shield.transport.ShieldTransportModule; import org.elasticsearch.shield.transport.filter.IPFilter; import org.elasticsearch.shield.transport.netty.ShieldNettyHttpServerTransport; import org.elasticsearch.shield.transport.netty.ShieldNettyTransport; +import org.elasticsearch.shield.user.AnonymousUser; import org.elasticsearch.xpack.XPackPlugin; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -207,11 +213,12 @@ public class Security { // authentication settings FileRolesStore.registerSettings(settingsModule); - AnonymousService.registerSettings(settingsModule); + AnonymousUser.registerSettings(settingsModule); Realms.registerSettings(settingsModule); NativeUsersStore.registerSettings(settingsModule); NativeRolesStore.registerSettings(settingsModule); InternalAuthenticationService.registerSettings(settingsModule); + InternalAuthorizationService.registerSettings(settingsModule); // HTTP settings ShieldNettyHttpServerTransport.registerSettings(settingsModule); @@ -278,6 +285,8 @@ public class Security { module.registerAction(GetRolesAction.INSTANCE, TransportGetRolesAction.class); module.registerAction(PutRoleAction.INSTANCE, TransportPutRoleAction.class); module.registerAction(DeleteRoleAction.INSTANCE, TransportDeleteRoleAction.class); + module.registerAction(ChangePasswordAction.INSTANCE, TransportChangePasswordAction.class); + module.registerAction(AuthenticateAction.INSTANCE, TransportAuthenticateAction.class); } public void onModule(NetworkModule module) { @@ -305,6 +314,7 @@ public class Security { module.registerRestHandler(RestGetRolesAction.class); module.registerRestHandler(RestPutRoleAction.class); module.registerRestHandler(RestDeleteRoleAction.class); + module.registerRestHandler(RestChangePasswordAction.class); module.registerHttpTransport(Security.NAME, ShieldNettyHttpServerTransport.class); } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SecurityContext.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SecurityContext.java index 398ae51a3ab..e065e0ca983 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SecurityContext.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SecurityContext.java @@ -9,6 +9,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.shield.authc.AuthenticationService; +import org.elasticsearch.shield.user.User; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; @@ -23,6 +24,8 @@ public interface SecurityContext { V executeAs(User user, Callable callable); + User getUser(); + class Insecure implements SecurityContext { public static final Insecure INSTANCE = new Insecure(); @@ -43,6 +46,11 @@ public interface SecurityContext { throw new ElasticsearchException(e); } } + + @Override + public User getUser() { + return null; + } } class Secure implements SecurityContext { @@ -72,6 +80,11 @@ public interface SecurityContext { } } + @Override + public User getUser() { + return authcService.getCurrentUser(); + } + private void setUser(User user) { try { authcService.attachUserHeaderIfMissing(user); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/XPackUser.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/XPackUser.java deleted file mode 100644 index 218b8b3160e..00000000000 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/XPackUser.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.shield; - -import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesAction; -import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction; -import org.elasticsearch.shield.action.realm.ClearRealmCacheAction; -import org.elasticsearch.shield.action.role.ClearRolesCacheAction; -import org.elasticsearch.shield.authz.permission.Role; -import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; -import org.elasticsearch.shield.authz.privilege.IndexPrivilege; -import org.elasticsearch.shield.authz.privilege.Privilege; - -/** - * XPack internal user that manages xpack. Has all cluster/indices permissions for watcher, - * shield and monitoring to operate. - */ -public class XPackUser extends User { - - public static final String NAME = "__es_internal_user"; - - public static final Role ROLE = Role.builder("__es_internal_role") - .cluster(ClusterPrivilege.get(new Privilege.Name( - ClearRealmCacheAction.NAME + "*", // shield - ClearRolesCacheAction.NAME + "*", // shield - PutIndexTemplateAction.NAME, // shield, marvel, watcher - GetIndexTemplatesAction.NAME + "*", // marvel - ClusterPrivilege.MONITOR.name().toString()))) // marvel - - - // for now, the watches will be executed under the watcher user, meaning, all actions - // taken as part of the execution will be executed on behalf of this user. this includes - // the index action, search input and search transform. For this reason the watcher user - // requires full access to all indices in the cluster. - // - // at later phases we'll want to execute the watch on behalf of the user who registers - // it. this will require some work to attach/persist that user to/with the watch. - .add(IndexPrivilege.ALL, "*") - - -// these will be the index permissions required by shield (will uncomment once we optimize watcher permissions) - -// .add(IndexPrivilege.ALL, ShieldTemplateService.SECURITY_INDEX_NAME) -// .add(IndexPrivilege.ALL, IndexAuditTrail.INDEX_NAME_PREFIX + "*") - - - -// these will be the index permissions required by monitoring (will uncomment once we optimize monitoring permissions) - -// // we need all monitoring access -// .add(IndexPrivilege.MONITOR, "*") -// // and full access to .monitoring-es-* and .monitoring-es-data indices -// .add(IndexPrivilege.ALL, MarvelSettings.MONITORING_INDICES_PREFIX + "*") - - .build(); - - public static final XPackUser INSTANCE = new XPackUser(); - - XPackUser() { - 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); - } -} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionModule.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionModule.java index 402094b3c51..963f35e0ea4 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionModule.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionModule.java @@ -7,6 +7,7 @@ package org.elasticsearch.shield.action; import org.elasticsearch.common.inject.multibindings.Multibinder; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.action.filter.ShieldActionFilter; import org.elasticsearch.shield.action.interceptor.BulkRequestInterceptor; import org.elasticsearch.shield.action.interceptor.RealtimeRequestInterceptor; import org.elasticsearch.shield.action.interceptor.RequestInterceptor; @@ -23,8 +24,10 @@ public class ShieldActionModule extends AbstractShieldModule.Node { @Override protected void configureNode() { bind(ShieldActionMapper.class).asEagerSingleton(); - // we need to ensure that there's only a single instance of this filter. + // we need to ensure that there's only a single instance of the action filters bind(ShieldActionFilter.class).asEagerSingleton(); + + // TODO: we should move these to action filters and only have 1 chain. Multibinder multibinder = Multibinder.newSetBinder(binder(), RequestInterceptor.class); multibinder.addBinding().to(RealtimeRequestInterceptor.class); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/filter/ShieldActionFilter.java similarity index 75% rename from elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java rename to elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/filter/ShieldActionFilter.java index 41f75e9769b..426fcc7240e 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/filter/ShieldActionFilter.java @@ -3,7 +3,7 @@ * 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.shield.action; +package org.elasticsearch.shield.action.filter; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; @@ -20,8 +20,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.license.plugin.core.LicenseUtils; import org.elasticsearch.shield.Security; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.action.ShieldActionMapper; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.action.interceptor.RequestInterceptor; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.authc.AuthenticationService; @@ -31,8 +32,6 @@ import org.elasticsearch.shield.authz.AuthorizationUtils; import org.elasticsearch.shield.authz.privilege.HealthAndStatsPrivilege; import org.elasticsearch.shield.crypto.CryptoService; import org.elasticsearch.shield.license.ShieldLicenseState; -import org.elasticsearch.shield.support.AutomatonPredicate; -import org.elasticsearch.shield.support.Automatons; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; @@ -50,8 +49,6 @@ import static org.elasticsearch.shield.support.Exceptions.authorizationError; public class ShieldActionFilter extends AbstractComponent implements ActionFilter { private static final Predicate LICENSE_EXPIRATION_ACTION_MATCHER = HealthAndStatsPrivilege.INSTANCE.predicate(); - // FIXME clean up this hack - static final Predicate INTERNAL_PREDICATE = new AutomatonPredicate(Automatons.patterns("internal:*")); private final AuthenticationService authcService; private final AuthorizationService authzService; @@ -100,43 +97,12 @@ public class ShieldActionFilter extends AbstractComponent implements ActionFilte if (licenseState.securityEnabled()) { if (AuthorizationUtils.shouldReplaceUserWithSystem(threadContext, action)) { try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { - String shieldAction = actionMapper.action(action, request); - User user = authcService.authenticate(shieldAction, request, SystemUser.INSTANCE); - authzService.authorize(user, shieldAction, request); - request = unsign(user, shieldAction, request); - - for (RequestInterceptor interceptor : requestInterceptors) { - if (interceptor.supports(request)) { - interceptor.intercept(request, user); - } - } - // we should always restore the original here because we forcefully changed to the system user - chain.proceed(task, action, request, new SigningListener(this, listener, original)); - return; + applyInternal(task, action, request, new SigningListener(this, listener, original), chain); } + } else { + applyInternal(task, action, request, + new SigningListener(this, listener, restoreOriginalContext ? original : null), chain); } - - /** - here we fallback on the system user. Internal system requests are requests that are triggered by - the system itself (e.g. pings, update mappings, share relocation, etc...) and were not originated - by user interaction. Since these requests are triggered by es core modules, they are security - agnostic and therefore not associated with any user. When these requests execute locally, they - are executed directly on their relevant action. Since there is no other way a request can make - it to the action without an associated user (not via REST or transport - this is taken care of by - the {@link Rest} filter and the {@link ServerTransport} filter respectively), it's safe to assume a system user - here if a request is not associated with any other user. - */ - String shieldAction = actionMapper.action(action, request); - User user = authcService.authenticate(shieldAction, request, SystemUser.INSTANCE); - authzService.authorize(user, shieldAction, request); - request = unsign(user, shieldAction, request); - - for (RequestInterceptor interceptor : requestInterceptors) { - if (interceptor.supports(request)) { - interceptor.intercept(request, user); - } - } - chain.proceed(task, action, request, new SigningListener(this, listener, restoreOriginalContext ? original : null)); } else { chain.proceed(task, action, request, listener); } @@ -155,6 +121,36 @@ public class ShieldActionFilter extends AbstractComponent implements ActionFilte return Integer.MIN_VALUE; } + private void applyInternal(Task task, String action, ActionRequest request, ActionListener listener, ActionFilterChain chain) + throws IOException { + /** + here we fallback on the system user. Internal system requests are requests that are triggered by + the system itself (e.g. pings, update mappings, share relocation, etc...) and were not originated + by user interaction. Since these requests are triggered by es core modules, they are security + agnostic and therefore not associated with any user. When these requests execute locally, they + are executed directly on their relevant action. Since there is no other way a request can make + it to the action without an associated user (not via REST or transport - this is taken care of by + the {@link Rest} filter and the {@link ServerTransport} filter respectively), it's safe to assume a system user + here if a request is not associated with any other user. + */ + String shieldAction = actionMapper.action(action, request); + User user = authcService.authenticate(shieldAction, request, SystemUser.INSTANCE); + authzService.authorize(user, shieldAction, request); + request = unsign(user, shieldAction, request); + + /* + * We use a separate concept for code that needs to be run after authentication and authorization that could effect the running of + * the action. This is done to make it more clear of the state of the request. + */ + for (RequestInterceptor interceptor : requestInterceptors) { + if (interceptor.supports(request)) { + interceptor.intercept(request, user); + } + } + // we should always restore the original here because we forcefully changed to the system user + chain.proceed(task, action, request, listener); + } + Request unsign(User user, String action, Request request) { try { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/BulkRequestInterceptor.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/BulkRequestInterceptor.java index 46709d1aa66..ac6130f5bae 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/BulkRequestInterceptor.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/BulkRequestInterceptor.java @@ -14,7 +14,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authz.InternalAuthorizationService; import org.elasticsearch.shield.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.threadpool.ThreadPool; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java index ba85735056b..d8cdd452c93 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java @@ -11,7 +11,7 @@ import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authz.InternalAuthorizationService; import org.elasticsearch.shield.authz.accesscontrol.IndicesAccessControl; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/RequestInterceptor.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/RequestInterceptor.java index 43dac507b94..172d2bc7889 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/RequestInterceptor.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/interceptor/RequestInterceptor.java @@ -5,7 +5,7 @@ */ package org.elasticsearch.shield.action.interceptor; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.transport.TransportRequest; /** diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequest.java index f344a3db433..936f1f81123 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequest.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequest.java @@ -17,58 +17,58 @@ import java.io.IOException; */ public class ClearRolesCacheRequest extends BaseNodesRequest { - String[] roles; + String[] names; /** * Sets the roles for which caches will be evicted. When not set all the roles will be evicted from the cache. * - * @param roles The role names + * @param names The role names */ - public ClearRolesCacheRequest roles(String... roles) { - this.roles = roles; + public ClearRolesCacheRequest names(String... names) { + this.names = names; return this; } /** * @return an array of role names that will have the cache evicted or null if all */ - public String[] roles() { - return roles; + public String[] names() { + return names; } @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - roles = in.readOptionalStringArray(); + names = in.readOptionalStringArray(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - out.writeOptionalStringArray(roles); + out.writeOptionalStringArray(names); } public static class Node extends BaseNodeRequest { - String[] roles; + String[] names; public Node() { } public Node(ClearRolesCacheRequest request, String nodeId) { super(nodeId); - this.roles = request.roles(); + this.names = request.names(); } @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - roles = in.readOptionalStringArray(); + names = in.readOptionalStringArray(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - out.writeOptionalStringArray(roles); + out.writeOptionalStringArray(names); } } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequestBuilder.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequestBuilder.java index 949c7cbdfae..5f964ea88d1 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequestBuilder.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/ClearRolesCacheRequestBuilder.java @@ -25,11 +25,11 @@ public class ClearRolesCacheRequestBuilder extends NodesOperationRequestBuilder< /** * Set the roles to be cleared * - * @param roles the names of the roles that should be cleared + * @param names the names of the roles that should be cleared * @return the builder instance */ - public ClearRolesCacheRequestBuilder roles(String... roles) { - request.roles(roles); + public ClearRolesCacheRequestBuilder names(String... names) { + request.names(names); return this; } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportClearRolesCacheAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportClearRolesCacheAction.java index baca1f79a97..1d3d69ae281 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportClearRolesCacheAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportClearRolesCacheAction.java @@ -65,10 +65,10 @@ public class TransportClearRolesCacheAction extends TransportNodesAction listener) { + if (ReservedRolesStore.isReserved(request.name())) { + listener.onFailure(new IllegalArgumentException("role [" + request.name() + "] is reserved and cannot be deleted")); + return; + } + try { rolesStore.deleteRole(request, new ActionListener() { @Override diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportGetRolesAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportGetRolesAction.java index 74882a5114b..2e8f1b70b2f 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportGetRolesAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/role/TransportGetRolesAction.java @@ -13,38 +13,67 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.permission.KibanaRole; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; import org.elasticsearch.shield.authz.store.NativeRolesStore; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import java.util.ArrayList; import java.util.List; public class TransportGetRolesAction extends HandledTransportAction { - private final NativeRolesStore rolesStore; + private final NativeRolesStore nativeRolesStore; + private final ReservedRolesStore reservedRolesStore; @Inject public TransportGetRolesAction(Settings settings, ThreadPool threadPool, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, - NativeRolesStore rolesStore, TransportService transportService) { + NativeRolesStore nativeRolesStore, TransportService transportService, + ReservedRolesStore reservedRolesStore) { super(settings, GetRolesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, GetRolesRequest::new); - this.rolesStore = rolesStore; + this.nativeRolesStore = nativeRolesStore; + this.reservedRolesStore = reservedRolesStore; } @Override protected void doExecute(final GetRolesRequest request, final ActionListener listener) { - if (request.names().length == 1) { - final String rolename = request.names()[0]; + final String[] requestedRoles = request.names(); + final boolean specificRolesRequested = requestedRoles != null && requestedRoles.length > 0; + final List rolesToSearchFor = new ArrayList<>(); + final List roles = new ArrayList<>(); + + if (specificRolesRequested) { + for (String role : requestedRoles) { + if (ReservedRolesStore.isReserved(role)) { + RoleDescriptor rd = reservedRolesStore.roleDescriptor(role); + if (rd != null) { + roles.add(rd); + } else { + // the kibana role name is reseved but is only visible to the Kibana user, so this should be the only null + // descriptor. More details in the ReservedRolesStore + assert KibanaRole.NAME.equals(role); + } + } else { + rolesToSearchFor.add(role); + } + } + } else { + roles.addAll(reservedRolesStore.roleDescriptors()); + } + + if (rolesToSearchFor.size() == 1) { + final String rolename = rolesToSearchFor.get(0); // We can fetch a single role with a get, much easier - rolesStore.getRoleDescriptor(rolename, new ActionListener() { + nativeRolesStore.getRoleDescriptor(rolename, new ActionListener() { @Override public void onResponse(RoleDescriptor roleD) { - if (roleD == null) { - listener.onResponse(new GetRolesResponse()); - } else { - listener.onResponse(new GetRolesResponse(roleD)); + if (roleD != null) { + roles.add(roleD); } + listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()]))); } @Override @@ -53,10 +82,15 @@ public class TransportGetRolesAction extends HandledTransportAction>() { + nativeRolesStore.getRoleDescriptors( + rolesToSearchFor.toArray(new String[rolesToSearchFor.size()]), new ActionListener>() { @Override - public void onResponse(List roles) { + public void onResponse(List foundRoles) { + roles.addAll(foundRoles); listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()]))); } @@ -64,6 +98,7 @@ public class TransportGetRolesAction extends HandledTransportAction listener) { + final String name = request.roleDescriptor().getName(); + if (ReservedRolesStore.isReserved(name)) { + listener.onFailure(new IllegalArgumentException("role [" + name + "] is reserved and cannot be modified.")); + return; + } + rolesStore.putRole(request, request.roleDescriptor(), new ActionListener() { @Override public void onResponse(Boolean created) { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateAction.java new file mode 100644 index 00000000000..c282a2eeeef --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateAction.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.shield.action.user; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * + */ +public class AuthenticateAction extends Action { + + public static final String NAME = "cluster:admin/xpack/security/user/authenticate"; + public static final AuthenticateAction INSTANCE = new AuthenticateAction(); + + public AuthenticateAction() { + super(NAME); + } + + @Override + public AuthenticateRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new AuthenticateRequestBuilder(client); + } + + @Override + public AuthenticateResponse newResponse() { + return new AuthenticateResponse(); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateRequest.java new file mode 100644 index 00000000000..2415ba5fbff --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateRequest.java @@ -0,0 +1,64 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.shield.support.Validation; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * + */ +public class AuthenticateRequest extends ActionRequest implements UserRequest { + + private String username; + + public AuthenticateRequest() {} + + public AuthenticateRequest(String username) { + this.username = username; + } + + @Override + public ActionRequestValidationException validate() { + Validation.Error error = Validation.Users.validateUsername(username); + if (error != null) { + return addValidationError(error.toString(), null); + } + return null; + } + + public String username() { + return username; + } + + public void username(String username) { + this.username = username; + } + + @Override + public String[] usernames() { + return new String[] { username }; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + username = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(username); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateRequestBuilder.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateRequestBuilder.java new file mode 100644 index 00000000000..5188d51f579 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateRequestBuilder.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.action.user; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + */ +public class AuthenticateRequestBuilder + extends ActionRequestBuilder { + + public AuthenticateRequestBuilder(ElasticsearchClient client) { + this(client, AuthenticateAction.INSTANCE); + } + + public AuthenticateRequestBuilder(ElasticsearchClient client, AuthenticateAction action) { + super(client, action, new AuthenticateRequest()); + } + + public AuthenticateRequestBuilder username(String username) { + request.username(username); + return this; + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateResponse.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateResponse.java new file mode 100644 index 00000000000..953886c1aeb --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/AuthenticateResponse.java @@ -0,0 +1,42 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.shield.user.User; + +import java.io.IOException; + +/** + */ +public class AuthenticateResponse extends ActionResponse { + + private User user; + + public AuthenticateResponse() {} + + public AuthenticateResponse(User user) { + this.user = user; + } + + public User user() { + return user; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + User.writeTo(user, out); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + user = User.readFrom(in); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordAction.java new file mode 100644 index 00000000000..85452887495 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordAction.java @@ -0,0 +1,31 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + */ +public class ChangePasswordAction extends Action { + + public static final ChangePasswordAction INSTANCE = new ChangePasswordAction(); + public static final String NAME = "cluster:admin/xpack/security/user/change_password"; + + protected ChangePasswordAction() { + super(NAME); + } + + @Override + public ChangePasswordRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new ChangePasswordRequestBuilder(client, this); + } + + @Override + public ChangePasswordResponse newResponse() { + return new ChangePasswordResponse(); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordRequest.java new file mode 100644 index 00000000000..50263bbfe24 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordRequest.java @@ -0,0 +1,81 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.shield.authc.support.CharArrays; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + */ +public class ChangePasswordRequest extends ActionRequest implements UserRequest { + + private String username; + private char[] passwordHash; + private boolean refresh = true; + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (username == null) { + validationException = addValidationError("username is missing", validationException); + } + if (passwordHash == null) { + validationException = addValidationError("password is missing", validationException); + } + return validationException; + } + + public String username() { + return username; + } + + public void username(String username) { + this.username = username; + } + + public char[] passwordHash() { + return passwordHash; + } + + public void passwordHash(char[] passwordHash) { + this.passwordHash = passwordHash; + } + + public boolean refresh() { + return refresh; + } + + public void refresh(boolean refresh) { + this.refresh = refresh; + } + + @Override + public String[] usernames() { + return new String[] { username }; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + username = in.readString(); + passwordHash = CharArrays.utf8BytesToChars(in.readBytesReference().array()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(username); + out.writeBytesReference(new BytesArray(CharArrays.toUtf8Bytes(passwordHash))); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordRequestBuilder.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordRequestBuilder.java new file mode 100644 index 00000000000..994f2901eea --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordRequestBuilder.java @@ -0,0 +1,89 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.ParseFieldMatcher; +import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.support.Validation; +import org.elasticsearch.xpack.common.xcontent.XContentUtils; + +import java.io.IOException; +import java.util.Arrays; + +/** + */ +public class ChangePasswordRequestBuilder + extends ActionRequestBuilder { + + public ChangePasswordRequestBuilder(ElasticsearchClient client) { + this(client, ChangePasswordAction.INSTANCE); + } + + public ChangePasswordRequestBuilder(ElasticsearchClient client, ChangePasswordAction action) { + super(client, action, new ChangePasswordRequest()); + } + + public ChangePasswordRequestBuilder username(String username) { + request.username(username); + return this; + } + + public ChangePasswordRequestBuilder password(char[] password) { + Validation.Error error = Validation.Users.validatePassword(password); + if (error != null) { + ValidationException validationException = new ValidationException(); + validationException.addValidationError(error.toString()); + throw validationException; + } + + try (SecuredString securedString = new SecuredString(password)) { + request.passwordHash(Hasher.BCRYPT.hash(securedString)); + } + return this; + } + + public ChangePasswordRequestBuilder source(BytesReference source) throws IOException { + try (XContentParser parser = XContentHelper.createParser(source)) { + XContentUtils.verifyObject(parser); + XContentParser.Token token; + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.PASSWORD)) { + if (token == XContentParser.Token.VALUE_STRING) { + String password = parser.text(); + char[] passwordChars = password.toCharArray(); + password(passwordChars); + password = null; + Arrays.fill(passwordChars, (char) 0); + } else { + throw new ElasticsearchParseException( + "expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token); + } + } else { + throw new ElasticsearchParseException("failed to parse change password request. unexpected field [{}]", + currentFieldName); + } + } + } + return this; + } + + public ChangePasswordRequestBuilder refresh(boolean refresh) { + request.refresh(refresh); + return this; + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordResponse.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordResponse.java new file mode 100644 index 00000000000..0bf8fd202d8 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/ChangePasswordResponse.java @@ -0,0 +1,15 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.action.ActionResponse; + +/** + */ +public class ChangePasswordResponse extends ActionResponse { + + public ChangePasswordResponse() {} +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/DeleteUserRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/DeleteUserRequest.java index 082edd7aa17..c899747ce0e 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/DeleteUserRequest.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/DeleteUserRequest.java @@ -17,7 +17,7 @@ import static org.elasticsearch.action.ValidateActions.addValidationError; /** * A request to delete a native user. */ -public class DeleteUserRequest extends ActionRequest { +public class DeleteUserRequest extends ActionRequest implements UserRequest { private String username; private boolean refresh = true; @@ -54,6 +54,11 @@ public class DeleteUserRequest extends ActionRequest { this.refresh = refresh; } + @Override + public String[] usernames() { + return new String[] { username }; + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersRequest.java index d174c53b170..50cfaf83375 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersRequest.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersRequest.java @@ -18,7 +18,7 @@ import static org.elasticsearch.action.ValidateActions.addValidationError; /** * Request to retrieve a native user. */ -public class GetUsersRequest extends ActionRequest { +public class GetUsersRequest extends ActionRequest implements UserRequest { private String[] usernames; @@ -39,6 +39,7 @@ public class GetUsersRequest extends ActionRequest { this.usernames = usernames; } + @Override public String[] usernames() { return usernames; } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersResponse.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersResponse.java index 506ed9f82b6..c0e16271a95 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersResponse.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/GetUsersResponse.java @@ -8,7 +8,7 @@ package org.elasticsearch.shield.action.user; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import java.io.IOException; import java.util.Collection; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequest.java index 752d642da37..ce9fa89a747 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequest.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequest.java @@ -22,7 +22,7 @@ import static org.elasticsearch.action.ValidateActions.addValidationError; /** * Request object to put a native user. */ -public class PutUserRequest extends ActionRequest { +public class PutUserRequest extends ActionRequest implements UserRequest { private String username; private String[] roles; @@ -105,6 +105,11 @@ public class PutUserRequest extends ActionRequest { return refresh; } + @Override + public String[] usernames() { + return new String[] { username }; + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequestBuilder.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequestBuilder.java index 0d38aa9c04d..a9f73bd757d 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequestBuilder.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserRequestBuilder.java @@ -15,7 +15,7 @@ import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.support.Hasher; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.support.Validation; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserResponse.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserResponse.java index 2dea005c41d..d5ecda1fcc4 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserResponse.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/PutUserResponse.java @@ -28,6 +28,10 @@ public class PutUserResponse extends ActionResponse implements ToXContent { this.created = created; } + public boolean created() { + return created; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject().field("created", created).endObject(); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportAuthenticateAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportAuthenticateAction.java new file mode 100644 index 00000000000..92a676f8a99 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportAuthenticateAction.java @@ -0,0 +1,50 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +/** + */ +public class TransportAuthenticateAction extends HandledTransportAction { + + private final SecurityContext securityContext; + + @Inject + public TransportAuthenticateAction(Settings settings, ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, + SecurityContext securityContext) { + super(settings, AuthenticateAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, + AuthenticateRequest::new); + this.securityContext = securityContext; + } + + @Override + protected void doExecute(AuthenticateRequest request, ActionListener listener) { + final User user = securityContext.getUser(); + if (SystemUser.is(user)) { + listener.onFailure(new IllegalArgumentException("user [" + user.principal() + "] is internal")); + return; + } + + if (user == null) { + listener.onFailure(new ElasticsearchSecurityException("did not find an authenticated user")); + return; + } + listener.onResponse(new AuthenticateResponse(user)); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportChangePasswordAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportChangePasswordAction.java new file mode 100644 index 00000000000..c01f3923354 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportChangePasswordAction.java @@ -0,0 +1,58 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +/** + */ +public class TransportChangePasswordAction extends HandledTransportAction { + + private final NativeUsersStore nativeUsersStore; + + @Inject + public TransportChangePasswordAction(Settings settings, ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, + NativeUsersStore nativeUsersStore) { + super(settings, ChangePasswordAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, + ChangePasswordRequest::new); + this.nativeUsersStore = nativeUsersStore; + } + + @Override + protected void doExecute(ChangePasswordRequest request, ActionListener listener) { + final String username = request.username(); + if (AnonymousUser.isAnonymousUsername(username)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is anonymous and cannot be modified via the API")); + return; + } else if (SystemUser.NAME.equals(username)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); + return; + } + nativeUsersStore.changePassword(request, new ActionListener() { + @Override + public void onResponse(Void v) { + listener.onResponse(new ChangePasswordResponse()); + } + + @Override + public void onFailure(Throwable e) { + listener.onFailure(e); + } + }); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportDeleteUserAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportDeleteUserAction.java index a0a0c24a685..8e37748519c 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportDeleteUserAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportDeleteUserAction.java @@ -12,6 +12,9 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -30,21 +33,30 @@ public class TransportDeleteUserAction extends HandledTransportAction listener) { - try { - usersStore.deleteUser(request, new ActionListener() { - @Override - public void onResponse(Boolean found) { - listener.onResponse(new DeleteUserResponse(found)); - } - - @Override - public void onFailure(Throwable e) { - listener.onFailure(e); - } - }); - } catch (Exception e) { - logger.error("failed to delete user [{}]", e, request.username()); - listener.onFailure(e); + final String username = request.username(); + if (ReservedRealm.isReserved(username)) { + if (AnonymousUser.isAnonymousUsername(username)) { + 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)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); + return; } + + usersStore.deleteUser(request, new ActionListener() { + @Override + public void onResponse(Boolean found) { + listener.onResponse(new DeleteUserResponse(found)); + } + + @Override + public void onFailure(Throwable e) { + listener.onFailure(e); + } + }); } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportGetUsersAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportGetUsersAction.java index 8ac7a1adb0f..91ff6fabe76 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportGetUsersAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportGetUsersAction.java @@ -12,11 +12,15 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import java.util.ArrayList; import java.util.List; public class TransportGetUsersAction extends HandledTransportAction { @@ -34,17 +38,43 @@ public class TransportGetUsersAction extends HandledTransportAction listener) { - if (request.usernames().length == 1) { - final String username = request.usernames()[0]; + final String[] requestedUsers = request.usernames(); + final boolean specificUsersRequested = requestedUsers != null && requestedUsers.length > 0; + final List usersToSearchFor = new ArrayList<>(); + final List users = new ArrayList<>(); + + if (specificUsersRequested) { + for (String username : requestedUsers) { + if (ReservedRealm.isReserved(username)) { + User user = ReservedRealm.getUser(username); + if (user != null) { + users.add(user); + } else { + // the only time a user should be null is if username matches for the anonymous user and the anonymous user is not + // enabled! + assert AnonymousUser.enabled() == false && AnonymousUser.isAnonymousUsername(username); + } + } else if (SystemUser.NAME.equals(username)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); + return; + } else { + usersToSearchFor.add(username); + } + } + } else { + users.addAll(ReservedRealm.users()); + } + + if (usersToSearchFor.size() == 1) { + final String username = usersToSearchFor.get(0); // We can fetch a single user with a get, much cheaper: usersStore.getUser(username, new ActionListener() { @Override public void onResponse(User user) { - if (user == null) { - listener.onResponse(new GetUsersResponse()); - } else { - listener.onResponse(new GetUsersResponse(user)); + if (user != null) { + users.add(user); } + listener.onResponse(new GetUsersResponse(users)); } @Override @@ -53,10 +83,13 @@ public class TransportGetUsersAction extends HandledTransportAction>() { + usersStore.getUsers(usersToSearchFor.toArray(new String[usersToSearchFor.size()]), new ActionListener>() { @Override - public void onResponse(List users) { + public void onResponse(List usersFound) { + users.addAll(usersFound); listener.onResponse(new GetUsersResponse(users)); } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportPutUserAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportPutUserAction.java index 254fbd3da9a..4af577c4a72 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportPutUserAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/TransportPutUserAction.java @@ -12,6 +12,9 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -29,6 +32,21 @@ public class TransportPutUserAction extends HandledTransportAction listener) { + final String username = request.username(); + if (ReservedRealm.isReserved(username)) { + if (AnonymousUser.isAnonymousUsername(username)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is anonymous and cannot be modified via the API")); + return; + } else { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is reserved and only the " + + "password can be changed")); + return; + } + } else if (SystemUser.NAME.equals(username)) { + listener.onFailure(new IllegalArgumentException("user [" + username + "] is internal")); + return; + } + usersStore.putUser(request, new ActionListener() { @Override public void onResponse(Boolean created) { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/UserRequest.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/UserRequest.java new file mode 100644 index 00000000000..f91015b3866 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/action/user/UserRequest.java @@ -0,0 +1,17 @@ +/* + * 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.shield.action.user; + +/** + * Interface for requests that involve user operations + */ +public interface UserRequest { + + /** + * Accessor for the usernames that this request pertains to. null should never be returned! + */ + String[] usernames(); +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrail.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrail.java index 07d899da03d..2e8f3238356 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrail.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrail.java @@ -6,7 +6,7 @@ package org.elasticsearch.shield.audit; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.transport.filter.ShieldIpFilterRule; import org.elasticsearch.transport.TransportMessage; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrailService.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrailService.java index ef6a2cc6732..a3fdfc7defe 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrailService.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/AuditTrailService.java @@ -9,7 +9,7 @@ import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.transport.filter.ShieldIpFilterRule; import org.elasticsearch.transport.TransportMessage; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/index/IndexAuditTrail.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/index/IndexAuditTrail.java index 4d9a5258bc4..98b20ace6ea 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/index/IndexAuditTrail.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/index/IndexAuditTrail.java @@ -47,9 +47,9 @@ import org.elasticsearch.gateway.GatewayService; import org.elasticsearch.node.Node; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.shield.InternalClient; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.XPackUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authz.privilege.SystemPrivilege; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrail.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrail.java index dde6eb2c748..0d1d4eb6c7f 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrail.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrail.java @@ -20,9 +20,9 @@ import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.XPackUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authz.privilege.SystemPrivilege; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AnonymousService.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AnonymousService.java deleted file mode 100644 index 65a9936e373..00000000000 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AnonymousService.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.shield.authc; - -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.inject.Inject; -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.shield.User; - -import java.util.Collections; -import java.util.List; - -import static org.elasticsearch.shield.Security.setting; - -public class AnonymousService { - - static final String ANONYMOUS_USERNAME = "_es_anonymous_user"; - public static final Setting SETTING_AUTHORIZATION_EXCEPTION_ENABLED = - Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope); - public static final Setting> ROLES_SETTING = - Setting.listSetting(setting("authc.anonymous.roles"), Collections.emptyList(), s -> s, Property.NodeScope); - public static final Setting USERNAME_SETTING = - new Setting<>(setting("authc.anonymous.username"), ANONYMOUS_USERNAME, s -> s, Property.NodeScope); - - @Nullable - private final User anonymousUser; - private final boolean authzExceptionEnabled; - - @Inject - public AnonymousService(Settings settings) { - anonymousUser = resolveAnonymousUser(settings); - authzExceptionEnabled = SETTING_AUTHORIZATION_EXCEPTION_ENABLED.get(settings); - } - - public boolean enabled() { - return anonymousUser != null; - } - - public boolean isAnonymous(User user) { - if (enabled()) { - return anonymousUser.equals(user); - } - return false; - } - - public User anonymousUser() { - return anonymousUser; - } - - public boolean authorizationExceptionsEnabled() { - return authzExceptionEnabled; - } - - static User resolveAnonymousUser(Settings settings) { - List roles = ROLES_SETTING.get(settings); - if (roles.isEmpty()) { - return null; - } - String username = USERNAME_SETTING.get(settings); - return new User(username, roles.toArray(Strings.EMPTY_ARRAY)); - } - - public static void registerSettings(SettingsModule settingsModule) { - settingsModule.registerSetting(ROLES_SETTING); - settingsModule.registerSetting(USERNAME_SETTING); - settingsModule.registerSetting(SETTING_AUTHORIZATION_EXCEPTION_ENABLED); - } -} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java index 963ac42b150..5b0cc1186eb 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java @@ -11,9 +11,11 @@ import org.elasticsearch.shield.authc.activedirectory.ActiveDirectoryRealm; import org.elasticsearch.shield.authc.esnative.NativeRealm; import org.elasticsearch.shield.authc.esnative.NativeUsersStore; import org.elasticsearch.shield.authc.file.FileRealm; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; import org.elasticsearch.shield.authc.ldap.LdapRealm; import org.elasticsearch.shield.authc.pki.PkiRealm; import org.elasticsearch.shield.support.AbstractShieldModule; +import org.elasticsearch.shield.user.AnonymousUser; import java.util.Arrays; import java.util.HashMap; @@ -27,7 +29,7 @@ import java.util.Map.Entry; public class AuthenticationModule extends AbstractShieldModule.Node { static final List INTERNAL_REALM_TYPES = - Arrays.asList(NativeRealm.TYPE, FileRealm.TYPE, ActiveDirectoryRealm.TYPE, LdapRealm.TYPE, PkiRealm.TYPE); + Arrays.asList(ReservedRealm.TYPE, NativeRealm.TYPE, FileRealm.TYPE, ActiveDirectoryRealm.TYPE, LdapRealm.TYPE, PkiRealm.TYPE); private final Map>>> customRealms = new HashMap<>(); @@ -40,26 +42,27 @@ public class AuthenticationModule extends AbstractShieldModule.Node { @Override protected void configureNode() { + AnonymousUser.initialize(settings); MapBinder mapBinder = MapBinder.newMapBinder(binder(), String.class, Realm.Factory.class); mapBinder.addBinding(FileRealm.TYPE).to(FileRealm.Factory.class).asEagerSingleton(); mapBinder.addBinding(NativeRealm.TYPE).to(NativeRealm.Factory.class).asEagerSingleton(); mapBinder.addBinding(ActiveDirectoryRealm.TYPE).to(ActiveDirectoryRealm.Factory.class).asEagerSingleton(); mapBinder.addBinding(LdapRealm.TYPE).to(LdapRealm.Factory.class).asEagerSingleton(); mapBinder.addBinding(PkiRealm.TYPE).to(PkiRealm.Factory.class).asEagerSingleton(); - for (Entry>>> entry : customRealms.entrySet - ()) { + for (Entry>>> entry : + customRealms.entrySet()) { mapBinder.addBinding(entry.getKey()).to(entry.getValue()).asEagerSingleton(); } + bind(ReservedRealm.class).asEagerSingleton(); bind(Realms.class).asEagerSingleton(); - bind(AnonymousService.class).asEagerSingleton(); if (authcFailureHandler == null) { bind(AuthenticationFailureHandler.class).to(DefaultAuthenticationFailureHandler.class).asEagerSingleton(); } else { bind(AuthenticationFailureHandler.class).to(authcFailureHandler).asEagerSingleton(); } - bind(AuthenticationService.class).to(InternalAuthenticationService.class).asEagerSingleton(); bind(NativeUsersStore.class).asEagerSingleton(); + bind(AuthenticationService.class).to(InternalAuthenticationService.class).asEagerSingleton(); } /** diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationService.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationService.java index a1c05180cf8..b8c2605f674 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationService.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/AuthenticationService.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield.authc; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.transport.TransportMessage; import java.io.IOException; @@ -58,4 +58,6 @@ public interface AuthenticationService { * @param user The user to be attached if the header is missing */ void attachUserHeaderIfMissing(User user) throws IOException; + + User getCurrentUser(); } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/InternalAuthenticationService.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/InternalAuthenticationService.java index efcf84490d6..7c693179a57 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/InternalAuthenticationService.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/InternalAuthenticationService.java @@ -20,7 +20,8 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.crypto.CryptoService; import org.elasticsearch.threadpool.ThreadPool; @@ -50,7 +51,6 @@ public class InternalAuthenticationService extends AbstractComponent implements private final Realms realms; private final AuditTrail auditTrail; private final CryptoService cryptoService; - private final AnonymousService anonymousService; private final AuthenticationFailureHandler failureHandler; private final ThreadContext threadContext; private final boolean signUserHeader; @@ -58,13 +58,11 @@ public class InternalAuthenticationService extends AbstractComponent implements @Inject public InternalAuthenticationService(Settings settings, Realms realms, AuditTrail auditTrail, CryptoService cryptoService, - AnonymousService anonymousService, AuthenticationFailureHandler failureHandler, - ThreadPool threadPool) { + AuthenticationFailureHandler failureHandler, ThreadPool threadPool) { super(settings); this.realms = realms; this.auditTrail = auditTrail; this.cryptoService = cryptoService; - this.anonymousService = anonymousService; this.failureHandler = failureHandler; this.threadContext = threadPool.getThreadContext(); this.signUserHeader = SIGN_USER_HEADER.get(settings); @@ -92,11 +90,12 @@ public class InternalAuthenticationService extends AbstractComponent implements } if (token == null) { - if (anonymousService.enabled()) { + if (AnonymousUser.enabled()) { + User anonymousUser = AnonymousUser.INSTANCE; // we must put the user in the request context, so it'll be copied to the // transport request - without it, the transport will assume system user - setUser(anonymousService.anonymousUser()); - return anonymousService.anonymousUser(); + setUser(anonymousUser); + return anonymousUser; } auditTrail.anonymousAccessDenied(request); throw failureHandler.missingToken(request); @@ -204,6 +203,11 @@ public class InternalAuthenticationService extends AbstractComponent implements setUser(user); } + @Override + public User getCurrentUser() { + return getUserFromContext(); + } + void setUserHeader(User user) throws IOException { String userHeader = signUserHeader ? cryptoService.sign(encodeUser(user, logger)) : encodeUser(user, logger); threadContext.putHeader(USER_KEY, userHeader); @@ -288,8 +292,8 @@ public class InternalAuthenticationService extends AbstractComponent implements if (fallbackUser != null) { return fallbackUser; } - if (anonymousService.enabled()) { - return anonymousService.anonymousUser(); + if (AnonymousUser.enabled()) { + return AnonymousUser.INSTANCE; } auditTrail.anonymousAccessDenied(action, message); throw failureHandler.missingToken(message, action); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realm.java index a22195c2bec..37618f3cb28 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realm.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realm.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield.authc; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; /** * An authentication mechanism to which the default authentication {@link org.elasticsearch.shield.authc.AuthenticationService service} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realms.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realms.java index b9b829c386f..0bc68a60247 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realms.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/Realms.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; import org.elasticsearch.env.Environment; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; import org.elasticsearch.shield.authc.esnative.NativeRealm; import org.elasticsearch.shield.authc.file.FileRealm; import org.elasticsearch.shield.license.ShieldLicenseState; @@ -37,21 +38,25 @@ public class Realms extends AbstractLifecycleComponent implements Iterab private final Environment env; private final Map factories; private final ShieldLicenseState shieldLicenseState; + private final ReservedRealm reservedRealm; protected List realms = Collections.emptyList(); // a list of realms that are "internal" in that they are provided by shield and not a third party protected List internalRealmsOnly = Collections.emptyList(); @Inject - public Realms(Settings settings, Environment env, Map factories, ShieldLicenseState shieldLicenseState) { + public Realms(Settings settings, Environment env, Map factories, ShieldLicenseState shieldLicenseState, + ReservedRealm reservedRealm) { super(settings); this.env = env; this.factories = factories; this.shieldLicenseState = shieldLicenseState; + this.reservedRealm = reservedRealm; } @Override protected void doStart() throws ElasticsearchException { + assert factories.get(ReservedRealm.TYPE) == null; this.realms = initRealms(); // pre-computing a list of internal only realms allows us to have much cheaper iteration than a custom iterator // and is also simpler in terms of logic. These lists are small, so the duplication should not be a real issue here @@ -65,8 +70,13 @@ public class Realms extends AbstractLifecycleComponent implements Iterab if (internalRealms.isEmpty()) { addInternalRealms(internalRealms); } - this.internalRealmsOnly = Collections.unmodifiableList(internalRealms); + if (internalRealms.contains(reservedRealm) == false) { + internalRealms.add(0, reservedRealm); + } + assert internalRealms.get(0) == reservedRealm; + + this.internalRealmsOnly = Collections.unmodifiableList(internalRealms); } @Override @@ -133,11 +143,12 @@ public class Realms extends AbstractLifecycleComponent implements Iterab if (!realms.isEmpty()) { Collections.sort(realms); - return realms; + } else { + // there is no "realms" configuration, add the defaults + addInternalRealms(realms); } - - // there is no "realms" configuration, add the defaults - addInternalRealms(realms); + // always add built in first! + realms.add(0, reservedRealm); return realms; } @@ -167,14 +178,14 @@ public class Realms extends AbstractLifecycleComponent implements Iterab } private void addInternalRealms(List realms) { - Realm.Factory indexRealmFactory = factories.get(NativeRealm.TYPE); - if (indexRealmFactory != null) { - realms.add(indexRealmFactory.createDefault("default_" + NativeRealm.TYPE)); - } Realm.Factory fileRealm = factories.get(FileRealm.TYPE); if (fileRealm != null) { realms.add(fileRealm.createDefault("default_" + FileRealm.TYPE)); } + Realm.Factory indexRealmFactory = factories.get(NativeRealm.TYPE); + if (indexRealmFactory != null) { + realms.add(indexRealmFactory.createDefault("default_" + NativeRealm.TYPE)); + } } public static void registerSettings(SettingsModule settingsModule) { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeRealm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeRealm.java index 6106632b833..a7eadbdaef9 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeRealm.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeRealm.java @@ -11,7 +11,7 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.env.Environment; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.Realm; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeUsersStore.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeUsersStore.java index 28a469d5614..9b40c45a5a6 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeUsersStore.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/NativeUsersStore.java @@ -9,7 +9,6 @@ import com.carrotsearch.hppc.ObjectHashSet; import com.carrotsearch.hppc.ObjectLongHashMap; import com.carrotsearch.hppc.ObjectLongMap; import com.carrotsearch.hppc.cursors.ObjectCursor; -import com.carrotsearch.hppc.cursors.ObjectLongCursor; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; @@ -50,9 +49,12 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.shield.InternalClient; import org.elasticsearch.shield.ShieldTemplateService; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.User.Fields; import org.elasticsearch.shield.action.realm.ClearRealmCacheRequest; import org.elasticsearch.shield.action.realm.ClearRealmCacheResponse; +import org.elasticsearch.shield.action.user.ChangePasswordRequest; import org.elasticsearch.shield.action.user.DeleteUserRequest; import org.elasticsearch.shield.action.user.PutUserRequest; import org.elasticsearch.shield.authc.support.Hasher; @@ -61,7 +63,6 @@ import org.elasticsearch.shield.client.SecurityClient; import org.elasticsearch.shield.support.SelfReschedulingRunnable; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool.Names; -import org.elasticsearch.transport.RemoteTransportException; import java.util.ArrayList; import java.util.Arrays; @@ -72,6 +73,7 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.shield.Security.setting; @@ -105,12 +107,9 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL FAILED } - // TODO - perhaps separate indices for users/roles instead of types? public static final String USER_DOC_TYPE = "user"; + static final String RESERVED_USER_DOC_TYPE = "reserved-user"; - // 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 versionMap = new ObjectLongHashMap<>(); private final Hasher hasher = Hasher.BCRYPT; private final List listeners = new CopyOnWriteArrayList<>(); private final AtomicReference state = new AtomicReference<>(State.INITIALIZED); @@ -162,13 +161,13 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL public void onFailure(Throwable t) { if (t instanceof IndexNotFoundException) { logger.trace("failed to retrieve user [{}] since security index does not exist", username); + // We don't invoke the onFailure listener here, instead + // we call the response with a null user + listener.onResponse(null); } else { logger.debug("failed to retrieve user [{}]", t, username); + listener.onFailure(t); } - - // We don't invoke the onFailure listener here, instead - // we call the response with a null user - listener.onResponse(null); } }); } @@ -307,6 +306,73 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL } } + 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; + } + + final String docType; + if (ReservedRealm.isReserved(username)) { + docType = RESERVED_USER_DOC_TYPE; + } else { + docType = USER_DOC_TYPE; + } + + client.prepareUpdate(ShieldTemplateService.SECURITY_INDEX_NAME, docType, username) + .setDoc(Fields.PASSWORD.getPreferredName(), String.valueOf(request.passwordHash())) + .setRefresh(request.refresh()) + .execute(new ActionListener() { + @Override + public void onResponse(UpdateResponse updateResponse) { + assert updateResponse.isCreated() == false; + clearRealmCache(request.username(), listener, null); + } + + @Override + public void onFailure(Throwable 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 (docType.equals(RESERVED_USER_DOC_TYPE)) { + createReservedUser(username, request.passwordHash(), request.refresh(), listener); + } else { + logger.debug("failed to change password for user [{}]", cause, request.username()); + ValidationException validationException = new ValidationException(); + validationException.addValidationError("user must exist in order to change password"); + listener.onFailure(validationException); + } + } + }); + } + + private void createReservedUser(String username, char[] passwordHash, boolean refresh, ActionListener listener) { + client.prepareIndex(ShieldTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username) + .setSource(Fields.PASSWORD.getPreferredName(), String.valueOf(passwordHash)) + .setRefresh(refresh) + .execute(new ActionListener() { + @Override + public void onResponse(IndexResponse indexResponse) { + clearRealmCache(username, listener, null); + } + + @Override + public void onFailure(Throwable e) { + listener.onFailure(e); + } + }); + } + 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")); @@ -345,7 +411,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL @Override public void onFailure(Throwable e) { Throwable cause = e; - if (e instanceof RemoteTransportException) { + if (e instanceof ElasticsearchException) { cause = ExceptionsHelper.unwrapCause(e); if ((cause instanceof IndexNotFoundException) == false && (cause instanceof DocumentMissingException) == false) { @@ -450,6 +516,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL if (clusterState.routingTable().index(ShieldTemplateService.SECURITY_INDEX_NAME).allPrimaryShardsActive()) { logger.debug("security index [{}] all primary shards started, so service can start", ShieldTemplateService.SECURITY_INDEX_NAME); + shieldIndexExists = true; return true; } return false; @@ -462,7 +529,6 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL this.scrollSize = SCROLL_SIZE_SETTING.get(settings); this.scrollKeepAlive = SCROLL_KEEP_ALIVE_SETTING.get(settings); - // FIXME only start if a realm is using this UserStorePoller poller = new UserStorePoller(); try { poller.doRun(); @@ -519,6 +585,62 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL listeners.add(listener); } + boolean started() { + return state() == State.STARTED; + } + + boolean shieldIndexExists() { + return shieldIndexExists; + } + + char[] reservedUserPassword(String username) throws Throwable { + assert started(); + final AtomicReference passwordHash = new AtomicReference<>(); + final AtomicReference failure = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + client.prepareGet(ShieldTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username) + .execute(new LatchedActionListener<>(new ActionListener() { + @Override + public void onResponse(GetResponse getResponse) { + if (getResponse.isExists()) { + Map sourceMap = getResponse.getSourceAsMap(); + String password = (String) sourceMap.get(User.Fields.PASSWORD.getPreferredName()); + if (password == null || password.isEmpty()) { + failure.set(new IllegalStateException("password hash must not be empty!")); + return; + } + passwordHash.set(password.toCharArray()); + } + } + + @Override + public void onFailure(Throwable e) { + if (e instanceof IndexNotFoundException) { + logger.trace("could not retrieve built in user [{}] password since security index does not exist", e, username); + } else { + logger.error("failed to retrieve built in user [{}] password", e, username); + 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 user [" + username + "]")); + } + } catch (InterruptedException e) { + failure.set(e); + } + + Throwable 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 passwordHash.get(); + } + private void clearScrollResponse(String scrollId) { ClearScrollRequest clearScrollRequest = client.prepareClearScroll().addScrollId(scrollId).request(); client.clearScroll(clearScrollRequest, new ActionListener() { @@ -579,7 +701,6 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL if (state != State.STOPPED && state != State.FAILED) { throw new IllegalStateException("can only reset if stopped!!!"); } - this.versionMap.clear(); this.listeners.clear(); this.client = null; this.shieldIndexExists = false; @@ -606,8 +727,16 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL 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; } @@ -617,50 +746,86 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL return; } - // 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; - logger.trace("starting polling of user index to check for changes"); - // create a copy of all known users - ObjectHashSet knownUsers = new ObjectHashSet<>(versionMap.keys()); - List changedUsers = new ArrayList<>(); - - ObjectLongMap currentUsersMap = collectUsersAndVersions(client); - Iterator> iterator = currentUsersMap.iterator(); - while (iterator.hasNext()) { - ObjectLongCursor cursor = iterator.next(); - String username = cursor.key; - long version = cursor.value; - if (knownUsers.contains(username)) { - final long lastKnownVersion = versionMap.get(username); - if (version != lastKnownVersion) { - // version is only changed by this method - assert version > lastKnownVersion; - versionMap.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 { - versionMap.put(username, version); - } - } - - // exit before comparing with known users + 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(ShieldTemplateService.SECURITY_INDEX_NAME).get(); + response = client.prepareSearch(ShieldTemplateService.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; - versionMap.remove(user); + usersMap.remove(user); changedUsers.add(user); } + return changedUsers; + } + + private void notifyListeners(List changedUsers) { if (changedUsers.isEmpty()) { return; } @@ -689,47 +854,6 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL logger.error("error occurred while checking the native users for changes", t); } - private ObjectLongMap collectUsersAndVersions(Client client) { - final ObjectLongMap map = new ObjectLongHashMap<>(); - SearchResponse response = null; - try { - client.admin().indices().prepareRefresh(ShieldTemplateService.SECURITY_INDEX_NAME).get(); - SearchRequest request = client.prepareSearch(ShieldTemplateService.SECURITY_INDEX_NAME) - .setScroll(scrollKeepAlive) - .setQuery(QueryBuilders.typeQuery(USER_DOC_TYPE)) - .setSize(scrollSize) - .setVersion(true) - .setFetchSource(false) // we only need id and version - .request(); - response = client.search(request).actionGet(); - - boolean keepScrolling = response.getHits().getHits().length > 0; - while (keepScrolling) { - if (isStopped()) { - // instead of throwing an exception we return an empty map so nothing is processed and we exit early - return new ObjectLongHashMap<>(); - } - for (SearchHit hit : response.getHits().getHits()) { - String username = hit.id(); - long version = hit.version(); - map.put(username, version); - } - SearchScrollRequest scrollRequest = - client.prepareSearchScroll(response.getScrollId()).setScroll(scrollKeepAlive).request(); - response = client.searchScroll(scrollRequest).actionGet(); - keepScrolling = response.getHits().getHits().length > 0; - } - } catch (IndexNotFoundException e) { - logger.trace("security index does not exist", e); - } finally { - if (response != null) { - ClearScrollRequest clearScrollRequest = client.prepareClearScroll().addScrollId(response.getScrollId()).request(); - client.clearScroll(clearScrollRequest).actionGet(); - } - } - return map; - } - private boolean isStopped() { State state = state(); return state == State.STOPPED || state == State.STOPPING; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/ReservedRealm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/ReservedRealm.java new file mode 100644 index 00000000000..c1b70169a06 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/ReservedRealm.java @@ -0,0 +1,139 @@ +/* + * 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.shield.authc.esnative; + +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.shield.authc.RealmConfig; +import org.elasticsearch.shield.authc.esnative.NativeUsersStore.ChangeListener; +import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.authc.support.UsernamePasswordToken; +import org.elasticsearch.shield.support.Exceptions; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * A realm for predefined users. These users can only be modified in terms of changing their passwords; no other modifications are allowed. + * This realm is always enabled. + */ +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())); + + private final NativeUsersStore nativeUsersStore; + + @Inject + public ReservedRealm(Environment env, Settings settings, NativeUsersStore nativeUsersStore) { + 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); + } + }); + + } + + @Override + protected User doAuthenticate(UsernamePasswordToken token) { + final User user = getUser(token.principal()); + if (user == null) { + return null; + } + + final char[] passwordHash = getPasswordHash(user.principal()); + if (passwordHash != null) { + try { + if (Hasher.BCRYPT.verify(token.credentials(), passwordHash)) { + return user; + } + } finally { + if (passwordHash != DEFAULT_PASSWORD_HASH) { + Arrays.fill(passwordHash, (char) 0); + } + } + } + // this was a reserved username - don't allow this to go to another realm... + throw Exceptions.authenticationError("failed to authenticate user [{}]", token.principal()); + } + + @Override + protected User doLookupUser(String username) { + return getUser(username); + } + + @Override + public boolean userLookupSupported() { + return true; + } + + public static boolean isReserved(String username) { + assert username != null; + switch (username) { + case XPackUser.NAME: + case KibanaUser.NAME: + return true; + default: + return AnonymousUser.isAnonymousUsername(username); + } + } + + public static User getUser(String username) { + assert username != null; + switch (username) { + case XPackUser.NAME: + return XPackUser.INSTANCE; + case KibanaUser.NAME: + return KibanaUser.INSTANCE; + default: + if (AnonymousUser.enabled() && AnonymousUser.isAnonymousUsername(username)) { + return AnonymousUser.INSTANCE; + } + return null; + } + } + + public static Collection users() { + if (AnonymousUser.enabled()) { + return Arrays.asList(XPackUser.INSTANCE, KibanaUser.INSTANCE, AnonymousUser.INSTANCE); + } + return Arrays.asList(XPackUser.INSTANCE, KibanaUser.INSTANCE); + } + + private char[] getPasswordHash(final String username) { + if (nativeUsersStore.started() == false) { + // we need to be able to check for the user store being started... + return null; + } + + if (nativeUsersStore.shieldIndexExists() == false) { + return DEFAULT_PASSWORD_HASH; + } + try { + char[] passwordHash = nativeUsersStore.reservedUserPassword(username); + if (passwordHash == null) { + return DEFAULT_PASSWORD_HASH; + } + return passwordHash; + } catch (Throwable e) { + logger.error("failed to retrieve password hash for reserved user [{}]", e, username); + return null; + } + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/UserAndPassword.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/UserAndPassword.java index 219eca5dc50..701b9764b8a 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/UserAndPassword.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/esnative/UserAndPassword.java @@ -5,7 +5,7 @@ */ package org.elasticsearch.shield.authc.esnative; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; /** * Like User, but includes the hashed password diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/FileRealm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/FileRealm.java index 3260ec6eb85..9f7982ed1cb 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/FileRealm.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/FileRealm.java @@ -9,7 +9,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.rest.RestController; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.shield.authc.support.RefreshListener; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/tool/UsersTool.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/tool/UsersTool.java index 3959c004f47..0dc39b6ddad 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/tool/UsersTool.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/file/tool/UsersTool.java @@ -35,6 +35,7 @@ import org.elasticsearch.shield.authc.file.FileUserRolesStore; import org.elasticsearch.shield.authc.support.Hasher; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authz.store.FileRolesStore; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; import org.elasticsearch.shield.support.FileAttributesChecker; import org.elasticsearch.shield.support.Validation; import org.elasticsearch.shield.support.Validation.Users; @@ -326,7 +327,7 @@ public class UsersTool extends MultiCommand { Set users = FileUserPasswdStore.parseFile(userFilePath, null).keySet(); Path rolesFilePath = FileRolesStore.resolveFile(env.settings(), env); - Set knownRoles = FileRolesStore.parseFileForRoleNames(rolesFilePath, null); + Set knownRoles = Sets.union(FileRolesStore.parseFileForRoleNames(rolesFilePath, null), ReservedRolesStore.names()); if (username != null) { if (!users.contains(username)) { @@ -343,8 +344,8 @@ public class UsersTool extends MultiCommand { // at least one role is marked... so printing the legend Path rolesFile = FileRolesStore.resolveFile(fileSettings, env).toAbsolutePath(); terminal.println(""); - terminal.println(" [*] An unknown role. " - + "Please check [" + rolesFile.toAbsolutePath() + "] to see available roles"); + terminal.println(" [*] Role is not in the [" + rolesFile.toAbsolutePath() + "] file. If the role has been created " + + "using the API, please disregard this message."); } } else { terminal.println(String.format(Locale.ROOT, "%-15s: -", username)); @@ -377,8 +378,8 @@ public class UsersTool extends MultiCommand { // at least one role is marked... so printing the legend Path rolesFile = FileRolesStore.resolveFile(fileSettings, env).toAbsolutePath(); terminal.println(""); - terminal.println(" [*] An unknown role. " - + "Please check [" + rolesFile.toAbsolutePath() + "] to see available roles"); + terminal.println(" [*] Role is not in the [" + rolesFile.toAbsolutePath() + "] file. If the role has been created " + + "using the API, please disregard this message."); } } } @@ -439,11 +440,12 @@ public class UsersTool extends MultiCommand { private static void verifyRoles(Terminal terminal, Settings settings, Environment env, String[] roles) { Path rolesFile = FileRolesStore.resolveFile(settings, env); assert Files.exists(rolesFile); - Set knownRoles = FileRolesStore.parseFileForRoleNames(rolesFile, null); + Set knownRoles = Sets.union(FileRolesStore.parseFileForRoleNames(rolesFile, null), ReservedRolesStore.names()); Set unknownRoles = Sets.difference(Sets.newHashSet(roles), knownRoles); if (!unknownRoles.isEmpty()) { - terminal.println(String.format(Locale.ROOT, "Warning: The following roles [%s] are unknown. Make sure to add them to the [%s]" + - " file. Nonetheless the user will still be associated with all specified roles", + terminal.println(String.format(Locale.ROOT, "Warning: The following roles [%s] are not in the [%s] file. Make sure the names " + + "are correct. If the names are correct and the roles were created using the API please disregard this message. " + + "Nonetheless the user will still be associated with all specified roles", Strings.collectionToCommaDelimitedString(unknownRoles), rolesFile.toAbsolutePath())); terminal.println("Known roles: " + knownRoles.toString()); } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java index 01a61722716..01ff5c9dede 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java @@ -6,7 +6,7 @@ package org.elasticsearch.shield.authc.ldap.support; import org.elasticsearch.rest.RestController; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.shield.authc.support.DnRoleMapper; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/pki/PkiRealm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/pki/PkiRealm.java index dd4492f3e5b..4e3a362f175 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/pki/PkiRealm.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/pki/PkiRealm.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.shield.Security; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authc.Realm; import org.elasticsearch.shield.authc.RealmConfig; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java index b32c63c22ff..af61e98718e 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java @@ -5,13 +5,14 @@ */ package org.elasticsearch.shield.authc.support; +import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.cache.CacheLoader; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.shield.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.support.Exceptions; +import org.elasticsearch.shield.user.User; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -119,6 +120,11 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm return userWithHash.user; } catch (Exception ee) { + if (ee instanceof ElasticsearchSecurityException) { + // this should bubble out + throw ee; + } + if (logger.isTraceEnabled()) { logger.trace("realm [{}] could not authenticate [{}]", ee, type(), token.principal()); } else if (logger.isDebugEnabled()) { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/SecuredString.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/SecuredString.java index 847f4c02fdc..a581325f70e 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/SecuredString.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authc/support/SecuredString.java @@ -18,7 +18,7 @@ import java.util.Arrays; * TODO: dot net's SecureString implementation does some obfuscation of the password to prevent gleaming passwords * from memory dumps. (this is hard as dot net uses windows system crypto. Thats probably the reason java still doesn't have it) */ -public class SecuredString implements CharSequence { +public class SecuredString implements CharSequence, AutoCloseable { public static final SecuredString EMPTY = new SecuredString(new char[0]); @@ -223,4 +223,9 @@ public class SecuredString implements CharSequence { return equals == 0; } + + @Override + public void close() { + clear(); + } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationModule.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationModule.java index 0fa920064a2..f38516fa0c5 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationModule.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationModule.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.shield.authz.store.CompositeRolesStore; import org.elasticsearch.shield.authz.store.FileRolesStore; import org.elasticsearch.shield.authz.store.NativeRolesStore; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; import org.elasticsearch.shield.authz.store.RolesStore; import org.elasticsearch.shield.support.AbstractShieldModule; @@ -25,6 +26,7 @@ public class AuthorizationModule extends AbstractShieldModule.Node { protected void configureNode() { // First the file and native roles stores must be bound... + bind(ReservedRolesStore.class).asEagerSingleton(); bind(FileRolesStore.class).asEagerSingleton(); bind(NativeRolesStore.class).asEagerSingleton(); // Then the composite roles store (which combines both) can be bound diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationService.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationService.java index 67f62421dca..e914791de99 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationService.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationService.java @@ -6,7 +6,7 @@ package org.elasticsearch.shield.authz; import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.transport.TransportRequest; import java.util.List; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationUtils.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationUtils.java index f6916e68736..f8db520203a 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationUtils.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/AuthorizationUtils.java @@ -6,8 +6,8 @@ package org.elasticsearch.shield.authz; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.InternalAuthenticationService; import org.elasticsearch.shield.support.AutomatonPredicate; import org.elasticsearch.shield.support.Automatons; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/InternalAuthorizationService.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/InternalAuthorizationService.java index 168fd460021..e92bc8ecf11 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/InternalAuthorizationService.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/InternalAuthorizationService.java @@ -16,23 +16,28 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; +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.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.search.action.SearchTransportService; import org.elasticsearch.shield.ShieldTemplateService; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.XPackUser; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.shield.audit.AuditTrail; -import org.elasticsearch.shield.authc.AnonymousService; import org.elasticsearch.shield.authc.AuthenticationFailureHandler; import org.elasticsearch.shield.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.shield.authz.indicesresolver.DefaultIndicesAndAliasesResolver; import org.elasticsearch.shield.authz.indicesresolver.IndicesAndAliasesResolver; import org.elasticsearch.shield.authz.permission.ClusterPermission; +import org.elasticsearch.shield.authz.permission.DefaultRole; import org.elasticsearch.shield.authz.permission.GlobalPermission; import org.elasticsearch.shield.authz.permission.Role; import org.elasticsearch.shield.authz.permission.RunAsPermission; @@ -49,6 +54,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import static org.elasticsearch.shield.Security.setting; import static org.elasticsearch.shield.support.Exceptions.authorizationError; /** @@ -56,6 +62,8 @@ import static org.elasticsearch.shield.support.Exceptions.authorizationError; */ public class InternalAuthorizationService extends AbstractComponent implements AuthorizationService { + public static final Setting ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING = + Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope); public static final String INDICES_PERMISSIONS_KEY = "_indices_permissions"; static final String ORIGINATING_ACTION_KEY = "_originating_action_name"; @@ -65,14 +73,13 @@ public class InternalAuthorizationService extends AbstractComponent implements A private final RolesStore rolesStore; private final AuditTrail auditTrail; private final IndicesAndAliasesResolver[] indicesAndAliasesResolvers; - private final AnonymousService anonymousService; private final AuthenticationFailureHandler authcFailureHandler; private final ThreadContext threadContext; + private final boolean anonymousAuthzExceptionEnabled; @Inject public InternalAuthorizationService(Settings settings, RolesStore rolesStore, ClusterService clusterService, - AuditTrail auditTrail, AnonymousService anonymousService, - AuthenticationFailureHandler authcFailureHandler, ThreadPool threadPool) { + AuditTrail auditTrail, AuthenticationFailureHandler authcFailureHandler, ThreadPool threadPool) { super(settings); this.rolesStore = rolesStore; this.clusterService = clusterService; @@ -80,30 +87,35 @@ public class InternalAuthorizationService extends AbstractComponent implements A this.indicesAndAliasesResolvers = new IndicesAndAliasesResolver[]{ new DefaultIndicesAndAliasesResolver(this) }; - this.anonymousService = anonymousService; this.authcFailureHandler = authcFailureHandler; this.threadContext = threadPool.getThreadContext(); + this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings); } @Override public List authorizedIndicesAndAliases(User user, String action) { - Predicate predicate; - if (XPackUser.is(user)) { - predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action); - } else { - String[] rolesNames = user.roles(); - if (rolesNames.length == 0) { - return Collections.emptyList(); + final String[] anonymousRoles = AnonymousUser.enabled() ? AnonymousUser.getRoles() : Strings.EMPTY_ARRAY; + String[] rolesNames = user.roles(); + if (rolesNames.length == 0 && anonymousRoles.length == 0) { + return Collections.emptyList(); + } + + List> predicates = new ArrayList<>(); + for (String roleName : rolesNames) { + Role role = rolesStore.role(roleName); + if (role != null) { + predicates.add(role.indices().allowedIndicesMatcher(action)); } - List> predicates = new ArrayList<>(); - for (String roleName : rolesNames) { + } + if (AnonymousUser.is(user) == false) { + for (String roleName : anonymousRoles) { Role role = rolesStore.role(roleName); if (role != null) { predicates.add(role.indices().allowedIndicesMatcher(action)); } } - predicate = predicates.stream().reduce(s -> false, (p1, p2) -> p1.or(p2)); } + Predicate predicate = predicates.stream().reduce(s -> false, (p1, p2) -> p1.or(p2)); List indicesAndAliases = new ArrayList<>(); MetaData metaData = clusterService.state().metaData(); @@ -129,6 +141,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A public void authorize(User user, String action, TransportRequest request) throws ElasticsearchSecurityException { // prior to doing any authorization lets set the originating action in the context only setOriginatingAction(action); + User effectiveUser = user; // first we need to check if the user is the system. If it is, we'll just authorize the system access if (SystemUser.is(user)) { @@ -140,7 +153,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A throw denial(user, action, request); } - GlobalPermission permission = XPackUser.is(user) ? XPackUser.ROLE : permission(user.roles()); + GlobalPermission permission = permission(user.roles()); final boolean isRunAs = user.runAs() != null; // permission can be null as it might be that the user's role @@ -167,6 +180,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A if (permission == null || permission.isEmpty()) { throw denial(user, action, request); } + effectiveUser = user.runAs(); } else { throw denyRunAs(user, action, request); } @@ -176,7 +190,8 @@ public class InternalAuthorizationService extends AbstractComponent implements A // against the cluster permissions if (ClusterPrivilege.ACTION_MATCHER.test(action)) { ClusterPermission cluster = permission.cluster(); - if (cluster != null && cluster.check(action)) { + // we use the effectiveUser for permission checking since we are running as a user! + if (cluster != null && cluster.check(action, request, effectiveUser)) { setIndicesAccessControl(IndicesAccessControl.ALLOW_ALL); grant(user, action, request); return; @@ -267,17 +282,17 @@ public class InternalAuthorizationService extends AbstractComponent implements A private GlobalPermission permission(String[] roleNames) { if (roleNames.length == 0) { - return GlobalPermission.NONE; + return DefaultRole.INSTANCE; } if (roleNames.length == 1) { Role role = rolesStore.role(roleNames[0]); - return role == null ? GlobalPermission.NONE : role; + return role == null ? DefaultRole.INSTANCE : GlobalPermission.Compound.builder().add(DefaultRole.INSTANCE).add(role).build(); } // we'll take all the roles and combine their associated permissions - GlobalPermission.Compound.Builder roles = GlobalPermission.Compound.builder(); + GlobalPermission.Compound.Builder roles = GlobalPermission.Compound.builder().add(DefaultRole.INSTANCE); for (String roleName : roleNames) { Role role = rolesStore.role(roleName); if (role != null) { @@ -328,8 +343,8 @@ public class InternalAuthorizationService extends AbstractComponent implements A private ElasticsearchSecurityException denialException(User user, String action) { // Special case for anonymous user - if (anonymousService.isAnonymous(user)) { - if (!anonymousService.authorizationExceptionsEnabled()) { + if (AnonymousUser.enabled() && AnonymousUser.is(user)) { + if (anonymousAuthzExceptionEnabled == false) { throw authcFailureHandler.authenticationRequired(action); } } @@ -339,4 +354,8 @@ public class InternalAuthorizationService extends AbstractComponent implements A } return authorizationError("action [{}] is unauthorized for user [{}]", action, user.principal()); } + + public static void registerSettings(SettingsModule settingsModule) { + settingsModule.registerSetting(ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING); + } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/IndicesAccessControl.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/IndicesAccessControl.java index ab878676939..25f9b1faba4 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/IndicesAccessControl.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/IndicesAccessControl.java @@ -62,7 +62,7 @@ public class IndicesAccessControl { } /** - * @return Whether any role / permission group is allowed to this index. + * @return Whether any role / permission group is allowed to this index. */ public boolean isGranted() { return granted; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesAndAliasesResolver.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesAndAliasesResolver.java index 84c29bc7e6c..2732d7c72de 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesAndAliasesResolver.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesAndAliasesResolver.java @@ -17,7 +17,7 @@ import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.IndexNotFoundException; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authz.AuthorizationService; import org.elasticsearch.transport.TransportRequest; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/IndicesAndAliasesResolver.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/IndicesAndAliasesResolver.java index e7fe6e4c22e..53e71f0598d 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/IndicesAndAliasesResolver.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/indicesresolver/IndicesAndAliasesResolver.java @@ -6,7 +6,7 @@ package org.elasticsearch.shield.authz.indicesresolver; import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.transport.TransportRequest; import java.util.Set; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/ClusterPermission.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/ClusterPermission.java index 11f7cb2c557..91370ba9f45 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/ClusterPermission.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/ClusterPermission.java @@ -6,6 +6,8 @@ package org.elasticsearch.shield.authz.permission; import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.transport.TransportRequest; import java.util.List; import java.util.function.Predicate; @@ -15,13 +17,13 @@ import java.util.function.Predicate; */ public interface ClusterPermission extends Permission { - boolean check(String action); + boolean check(String action, TransportRequest request, User user); public static class Core implements ClusterPermission { public static final Core NONE = new Core(ClusterPrivilege.NONE) { @Override - public boolean check(String action) { + public boolean check(String action, TransportRequest request, User user) { return false; } @@ -44,7 +46,7 @@ public interface ClusterPermission extends Permission { } @Override - public boolean check(String action) { + public boolean check(String action, TransportRequest request, User user) { return predicate.test(action); } @@ -63,12 +65,15 @@ public interface ClusterPermission extends Permission { } @Override - public boolean check(String action) { + public boolean check(String action, TransportRequest request, User user) { if (globals == null) { return false; } for (GlobalPermission global : globals) { - if (global.cluster().check(action)) { + if (global == null || global.cluster() == null) { + throw new RuntimeException(); + } + if (global.cluster().check(action, request, user)) { return true; } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/DefaultRole.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/DefaultRole.java new file mode 100644 index 00000000000..80bf9874a06 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/DefaultRole.java @@ -0,0 +1,57 @@ +/* + * 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.shield.authz.permission; + +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.action.user.AuthenticateAction; +import org.elasticsearch.shield.action.user.ChangePasswordAction; +import org.elasticsearch.shield.action.user.UserRequest; +import org.elasticsearch.shield.authz.permission.RunAsPermission.Core; +import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; +import org.elasticsearch.shield.authz.privilege.Privilege.Name; +import org.elasticsearch.transport.TransportRequest; + +/** + * A default role that will be applied to all users other than the internal {@link org.elasticsearch.shield.user.SystemUser}. This role + * grants access to actions that every user should be able to execute such as the ability to change their password and execute the + * authenticate endpoint to get information about themselves + */ +public class DefaultRole extends Role { + + private static final ClusterPermission.Core CLUSTER_PERMISSION = + new SameUserClusterPermission(ClusterPrivilege.get(new Name(ChangePasswordAction.NAME, AuthenticateAction.NAME))); + private static final IndicesPermission.Core INDICES_PERMISSION = IndicesPermission.Core.NONE; + private static final RunAsPermission.Core RUN_AS_PERMISSION = Core.NONE; + + public static final String NAME = "__default_role"; + public static final DefaultRole INSTANCE = new DefaultRole(); + + private DefaultRole() { + super(NAME, CLUSTER_PERMISSION, INDICES_PERMISSION, RUN_AS_PERMISSION); + } + + private static class SameUserClusterPermission extends ClusterPermission.Core { + + private SameUserClusterPermission(ClusterPrivilege privilege) { + super(privilege); + } + + @Override + public boolean check(String action, TransportRequest request, User user) { + final boolean actionAllowed = super.check(action, request, user); + if (actionAllowed) { + assert request instanceof UserRequest; + UserRequest userRequest = (UserRequest) request; + String[] usernames = userRequest.usernames(); + assert usernames != null && usernames.length == 1; + final String username = usernames[0]; + assert username != null; + return user.principal().equals(username); + } + return false; + } + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/IndicesPermission.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/IndicesPermission.java index e3bcfadb23a..3fab48e5d34 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/IndicesPermission.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/IndicesPermission.java @@ -61,6 +61,10 @@ public interface IndicesPermission extends Permission, Iterable groups) { + this(groups.toArray(new Group[groups.size()])); + } + public Core(Group... groups) { this.groups = groups; loadingFunction = (action) -> { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/KibanaRole.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/KibanaRole.java new file mode 100644 index 00000000000..b35d1cebfad --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/KibanaRole.java @@ -0,0 +1,31 @@ +/* + * 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.shield.authz.permission; + +import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; +import org.elasticsearch.shield.authz.privilege.Privilege.Name; + +/** + * + */ +public class KibanaRole extends Role { + + private static final String[] CLUSTER_PRIVILEGES = new String[] { "monitor" }; + private static final RoleDescriptor.IndicesPrivileges[] INDICES_PRIVILEGES = new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder().indices(".kibana" ).privileges("all").build() }; + + public static final String NAME = "kibana"; + public static final RoleDescriptor DESCRIPTOR = new RoleDescriptor(NAME, CLUSTER_PRIVILEGES, INDICES_PRIVILEGES, null); + public static final KibanaRole INSTANCE = new KibanaRole(); + + private KibanaRole() { + super(DESCRIPTOR.getName(), + new ClusterPermission.Core(ClusterPrivilege.get(new Name(DESCRIPTOR.getClusterPrivileges()))), + new IndicesPermission.Core(Role.Builder.convertFromIndicesPrivileges(DESCRIPTOR.getIndicesPrivileges())), + RunAsPermission.Core.NONE); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/Role.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/Role.java index 5496c04e826..e9b711e3b08 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/Role.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/Role.java @@ -11,6 +11,7 @@ import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; import org.elasticsearch.shield.authz.privilege.GeneralPrivilege; import org.elasticsearch.shield.authz.privilege.IndexPrivilege; import org.elasticsearch.shield.authz.privilege.Privilege; +import org.elasticsearch.shield.authz.privilege.Privilege.Name; import java.util.ArrayList; import java.util.Arrays; @@ -23,7 +24,7 @@ public class Role extends GlobalPermission { private final String name; - private Role(String name, ClusterPermission.Core cluster, IndicesPermission.Core indices, RunAsPermission.Core runAs) { + Role(String name, ClusterPermission.Core cluster, IndicesPermission.Core indices, RunAsPermission.Core runAs) { super(cluster, indices, runAs); this.name = name; } @@ -73,12 +74,7 @@ public class Role extends GlobalPermission { } else { this.cluster(ClusterPrivilege.get((new Privilege.Name(rd.getClusterPrivileges())))); } - for (RoleDescriptor.IndicesPrivileges iGroup : rd.getIndicesPrivileges()) { - this.add(iGroup.getFields() == null ? null : Arrays.asList(iGroup.getFields()), - iGroup.getQuery(), - IndexPrivilege.get(new Privilege.Name(iGroup.getPrivileges())), - iGroup.getIndices()); - } + groups.addAll(convertFromIndicesPrivileges(rd.getIndicesPrivileges())); String[] rdRunAs = rd.getRunAs(); if (rdRunAs != null && rdRunAs.length > 0) { this.runAs(new GeneralPrivilege(new Privilege.Name(rdRunAs), rdRunAs)); @@ -111,5 +107,17 @@ public class Role extends GlobalPermission { new IndicesPermission.Core(groups.toArray(new IndicesPermission.Group[groups.size()])); return new Role(name, cluster, indices, runAs); } + + static List convertFromIndicesPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges) { + List list = new ArrayList<>(indicesPrivileges.length); + for (RoleDescriptor.IndicesPrivileges privilege : indicesPrivileges) { + list.add(new IndicesPermission.Group(IndexPrivilege.get(new Privilege.Name(privilege.getPrivileges())), + privilege.getFields() == null ? null : Arrays.asList(privilege.getFields()), + privilege.getQuery(), + privilege.getIndices())); + + } + return list; + } } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/SuperuserRole.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/SuperuserRole.java new file mode 100644 index 00000000000..2aa54100f0f --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/SuperuserRole.java @@ -0,0 +1,31 @@ +/* + * 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.shield.authz.permission; + +import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; +import org.elasticsearch.shield.authz.privilege.GeneralPrivilege; +import org.elasticsearch.shield.authz.privilege.Privilege.Name; + +/** + * + */ +public class SuperuserRole extends Role { + + public static final String NAME = "superuser"; + public static final RoleDescriptor DESCRIPTOR = new RoleDescriptor(NAME, new String[] { "all" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build()}, + new String[] { "*" }); + public static final SuperuserRole INSTANCE = new SuperuserRole(); + + private SuperuserRole() { + super(DESCRIPTOR.getName(), + new ClusterPermission.Core(ClusterPrivilege.get(new Name(DESCRIPTOR.getClusterPrivileges()))), + new IndicesPermission.Core(Role.Builder.convertFromIndicesPrivileges(DESCRIPTOR.getIndicesPrivileges())), + new RunAsPermission.Core(GeneralPrivilege.ALL)); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/TransportClientRole.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/TransportClientRole.java new file mode 100644 index 00000000000..f748a3bd0c7 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/permission/TransportClientRole.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.authz.permission; + +import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; +import org.elasticsearch.shield.authz.privilege.Privilege.Name; + +/** + * Reserved role for the transport client + */ +public class TransportClientRole extends Role { + + public static final String NAME = "transport_client"; + private static final String[] CLUSTER_PRIVILEGES = new String[] { "transport_client" }; + + public static final RoleDescriptor DESCRIPTOR = new RoleDescriptor(NAME, CLUSTER_PRIVILEGES, null, null); + public static final TransportClientRole INSTANCE = new TransportClientRole(); + + private TransportClientRole() { + super(DESCRIPTOR.getName(), + new ClusterPermission.Core(ClusterPrivilege.get(new Name(DESCRIPTOR.getClusterPrivileges()))), + IndicesPermission.Core.NONE, RunAsPermission.Core.NONE); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/privilege/GeneralPrivilege.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/privilege/GeneralPrivilege.java index b1884688996..5968bfcdaea 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/privilege/GeneralPrivilege.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/privilege/GeneralPrivilege.java @@ -14,6 +14,7 @@ import dk.brics.automaton.BasicAutomata; public class GeneralPrivilege extends AbstractAutomatonPrivilege { public static final GeneralPrivilege NONE = new GeneralPrivilege(Name.NONE, BasicAutomata.makeEmpty()); + public static final GeneralPrivilege ALL = new GeneralPrivilege(Name.ALL, "*"); public GeneralPrivilege(String name, String... patterns) { super(name, patterns); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/CompositeRolesStore.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/CompositeRolesStore.java index a68fd493afa..b17bd4d39f7 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/CompositeRolesStore.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/CompositeRolesStore.java @@ -9,22 +9,30 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.shield.authz.permission.Role; /** - * A composite roles store that combines file-based and index-based roles - * lookups. Checks the file first, then the index. + * A composite roles store that combines built in roles, file-based roles, and index-based roles. Checks the built in roles first, then the + * file roles, and finally the index roles. */ public class CompositeRolesStore implements RolesStore { private final FileRolesStore fileRolesStore; private final NativeRolesStore nativeRolesStore; - + private final ReservedRolesStore reservedRolesStore; + @Inject - public CompositeRolesStore(FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore) { + public CompositeRolesStore(FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore, ReservedRolesStore reservedRolesStore) { this.fileRolesStore = fileRolesStore; this.nativeRolesStore = nativeRolesStore; + this.reservedRolesStore = reservedRolesStore; } public Role role(String role) { - // Try the file first, then the index if it isn't there + // builtins first + Role builtIn = reservedRolesStore.role(role); + if (builtIn != null) { + return builtIn; + } + + // Try the file next, then the index if it isn't there Role fileRole = fileRolesStore.role(role); if (fileRole != null) { return fileRole; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/FileRolesStore.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/FileRolesStore.java index 172226ccd3c..389c70254e0 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/FileRolesStore.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/FileRolesStore.java @@ -19,8 +19,6 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.yaml.YamlXContent; import org.elasticsearch.env.Environment; import org.elasticsearch.shield.Security; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.XPackUser; import org.elasticsearch.shield.authc.support.RefreshListener; import org.elasticsearch.shield.authz.RoleDescriptor; import org.elasticsearch.shield.authz.permission.Role; @@ -135,7 +133,7 @@ public class FileRolesStore extends AbstractLifecycleComponent imple for (String segment : roleSegments) { Role role = parseRole(segment, path, logger, resolvePermission, settings); if (role != null) { - if (SystemUser.ROLE_NAME.equals(role.name()) || XPackUser.ROLE.name().equals(role.name())) { + if (ReservedRolesStore.isReserved(role.name())) { logger.warn("role [{}] is reserved. the relevant role definition in the mapping file will be ignored", role.name()); } else { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/NativeRolesStore.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/NativeRolesStore.java index 7d7b59e3a1c..afb7d220080 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/NativeRolesStore.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/NativeRolesStore.java @@ -433,7 +433,7 @@ public class NativeRolesStore extends AbstractComponent implements RolesStore, C } private void clearRoleCache(final String role, ActionListener listener, Response response) { - ClearRolesCacheRequest request = new ClearRolesCacheRequest().roles(role); + ClearRolesCacheRequest request = new ClearRolesCacheRequest().names(role); securityClient.clearRolesCache(request, new ActionListener() { @Override public void onResponse(ClearRolesCacheResponse nodes) { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/ReservedRolesStore.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/ReservedRolesStore.java new file mode 100644 index 00000000000..ee71b73abb3 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/authz/store/ReservedRolesStore.java @@ -0,0 +1,96 @@ +/* + * 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.shield.authz.store; + +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.permission.KibanaRole; +import org.elasticsearch.shield.authz.permission.Role; +import org.elasticsearch.shield.authz.permission.SuperuserRole; +import org.elasticsearch.shield.authz.permission.TransportClientRole; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.SystemUser; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +/** + * + */ +public class ReservedRolesStore implements RolesStore { + + private final SecurityContext securityContext; + + @Inject + public ReservedRolesStore(SecurityContext securityContext) { + this.securityContext = securityContext; + } + + @Override + public Role role(String role) { + switch (role) { + case SuperuserRole.NAME: + return SuperuserRole.INSTANCE; + case TransportClientRole.NAME: + return TransportClientRole.INSTANCE; + 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())) { + return KibanaRole.INSTANCE; + } + return null; + default: + return null; + } + } + + public RoleDescriptor roleDescriptor(String role) { + switch (role) { + case SuperuserRole.NAME: + return SuperuserRole.DESCRIPTOR; + case TransportClientRole.NAME: + return TransportClientRole.DESCRIPTOR; + 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())) { + return KibanaRole.DESCRIPTOR; + } + return null; + default: + return null; + } + } + + public Collection roleDescriptors() { + if (KibanaUser.is(securityContext.getUser())) { + return Arrays.asList(SuperuserRole.DESCRIPTOR, TransportClientRole.DESCRIPTOR, KibanaRole.DESCRIPTOR); + } + return Arrays.asList(SuperuserRole.DESCRIPTOR, TransportClientRole.DESCRIPTOR); + } + + public static Set names() { + return Sets.newHashSet(SuperuserRole.NAME, KibanaRole.NAME, TransportClientRole.NAME); + } + + public static boolean isReserved(String role) { + switch (role) { + case SuperuserRole.NAME: + case KibanaRole.NAME: + case TransportClientRole.NAME: + case SystemUser.ROLE_NAME: + return true; + default: + return false; + } + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/client/SecurityClient.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/client/SecurityClient.java index 8277ab97b97..8fc7ec3b971 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/client/SecurityClient.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/client/SecurityClient.java @@ -29,6 +29,10 @@ import org.elasticsearch.shield.action.role.PutRoleAction; import org.elasticsearch.shield.action.role.PutRoleRequest; import org.elasticsearch.shield.action.role.PutRoleRequestBuilder; import org.elasticsearch.shield.action.role.PutRoleResponse; +import org.elasticsearch.shield.action.user.ChangePasswordAction; +import org.elasticsearch.shield.action.user.ChangePasswordRequest; +import org.elasticsearch.shield.action.user.ChangePasswordRequestBuilder; +import org.elasticsearch.shield.action.user.ChangePasswordResponse; import org.elasticsearch.shield.action.user.DeleteUserAction; import org.elasticsearch.shield.action.user.DeleteUserRequest; import org.elasticsearch.shield.action.user.DeleteUserRequestBuilder; @@ -147,6 +151,18 @@ public class SecurityClient { client.execute(PutUserAction.INSTANCE, request, listener); } + public ChangePasswordRequestBuilder prepareChangePassword(String username, char[] password) { + return new ChangePasswordRequestBuilder(client).username(username).password(password); + } + + public ChangePasswordRequestBuilder prepareChangePassword(String username, BytesReference source) throws IOException { + return new ChangePasswordRequestBuilder(client).username(username).source(source); + } + + public void changePassword(ChangePasswordRequest request, ActionListener listener) { + client.execute(ChangePasswordAction.INSTANCE, request, listener); + } + /** Role Management */ public GetRolesRequestBuilder prepareGetRoles(String... names) { diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/RestAuthenticateAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/RestAuthenticateAction.java index 3015633c457..99428b51b28 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/RestAuthenticateAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/RestAuthenticateAction.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.shield.rest.action; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.client.Client; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; @@ -16,36 +15,43 @@ 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.shield.SystemUser; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.XPackUser; -import org.elasticsearch.shield.authc.AuthenticationService; +import org.elasticsearch.rest.action.support.RestBuilderListener; +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.action.user.AuthenticateAction; +import org.elasticsearch.shield.action.user.AuthenticateRequest; +import org.elasticsearch.shield.action.user.AuthenticateResponse; import static org.elasticsearch.rest.RestRequest.Method.GET; public class RestAuthenticateAction extends BaseRestHandler { - private final AuthenticationService authenticationService; + private final SecurityContext securityContext; @Inject - public RestAuthenticateAction(Settings settings, RestController controller, Client client, - AuthenticationService authenticationService) { + public RestAuthenticateAction(Settings settings, RestController controller, Client client, SecurityContext securityContext) { super(settings, client); - this.authenticationService = authenticationService; - controller.registerHandler(GET, "/_shield/authenticate", this); + this.securityContext = securityContext; + controller.registerHandler(GET, "/_shield/authenticate", this); // deprecate + controller.registerHandler(GET, "/_shield/_authenticate", this); } @Override protected void handleRequest(RestRequest request, RestChannel channel, Client client) throws Exception { - // we should be authenticated at this point, but we call the authc service to retrieve the user from the context - User user = authenticationService.authenticate(request); + final User user = securityContext.getUser(); assert user != null; - if (SystemUser.is(user) || XPackUser.is(user)) { - throw new ElasticsearchSecurityException("the authenticate API cannot be used for the internal users"); - } - XContentBuilder builder = channel.newBuilder(); - user.toXContent(builder, ToXContent.EMPTY_PARAMS); - channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); + final String username = user.runAs() == null ? user.principal() : user.runAs().principal(); + + client.execute(AuthenticateAction.INSTANCE, new AuthenticateRequest(username), + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(AuthenticateResponse authenticateResponse, XContentBuilder builder) throws Exception { + authenticateResponse.user().toXContent(builder, ToXContent.EMPTY_PARAMS); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } } diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/role/RestClearRolesCacheAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/role/RestClearRolesCacheAction.java index f60f56c75ac..95460af5a7b 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/role/RestClearRolesCacheAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/role/RestClearRolesCacheAction.java @@ -40,7 +40,7 @@ public class RestClearRolesCacheAction extends BaseRestHandler { String[] roles = request.paramAsStringArrayOrEmptyIfAll("name"); - ClearRolesCacheRequest req = new ClearRolesCacheRequest().roles(roles); + ClearRolesCacheRequest req = new ClearRolesCacheRequest().names(roles); new SecurityClient(client).clearRolesCache(req, new RestBuilderListener(channel) { @Override diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestChangePasswordAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestChangePasswordAction.java new file mode 100644 index 00000000000..763591d0742 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestChangePasswordAction.java @@ -0,0 +1,59 @@ +/* + * 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.shield.rest.action.user; + +import org.elasticsearch.client.Client; +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.support.RestBuilderListener; +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.action.user.ChangePasswordResponse; +import org.elasticsearch.shield.client.SecurityClient; + +/** + */ +public class RestChangePasswordAction extends BaseRestHandler { + + private final SecurityContext securityContext; + + @Inject + public RestChangePasswordAction(Settings settings, Client client, RestController controller, SecurityContext securityContext) { + super(settings, client); + this.securityContext = securityContext; + controller.registerHandler(RestRequest.Method.POST, "/_shield/user/{username}/_password", this); + controller.registerHandler(RestRequest.Method.PUT, "/_shield/user/{username}/_password", this); + controller.registerHandler(RestRequest.Method.POST, "/_shield/user/_password", this); + controller.registerHandler(RestRequest.Method.PUT, "/_shield/user/_password", this); + } + + @Override + protected void handleRequest(RestRequest request, RestChannel channel, Client client) throws Exception { + final User user = securityContext.getUser(); + String username = request.param("username"); + if (username == null) { + username = user.runAs() == null ? user.principal() : user.runAs().principal();; + } + + new SecurityClient(client).prepareChangePassword(username, request.content()) + .refresh(request.paramAsBoolean("refresh", true)) + .execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(ChangePasswordResponse changePasswordResponse, XContentBuilder builder) throws + Exception { + return new BytesRestResponse(RestStatus.OK, channel.newBuilder().startObject().endObject()); + } + }); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestGetUsersAction.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestGetUsersAction.java index adbfd99c85e..93d6a1483e6 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestGetUsersAction.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/rest/action/user/RestGetUsersAction.java @@ -19,7 +19,7 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.support.RestBuilderListener; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.action.user.GetUsersResponse; import org.elasticsearch.shield.client.SecurityClient; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ClientTransportFilter.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ClientTransportFilter.java index 718cba28187..540319bfbca 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ClientTransportFilter.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ClientTransportFilter.java @@ -6,8 +6,8 @@ package org.elasticsearch.shield.transport; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.shield.SystemUser; import org.elasticsearch.shield.authc.AuthenticationService; +import org.elasticsearch.shield.user.SystemUser; import org.elasticsearch.transport.TransportRequest; import java.io.IOException; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ServerTransportFilter.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ServerTransportFilter.java index 22fe7d4d919..eb22fa17821 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ServerTransportFilter.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/transport/ServerTransportFilter.java @@ -8,7 +8,7 @@ package org.elasticsearch.shield.transport; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.action.ShieldActionMapper; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authc.pki.PkiRealm; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/AnonymousUser.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/AnonymousUser.java new file mode 100644 index 00000000000..e55ae3f5a95 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/AnonymousUser.java @@ -0,0 +1,87 @@ +/* + * 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.shield.user; + +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.shield.user.User.ReservedUser; + +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.shield.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). + */ +public class AnonymousUser extends ReservedUser { + + public static final String DEFAULT_ANONYMOUS_USERNAME = "_es_anonymous_user"; + public static final Setting USERNAME_SETTING = + new Setting<>(setting("authc.anonymous.username"), DEFAULT_ANONYMOUS_USERNAME, s -> s, Property.NodeScope); + 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); + } + + @Override + public String principal() { + return username; + } + + @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 void registerSettings(SettingsModule settingsModule) { + settingsModule.registerSetting(USERNAME_SETTING); + settingsModule.registerSetting(ROLES_SETTING); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/KibanaUser.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/KibanaUser.java new file mode 100644 index 00000000000..99a8f2cc88e --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/KibanaUser.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.shield.user; + +import org.elasticsearch.shield.authz.permission.KibanaRole; +import org.elasticsearch.shield.user.User.ReservedUser; + +/** + * + */ +public class KibanaUser extends ReservedUser { + + 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); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SystemUser.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/SystemUser.java similarity index 97% rename from elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SystemUser.java rename to elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/SystemUser.java index c453119d57e..9b88b280ff7 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/SystemUser.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/SystemUser.java @@ -3,7 +3,7 @@ * 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.shield; +package org.elasticsearch.shield.user; import org.elasticsearch.shield.authz.privilege.SystemPrivilege; diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/User.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/User.java similarity index 85% rename from elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/User.java rename to elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/User.java index 74c0b00f96b..b47aa1067d9 100644 --- a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/User.java +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/User.java @@ -3,7 +3,7 @@ * 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.shield; +package org.elasticsearch.shield.user; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.ParseField; @@ -13,6 +13,8 @@ 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.shield.authc.esnative.ReservedRealm; + import java.io.IOException; import java.lang.reflect.Array; @@ -26,6 +28,8 @@ import java.util.Map; */ public class User implements ToXContent { + static final String RESERVED_PREFIX = "_"; + private final String username; private final String[] roles; private final User runAs; @@ -49,6 +53,7 @@ public class User implements ToXContent { this.fullName = fullName; this.email = email; this.runAs = null; + verifyNoReservedMetadata(this.username, this.metadata); } public User(String username, String[] roles, String fullName, String email, Map metadata, User runAs) { @@ -62,6 +67,7 @@ public class User implements ToXContent { throw new ElasticsearchSecurityException("invalid run_as user"); } this.runAs = runAs; + verifyNoReservedMetadata(this.username, this.metadata); } /** @@ -158,25 +164,37 @@ public class User implements ToXContent { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(Fields.USERNAME.getPreferredName(), username); - builder.array(Fields.ROLES.getPreferredName(), roles); - builder.field(Fields.FULL_NAME.getPreferredName(), fullName); - builder.field(Fields.EMAIL.getPreferredName(), email); - builder.field(Fields.METADATA.getPreferredName(), metadata); + builder.field(Fields.USERNAME.getPreferredName(), principal()); + builder.array(Fields.ROLES.getPreferredName(), roles()); + builder.field(Fields.FULL_NAME.getPreferredName(), fullName()); + builder.field(Fields.EMAIL.getPreferredName(), email()); + builder.field(Fields.METADATA.getPreferredName(), metadata()); return builder.endObject(); } + void verifyNoReservedMetadata(String principal, Map metadata) { + if (this instanceof ReservedUser) { + return; + } + + for (String key : metadata.keySet()) { + if (key.startsWith(RESERVED_PREFIX)) { + throw new IllegalArgumentException("invalid user metadata. [" + key + "] is a reserved for internal uses"); + } + } + } + public static User readFrom(StreamInput input) throws IOException { if (input.readBoolean()) { String name = input.readString(); - switch (name) { - case SystemUser.NAME: - return SystemUser.INSTANCE; - case XPackUser.NAME: - return XPackUser.INSTANCE; - default: - throw new IllegalStateException("invalid internal user"); + if (SystemUser.NAME.equals(name)) { + return SystemUser.INSTANCE; } + User user = ReservedRealm.getUser(name); + if (user == null) { + throw new IllegalStateException("invalid internal user"); + } + return user; } String username = input.readString(); String[] roles = input.readStringArray(); @@ -199,9 +217,9 @@ public class User implements ToXContent { if (SystemUser.is(user)) { output.writeBoolean(true); output.writeString(SystemUser.NAME); - } if (XPackUser.is(user)) { + } else if (ReservedRealm.isReserved(user.principal())) { output.writeBoolean(true); - output.writeString(XPackUser.NAME); + output.writeString(user.principal()); } else { output.writeBoolean(false); output.writeString(user.username); @@ -259,6 +277,15 @@ public class User implements ToXContent { } } + abstract static class ReservedUser extends User { + + private static final String RESERVED_KEY = User.RESERVED_PREFIX + "reserved"; + + ReservedUser(String username, String... roles) { + super(username, roles, null, null, Collections.singletonMap(RESERVED_KEY, true)); + } + } + public interface Fields { ParseField USERNAME = new ParseField("username"); ParseField PASSWORD = new ParseField("password"); diff --git a/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/XPackUser.java b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/XPackUser.java new file mode 100644 index 00000000000..12a826ca789 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/main/java/org/elasticsearch/shield/user/XPackUser.java @@ -0,0 +1,38 @@ +/* + * 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.shield.user; + +import org.elasticsearch.shield.authz.permission.SuperuserRole; +import org.elasticsearch.shield.user.User.ReservedUser; + +/** + * XPack internal user that manages xpack. Has all cluster/indices permissions for watcher, + * shield and monitoring to operate. + */ +public class XPackUser extends ReservedUser { + + public static final String NAME = "elastic"; + public static final String ROLE_NAME = SuperuserRole.NAME; + public static final XPackUser INSTANCE = new XPackUser(); + + XPackUser() { + 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); + } +} diff --git a/elasticsearch/x-pack/shield/src/main/resources/security-index-template.json b/elasticsearch/x-pack/shield/src/main/resources/security-index-template.json index 42ec085e8e9..a5557edeb4c 100644 --- a/elasticsearch/x-pack/shield/src/main/resources/security-index-template.json +++ b/elasticsearch/x-pack/shield/src/main/resources/security-index-template.json @@ -88,6 +88,16 @@ "type" : "keyword" } } + }, + "reserved-user" : { + "dynamic" : "strict", + "properties" : { + "password": { + "type" : "keyword", + "index" : false, + "doc_values" : false + } + } } } } diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java index 0db655986c1..35b994848f5 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.action.realm.ClearRealmCacheRequest; import org.elasticsearch.shield.action.realm.ClearRealmCacheResponse; import org.elasticsearch.shield.authc.Realm; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java index 5ba86b58598..79342815753 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java @@ -142,7 +142,7 @@ public class ClearRolesCacheTests extends NativeRealmIntegTestCase { .execute(); assertThat(response.getStatusCode(), is(RestStatus.OK.getStatus())); } else { - securityClient.prepareClearRolesCache().roles(rolesToClear).get(); + securityClient.prepareClearRolesCache().names(rolesToClear).get(); } assertRolesAreCorrect(securityClient, toModify); diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/filter/ShieldActionFilterTests.java similarity index 97% rename from elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java rename to elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/filter/ShieldActionFilterTests.java index 4e835fe26e7..24e7c4feb34 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/filter/ShieldActionFilterTests.java @@ -3,7 +3,7 @@ * 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.shield.action; +package org.elasticsearch.shield.action.filter; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; @@ -12,9 +12,9 @@ import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.ActionFilterChain; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.action.interceptor.RequestInterceptor; +import org.elasticsearch.shield.action.ShieldActionMapper; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authz.AuthorizationService; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportDeleteRoleActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportDeleteRoleActionTests.java new file mode 100644 index 00000000000..1bba8339fb7 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportDeleteRoleActionTests.java @@ -0,0 +1,151 @@ +/* + * 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.shield.action.role; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.authz.store.NativeRolesStore; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +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; + +public class TransportDeleteRoleActionTests extends ESTestCase { + + public void testReservedRole() { + final String roleName = randomFrom(new ArrayList<>(ReservedRolesStore.names())); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + TransportDeleteRoleAction action = new TransportDeleteRoleAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class)); + + DeleteRoleRequest request = new DeleteRoleRequest(); + request.name(roleName); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteRoleResponse deleteRoleResponse) { + responseRef.set(deleteRoleResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(instanceOf(IllegalArgumentException.class))); + assertThat(throwableRef.get().getMessage(), containsString("is reserved and cannot be deleted")); + verifyZeroInteractions(rolesStore); + } + + public void testValidRole() { + final String roleName = randomFrom("admin", "dept_a", "restricted"); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + TransportDeleteRoleAction action = new TransportDeleteRoleAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class)); + + DeleteRoleRequest request = new DeleteRoleRequest(); + request.name(roleName); + + final boolean found = randomBoolean(); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(found); + return null; + } + }).when(rolesStore).deleteRole(eq(request), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteRoleResponse deleteRoleResponse) { + responseRef.set(deleteRoleResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().found(), is(found)); + assertThat(throwableRef.get(), is(nullValue())); + verify(rolesStore, times(1)).deleteRole(eq(request), any(ActionListener.class)); + } + + public void testException() { + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException()); + final String roleName = randomFrom("admin", "dept_a", "restricted"); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + TransportDeleteRoleAction action = new TransportDeleteRoleAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class)); + + DeleteRoleRequest request = new DeleteRoleRequest(); + request.name(roleName); + + final boolean found = randomBoolean(); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onFailure(t); + return null; + } + }).when(rolesStore).deleteRole(eq(request), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteRoleResponse deleteRoleResponse) { + responseRef.set(deleteRoleResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), is(sameInstance(t))); + verify(rolesStore, times(1)).deleteRole(eq(request), any(ActionListener.class)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportGetRolesActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportGetRolesActionTests.java new file mode 100644 index 00000000000..70cf6686771 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportGetRolesActionTests.java @@ -0,0 +1,337 @@ +/* + * 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.shield.action.role; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.permission.KibanaRole; +import org.elasticsearch.shield.authz.store.NativeRolesStore; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.AdditionalMatchers.aryEq; +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; + +public class TransportGetRolesActionTests extends ESTestCase { + + public void testReservedRoles() { + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + SecurityContext context = mock(SecurityContext.class); + TransportGetRolesAction action = new TransportGetRolesAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class), new ReservedRolesStore(context)); + + final boolean isKibanaUser = randomBoolean(); + if (isKibanaUser) { + when(context.getUser()).thenReturn(KibanaUser.INSTANCE); + } + final int size = randomIntBetween(1, ReservedRolesStore.names().size()); + final List names = randomSubsetOf(size, ReservedRolesStore.names()); + + final List expectedNames = new ArrayList<>(names); + if (isKibanaUser == false) { + expectedNames.remove(KibanaRole.NAME); + } + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener> listener = (ActionListener>) args[1]; + listener.onResponse(Collections.emptyList()); + return null; + } + }).when(rolesStore).getRoleDescriptors(aryEq(Strings.EMPTY_ARRAY), any(ActionListener.class)); + + GetRolesRequest request = new GetRolesRequest(); + request.names(names.toArray(Strings.EMPTY_ARRAY)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetRolesResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + if (isKibanaUser && expectedNames.isEmpty()) { + assertThat(responseRef.get().roles(), is(emptyArray())); + verify(rolesStore, times(1)).getRoleDescriptors(eq(Strings.EMPTY_ARRAY), any(ActionListener.class)); + } else { + List retrievedRoleNames = + Arrays.asList(responseRef.get().roles()).stream().map(RoleDescriptor::getName).collect(Collectors.toList()); + assertThat(retrievedRoleNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY))); + verifyZeroInteractions(rolesStore); + } + } + + public void testStoreRoles() { + final List storeRoleDescriptors = randomRoleDescriptors(); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + SecurityContext context = mock(SecurityContext.class); + TransportGetRolesAction action = new TransportGetRolesAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class), new ReservedRolesStore(context)); + + final boolean isKibanaUser = randomBoolean(); + if (isKibanaUser) { + when(context.getUser()).thenReturn(KibanaUser.INSTANCE); + } + + GetRolesRequest request = new GetRolesRequest(); + request.names(storeRoleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)); + + if (request.names().length == 1) { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + String requestedName = (String) args[0]; + ActionListener listener = (ActionListener) args[1]; + Optional rd = + storeRoleDescriptors.stream().filter(r -> r.getName().equals(requestedName)).findFirst(); + listener.onResponse(rd.get()); + return null; + } + }).when(rolesStore).getRoleDescriptor(eq(request.names()[0]), any(ActionListener.class)); + } else { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener> listener = (ActionListener>) args[1]; + listener.onResponse(storeRoleDescriptors); + return null; + } + }).when(rolesStore).getRoleDescriptors(aryEq(request.names()), any(ActionListener.class)); + } + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetRolesResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + List retrievedRoleNames = + Arrays.asList(responseRef.get().roles()).stream().map(RoleDescriptor::getName).collect(Collectors.toList()); + assertThat(retrievedRoleNames, containsInAnyOrder(request.names())); + } + + public void testGetAllOrMix() { + final boolean all = randomBoolean(); + final List storeRoleDescriptors = randomRoleDescriptors(); + final List storeNames = storeRoleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toList()); + final List reservedRoleNames = new ArrayList<>(ReservedRolesStore.names()); + + final List requestedNames = new ArrayList<>(); + List specificStoreNames = new ArrayList<>(); + if (all == false) { + requestedNames.addAll(randomSubsetOf(randomIntBetween(1, ReservedRolesStore.names().size()), ReservedRolesStore.names())); + specificStoreNames.addAll(randomSubsetOf(randomIntBetween(1, storeNames.size()), storeNames)); + requestedNames.addAll(specificStoreNames); + } + + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + SecurityContext context = mock(SecurityContext.class); + TransportGetRolesAction action = new TransportGetRolesAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class), new ReservedRolesStore(context)); + + final boolean isKibanaUser = randomBoolean(); + final List expectedNames = new ArrayList<>(); + if (all) { + expectedNames.addAll(reservedRoleNames); + expectedNames.addAll(storeNames); + } else { + expectedNames.addAll(requestedNames); + } + + if (isKibanaUser) { + when(context.getUser()).thenReturn(KibanaUser.INSTANCE); + } else { + expectedNames.remove(KibanaRole.NAME); + } + + GetRolesRequest request = new GetRolesRequest(); + request.names(requestedNames.toArray(Strings.EMPTY_ARRAY)); + + if (specificStoreNames.size() == 1) { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + String requestedName = (String) args[0]; + ActionListener listener = (ActionListener) args[1]; + Optional rd = + storeRoleDescriptors.stream().filter(r -> r.getName().equals(requestedName)).findFirst(); + listener.onResponse(rd.get()); + return null; + } + }).when(rolesStore).getRoleDescriptor(eq(specificStoreNames.get(0)), any(ActionListener.class)); + } else { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + String[] requestedNames = (String[]) args[0]; + ActionListener> listener = (ActionListener>) args[1]; + if (requestedNames.length == 0) { + listener.onResponse(storeRoleDescriptors); + } else { + List requestedNamesList = Arrays.asList(requestedNames); + listener.onResponse(storeRoleDescriptors.stream() + .filter(r -> requestedNamesList.contains(r.getName())) + .collect(Collectors.toList())); + } + return null; + } + }).when(rolesStore).getRoleDescriptors(aryEq(specificStoreNames.toArray(Strings.EMPTY_ARRAY)), any(ActionListener.class)); + } + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetRolesResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + List retrievedRoleNames = + Arrays.asList(responseRef.get().roles()).stream().map(RoleDescriptor::getName).collect(Collectors.toList()); + assertThat(retrievedRoleNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY))); + + if (all) { + verify(rolesStore, times(1)).getRoleDescriptors(aryEq(Strings.EMPTY_ARRAY), any(ActionListener.class)); + } else if (specificStoreNames.size() == 1) { + verify(rolesStore, times(1)).getRoleDescriptor(eq(specificStoreNames.get(0)), any(ActionListener.class)); + } else { + verify(rolesStore, times(1)) + .getRoleDescriptors(aryEq(specificStoreNames.toArray(Strings.EMPTY_ARRAY)), any(ActionListener.class)); + } + } + + public void testException() { + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException()); + final List storeRoleDescriptors = randomRoleDescriptors(); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + SecurityContext context = mock(SecurityContext.class); + TransportGetRolesAction action = new TransportGetRolesAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class), new ReservedRolesStore(context)); + + GetRolesRequest request = new GetRolesRequest(); + request.names(storeRoleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)); + + if (request.names().length == 1) { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onFailure(t); + return null; + } + }).when(rolesStore).getRoleDescriptor(eq(request.names()[0]), any(ActionListener.class)); + } else { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener> listener = (ActionListener>) args[1]; + listener.onFailure(t); + return null; + } + }).when(rolesStore).getRoleDescriptors(aryEq(request.names()), any(ActionListener.class)); + } + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetRolesResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), is(t)); + assertThat(responseRef.get(), is(nullValue())); + } + + private List randomRoleDescriptors() { + int size = scaledRandomIntBetween(1, 10); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(new RoleDescriptor("role_" + i, null, null, null)); + } + return list; + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportPutRoleActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportPutRoleActionTests.java new file mode 100644 index 00000000000..f4e64544fae --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/role/TransportPutRoleActionTests.java @@ -0,0 +1,151 @@ +/* + * 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.shield.action.role; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.authz.RoleDescriptor; +import org.elasticsearch.shield.authz.store.NativeRolesStore; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +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; + +public class TransportPutRoleActionTests extends ESTestCase { + + public void testReservedRole() { + final String roleName = randomFrom(new ArrayList<>(ReservedRolesStore.names())); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + TransportPutRoleAction action = new TransportPutRoleAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class)); + + PutRoleRequest request = new PutRoleRequest(); + request.name(roleName); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutRoleResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(instanceOf(IllegalArgumentException.class))); + assertThat(throwableRef.get().getMessage(), containsString("is reserved and cannot be modified")); + verifyZeroInteractions(rolesStore); + } + + public void testValidRole() { + final String roleName = randomFrom("admin", "dept_a", "restricted"); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + TransportPutRoleAction action = new TransportPutRoleAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class)); + + final boolean created = randomBoolean(); + PutRoleRequest request = new PutRoleRequest(); + request.name(roleName); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 3; + ActionListener listener = (ActionListener) args[2]; + listener.onResponse(created); + return null; + } + }).when(rolesStore).putRole(eq(request), any(RoleDescriptor.class), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutRoleResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().isCreated(), is(created)); + assertThat(throwableRef.get(), is(nullValue())); + verify(rolesStore, times(1)).putRole(eq(request), any(RoleDescriptor.class), any(ActionListener.class)); + } + + public void testException() { + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException()); + final String roleName = randomFrom("admin", "dept_a", "restricted"); + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + TransportPutRoleAction action = new TransportPutRoleAction(Settings.EMPTY, mock(ThreadPool.class), mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), rolesStore, mock(TransportService.class)); + + PutRoleRequest request = new PutRoleRequest(); + request.name(roleName); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assert args.length == 3; + ActionListener listener = (ActionListener) args[2]; + listener.onFailure(t); + return null; + } + }).when(rolesStore).putRole(eq(request), any(RoleDescriptor.class), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutRoleResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), is(sameInstance(t))); + verify(rolesStore, times(1)).putRole(eq(request), any(RoleDescriptor.class), any(ActionListener.class)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportAuthenticateActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportAuthenticateActionTests.java new file mode 100644 index 00000000000..20e553750bc --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportAuthenticateActionTests.java @@ -0,0 +1,111 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TransportAuthenticateActionTests extends ESTestCase { + + public void testSystemUser() { + SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getUser()).thenReturn(SystemUser.INSTANCE); + TransportAuthenticateAction action = new TransportAuthenticateAction(Settings.EMPTY, mock(ThreadPool.class), + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), + securityContext); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(new AuthenticateRequest(), new ActionListener() { + @Override + public void onResponse(AuthenticateResponse authenticateResponse) { + responseRef.set(authenticateResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), nullValue()); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is internal")); + } + + public void testNullUser() { + SecurityContext securityContext = mock(SecurityContext.class); + TransportAuthenticateAction action = new TransportAuthenticateAction(Settings.EMPTY, mock(ThreadPool.class), + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), + securityContext); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(new AuthenticateRequest(), new ActionListener() { + @Override + public void onResponse(AuthenticateResponse authenticateResponse) { + responseRef.set(authenticateResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), nullValue()); + assertThat(throwableRef.get(), instanceOf(ElasticsearchSecurityException.class)); + assertThat(throwableRef.get().getMessage(), containsString("did not find an authenticated user")); + } + + public void testValidUser() { + final User user = randomFrom(XPackUser.INSTANCE, KibanaUser.INSTANCE, new User("joe")); + SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getUser()).thenReturn(user); + TransportAuthenticateAction action = new TransportAuthenticateAction(Settings.EMPTY, mock(ThreadPool.class), + mock(TransportService.class), mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), + securityContext); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(new AuthenticateRequest(), new ActionListener() { + @Override + public void onResponse(AuthenticateResponse authenticateResponse) { + responseRef.set(authenticateResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), notNullValue()); + assertThat(responseRef.get().user(), sameInstance(user)); + assertThat(throwableRef.get(), nullValue()); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportChangePasswordActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportChangePasswordActionTests.java new file mode 100644 index 00000000000..ab51cb976ee --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportChangePasswordActionTests.java @@ -0,0 +1,187 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.junit.After; +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; + +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); + 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(AnonymousUser.INSTANCE.principal()); + request.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(ChangePasswordResponse changePasswordResponse) { + responseRef.set(changePasswordResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is anonymous and cannot be modified")); + verifyZeroInteractions(usersStore); + } + + public void testSystemUser() { + 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.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(ChangePasswordResponse changePasswordResponse) { + responseRef.set(changePasswordResponse); + } + + @Override + public void onFailure(Throwable 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() { + final User user = randomFrom(XPackUser.INSTANCE, KibanaUser.INSTANCE, new User("joe")); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + ChangePasswordRequest request = new ChangePasswordRequest(); + request.username(user.principal()); + request.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(null); + return null; + } + }).when(usersStore).changePassword(eq(request), any(ActionListener.class)); + TransportChangePasswordAction action = new TransportChangePasswordAction(Settings.EMPTY, mock(ThreadPool.class), + 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(ChangePasswordResponse changePasswordResponse) { + responseRef.set(changePasswordResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get(), instanceOf(ChangePasswordResponse.class)); + assertThat(throwableRef.get(), is(nullValue())); + verify(usersStore, times(1)).changePassword(eq(request), any(ActionListener.class)); + } + + public void testException() { + final User user = randomFrom(XPackUser.INSTANCE, KibanaUser.INSTANCE, new User("joe")); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + ChangePasswordRequest request = new ChangePasswordRequest(); + request.username(user.principal()); + request.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException(), new RuntimeException()); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onFailure(t); + return null; + } + }).when(usersStore).changePassword(eq(request), any(ActionListener.class)); + TransportChangePasswordAction action = new TransportChangePasswordAction(Settings.EMPTY, mock(ThreadPool.class), + 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(ChangePasswordResponse changePasswordResponse) { + responseRef.set(changePasswordResponse); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), sameInstance(t)); + verify(usersStore, times(1)).changePassword(eq(request), any(ActionListener.class)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportDeleteUserActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportDeleteUserActionTests.java new file mode 100644 index 00000000000..872f0ca8b0c --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportDeleteUserActionTests.java @@ -0,0 +1,207 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.junit.After; +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; + +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), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + DeleteUserRequest request = new DeleteUserRequest(AnonymousUser.INSTANCE.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is anonymous and cannot be deleted")); + verifyZeroInteractions(usersStore); + } + + public void testSystemUser() { + 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()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable 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 testReservedUser() { + final User reserved = randomFrom(ReservedRealm.users().toArray(new User[0])); + 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(reserved.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is reserved and cannot be deleted")); + verifyZeroInteractions(usersStore); + } + + public void testValidUser() { + final User user = new User("joe"); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportDeleteUserAction action = new TransportDeleteUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + final boolean found = randomBoolean(); + final DeleteUserRequest request = new DeleteUserRequest(user.principal()); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(found); + return null; + } + }).when(usersStore).deleteUser(eq(request), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().found(), is(found)); + assertThat(throwableRef.get(), is(nullValue())); + verify(usersStore, times(1)).deleteUser(eq(request), any(ActionListener.class)); + } + + public void testException() { + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException(), new RuntimeException()); + final User user = new User("joe"); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportDeleteUserAction action = new TransportDeleteUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + final DeleteUserRequest request = new DeleteUserRequest(user.principal()); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onFailure(t); + return null; + } + }).when(usersStore).deleteUser(eq(request), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(DeleteUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), sameInstance(t)); + verify(usersStore, times(1)).deleteUser(eq(request), any(ActionListener.class)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportGetUsersActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportGetUsersActionTests.java new file mode 100644 index 00000000000..8a967338c9d --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportGetUsersActionTests.java @@ -0,0 +1,329 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +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.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.junit.After; +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.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyArray; +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.AdditionalMatchers.aryEq; +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; + +public class TransportGetUsersActionTests 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 testAnonymousUser() { + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + GetUsersRequest request = new GetUsersRequest(); + request.usernames(AnonymousUser.INSTANCE.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetUsersResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + if (anonymousEnabled) { + assertThat(responseRef.get().users(), arrayContaining(AnonymousUser.INSTANCE)); + } else { + assertThat(responseRef.get().users(), emptyArray()); + } + verifyZeroInteractions(usersStore); + } + + public void testSystemUser() { + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + GetUsersRequest request = new GetUsersRequest(); + request.usernames(SystemUser.INSTANCE.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetUsersResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is internal")); + assertThat(responseRef.get(), is(nullValue())); + verifyZeroInteractions(usersStore); + } + + 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); + TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + GetUsersRequest request = new GetUsersRequest(); + request.usernames(names.toArray(new String[names.size()])); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetUsersResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().users(), arrayContaining(reservedUsers.toArray(new User[reservedUsers.size()]))); + verifyZeroInteractions(usersStore); + } + + 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); + TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + GetUsersRequest request = new GetUsersRequest(); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener> listener = (ActionListener>) args[1]; + listener.onResponse(storeUsers); + return null; + } + }).when(usersStore).getUsers(eq(Strings.EMPTY_ARRAY), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetUsersResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + final List expectedList = new ArrayList<>(); + expectedList.addAll(ReservedRealm.users()); + expectedList.addAll(storeUsers); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().users(), arrayContaining(expectedList.toArray(new User[expectedList.size()]))); + verify(usersStore, times(1)).getUsers(aryEq(Strings.EMPTY_ARRAY), any(ActionListener.class)); + } + + public void testGetStoreOnlyUsers() { + final List storeUsers = + randomFrom(Collections.singletonList(new User("joe")), Arrays.asList(new User("jane"), new User("fred")), randomUsers()); + 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)); + + GetUsersRequest request = new GetUsersRequest(); + request.usernames(storeUsernames); + if (storeUsernames.length > 1) { + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener> listener = (ActionListener>) args[1]; + listener.onResponse(storeUsers); + return null; + } + }).when(usersStore).getUsers(aryEq(storeUsernames), any(ActionListener.class)); + } else { + assertThat(storeUsernames.length, is(1)); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(storeUsers.get(0)); + return null; + } + }).when(usersStore).getUser(eq(storeUsernames[0]), any(ActionListener.class)); + } + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetUsersResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + final List expectedList = new ArrayList<>(); + expectedList.addAll(storeUsers); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().users(), arrayContaining(expectedList.toArray(new User[expectedList.size()]))); + if (storeUsers.size() > 1) { + verify(usersStore, times(1)).getUsers(aryEq(storeUsernames), any(ActionListener.class)); + } else { + verify(usersStore, times(1)).getUser(eq(storeUsernames[0]), any(ActionListener.class)); + } + } + + public void testException() { + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException(), new ValidationException()); + final List storeUsers = + randomFrom(Collections.singletonList(new User("joe")), Arrays.asList(new User("jane"), new User("fred")), randomUsers()); + 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)); + + GetUsersRequest request = new GetUsersRequest(); + request.usernames(storeUsernames); + if (storeUsernames.length > 1) { + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener> listener = (ActionListener>) args[1]; + listener.onFailure(t); + return null; + } + }).when(usersStore).getUsers(aryEq(storeUsernames), any(ActionListener.class)); + } else { + assertThat(storeUsernames.length, is(1)); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onFailure(t); + return null; + } + }).when(usersStore).getUser(eq(storeUsernames[0]), any(ActionListener.class)); + } + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(GetUsersResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), is(sameInstance(t))); + assertThat(responseRef.get(), is(nullValue())); + if (request.usernames().length == 1) { + verify(usersStore, times(1)).getUser(eq(request.usernames()[0]), any(ActionListener.class)); + } else { + verify(usersStore, times(1)).getUsers(aryEq(storeUsernames), any(ActionListener.class)); + } + } + + private List randomUsers() { + int size = scaledRandomIntBetween(3, 16); + List users = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + users.add(new User("user_" + i, randomAsciiOfLengthBetween(4, 12))); + } + return users; + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportPutUserActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportPutUserActionTests.java new file mode 100644 index 00000000000..d428eb29741 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/action/user/TransportPutUserActionTests.java @@ -0,0 +1,219 @@ +/* + * 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.shield.action.user; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +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.shield.authc.esnative.NativeUsersStore; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.junit.After; +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; + +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); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + PutUserRequest request = new PutUserRequest(); + request.username(AnonymousUser.INSTANCE.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is anonymous and cannot be modified")); + verifyZeroInteractions(usersStore); + } + + public void testSystemUser() { + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + PutUserRequest request = new PutUserRequest(); + request.username(SystemUser.INSTANCE.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable 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 testReservedUser() { + final User reserved = randomFrom(ReservedRealm.users().toArray(new User[0])); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + PutUserRequest request = new PutUserRequest(); + request.username(reserved.principal()); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), instanceOf(IllegalArgumentException.class)); + assertThat(throwableRef.get().getMessage(), containsString("is reserved and only the password")); + verifyZeroInteractions(usersStore); + } + + public void testValidUser() { + final User user = new User("joe"); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + final boolean isCreate = randomBoolean(); + final PutUserRequest request = new PutUserRequest(); + request.username(user.principal()); + if (isCreate) { + request.passwordHash(Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()))); + } + final boolean created = isCreate ? randomBoolean() : false; // updates should always return false for create + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(created); + return null; + } + }).when(usersStore).putUser(eq(request), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(notNullValue())); + assertThat(responseRef.get().created(), is(created)); + assertThat(throwableRef.get(), is(nullValue())); + verify(usersStore, times(1)).putUser(eq(request), any(ActionListener.class)); + } + + public void testException() { + final Throwable t = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException(), new ValidationException()); + final User user = new User("joe"); + NativeUsersStore usersStore = mock(NativeUsersStore.class); + TransportPutUserAction action = new TransportPutUserAction(Settings.EMPTY, mock(ThreadPool.class), + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), usersStore, mock(TransportService.class)); + + final PutUserRequest request = new PutUserRequest(); + request.username(user.principal()); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + assert args.length == 2; + ActionListener listener = (ActionListener) args[1]; + listener.onFailure(t); + return null; + } + }).when(usersStore).putUser(eq(request), any(ActionListener.class)); + + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(request, new ActionListener() { + @Override + public void onResponse(PutUserResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Throwable e) { + throwableRef.set(e); + } + }); + + assertThat(responseRef.get(), is(nullValue())); + assertThat(throwableRef.get(), is(notNullValue())); + assertThat(throwableRef.get(), sameInstance(t)); + verify(usersStore, times(1)).putUser(eq(request), any(ActionListener.class)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/AuditTrailServiceTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/AuditTrailServiceTests.java index e038447dec8..4c593d2e70b 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/AuditTrailServiceTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/AuditTrailServiceTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield.audit; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.transport.filter.IPFilter; import org.elasticsearch.shield.transport.filter.ShieldIpFilterRule; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/index/IndexAuditTrailTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/index/IndexAuditTrailTests.java index 8d00afc6677..fe71c43179b 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/index/IndexAuditTrailTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/index/IndexAuditTrailTests.java @@ -25,8 +25,8 @@ import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.search.SearchHit; import org.elasticsearch.shield.Security; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.transport.filter.IPFilter; import org.elasticsearch.shield.transport.filter.ShieldIpFilterRule; @@ -68,8 +68,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrailTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrailTests.java index 36b5c854d1d..521f9dc495e 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrailTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/audit/logfile/LoggingAuditTrailTests.java @@ -19,8 +19,8 @@ import org.elasticsearch.common.transport.LocalTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.audit.logfile.CapturingLogger.Level; import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.rest.RemoteHostHeader; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/AnonymousUserHolderTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/AnonymousUserHolderTests.java deleted file mode 100644 index 5c8c155f34a..00000000000 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/AnonymousUserHolderTests.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.shield.authc; - -import org.elasticsearch.common.io.stream.ByteBufferStreamInput; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.shield.User; -import org.elasticsearch.test.ESTestCase; - -import java.nio.ByteBuffer; - -import static org.hamcrest.Matchers.arrayContainingInAnyOrder; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; - -public class AnonymousUserHolderTests extends ESTestCase { - public void testResolveAnonymousUser() throws Exception { - Settings settings = Settings.builder() - .put(AnonymousService.USERNAME_SETTING.getKey(), "anonym1") - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3") - .build(); - User user = AnonymousService.resolveAnonymousUser(settings); - assertThat(user, notNullValue()); - assertThat(user.principal(), equalTo("anonym1")); - assertThat(user.roles(), arrayContainingInAnyOrder("r1", "r2", "r3")); - - settings = Settings.builder() - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3") - .build(); - user = AnonymousService.resolveAnonymousUser(settings); - assertThat(user, notNullValue()); - assertThat(user.principal(), equalTo(AnonymousService.ANONYMOUS_USERNAME)); - assertThat(user.roles(), arrayContainingInAnyOrder("r1", "r2", "r3")); - } - - public void testResolveAnonymousUser_NoSettings() throws Exception { - Settings settings = randomBoolean() ? - Settings.EMPTY : - Settings.builder().put(AnonymousService.USERNAME_SETTING.getKey(), "user1").build(); - User user = AnonymousService.resolveAnonymousUser(settings); - assertThat(user, nullValue()); - } - - public void testWhenAnonymousDisabled() { - AnonymousService anonymousService = new AnonymousService(Settings.EMPTY); - assertThat(anonymousService.enabled(), is(false)); - assertThat(anonymousService.isAnonymous(new User(randomAsciiOfLength(10), randomAsciiOfLength(5))), is(false)); - assertThat(anonymousService.anonymousUser(), nullValue()); - assertThat(anonymousService.authorizationExceptionsEnabled(), is(true)); - } - - public void testWhenAnonymousEnabled() throws Exception { - Settings settings = Settings.builder() - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3") - .build(); - AnonymousService anonymousService = new AnonymousService(settings); - assertThat(anonymousService.enabled(), is(true)); - assertThat(anonymousService.anonymousUser(), notNullValue()); - assertThat(anonymousService.isAnonymous(anonymousService.anonymousUser()), is(true)); - assertThat(anonymousService.authorizationExceptionsEnabled(), is(true)); - - // make sure check works with serialization - BytesStreamOutput output = new BytesStreamOutput(); - User.writeTo(anonymousService.anonymousUser(), output); - User anonymousSerialized = User.readFrom(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytes()))); - assertThat(anonymousService.isAnonymous(anonymousSerialized), is(true)); - } - - public void testDisablingAuthorizationExceptions() { - Settings settings = Settings.builder() - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3") - .put(AnonymousService.SETTING_AUTHORIZATION_EXCEPTION_ENABLED.getKey(), false) - .build(); - AnonymousService holder = new AnonymousService(settings); - assertThat(holder.authorizationExceptionsEnabled(), is(false)); - } -} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/InternalAuthenticationServiceTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/InternalAuthenticationServiceTests.java index 3c069eac172..7d47ed264f8 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/InternalAuthenticationServiceTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/InternalAuthenticationServiceTests.java @@ -15,9 +15,11 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.audit.AuditTrail; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.crypto.CryptoService; @@ -71,7 +73,6 @@ public class InternalAuthenticationServiceTests extends ESTestCase { AuditTrail auditTrail; AuthenticationToken token; CryptoService cryptoService; - AnonymousService anonymousService; ThreadPool threadPool; ThreadContext threadContext; @@ -87,7 +88,8 @@ public class InternalAuthenticationServiceTests extends ESTestCase { Settings settings = Settings.builder().put("path.home", createTempDir()).build(); ShieldLicenseState shieldLicenseState = mock(ShieldLicenseState.class); when(shieldLicenseState.customRealmsEnabled()).thenReturn(true); - realms = new Realms(Settings.EMPTY, new Environment(settings), Collections.emptyMap(), shieldLicenseState) { + realms = new Realms(Settings.EMPTY, new Environment(settings), Collections.emptyMap(), shieldLicenseState, + mock(ReservedRealm.class)) { @Override protected void doStart() { @@ -100,11 +102,11 @@ public class InternalAuthenticationServiceTests extends ESTestCase { cryptoService = mock(CryptoService.class); auditTrail = mock(AuditTrail.class); - anonymousService = mock(AnonymousService.class); threadPool = mock(ThreadPool.class); threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); - service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService, + AnonymousUser.initialize(Settings.EMPTY); + service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); } @@ -344,7 +346,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase { InternalMessage message1 = new InternalMessage(); ThreadContext threadContext1 = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext1); - service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService, + service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); threadContext1.putTransient(InternalAuthenticationService.USER_KEY, @@ -358,7 +360,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase { // checking authentication from the user header threadContext1 = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext1); - service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService, + service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); threadContext1.putHeader(InternalAuthenticationService.USER_KEY, threadContext.getHeader(InternalAuthenticationService.USER_KEY)); when(cryptoService.unsignAndVerify("_signed_user")).thenReturn(InternalAuthenticationService.encodeUser(user1, null)); @@ -370,7 +372,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase { threadContext1.readHeaders(input); when(threadPool.getThreadContext()).thenReturn(threadContext1); - service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService, + service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); user = service.authenticate("_action", new InternalMessage(), SystemUser.INSTANCE); assertThat(user, equalTo(user1)); @@ -379,7 +381,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase { public void testAutheticateTransportContextAndHeaderNoSigning() throws Exception { Settings settings = Settings.builder().put(InternalAuthenticationService.SIGN_USER_HEADER.getKey(), false).build(); - service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, anonymousService, + service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); User user1 = new User("username", "r1", "r2"); @@ -398,7 +400,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase { InternalMessage message1 = new InternalMessage(); ThreadContext threadContext1 = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext1); - service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService, + service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); threadContext1.putTransient(InternalAuthenticationService.USER_KEY, threadContext.getTransient(InternalAuthenticationService.USER_KEY)); @@ -419,7 +421,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase { threadContext1.readHeaders(input); when(threadPool.getThreadContext()).thenReturn(threadContext1); - service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, anonymousService, + service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); user = service.authenticate("_action", new InternalMessage(), SystemUser.INSTANCE); assertThat(user, equalTo(user1)); @@ -470,16 +472,16 @@ public class InternalAuthenticationServiceTests extends ESTestCase { } public void testAnonymousUserRest() throws Exception { - String username = randomBoolean() ? AnonymousService.ANONYMOUS_USERNAME : "user1"; + String username = randomBoolean() ? AnonymousUser.DEFAULT_ANONYMOUS_USERNAME : "user1"; Settings.Builder builder = Settings.builder() - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3"); - if (username != AnonymousService.ANONYMOUS_USERNAME) { - builder.put(AnonymousService.USERNAME_SETTING.getKey(), username); + .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3"); + if (username.equals(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME) == false) { + builder.put(AnonymousUser.USERNAME_SETTING.getKey(), username); } Settings settings = builder.build(); - AnonymousService holder = new AnonymousService(settings); - service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, holder, - new DefaultAuthenticationFailureHandler(), threadPool); + AnonymousUser.initialize(settings); + service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), + threadPool); RestRequest request = new FakeRestRequest(); @@ -494,24 +496,26 @@ public class InternalAuthenticationServiceTests extends ESTestCase { public void testAnonymousUserTransportNoDefaultUser() throws Exception { Settings settings = Settings.builder() - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3") + .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3") .build(); - service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new AnonymousService(settings), + AnonymousUser.initialize(settings); + service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); InternalMessage message = new InternalMessage(); User user = service.authenticate("_action", message, null); assertThat(user, notNullValue()); - assertThat(user.principal(), equalTo(AnonymousService.ANONYMOUS_USERNAME)); + assertThat(user.principal(), equalTo(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME)); assertThat(user.roles(), arrayContainingInAnyOrder("r1", "r2", "r3")); } public void testAnonymousUserTransportWithDefaultUser() throws Exception { Settings settings = Settings.builder() - .putArray(AnonymousService.ROLES_SETTING.getKey(), "r1", "r2", "r3") + .putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3") .build(); - service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new AnonymousService(settings), + AnonymousUser.initialize(settings); + service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new DefaultAuthenticationFailureHandler(), threadPool); InternalMessage message = new InternalMessage(); diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/RealmsTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/RealmsTests.java index cb8d6f0ab6c..c9e5f0ea54a 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/RealmsTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/RealmsTests.java @@ -8,7 +8,8 @@ package org.elasticsearch.shield.authc; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.authc.esnative.ReservedRealm; import org.elasticsearch.shield.authc.esnative.NativeRealm; import org.elasticsearch.shield.authc.file.FileRealm; import org.elasticsearch.shield.authc.ldap.LdapRealm; @@ -27,7 +28,6 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -38,6 +38,7 @@ import static org.mockito.Mockito.when; public class RealmsTests extends ESTestCase { private Map factories; private ShieldLicenseState shieldLicenseState; + private ReservedRealm reservedRealm; @Before public void init() throws Exception { @@ -49,6 +50,7 @@ public class RealmsTests extends ESTestCase { factories.put("type_" + i, factory); } shieldLicenseState = mock(ShieldLicenseState.class); + reservedRealm = mock(ReservedRealm.class); when(shieldLicenseState.customRealmsEnabled()).thenReturn(true); } @@ -68,10 +70,17 @@ public class RealmsTests extends ESTestCase { } Settings settings = builder.build(); Environment env = new Environment(settings); - Realms realms = new Realms(settings, env, factories, shieldLicenseState); + Realms realms = new Realms(settings, env, factories, shieldLicenseState, reservedRealm); realms.start(); + + Iterator iterator = realms.iterator(); + assertThat(iterator.hasNext(), is(true)); + Realm realm = iterator.next(); + assertThat(realm, is(reservedRealm)); + int i = 0; - for (Realm realm : realms) { + while (iterator.hasNext()) { + realm = iterator.next(); assertThat(realm.order(), equalTo(i)); int index = orderToIndex.get(i); assertThat(realm.type(), equalTo("type_" + index)); @@ -90,7 +99,7 @@ public class RealmsTests extends ESTestCase { .build(); Environment env = new Environment(settings); try { - new Realms(settings, env, factories, shieldLicenseState).start(); + new Realms(settings, env, factories, shieldLicenseState, reservedRealm).start(); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), containsString("multiple [file] realms are configured")); @@ -99,18 +108,20 @@ public class RealmsTests extends ESTestCase { public void testWithEmptySettings() throws Exception { Realms realms = new Realms(Settings.EMPTY, new Environment(Settings.builder().put("path.home", createTempDir()).build()), - factories, shieldLicenseState); + factories, shieldLicenseState, reservedRealm); realms.start(); Iterator iter = realms.iterator(); assertThat(iter.hasNext(), is(true)); Realm realm = iter.next(); - assertThat(realm, notNullValue()); - assertThat(realm.type(), equalTo(NativeRealm.TYPE)); - assertThat(realm.name(), equalTo("default_" + NativeRealm.TYPE)); + assertThat(realm, is(reservedRealm)); assertThat(iter.hasNext(), is(true)); realm = iter.next(); assertThat(realm.type(), equalTo(FileRealm.TYPE)); assertThat(realm.name(), equalTo("default_" + FileRealm.TYPE)); + assertThat(iter.hasNext(), is(true)); + realm = iter.next(); + assertThat(realm.type(), equalTo(NativeRealm.TYPE)); + assertThat(realm.name(), equalTo("default_" + NativeRealm.TYPE)); assertThat(iter.hasNext(), is(false)); } @@ -130,11 +141,17 @@ public class RealmsTests extends ESTestCase { } Settings settings = builder.build(); Environment env = new Environment(settings); - Realms realms = new Realms(settings, env, factories, shieldLicenseState); + Realms realms = new Realms(settings, env, factories, shieldLicenseState, reservedRealm); realms.start(); - int i = 0; + // this is the iterator when licensed - for (Realm realm : realms) { + Iterator iter = realms.iterator(); + assertThat(iter.hasNext(), is(true)); + Realm realm = iter.next(); + assertThat(realm, is(reservedRealm)); + int i = 0; + while (iter.hasNext()) { + realm = iter.next(); assertThat(realm.order(), equalTo(i)); int index = orderToIndex.get(i); assertThat(realm.type(), equalTo("type_" + index)); @@ -142,13 +159,21 @@ public class RealmsTests extends ESTestCase { i++; } - i = 0; when(shieldLicenseState.customRealmsEnabled()).thenReturn(false); - for (Realm realm : realms) { - assertThat(realm.type, isOneOf(FileRealm.TYPE, NativeRealm.TYPE)); - i++; - } - assertThat(i, is(2)); + + iter = realms.iterator(); + assertThat(iter.hasNext(), is(true)); + realm = iter.next(); + assertThat(realm, is(reservedRealm)); + assertThat(iter.hasNext(), is(true)); + realm = iter.next(); + assertThat(realm.type(), equalTo(FileRealm.TYPE)); + assertThat(realm.name(), equalTo("default_" + FileRealm.TYPE)); + assertThat(iter.hasNext(), is(true)); + realm = iter.next(); + assertThat(realm.type(), equalTo(NativeRealm.TYPE)); + assertThat(realm.name(), equalTo("default_" + NativeRealm.TYPE)); + assertThat(iter.hasNext(), is(false)); } public void testUnlicensedWithInternalRealms() throws Exception { @@ -162,20 +187,31 @@ public class RealmsTests extends ESTestCase { .put("xpack.security.authc.realms.custom.order", "1"); Settings settings = builder.build(); Environment env = new Environment(settings); - Realms realms = new Realms(settings, env, factories, shieldLicenseState); + Realms realms = new Realms(settings, env, factories, shieldLicenseState, reservedRealm); realms.start(); + Iterator iter = realms.iterator(); + assertThat(iter.hasNext(), is(true)); + Realm realm = iter.next(); + assertThat(realm, is(reservedRealm)); + int i = 0; // this is the iterator when licensed List types = new ArrayList<>(); - for (Realm realm : realms) { + while (iter.hasNext()) { + realm = iter.next(); i++; types.add(realm.type()); } assertThat(types, contains("ldap", "type_0")); - i = 0; when(shieldLicenseState.customRealmsEnabled()).thenReturn(false); - for (Realm realm : realms) { + iter = realms.iterator(); + assertThat(iter.hasNext(), is(true)); + realm = iter.next(); + assertThat(realm, is(reservedRealm)); + i = 0; + while (iter.hasNext()) { + realm = iter.next(); assertThat(realm.type, is("ldap")); i++; } @@ -203,22 +239,25 @@ public class RealmsTests extends ESTestCase { } Settings settings = builder.build(); Environment env = new Environment(settings); - Realms realms = new Realms(settings, env, factories, shieldLicenseState); + Realms realms = new Realms(settings, env, factories, shieldLicenseState, reservedRealm); realms.start(); Iterator iterator = realms.iterator(); + Realm realm = iterator.next(); + assertThat(realm, is(reservedRealm)); + assertThat(iterator.hasNext(), is(true)); int count = 0; while (iterator.hasNext()) { - Realm realm = iterator.next(); + realm = iterator.next(); Integer index = orderToIndex.get(realm.order()); if (index == null) { // Default realms are inserted when factories size is 1 and enabled is false - assertThat(realm.type(), equalTo(NativeRealm.TYPE)); - assertThat(realm.name(), equalTo("default_" + NativeRealm.TYPE)); - assertThat(iterator.hasNext(), is(true)); - realm = iterator.next(); assertThat(realm.type(), equalTo(FileRealm.TYPE)); assertThat(realm.name(), equalTo("default_" + FileRealm.TYPE)); + assertThat(iterator.hasNext(), is(true)); + realm = iterator.next(); + assertThat(realm.type(), equalTo(NativeRealm.TYPE)); + assertThat(realm.name(), equalTo("default_" + NativeRealm.TYPE)); assertThat(iterator.hasNext(), is(false)); } else { assertThat(realm.type(), equalTo("type_" + index)); diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryRealmTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryRealmTests.java index c060bf59846..f99c7b993c3 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryRealmTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryRealmTests.java @@ -14,7 +14,7 @@ import com.unboundid.ldap.sdk.schema.Schema; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.shield.authc.support.DnRoleMapper; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/NativeRealmIntegTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/NativeRealmIntegTests.java index 5fc13f76fce..432b1726b29 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/NativeRealmIntegTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/NativeRealmIntegTests.java @@ -13,9 +13,19 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.shield.ShieldTemplateService; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.action.user.AuthenticateAction; +import org.elasticsearch.shield.action.user.AuthenticateRequest; +import org.elasticsearch.shield.action.user.AuthenticateResponse; +import org.elasticsearch.shield.action.user.ChangePasswordResponse; +import org.elasticsearch.shield.authz.permission.KibanaRole; +import org.elasticsearch.shield.authz.permission.SuperuserRole; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.action.role.DeleteRoleResponse; import org.elasticsearch.shield.action.role.GetRolesResponse; import org.elasticsearch.shield.action.user.DeleteUserResponse; @@ -24,10 +34,13 @@ import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authz.RoleDescriptor; import org.elasticsearch.shield.authz.permission.Role; import org.elasticsearch.shield.client.SecurityClient; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.test.NativeRealmIntegTestCase; import org.elasticsearch.test.ShieldSettingsSource; +import org.junit.BeforeClass; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -42,6 +55,23 @@ import static org.hamcrest.Matchers.notNullValue; */ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { + private static boolean anonymousEnabled; + + @BeforeClass + public static void init() { + anonymousEnabled = randomBoolean(); + } + + @Override + public Settings nodeSettings(int nodeOrdinal) { + if (anonymousEnabled) { + return Settings.builder().put(super.nodeSettings(nodeOrdinal)) + .put(AnonymousUser.ROLES_SETTING.getKey(), ShieldSettingsSource.DEFAULT_ROLE) + .build(); + } + return super.nodeSettings(nodeOrdinal); + } + public void testDeletingNonexistingUserAndRole() throws Exception { SecurityClient c = securityClient(); DeleteUserResponse resp = c.prepareDeleteUser("joe").get(); @@ -60,6 +90,8 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { public void testAddAndGetUser() throws Exception { SecurityClient c = securityClient(); + final List existingUsers = Arrays.asList(c.prepareGetUsers().get().users()); + final int existing = existingUsers.size(); logger.error("--> creating user"); c.preparePutUser("joe", "s3kirt".toCharArray(), "role1", "user").get(); logger.error("--> waiting for .shield index"); @@ -76,10 +108,12 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { c.preparePutUser("joe3", "s3kirt3".toCharArray(), "role3", "user").get(); GetUsersResponse allUsersResp = c.prepareGetUsers().get(); assertTrue("users should exist", allUsersResp.hasUsers()); - assertEquals("should be 3 users total", 3, allUsersResp.users().length); + assertEquals("should be " + (3 + existing) + " users total", 3 + existing, allUsersResp.users().length); List names = new ArrayList<>(3); for (User u : allUsersResp.users()) { - names.add(u.principal()); + if (existingUsers.contains(u) == false) { + names.add(u.principal()); + } } CollectionUtil.timSort(names); assertArrayEquals(new String[] { "joe", "joe2", "joe3" }, names.toArray(Strings.EMPTY_ARRAY)); @@ -104,6 +138,8 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { public void testAddAndGetRole() throws Exception { SecurityClient c = securityClient(); + final List existingRoles = Arrays.asList(c.prepareGetRoles().get().roles()); + final int existing = existingRoles.size(); logger.error("--> creating role"); c.preparePutRole("test_role") .cluster("all", "none") @@ -135,9 +171,9 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { logger.info("--> retrieving all roles"); GetRolesResponse allRolesResp = c.prepareGetRoles().get(); assertTrue("roles should exist", allRolesResp.hasRoles()); - assertEquals("should be 3 roles total", 3, allRolesResp.roles().length); + assertEquals("should be " + (3 + existing) + " roles total", 3 + existing, allRolesResp.roles().length); - logger.info("--> retrieving all roles"); + logger.info("--> retrieving test_role and test_role3"); GetRolesResponse someRolesResp = c.prepareGetRoles().names("test_role", "test_role3").get(); assertTrue("roles should exist", someRolesResp.hasRoles()); assertEquals("should be 2 roles total", 2, someRolesResp.roles().length); @@ -277,7 +313,7 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { GetRolesResponse getRolesResponse = c.prepareGetRoles().names("test_role").get(); assertTrue("test_role does not exist!", getRolesResponse.hasRoles()); assertTrue("any cluster permission should be authorized", - Role.builder(getRolesResponse.roles()[0]).build().cluster().check("cluster:admin/foo")); + Role.builder(getRolesResponse.roles()[0]).build().cluster().check("cluster:admin/foo", null, null)); c.preparePutRole("test_role") .cluster("none") @@ -288,7 +324,7 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { assertTrue("test_role does not exist!", getRolesResponse.hasRoles()); assertFalse("no cluster permission should be authorized", - Role.builder(getRolesResponse.roles()[0]).build().cluster().check("cluster:admin/bar")); + Role.builder(getRolesResponse.roles()[0]).build().cluster().check("cluster:admin/bar", null, null)); } } @@ -419,4 +455,97 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase { assertThat(response.getIndices().get(ShieldTemplateService.SECURITY_INDEX_NAME).getIndex(), is(ShieldTemplateService.SECURITY_INDEX_NAME)); } + + public void testOperationsOnReservedUsers() throws Exception { + final String username = randomFrom(XPackUser.NAME, KibanaUser.NAME); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().preparePutUser(username, randomBoolean() ? "changeme".toCharArray() : null, "admin").get()); + assertThat(exception.getMessage(), containsString("user [" + username + "] is reserved")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().prepareDeleteUser(username).get()); + assertThat(exception.getMessage(), containsString("user [" + username + "] is reserved")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().prepareDeleteUser(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME).get()); + assertThat(exception.getMessage(), containsString("user [" + AnonymousUser.DEFAULT_ANONYMOUS_USERNAME + "] is anonymous")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().prepareChangePassword(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME, "foobar".toCharArray()).get()); + assertThat(exception.getMessage(), containsString("user [" + AnonymousUser.DEFAULT_ANONYMOUS_USERNAME + "] is anonymous")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().preparePutUser(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME, "foobar".toCharArray()).get()); + assertThat(exception.getMessage(), containsString("user [" + AnonymousUser.DEFAULT_ANONYMOUS_USERNAME + "] is anonymous")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().preparePutUser(SystemUser.NAME, "foobar".toCharArray()).get()); + assertThat(exception.getMessage(), containsString("user [" + SystemUser.NAME + "] is internal")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().prepareChangePassword(SystemUser.NAME, "foobar".toCharArray()).get()); + assertThat(exception.getMessage(), containsString("user [" + SystemUser.NAME + "] is internal")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().prepareDeleteUser(SystemUser.NAME).get()); + assertThat(exception.getMessage(), containsString("user [" + SystemUser.NAME + "] is internal")); + + // get should work + GetUsersResponse response = securityClient().prepareGetUsers(username).get(); + assertThat(response.hasUsers(), is(true)); + assertThat(response.users()[0].principal(), is(username)); + + // authenticate should work + AuthenticateResponse authenticateResponse = client() + .filterWithHeader(Collections.singletonMap("Authorization", + basicAuthHeaderValue(username, new SecuredString("changeme".toCharArray())))) + .execute(AuthenticateAction.INSTANCE, new AuthenticateRequest(username)) + .get(); + assertThat(authenticateResponse.user().principal(), is(username)); + } + + public void testOperationsOnReservedRoles() throws Exception { + final String name = randomFrom(SuperuserRole.NAME, KibanaRole.NAME); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().preparePutRole(name).cluster("monitor").get()); + assertThat(exception.getMessage(), containsString("role [" + name + "] is reserved")); + + exception = expectThrows(IllegalArgumentException.class, + () -> securityClient().prepareDeleteRole(name).get()); + assertThat(exception.getMessage(), containsString("role [" + name + "] is reserved")); + + // get role is allowed + GetRolesResponse response = securityClient().prepareGetRoles(name).get(); + if (KibanaRole.NAME.equals(name)) { + assertThat(response.hasRoles(), is(false)); + } else { + assertThat(response.hasRoles(), is(true)); + assertThat(response.roles()[0].getName(), is(name)); + } + } + + public void testCreateAndChangePassword() throws Exception { + securityClient().preparePutUser("joe", "s3krit".toCharArray(), ShieldSettingsSource.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)); + + ChangePasswordResponse passwordResponse = securityClient( + client().filterWithHeader(Collections.singletonMap("Authorization", token))) + .prepareChangePassword("joe", "changeme".toCharArray()) + .get(); + assertThat(passwordResponse, notNullValue()); + + + ElasticsearchSecurityException expected = expectThrows(ElasticsearchSecurityException.class, + () -> client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get()); + assertThat(expected.status(), is(RestStatus.UNAUTHORIZED)); + + response = client() + .filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue("joe", new SecuredString("changeme".toCharArray())))) + .admin().cluster().prepareHealth().get(); + assertThat(response.isTimedOut(), is(false)); + } } diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/ReservedRealmIntegTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/ReservedRealmIntegTests.java new file mode 100644 index 00000000000..7251bd85fed --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/ReservedRealmIntegTests.java @@ -0,0 +1,77 @@ +/* + * 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.shield.authc.esnative; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.XPackUser; +import org.elasticsearch.shield.action.user.ChangePasswordResponse; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.test.NativeRealmIntegTestCase; + +import java.util.Arrays; + +import static java.util.Collections.singletonMap; +import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Integration tests for the built in realm + */ +public class ReservedRealmIntegTests extends NativeRealmIntegTestCase { + + private static final SecuredString DEFAULT_PASSWORD = new SecuredString("changeme".toCharArray()); + + public void testAuthenticate() { + for (String username : Arrays.asList(XPackUser.NAME, KibanaUser.NAME)) { + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get(); + + assertThat(response.getClusterName(), is(cluster().getClusterName())); + } + } + + public void testChangingPassword() { + String username = randomFrom(XPackUser.NAME, KibanaUser.NAME); + final char[] newPassword = "supersecretvalue".toCharArray(); + + if (randomBoolean()) { + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get(); + assertThat(response.getClusterName(), is(cluster().getClusterName())); + } + + ChangePasswordResponse response = securityClient().prepareChangePassword(username, newPassword).get(); + assertThat(response, notNullValue()); + + ElasticsearchSecurityException elasticsearchSecurityException = expectThrows(ElasticsearchSecurityException.class, () -> client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get()); + assertThat(elasticsearchSecurityException.getMessage(), containsString("authenticate")); + + ClusterHealthResponse healthResponse = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, new SecuredString(newPassword)))) + .admin() + .cluster() + .prepareHealth() + .get(); + assertThat(healthResponse.getClusterName(), is(cluster().getClusterName())); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/ReservedRealmTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/ReservedRealmTests.java new file mode 100644 index 00000000000..8d30d0f6752 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/esnative/ReservedRealmTests.java @@ -0,0 +1,132 @@ +/* + * 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.shield.authc.esnative; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.shield.authc.esnative.NativeUsersStore.ChangeListener; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.authc.support.UsernamePasswordToken; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; +import org.elasticsearch.test.ESTestCase; +import org.junit.Before; + +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; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the {@link ReservedRealm} + */ +public class ReservedRealmTests extends ESTestCase { + + private static final SecuredString DEFAULT_PASSWORD = new SecuredString("changeme".toCharArray()); + private NativeUsersStore usersStore; + + @Before + public void setupMocks() { + 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 String principal = randomFrom(XPackUser.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); + } + + public void testDefaultPasswordAuthentication() throws Throwable { + final boolean shieldIndexExists = randomBoolean(); + if (shieldIndexExists) { + when(usersStore.shieldIndexExists()).thenReturn(true); + } + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); + final User expected = randomFrom((User) XPackUser.INSTANCE, (User) KibanaUser.INSTANCE); + 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)); + verify(usersStore).started(); + verify(usersStore).shieldIndexExists(); + if (shieldIndexExists) { + verify(usersStore).reservedUserPassword(principal); + } + verifyNoMoreInteractions(usersStore); + } + + public void testAuthenticationWithStoredPassword() throws Throwable { + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); + final User expectedUser = randomFrom((User) XPackUser.INSTANCE, (User) KibanaUser.INSTANCE); + final String principal = expectedUser.principal(); + final SecuredString newPassword = new SecuredString("foobar".toCharArray()); + when(usersStore.shieldIndexExists()).thenReturn(true); + when(usersStore.reservedUserPassword(principal)).thenReturn(Hasher.BCRYPT.hash(newPassword)); + + // test default password + ElasticsearchSecurityException expected = expectThrows(ElasticsearchSecurityException.class, + () -> reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, DEFAULT_PASSWORD))); + 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)); + + // test new password + final User authenticated = reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, newPassword)); + assertThat(authenticated, sameInstance(expectedUser)); + verify(usersStore).addListener(any(ChangeListener.class)); + verify(usersStore, times(2)).started(); + verify(usersStore, times(2)).shieldIndexExists(); + verify(usersStore, times(2)).reservedUserPassword(principal); + verifyNoMoreInteractions(usersStore); + } + + public void testLookup() { + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore); + final User expectedUser = randomFrom((User) XPackUser.INSTANCE, (User) KibanaUser.INSTANCE); + final String principal = expectedUser.principal(); + + final User user = reservedRealm.doLookupUser(principal); + assertThat(user, sameInstance(expectedUser)); + verify(usersStore).addListener(any(ChangeListener.class)); + verifyNoMoreInteractions(usersStore); + + final User doesntExist = reservedRealm.doLookupUser("foobar"); + assertThat(doesntExist, nullValue()); + } + + public void testHelperMethods() { + final User expectedUser = randomFrom((User) XPackUser.INSTANCE, (User) KibanaUser.INSTANCE); + final String principal = expectedUser.principal(); + assertThat(ReservedRealm.isReserved(principal), is(true)); + assertThat(ReservedRealm.getUser(principal), sameInstance(expectedUser)); + + final String notExpected = randomFrom("foobar", "", randomAsciiOfLengthBetween(1, 30)); + assertThat(ReservedRealm.isReserved(notExpected), is(false)); + assertThat(ReservedRealm.getUser(notExpected), nullValue()); + + assertThat(ReservedRealm.users(), containsInAnyOrder(XPackUser.INSTANCE, KibanaUser.INSTANCE)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/FileRealmTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/FileRealmTests.java index 59a7e584330..db562447d42 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/FileRealmTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/FileRealmTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield.authc.file; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.Hasher; import org.elasticsearch.shield.authc.support.SecuredStringTests; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/tool/UsersToolTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/tool/UsersToolTests.java index 73cdb37fe48..afd8bb601ee 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/tool/UsersToolTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/file/tool/UsersToolTests.java @@ -22,12 +22,14 @@ import org.elasticsearch.cli.Command; import org.elasticsearch.cli.CommandTestCase; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.UserError; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.PathUtilsForTesting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.shield.authc.support.Hasher; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.SecuredStringTests; +import org.elasticsearch.shield.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.XPackPlugin; import org.junit.AfterClass; import org.junit.Before; @@ -219,7 +221,15 @@ public class UsersToolTests extends CommandTestCase { public void testParseUnknownRole() throws Exception { UsersTool.parseRoles(terminal, new Environment(settingsBuilder.build()), "test_r1,r2,r3"); String output = terminal.getOutput(); - assertTrue(output, output.contains("The following roles [r2,r3] are unknown")); + assertTrue(output, output.contains("The following roles [r2,r3] are not in the [")); + } + + public void testParseReservedRole() throws Exception { + final String reservedRoleName = randomFrom(ReservedRolesStore.names().toArray(Strings.EMPTY_ARRAY)); + String rolesArg = randomBoolean() ? "test_r1," + reservedRoleName : reservedRoleName; + UsersTool.parseRoles(terminal, new Environment(settingsBuilder.build()), rolesArg); + String output = terminal.getOutput(); + assertTrue(output, output.isEmpty()); } public void testParseInvalidRole() throws Exception { diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTests.java index 521c09b431d..83c80eccb3d 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTests.java @@ -6,7 +6,7 @@ package org.elasticsearch.shield.authc.ldap; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope; import org.elasticsearch.shield.authc.ldap.support.LdapTestCase; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/pki/PkiRealmTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/pki/PkiRealmTests.java index ba596af63b6..4cda0e3fd2a 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/pki/PkiRealmTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/pki/PkiRealmTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield.authc.pki; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.DnRoleMapper; import org.elasticsearch.shield.authc.support.SecuredString; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealmTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealmTests.java index 41b6fbdbc22..3fb39db2386 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealmTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealmTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield.authc.support; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.Realm; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.test.ESTestCase; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/InternalAuthorizationServiceTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/InternalAuthorizationServiceTests.java index e9e1323c755..adf0e2d049e 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/InternalAuthorizationServiceTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/InternalAuthorizationServiceTests.java @@ -55,13 +55,14 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.search.action.SearchTransportService; import org.elasticsearch.shield.ShieldTemplateService; -import org.elasticsearch.shield.SystemUser; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.XPackUser; +import org.elasticsearch.shield.user.AnonymousUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.shield.audit.AuditTrail; -import org.elasticsearch.shield.authc.AnonymousService; import org.elasticsearch.shield.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.shield.authz.permission.Role; +import org.elasticsearch.shield.authz.permission.SuperuserRole; import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; import org.elasticsearch.shield.authz.privilege.GeneralPrivilege; import org.elasticsearch.shield.authz.privilege.IndexPrivilege; @@ -99,13 +100,13 @@ public class InternalAuthorizationServiceTests extends ESTestCase { rolesStore = mock(RolesStore.class); clusterService = mock(ClusterService.class); auditTrail = mock(AuditTrail.class); - AnonymousService anonymousService = new AnonymousService(Settings.EMPTY); threadContext = new ThreadContext(Settings.EMPTY); threadPool = mock(ThreadPool.class); when(threadPool.getThreadContext()).thenReturn(threadContext); + AnonymousUser.initialize(Settings.EMPTY); internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, - auditTrail, anonymousService, new DefaultAuthenticationFailureHandler(), threadPool); + auditTrail, new DefaultAuthenticationFailureHandler(), threadPool); } public void testActionsSystemUserIsAuthorized() { @@ -160,7 +161,7 @@ public class InternalAuthorizationServiceTests extends ESTestCase { } public void testNoRolesCausesDenial() { - TransportRequest request = mock(TransportRequest.class); + TransportRequest request = new SearchRequest(); User user = new User("test user"); try { internalAuthorizationService.authorize(user, "indices:a", request); @@ -173,7 +174,7 @@ public class InternalAuthorizationServiceTests extends ESTestCase { } public void testUnknownRoleCausesDenial() { - TransportRequest request = mock(TransportRequest.class); + TransportRequest request = new SearchRequest(); User user = new User("test user", "non-existent-role"); try { internalAuthorizationService.authorize(user, "indices:a", request); @@ -342,22 +343,21 @@ public class InternalAuthorizationServiceTests extends ESTestCase { public void testDenialForAnonymousUser() { TransportRequest request = new IndicesExistsRequest("b"); ClusterState state = mock(ClusterState.class); - AnonymousService anonymousService = - new AnonymousService(Settings.builder().put(AnonymousService.ROLES_SETTING.getKey(), "a_all").build()); + AnonymousUser.initialize(Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "a_all").build()); internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, - anonymousService, new DefaultAuthenticationFailureHandler(), threadPool); + new DefaultAuthenticationFailureHandler(), threadPool); 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 { - internalAuthorizationService.authorize(anonymousService.anonymousUser(), "indices:a", request); + internalAuthorizationService.authorize(AnonymousUser.INSTANCE, "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 [" + anonymousService.anonymousUser().principal() + "]")); - verify(auditTrail).accessDenied(anonymousService.anonymousUser(), "indices:a", request); + containsString("action [indices:a] is unauthorized for user [" + AnonymousUser.INSTANCE.principal() + "]")); + verify(auditTrail).accessDenied(AnonymousUser.INSTANCE, "indices:a", request); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(2)).state(); verify(state, times(3)).metaData(); @@ -367,23 +367,25 @@ public class InternalAuthorizationServiceTests extends ESTestCase { public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { TransportRequest request = new IndicesExistsRequest("b"); ClusterState state = mock(ClusterState.class); - AnonymousService anonymousService = new AnonymousService(Settings.builder() - .put(AnonymousService.ROLES_SETTING.getKey(), "a_all") - .put(AnonymousService.SETTING_AUTHORIZATION_EXCEPTION_ENABLED.getKey(), false) + AnonymousUser.initialize(Settings.builder() + .put(AnonymousUser.ROLES_SETTING.getKey(), "a_all") + .put(InternalAuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false) .build()); - internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, - anonymousService, new DefaultAuthenticationFailureHandler(), threadPool); + User anonymousUser = AnonymousUser.INSTANCE; + internalAuthorizationService = new InternalAuthorizationService( + Settings.builder().put(InternalAuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false).build(), + rolesStore, clusterService, auditTrail, new DefaultAuthenticationFailureHandler(), threadPool); 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 { - internalAuthorizationService.authorize(anonymousService.anonymousUser(), "indices:a", request); + internalAuthorizationService.authorize(anonymousUser, "indices:a", request); fail("indices request for b should be denied since there is no such index"); } catch (ElasticsearchSecurityException e) { assertAuthenticationException(e, containsString("action [indices:a] requires authentication")); - verify(auditTrail).accessDenied(anonymousService.anonymousUser(), "indices:a", request); + verify(auditTrail).accessDenied(anonymousUser, "indices:a", request); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(2)).state(); verify(state, times(3)).metaData(); @@ -570,6 +572,7 @@ public class InternalAuthorizationServiceTests extends ESTestCase { } public void testXPackUserCanExecuteOperationAgainstShieldIndex() { + when(rolesStore.role(SuperuserRole.NAME)).thenReturn(Role.builder(SuperuserRole.DESCRIPTOR).build()); ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesResolverTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesResolverTests.java index 90d7bd6e436..13eedf906aa 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesResolverTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/indicesresolver/DefaultIndicesResolverTests.java @@ -6,8 +6,6 @@ package org.elasticsearch.shield.authz.indicesresolver; import org.elasticsearch.Version; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesAction; @@ -31,13 +29,13 @@ import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.shield.ShieldTemplateService; -import org.elasticsearch.shield.User; -import org.elasticsearch.shield.XPackUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.shield.audit.AuditTrail; -import org.elasticsearch.shield.authc.AnonymousService; import org.elasticsearch.shield.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.shield.authz.InternalAuthorizationService; import org.elasticsearch.shield.authz.permission.Role; +import org.elasticsearch.shield.authz.permission.SuperuserRole; import org.elasticsearch.shield.authz.privilege.ClusterPrivilege; import org.elasticsearch.shield.authz.privilege.IndexPrivilege; import org.elasticsearch.shield.authz.store.RolesStore; @@ -92,13 +90,14 @@ public class DefaultIndicesResolverTests extends ESTestCase { String[] authorizedIndices = new String[] { "bar", "bar-closed", "foofoobar", "foofoo", "missing", "foofoo-closed" }; when(rolesStore.role("role")).thenReturn(Role.builder("role").add(IndexPrivilege.ALL, authorizedIndices).build()); when(rolesStore.role("test")).thenReturn(Role.builder("test").cluster(ClusterPrivilege.MONITOR).build()); + when(rolesStore.role(SuperuserRole.NAME)).thenReturn(Role.builder(SuperuserRole.DESCRIPTOR).build()); ClusterService clusterService = mock(ClusterService.class); ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(metaData); InternalAuthorizationService authzService = new InternalAuthorizationService(settings, rolesStore, clusterService, - mock(AuditTrail.class), new AnonymousService(settings), new DefaultAuthenticationFailureHandler(), mock(ThreadPool.class)); + mock(AuditTrail.class), new DefaultAuthenticationFailureHandler(), mock(ThreadPool.class)); defaultIndicesResolver = new DefaultIndicesAndAliasesResolver(authzService); } diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/DefaultRoleTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/DefaultRoleTests.java new file mode 100644 index 00000000000..5e7f52ed623 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/DefaultRoleTests.java @@ -0,0 +1,91 @@ +/* + * 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.shield.authz.permission; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.client.Client; +import org.elasticsearch.license.plugin.action.get.GetLicenseAction; +import org.elasticsearch.shield.action.user.AuthenticateRequestBuilder; +import org.elasticsearch.shield.action.user.ChangePasswordRequestBuilder; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.action.user.AuthenticateAction; +import org.elasticsearch.shield.action.user.AuthenticateRequest; +import org.elasticsearch.shield.action.user.ChangePasswordAction; +import org.elasticsearch.shield.action.user.ChangePasswordRequest; +import org.elasticsearch.shield.action.user.DeleteUserAction; +import org.elasticsearch.shield.action.user.PutUserAction; +import org.elasticsearch.shield.action.user.UserRequest; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; + +import java.util.Iterator; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Unit tests for the {@link DefaultRole} + */ +public class DefaultRoleTests extends ESTestCase { + + public void testDefaultRoleHasNoIndicesPrivileges() { + Iterator iter = DefaultRole.INSTANCE.indices().iterator(); + assertThat(iter.hasNext(), is(false)); + } + + public void testDefaultRoleHasNoRunAsPrivileges() { + assertThat(DefaultRole.INSTANCE.runAs().isEmpty(), is(true)); + } + + public void testDefaultRoleAllowsUser() { + final User user = new User("joe"); + final boolean changePasswordRequest = randomBoolean(); + final TransportRequest request = changePasswordRequest ? + new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(user.principal()).request(); + final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; + assertThat(request, instanceOf(UserRequest.class)); + + assertThat(DefaultRole.INSTANCE.cluster().check(action, request, user), is(true)); + } + + public void testDefaultRoleDoesNotAllowNonMatchingUsername() { + final User user = new User("joe"); + final boolean changePasswordRequest = randomBoolean(); + final String username = randomFrom("", "joe" + randomAsciiOfLengthBetween(1, 5), randomAsciiOfLengthBetween(3, 10)); + final TransportRequest request = changePasswordRequest ? + new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); + final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; + assertThat(request, instanceOf(UserRequest.class)); + + assertThat(DefaultRole.INSTANCE.cluster().check(action, request, user), is(false)); + + final User user2 = new User("admin", new String[] { "bar" }, user); + if (request instanceof ChangePasswordRequest) { + ((ChangePasswordRequest)request).username("joe"); + } else { + ((AuthenticateRequest)request).username("joe"); + } + // run as should not be checked by this role, it is up to the caller to provide the correct user + assertThat(DefaultRole.INSTANCE.cluster().check(action, request, user2), is(false)); + assertThat(DefaultRole.INSTANCE.cluster().check(action, request, user), is(true)); + } + + public void testDefaultRoleDoesNotAllowOtherActions() { + final User user = mock(User.class); + final TransportRequest request = mock(TransportRequest.class); + final String action = randomFrom(PutUserAction.NAME, DeleteUserAction.NAME, ClusterHealthAction.NAME, ClusterStateAction.NAME, + ClusterStatsAction.NAME, GetLicenseAction.NAME); + + assertThat(DefaultRole.INSTANCE.cluster().check(action, request, user), is(false)); + verifyZeroInteractions(user, request); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/KibanaRoleTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/KibanaRoleTests.java new file mode 100644 index 00000000000..7ce018a51e3 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/KibanaRoleTests.java @@ -0,0 +1,59 @@ +/* + * 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.shield.authz.permission; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; +import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.action.admin.indices.create.CreateIndexAction; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; + +import static org.hamcrest.Matchers.is; + +/** + * Tests for the kibana role + */ +public class KibanaRoleTests extends ESTestCase { + + public void testCluster() { + final User user = KibanaUser.INSTANCE; + final TransportRequest request = new TransportRequest.Empty(); + assertThat(KibanaRole.INSTANCE.cluster().check(ClusterHealthAction.NAME, request, user), is(true)); + assertThat(KibanaRole.INSTANCE.cluster().check(ClusterStateAction.NAME, request, user), is(true)); + assertThat(KibanaRole.INSTANCE.cluster().check(ClusterStatsAction.NAME, request, user), is(true)); + assertThat(KibanaRole.INSTANCE.cluster().check(PutIndexTemplateAction.NAME, request, user), is(false)); + assertThat(KibanaRole.INSTANCE.cluster().check(ClusterRerouteAction.NAME, request, user), is(false)); + assertThat(KibanaRole.INSTANCE.cluster().check(ClusterUpdateSettingsAction.NAME, request, user), is(false)); + } + + public void testRunAs() { + assertThat(KibanaRole.INSTANCE.runAs().isEmpty(), is(true)); + } + + public void testIndices() { + final String kibanaIndex = ".kibana"; + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher("indices:foo").test(kibanaIndex), is(true)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher("indices:bar").test(kibanaIndex), is(true)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(kibanaIndex), is(true)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(kibanaIndex), is(true)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher(IndexAction.NAME).test(kibanaIndex), is(true)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher(DeleteAction.NAME).test(kibanaIndex), is(true)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(kibanaIndex), is(true)); + + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); + assertThat(KibanaRole.INSTANCE.indices().allowedIndicesMatcher("indices:foo").test(randomAsciiOfLengthBetween(8, 24)), is(false)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/SuperuserRoleTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/SuperuserRoleTests.java new file mode 100644 index 00000000000..5b94d3eaf29 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/permission/SuperuserRoleTests.java @@ -0,0 +1,83 @@ +/* + * 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.shield.authz.permission; + +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.shield.action.role.PutRoleAction; +import org.elasticsearch.shield.action.user.PutUserAction; +import org.elasticsearch.shield.authz.accesscontrol.IndicesAccessControl.IndexAccessControl; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; + +import java.util.Map; + +import static org.hamcrest.Matchers.is; + +/** + * Tests for the superuser role + */ +public class SuperuserRoleTests extends ESTestCase { + + public void testCluster() { + final User user = new User("joe", SuperuserRole.NAME); + final TransportRequest request = new TransportRequest.Empty(); + + assertThat(SuperuserRole.INSTANCE.cluster().check(ClusterHealthAction.NAME, request, user), is(true)); + assertThat(SuperuserRole.INSTANCE.cluster().check(ClusterUpdateSettingsAction.NAME, request, user), is(true)); + assertThat(SuperuserRole.INSTANCE.cluster().check(PutUserAction.NAME, request, user), is(true)); + assertThat(SuperuserRole.INSTANCE.cluster().check(PutRoleAction.NAME, request, user), is(true)); + assertThat(SuperuserRole.INSTANCE.cluster().check(PutIndexTemplateAction.NAME, request, user), is(true)); + assertThat(SuperuserRole.INSTANCE.cluster().check("internal:admin/foo", request, user), is(false)); + } + + public void testIndices() { + final Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build(); + final MetaData metaData = new MetaData.Builder() + .put(new IndexMetaData.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetaData.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetaData.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetaData.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetaData.Builder("b") + .settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetaData.Builder("ab").build()) + .putAlias(new AliasMetaData.Builder("ba").build()) + .build(), true) + .build(); + + Map authzMap = + SuperuserRole.INSTANCE.indices().authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), metaData); + assertThat(authzMap.get("a1").isGranted(), is(true)); + assertThat(authzMap.get("b").isGranted(), is(true)); + authzMap = SuperuserRole.INSTANCE.indices().authorize(DeleteIndexAction.NAME, Sets.newHashSet("a1", "ba"), metaData); + assertThat(authzMap.get("a1").isGranted(), is(true)); + assertThat(authzMap.get("b").isGranted(), is(true)); + authzMap = SuperuserRole.INSTANCE.indices().authorize(IndexAction.NAME, Sets.newHashSet("a2", "ba"), metaData); + assertThat(authzMap.get("a2").isGranted(), is(true)); + assertThat(authzMap.get("b").isGranted(), is(true)); + authzMap = SuperuserRole.INSTANCE.indices().authorize(UpdateSettingsAction.NAME, Sets.newHashSet("aaaaaa", "ba"), metaData); + assertThat(authzMap.get("aaaaaa").isGranted(), is(true)); + assertThat(authzMap.get("b").isGranted(), is(true)); + } + + public void testRunAs() { + assertThat(SuperuserRole.INSTANCE.runAs().check(randomAsciiOfLengthBetween(1, 30)), is(true)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/FileRolesStoreTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/FileRolesStoreTests.java index ac1942a140e..6b5a83d147a 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/FileRolesStoreTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/FileRolesStoreTests.java @@ -233,13 +233,8 @@ public class FileRolesStoreTests extends ESTestCase { Path path = getDataPath("default_roles.yml"); Map roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY); assertThat(roles, notNullValue()); - assertThat(roles.size(), is(8)); + assertThat(roles.size(), is(3)); - assertThat(roles, hasKey("admin")); - assertThat(roles, hasKey("power_user")); - assertThat(roles, hasKey("user")); - assertThat(roles, hasKey("transport_client")); - assertThat(roles, hasKey("kibana4_server")); assertThat(roles, hasKey("logstash")); assertThat(roles, hasKey("monitoring_user")); assertThat(roles, hasKey("remote_monitoring_agent")); @@ -296,8 +291,8 @@ public class FileRolesStoreTests extends ESTestCase { role = store.role("role5"); assertThat(role, notNullValue()); assertThat(role.name(), equalTo("role5")); - assertThat(role.cluster().check("cluster:monitor/foo/bar"), is(true)); - assertThat(role.cluster().check("cluster:admin/foo/bar"), is(false)); + assertThat(role.cluster().check("cluster:monitor/foo/bar", null, null), is(true)); + assertThat(role.cluster().check("cluster:admin/foo/bar", null, null), is(false)); } finally { if (watcherService != null) { @@ -361,9 +356,11 @@ public class FileRolesStoreTests extends ESTestCase { List messages = logger.output(CapturingLogger.Level.WARN); assertThat(messages, notNullValue()); - assertThat(messages, hasSize(2)); + assertThat(messages, hasSize(4)); // the system role will always be checked first assertThat(messages.get(0).text, containsString("role [__es_system_role] is reserved")); - assertThat(messages.get(1).text, containsString("role [__es_internal_role] is reserved")); + assertThat(messages.get(1).text, containsString("role [superuser] is reserved")); + assertThat(messages.get(2).text, containsString("role [kibana] is reserved")); + assertThat(messages.get(3).text, containsString("role [transport_client] is reserved")); } } diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/ReservedRolesStoreTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/ReservedRolesStoreTests.java new file mode 100644 index 00000000000..72fa777da0d --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authz/store/ReservedRolesStoreTests.java @@ -0,0 +1,84 @@ +/* + * 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.shield.authz.store; + +import org.elasticsearch.shield.SecurityContext; +import org.elasticsearch.shield.authz.permission.KibanaRole; +import org.elasticsearch.shield.authz.permission.SuperuserRole; +import org.elasticsearch.shield.authz.permission.TransportClientRole; +import org.elasticsearch.shield.user.KibanaUser; +import org.elasticsearch.shield.user.SystemUser; +import org.elasticsearch.shield.user.User; +import org.elasticsearch.shield.user.XPackUser; +import org.elasticsearch.test.ESTestCase; +import org.junit.Before; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the {@link ReservedRolesStore} + */ +public class ReservedRolesStoreTests extends ESTestCase { + + private final User user = new User("joe"); + private SecurityContext securityContext; + private ReservedRolesStore reservedRolesStore; + + @Before + public void setupMocks() { + securityContext = mock(SecurityContext.class); + when(securityContext.getUser()).thenReturn(user); + reservedRolesStore = new ReservedRolesStore(securityContext); + } + + public void testRetrievingReservedRolesNonKibanaUser() { + if (randomBoolean()) { + when(securityContext.getUser()).thenReturn(XPackUser.INSTANCE); + } + + assertThat(reservedRolesStore.role(SuperuserRole.NAME), sameInstance(SuperuserRole.INSTANCE)); + assertThat(reservedRolesStore.roleDescriptor(SuperuserRole.NAME), sameInstance(SuperuserRole.DESCRIPTOR)); + + assertThat(reservedRolesStore.role(TransportClientRole.NAME), sameInstance(TransportClientRole.INSTANCE)); + assertThat(reservedRolesStore.roleDescriptor(TransportClientRole.NAME), sameInstance(TransportClientRole.DESCRIPTOR)); + + assertThat(reservedRolesStore.roleDescriptors(), contains(SuperuserRole.DESCRIPTOR, TransportClientRole.DESCRIPTOR)); + + assertThat(reservedRolesStore.role(KibanaRole.NAME), nullValue()); + assertThat(reservedRolesStore.roleDescriptor(KibanaRole.NAME), nullValue()); + + assertThat(reservedRolesStore.role(SystemUser.ROLE_NAME), nullValue()); + } + + public void testRetrievingReservedRoleKibanaUser() { + when(securityContext.getUser()).thenReturn(KibanaUser.INSTANCE); + assertThat(reservedRolesStore.role(SuperuserRole.NAME), sameInstance(SuperuserRole.INSTANCE)); + assertThat(reservedRolesStore.roleDescriptor(SuperuserRole.NAME), sameInstance(SuperuserRole.DESCRIPTOR)); + + assertThat(reservedRolesStore.role(TransportClientRole.NAME), sameInstance(TransportClientRole.INSTANCE)); + assertThat(reservedRolesStore.roleDescriptor(TransportClientRole.NAME), sameInstance(TransportClientRole.DESCRIPTOR)); + + assertThat(reservedRolesStore.role(KibanaRole.NAME), sameInstance(KibanaRole.INSTANCE)); + assertThat(reservedRolesStore.roleDescriptor(KibanaRole.NAME), sameInstance(KibanaRole.DESCRIPTOR)); + assertThat(reservedRolesStore.roleDescriptors(), + contains(SuperuserRole.DESCRIPTOR, TransportClientRole.DESCRIPTOR, KibanaRole.DESCRIPTOR)); + + assertThat(reservedRolesStore.role(SystemUser.ROLE_NAME), nullValue()); + } + + public void testIsReserved() { + assertThat(ReservedRolesStore.isReserved(KibanaRole.NAME), is(true)); + assertThat(ReservedRolesStore.isReserved(SuperuserRole.NAME), is(true)); + assertThat(ReservedRolesStore.isReserved("foobar"), is(false)); + assertThat(ReservedRolesStore.isReserved(SystemUser.ROLE_NAME), is(true)); + assertThat(ReservedRolesStore.isReserved(TransportClientRole.NAME), is(true)); + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java index 5696f9ec9bd..b65026fa7b5 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestFilterChain; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESTestCase; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/action/RestAuthenticateActionTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/action/RestAuthenticateActionTests.java index 3778d015f37..6aa5d03edb7 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/action/RestAuthenticateActionTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/rest/action/RestAuthenticateActionTests.java @@ -7,9 +7,9 @@ package org.elasticsearch.shield.rest.action; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.node.Node; -import org.elasticsearch.shield.authc.AnonymousService; import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.authz.InternalAuthorizationService; +import org.elasticsearch.shield.user.AnonymousUser; import org.elasticsearch.test.ShieldIntegTestCase; import org.elasticsearch.test.ShieldSettingsSource; import org.elasticsearch.test.rest.client.http.HttpResponse; @@ -39,9 +39,9 @@ public class RestAuthenticateActionTests extends ShieldIntegTestCase { .put(NetworkModule.HTTP_ENABLED.getKey(), true); if (anonymousEnabled) { - builder.put(AnonymousService.USERNAME_SETTING.getKey(), "anon") - .putArray(AnonymousService.ROLES_SETTING.getKey(), ShieldSettingsSource.DEFAULT_ROLE, "foo") - .put(AnonymousService.SETTING_AUTHORIZATION_EXCEPTION_ENABLED.getKey(), false); + builder.put(AnonymousUser.USERNAME_SETTING.getKey(), "anon") + .putArray(AnonymousUser.ROLES_SETTING.getKey(), ShieldSettingsSource.DEFAULT_ROLE, "foo") + .put(InternalAuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false); } return builder.build(); } diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ClientTransportFilterTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ClientTransportFilterTests.java index 35ec2100c00..19eb7ed089e 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ClientTransportFilterTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ClientTransportFilterTests.java @@ -5,7 +5,7 @@ */ package org.elasticsearch.shield.transport; -import org.elasticsearch.shield.SystemUser; +import org.elasticsearch.shield.user.SystemUser; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ServerTransportFilterTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ServerTransportFilterTests.java index 9a0d2b10b75..715df3c07e0 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ServerTransportFilterTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/transport/ServerTransportFilterTests.java @@ -8,7 +8,7 @@ package org.elasticsearch.shield.transport; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.shield.User; +import org.elasticsearch.shield.user.User; import org.elasticsearch.shield.action.ShieldActionMapper; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authz.AuthorizationService; diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/AnonymousUserTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/AnonymousUserIntegTests.java similarity index 88% rename from elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/AnonymousUserTests.java rename to elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/AnonymousUserIntegTests.java index 901bf595d45..db701be1b1f 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/authc/AnonymousUserTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/AnonymousUserIntegTests.java @@ -3,7 +3,7 @@ * 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.shield.authc; +package org.elasticsearch.shield.user; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -15,6 +15,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.shield.authz.InternalAuthorizationService; import org.elasticsearch.test.ShieldIntegTestCase; import java.io.InputStreamReader; @@ -27,7 +28,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -public class AnonymousUserTests extends ShieldIntegTestCase { +public class AnonymousUserIntegTests extends ShieldIntegTestCase { private boolean authorizationExceptionsEnabled = randomBoolean(); @Override @@ -35,22 +36,17 @@ public class AnonymousUserTests extends ShieldIntegTestCase { return Settings.builder() .put(super.nodeSettings(nodeOrdinal)) .put(NetworkModule.HTTP_ENABLED.getKey(), true) - .put(AnonymousService.ROLES_SETTING.getKey(), "anonymous") - .put(AnonymousService.SETTING_AUTHORIZATION_EXCEPTION_ENABLED.getKey(), authorizationExceptionsEnabled) + .put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous") + .put(InternalAuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), authorizationExceptionsEnabled) .build(); } - @Override - public boolean sslTransportEnabled() { - return false; - } - @Override public String configRoles() { return super.configRoles() + "\n" + "anonymous:\n" + " indices:\n" + - " - names: '*'" + + " - names: '*'\n" + " privileges: [ READ ]\n"; } diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/AnonymousUserTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/AnonymousUserTests.java new file mode 100644 index 00000000000..84d2a608d3e --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/AnonymousUserTests.java @@ -0,0 +1,80 @@ +/* + * 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.shield.user; + +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.junit.After; + +import java.nio.ByteBuffer; + +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +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; + 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; + assertThat(user.principal(), equalTo(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME)); + assertThat(user.roles(), arrayContainingInAnyOrder("r1", "r2", "r3")); + } + + public void testResolveAnonymousUser_NoSettings() throws Exception { + Settings settings = randomBoolean() ? + Settings.EMPTY : + Settings.builder().put(AnonymousUser.USERNAME_SETTING.getKey(), "user1").build(); + AnonymousUser.initialize(settings); + assertThat(AnonymousUser.enabled(), is(false)); + } + + public void testAnonymous() throws Exception { + Settings settings = Settings.builder().putArray(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3").build(); + if (randomBoolean()) { + 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)); + // make sure check works with serialization + BytesStreamOutput output = new BytesStreamOutput(); + User.writeTo(user, output); + + User anonymousSerialized = User.readFrom(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytes()))); + assertThat(AnonymousUser.is(anonymousSerialized), is(true)); + + // test with null anonymous + AnonymousUser.initialize(Settings.EMPTY); + assertThat(AnonymousUser.is(null), is(false)); + if (user.principal().equals(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME)) { + assertThat(AnonymousUser.isAnonymousUsername(user.principal()), is(true)); + } else { + assertThat(AnonymousUser.isAnonymousUsername(user.principal()), is(false)); + } + } +} diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/SystemInternalUserTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/SystemUserTests.java similarity index 91% rename from elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/SystemInternalUserTests.java rename to elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/SystemUserTests.java index 32526eed757..27500bdd149 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/SystemInternalUserTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/SystemUserTests.java @@ -3,7 +3,7 @@ * 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.shield; +package org.elasticsearch.shield.user; import org.elasticsearch.test.ESTestCase; @@ -12,7 +12,7 @@ import static org.hamcrest.Matchers.is; /** * */ -public class SystemInternalUserTests extends ESTestCase { +public class SystemUserTests extends ESTestCase { public void testIsAuthorized() throws Exception { assertThat(SystemUser.isAuthorized("indices:monitor/whatever"), is(true)); diff --git a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/UserTests.java b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/UserTests.java similarity index 81% rename from elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/UserTests.java rename to elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/UserTests.java index 04a7ae5d8eb..2f56594153f 100644 --- a/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/UserTests.java +++ b/elasticsearch/x-pack/shield/src/test/java/org/elasticsearch/shield/user/UserTests.java @@ -3,15 +3,17 @@ * 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.shield; +package org.elasticsearch.shield.user; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.ByteBufferStreamInput; import org.elasticsearch.common.io.stream.BytesStreamOutput; 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; @@ -66,15 +68,6 @@ public class UserTests extends ESTestCase { assertThat(readFrom.runAs(), is(nullValue())); } - public void testInternalShieldUserReadAndWrite() throws Exception { - BytesStreamOutput output = new BytesStreamOutput(); - - User.writeTo(XPackUser.INSTANCE, output); - User readFrom = User.readFrom(ByteBufferStreamInput.wrap(output.bytes())); - - assertThat(readFrom, is(sameInstance(XPackUser.INSTANCE))); - } - public void testFakeInternalUserSerialization() throws Exception { BytesStreamOutput output = new BytesStreamOutput(); output.writeBoolean(true); @@ -106,4 +99,30 @@ public class UserTests extends ESTestCase { assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=null,email=null,metadata={},runAs=[User[username=u2," + "roles=[r3],fullName=null,email=null,metadata={}]]]")); } + + public void testReservedUserSerialization() throws Exception { + BytesStreamOutput output = new BytesStreamOutput(); + User.writeTo(XPackUser.INSTANCE, output); + User readFrom = User.readFrom(ByteBufferStreamInput.wrap(output.bytes())); + + assertThat(readFrom, is(sameInstance(XPackUser.INSTANCE))); + + output = new BytesStreamOutput(); + User.writeTo(KibanaUser.INSTANCE, output); + readFrom = User.readFrom(ByteBufferStreamInput.wrap(output.bytes())); + + assertThat(readFrom, is(sameInstance(KibanaUser.INSTANCE))); + } + + public void testReservedMetadata() throws Exception { + Map validMetadata = Collections.singletonMap("foo", "bar"); + Map invalidMetadata = Collections.singletonMap(User.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); + } } diff --git a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/default_roles.yml b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/default_roles.yml index f998b18b427..8a15b4afcd7 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/default_roles.yml +++ b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/default_roles.yml @@ -1,43 +1,3 @@ -admin: - cluster: - - all - indices: - - names: '*' - privileges: - - all - -# monitoring cluster privileges -# All operations on all indices -power_user: - cluster: - - monitor - indices: - - names: '*' - privileges: - - all - -# Read-only operations on indices -user: - indices: - - names: '*' - privileges: - - read - -# Defines the required permissions for transport clients -transport_client: - cluster: - - transport_client - -# The required permissions for the kibana 4 server -kibana4_server: - cluster: - - monitor - indices: - - names: '.kibana' - privileges: - - all - -# The required role for logstash users logstash: cluster: - manage_index_templates diff --git a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/reserved_roles.yml b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/reserved_roles.yml index c3bad8a4ee8..754a4c46c3c 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/reserved_roles.yml +++ b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/shield/authz/store/reserved_roles.yml @@ -13,10 +13,20 @@ __es_system_role: privileges: - all -__es_internal_role: +superuser: cluster: - all indices: - names: '*' privileges: - - all \ No newline at end of file + - all + run_as: + - '*' + +kibana: + cluster: + - all + +transport_client: + cluster: + - all diff --git a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/actions b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/actions index a084b51def8..d4cc05f05e7 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/actions +++ b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/actions @@ -77,6 +77,8 @@ cluster:admin/xpack/license/delete cluster:admin/xpack/license/put cluster:admin/xpack/security/realm/cache/clear cluster:admin/xpack/security/roles/cache/clear +cluster:admin/xpack/security/user/authenticate +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 diff --git a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/handlers b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/handlers index 89ccddd0fec..036bdec28fb 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/handlers +++ b/elasticsearch/x-pack/shield/src/test/resources/org/elasticsearch/transport/handlers @@ -16,6 +16,8 @@ cluster:admin/xpack/security/roles/cache/clear[n] cluster:admin/xpack/security/role/put cluster:admin/xpack/security/role/delete cluster:admin/xpack/security/role/get +cluster:admin/xpack/security/user/authenticate +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 diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.change_password.json b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.change_password.json new file mode 100644 index 00000000000..5225209593c --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.change_password.json @@ -0,0 +1,27 @@ +{ + "shield.change_password": { + "documentation": "Change the password of a user", + "methods": [ "PUT", "POST" ], + "url": { + "path": "/_shield/user/{username}/_password", + "paths": [ "/_shield/user/{username}/_password", "/_shield/user/_password" ], + "parts": { + "username": { + "type" : "string", + "description" : "The username of the user to change the password for", + "required" : false + } + }, + "params": { + "refresh": { + "type" : "boolean", + "description" : "Refresh the index after performing the operation" + } + } + }, + "body": { + "description" : "the new password for the user", + "required" : true + } + } +} diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_role.json b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_role.json index ae549291207..4a911ec3484 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_role.json +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_role.json @@ -12,7 +12,12 @@ "required" : true } }, - "params": {} + "params": { + "refresh": { + "type" : "boolean", + "description" : "Refresh the index after performing the operation" + } + } }, "body": null } diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_user.json b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_user.json index e41c8ca49a4..6b31f4889f2 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_user.json +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.delete_user.json @@ -12,7 +12,12 @@ "required" : true } }, - "params": {} + "params": { + "refresh": { + "type" : "boolean", + "description" : "Refresh the index after performing the operation" + } + } }, "body": null } diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_role.json b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_role.json index a49c4f0b9d5..8909d35ca7b 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_role.json +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_role.json @@ -12,7 +12,12 @@ "required" : true } }, - "params": {} + "params": { + "refresh": { + "type" : "boolean", + "description" : "Refresh the index after performing the operation" + } + } }, "body": { "description" : "The role to add", diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_user.json b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_user.json index 780e3211bd3..18cc6112d9d 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_user.json +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/api/shield.put_user.json @@ -12,7 +12,12 @@ "required" : true } }, - "params": {} + "params": { + "refresh": { + "type" : "boolean", + "description" : "Refresh the index after performing the operation" + } + } }, "body": { "description" : "The user to add", diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/authenticate/10_basic.yaml b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/authenticate/10_basic.yaml index d990ae5f56c..f4563f90b43 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/authenticate/10_basic.yaml +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/authenticate/10_basic.yaml @@ -8,4 +8,4 @@ shield.authenticate: {} - match: { username: "test_user" } - - match: { roles.0: "admin" } + - match: { roles.0: "superuser" } diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/change_password/10_basic.yaml b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/change_password/10_basic.yaml new file mode 100644 index 00000000000..a2249bc09e5 --- /dev/null +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/change_password/10_basic.yaml @@ -0,0 +1,166 @@ +--- +"Test changing users password": + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + - do: + shield.put_user: + username: "joe" + body: > + { + "password": "s3krit", + "roles" : [ "superuser" ] + } + - match: { user: { created: true } } + +# test that the role actually works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# change password + - do: + shield.change_password: + username: "joe" + body: > + { + "password" : "s3krit2" + } + +# attempt to login with invalid credentials + - do: + catch: request + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + +# login with new credentials + - do: + headers: + Authorization: "Basic am9lOnMza3JpdDI=" + cluster.health: {} + - match: { timed_out: false } + +--- +"Test user changing their own password": + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + - do: + shield.put_role: + name: "user" + body: > + { + "cluster": ["monitor"], + "indices": [ + { + "names": "*", + "privileges": ["all"] + } + ] + } + - match: { role: { created: true } } + + - do: + shield.put_user: + username: "joe" + body: > + { + "password": "s3krit", + "roles" : [ "user" ] + } + - match: { user: { created: true } } + +# test that the role actually works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# change password as the current user. the power_user role only grants the ability to change their own password + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + shield.change_password: + body: > + { + "password" : "s3krit2" + } + +# attempt to login with invalid credentials + - do: + catch: request + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + +# login with new credentials + - do: + headers: + Authorization: "Basic am9lOnMza3JpdDI=" + cluster.health: {} + - match: { timed_out: false } + +--- +"Test unauthorized user changing anothers password": + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + - do: + shield.put_role: + name: "user" + body: > + { + "cluster": ["monitor"], + "indices": [ + { + "names": "*", + "privileges": ["all"] + } + ] + } + - match: { role: { created: true } } + + - do: + shield.put_user: + username: "joe" + body: > + { + "password": "s3krit", + "roles" : [ "user" ] + } + - match: { user: { created: true } } + +# test that the role actually works + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + cluster.health: {} + - match: { timed_out: false } + +# attempt to change another users password + - do: + headers: + Authorization: "Basic am9lOnMza3JpdA==" + catch: forbidden + shield.change_password: + username: "anotheruser" + body: > + { + "password" : "s3krit2" + } diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/10_basic.yaml b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/10_basic.yaml index c309fb3accb..9135dc41764 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/10_basic.yaml +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/10_basic.yaml @@ -13,7 +13,7 @@ body: > { "password" : "s3krit", - "roles" : [ "admin" ], + "roles" : [ "superuser" ], "full_name" : "Bazooka Joe", "email" : "joe@bazooka.gum", "metadata" : { @@ -33,7 +33,7 @@ shield.get_user: username: "joe" - match: { joe.username: "joe" } - - match: { joe.roles.0: "admin" } + - match: { joe.roles.0: "superuser" } - match: { joe.full_name: "Bazooka Joe" } - match: { joe.email: "joe@bazooka.gum" } - match: { joe.metadata.key1: "val1" } diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yaml b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yaml index c05a6d12ad4..1207987d90a 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yaml +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yaml @@ -13,7 +13,7 @@ body: > { "password": "s3krit", - "roles" : [ "admin" ] + "roles" : [ "superuser" ] } - match: { user: { created: true } } @@ -21,7 +21,7 @@ shield.get_user: username: "joe" - match: { joe.username: "joe" } - - match: { joe.roles.0: "admin" } + - match: { joe.roles.0: "superuser" } - do: headers: @@ -35,7 +35,7 @@ body: > { "password" : "s3krit2", - "roles" : [ "admin", "foo" ], + "roles" : [ "superuser", "foo" ], "full_name" : "Bazooka Joe", "email" : "joe@bazooka.gum", "metadata" : { @@ -49,7 +49,7 @@ shield.get_user: username: "joe" - match: { joe.username: "joe" } - - match: { joe.roles.0: "admin" } + - match: { joe.roles.0: "superuser" } - match: { joe.roles.1: "foo" } - match: { joe.full_name: "Bazooka Joe" } - match: { joe.email: "joe@bazooka.gum" } diff --git a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/16_update_user.yaml b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/16_update_user.yaml index ecc58f6f7be..63862bd79d9 100644 --- a/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/16_update_user.yaml +++ b/elasticsearch/x-pack/shield/src/test/resources/rest-api-spec/test/users/16_update_user.yaml @@ -10,7 +10,7 @@ username: "joe" body: > { - "roles" : [ "admin" ] + "roles" : [ "superuser" ] } - match: { error.root_cause.0.reason: 'Validation Failed: 1: password must be specified unless you are updating an existing user;' } @@ -29,7 +29,7 @@ body: > { "password": "s3krit", - "roles" : [ "admin" ] + "roles" : [ "superuser" ] } - match: { user: { created: true } } @@ -44,7 +44,7 @@ shield.get_user: username: "joe" - match: { joe.username: "joe" } - - match: { joe.roles.0: "admin" } + - match: { joe.roles.0: "superuser" } - is_false: joe.full_name - is_false: joe.email @@ -55,7 +55,7 @@ username: "joe" body: > { - "roles" : [ "admin", "foo" ], + "roles" : [ "superuser", "foo" ], "full_name" : "Bazooka Joe", "email" : "joe@bazooka.gum", "metadata" : { @@ -77,7 +77,7 @@ shield.get_user: username: "joe" - match: { joe.username: "joe" } - - match: { joe.roles.0: "admin" } + - match: { joe.roles.0: "superuser" } - match: { joe.roles.1: "foo" } - match: { joe.full_name: "Bazooka Joe" } - match: { joe.email: "joe@bazooka.gum" } @@ -91,7 +91,7 @@ body: > { "password" : "s3krit2", - "roles" : [ "admin" ], + "roles" : [ "superuser" ], "full_name" : "Bazooka Joe", "email" : "joe@bazooka.gum", "metadata" : { @@ -121,7 +121,7 @@ shield.get_user: username: "joe" - match: { joe.username: "joe" } - - match: { joe.roles.0: "admin" } + - match: { joe.roles.0: "superuser" } - match: { joe.full_name: "Bazooka Joe" } - match: { joe.email: "joe@bazooka.gum" } - match: { joe.metadata.key1: "val1" } diff --git a/elasticsearch/x-pack/watcher/src/main/java/org/elasticsearch/watcher/support/init/proxy/ScriptServiceProxy.java b/elasticsearch/x-pack/watcher/src/main/java/org/elasticsearch/watcher/support/init/proxy/ScriptServiceProxy.java index 48e3a492f0a..5da4d7c87e3 100644 --- a/elasticsearch/x-pack/watcher/src/main/java/org/elasticsearch/watcher/support/init/proxy/ScriptServiceProxy.java +++ b/elasticsearch/x-pack/watcher/src/main/java/org/elasticsearch/watcher/support/init/proxy/ScriptServiceProxy.java @@ -11,7 +11,7 @@ import org.elasticsearch.script.ExecutableScript; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; import org.elasticsearch.shield.SecurityContext; -import org.elasticsearch.shield.XPackUser; +import org.elasticsearch.shield.user.XPackUser; import org.elasticsearch.watcher.support.Script; import org.elasticsearch.xpack.common.init.LazyInitializable;