security: add reserved roles and users
This commit adds reserved or built-in user and role support to x-pack. The reserved roles cannot be modified by users. The reserved users also cannot be modified with the exception of changing the password for a user. In order to change the password for a user, a new API has been added. This API only supports changing passwords for native and reserved users. To support allowing a user to change their own password, a default role has been added to grant access. This default role only grants access to user operations that pertain to the user that is being authorized. In other words, the default role grants `joe` the ability to change their own password but does not allow them to change the password of a different user. Additionally, the authenticate API was made a transport action and is granted by the default role. Closes elastic/elasticsearch#1727 Closes elastic/elasticsearch#1185 Closes elastic/elasticsearch#1158 Original commit: elastic/x-pack-elasticsearch@1a6689d90f
This commit is contained in:
parent
2c1ccaa66e
commit
d08446e221
|
@ -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()}",
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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()}",
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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<UsernamePasswordToken> {
|
|||
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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()}",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> V executeAs(User user, Callable<V> 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<RequestInterceptor> multibinder
|
||||
= Multibinder.newSetBinder(binder(), RequestInterceptor.class);
|
||||
multibinder.addBinding().to(RealtimeRequestInterceptor.class);
|
||||
|
|
|
@ -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<String> LICENSE_EXPIRATION_ACTION_MATCHER = HealthAndStatsPrivilege.INSTANCE.predicate();
|
||||
// FIXME clean up this hack
|
||||
static final Predicate<String> 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 extends ActionRequest> Request unsign(User user, String action, Request request) {
|
||||
|
||||
try {
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,58 +17,58 @@ import java.io.IOException;
|
|||
*/
|
||||
public class ClearRolesCacheRequest extends BaseNodesRequest<ClearRolesCacheRequest> {
|
||||
|
||||
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 <code>null</code> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,10 +65,10 @@ public class TransportClearRolesCacheAction extends TransportNodesAction<ClearRo
|
|||
|
||||
@Override
|
||||
protected ClearRolesCacheResponse.Node nodeOperation(ClearRolesCacheRequest.Node request) {
|
||||
if (request.roles == null || request.roles.length == 0) {
|
||||
if (request.names == null || request.names.length == 0) {
|
||||
rolesStore.invalidateAll();
|
||||
} else {
|
||||
for (String role : request.roles) {
|
||||
for (String role : request.names) {
|
||||
rolesStore.invalidate(role);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
|||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authz.store.NativeRolesStore;
|
||||
import org.elasticsearch.shield.authz.store.ReservedRolesStore;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
|
||||
|
@ -30,6 +31,11 @@ public class TransportDeleteRoleAction extends HandledTransportAction<DeleteRole
|
|||
|
||||
@Override
|
||||
protected void doExecute(DeleteRoleRequest request, ActionListener<DeleteRoleResponse> 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<Boolean>() {
|
||||
@Override
|
||||
|
|
|
@ -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<GetRolesRequest, GetRolesResponse> {
|
||||
|
||||
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<GetRolesResponse> 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<String> rolesToSearchFor = new ArrayList<>();
|
||||
final List<RoleDescriptor> 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<RoleDescriptor>() {
|
||||
nativeRolesStore.getRoleDescriptor(rolename, new ActionListener<RoleDescriptor>() {
|
||||
@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<GetRolesRequ
|
|||
listener.onFailure(t);
|
||||
}
|
||||
});
|
||||
} else if (specificRolesRequested && rolesToSearchFor.isEmpty()) {
|
||||
// specific roles were requested but they were built in only, no need to hit the store
|
||||
listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()])));
|
||||
} else {
|
||||
rolesStore.getRoleDescriptors(request.names(), new ActionListener<List<RoleDescriptor>>() {
|
||||
nativeRolesStore.getRoleDescriptors(
|
||||
rolesToSearchFor.toArray(new String[rolesToSearchFor.size()]), new ActionListener<List<RoleDescriptor>>() {
|
||||
@Override
|
||||
public void onResponse(List<RoleDescriptor> roles) {
|
||||
public void onResponse(List<RoleDescriptor> foundRoles) {
|
||||
roles.addAll(foundRoles);
|
||||
listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()])));
|
||||
}
|
||||
|
||||
|
@ -64,6 +98,7 @@ public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequ
|
|||
public void onFailure(Throwable t) {
|
||||
logger.error("failed to retrieve role [{}]", t,
|
||||
Strings.arrayToDelimitedString(request.names(), ","));
|
||||
listener.onFailure(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
|||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authz.store.NativeRolesStore;
|
||||
import org.elasticsearch.shield.authz.store.ReservedRolesStore;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
|
||||
|
@ -29,6 +30,12 @@ public class TransportPutRoleAction extends HandledTransportAction<PutRoleReques
|
|||
|
||||
@Override
|
||||
protected void doExecute(final PutRoleRequest request, final ActionListener<PutRoleResponse> 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<Boolean>() {
|
||||
@Override
|
||||
public void onResponse(Boolean created) {
|
||||
|
|
|
@ -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<AuthenticateRequest, AuthenticateResponse, AuthenticateRequestBuilder> {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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<AuthenticateRequest> 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);
|
||||
}
|
||||
}
|
|
@ -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<AuthenticateRequest, AuthenticateResponse, AuthenticateRequestBuilder> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<ChangePasswordRequest, ChangePasswordResponse, ChangePasswordRequestBuilder> {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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<ChangePasswordRequest> 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)));
|
||||
}
|
||||
}
|
|
@ -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<ChangePasswordRequest, ChangePasswordResponse, ChangePasswordRequestBuilder> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -17,7 +17,7 @@ import static org.elasticsearch.action.ValidateActions.addValidationError;
|
|||
/**
|
||||
* A request to delete a native user.
|
||||
*/
|
||||
public class DeleteUserRequest extends ActionRequest<DeleteUserRequest> {
|
||||
public class DeleteUserRequest extends ActionRequest<DeleteUserRequest> implements UserRequest {
|
||||
|
||||
private String username;
|
||||
private boolean refresh = true;
|
||||
|
@ -54,6 +54,11 @@ public class DeleteUserRequest extends ActionRequest<DeleteUserRequest> {
|
|||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] usernames() {
|
||||
return new String[] { username };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
|
|
|
@ -18,7 +18,7 @@ import static org.elasticsearch.action.ValidateActions.addValidationError;
|
|||
/**
|
||||
* Request to retrieve a native user.
|
||||
*/
|
||||
public class GetUsersRequest extends ActionRequest<GetUsersRequest> {
|
||||
public class GetUsersRequest extends ActionRequest<GetUsersRequest> implements UserRequest {
|
||||
|
||||
private String[] usernames;
|
||||
|
||||
|
@ -39,6 +39,7 @@ public class GetUsersRequest extends ActionRequest<GetUsersRequest> {
|
|||
this.usernames = usernames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] usernames() {
|
||||
return usernames;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -22,7 +22,7 @@ import static org.elasticsearch.action.ValidateActions.addValidationError;
|
|||
/**
|
||||
* Request object to put a native user.
|
||||
*/
|
||||
public class PutUserRequest extends ActionRequest<PutUserRequest> {
|
||||
public class PutUserRequest extends ActionRequest<PutUserRequest> implements UserRequest {
|
||||
|
||||
private String username;
|
||||
private String[] roles;
|
||||
|
@ -105,6 +105,11 @@ public class PutUserRequest extends ActionRequest<PutUserRequest> {
|
|||
return refresh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] usernames() {
|
||||
return new String[] { username };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<AuthenticateRequest, AuthenticateResponse> {
|
||||
|
||||
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<AuthenticateResponse> 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));
|
||||
}
|
||||
}
|
|
@ -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<ChangePasswordRequest, ChangePasswordResponse> {
|
||||
|
||||
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<ChangePasswordResponse> 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<Void>() {
|
||||
@Override
|
||||
public void onResponse(Void v) {
|
||||
listener.onResponse(new ChangePasswordResponse());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<DeleteUser
|
|||
|
||||
@Override
|
||||
protected void doExecute(DeleteUserRequest request, final ActionListener<DeleteUserResponse> listener) {
|
||||
try {
|
||||
usersStore.deleteUser(request, new ActionListener<Boolean>() {
|
||||
@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<Boolean>() {
|
||||
@Override
|
||||
public void onResponse(Boolean found) {
|
||||
listener.onResponse(new DeleteUserResponse(found));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<GetUsersRequest, GetUsersResponse> {
|
||||
|
@ -34,17 +38,43 @@ public class TransportGetUsersAction extends HandledTransportAction<GetUsersRequ
|
|||
|
||||
@Override
|
||||
protected void doExecute(final GetUsersRequest request, final ActionListener<GetUsersResponse> 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<String> usersToSearchFor = new ArrayList<>();
|
||||
final List<User> 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<User>() {
|
||||
@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<GetUsersRequ
|
|||
listener.onFailure(e);
|
||||
}
|
||||
});
|
||||
} else if (specificUsersRequested && usersToSearchFor.isEmpty()) {
|
||||
listener.onResponse(new GetUsersResponse(users));
|
||||
} else {
|
||||
usersStore.getUsers(request.usernames(), new ActionListener<List<User>>() {
|
||||
usersStore.getUsers(usersToSearchFor.toArray(new String[usersToSearchFor.size()]), new ActionListener<List<User>>() {
|
||||
@Override
|
||||
public void onResponse(List<User> users) {
|
||||
public void onResponse(List<User> usersFound) {
|
||||
users.addAll(usersFound);
|
||||
listener.onResponse(new GetUsersResponse(users));
|
||||
}
|
||||
|
||||
|
|
|
@ -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<PutUserReques
|
|||
|
||||
@Override
|
||||
protected void doExecute(final PutUserRequest request, final ActionListener<PutUserResponse> 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<Boolean>() {
|
||||
@Override
|
||||
public void onResponse(Boolean created) {
|
||||
|
|
|
@ -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. <code>null</code> should never be returned!
|
||||
*/
|
||||
String[] usernames();
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Boolean> SETTING_AUTHORIZATION_EXCEPTION_ENABLED =
|
||||
Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope);
|
||||
public static final Setting<List<String>> ROLES_SETTING =
|
||||
Setting.listSetting(setting("authc.anonymous.roles"), Collections.emptyList(), s -> s, Property.NodeScope);
|
||||
public static final Setting<String> 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<String> 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);
|
||||
}
|
||||
}
|
|
@ -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<String> 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<String, Class<? extends Realm.Factory<? extends Realm<? extends AuthenticationToken>>>> customRealms =
|
||||
new HashMap<>();
|
||||
|
@ -40,26 +42,27 @@ public class AuthenticationModule extends AbstractShieldModule.Node {
|
|||
|
||||
@Override
|
||||
protected void configureNode() {
|
||||
AnonymousUser.initialize(settings);
|
||||
MapBinder<String, Realm.Factory> 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<String, Class<? extends Realm.Factory<? extends Realm<? extends AuthenticationToken>>>> entry : customRealms.entrySet
|
||||
()) {
|
||||
for (Entry<String, Class<? extends Realm.Factory<? extends Realm<? extends AuthenticationToken>>>> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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<Realms> implements Iterab
|
|||
private final Environment env;
|
||||
private final Map<String, Realm.Factory> factories;
|
||||
private final ShieldLicenseState shieldLicenseState;
|
||||
private final ReservedRealm reservedRealm;
|
||||
|
||||
protected List<Realm> realms = Collections.emptyList();
|
||||
// a list of realms that are "internal" in that they are provided by shield and not a third party
|
||||
protected List<Realm> internalRealmsOnly = Collections.emptyList();
|
||||
|
||||
@Inject
|
||||
public Realms(Settings settings, Environment env, Map<String, Realm.Factory> factories, ShieldLicenseState shieldLicenseState) {
|
||||
public Realms(Settings settings, Environment env, Map<String, Realm.Factory> 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<Realms> 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<Realms> 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<Realms> implements Iterab
|
|||
}
|
||||
|
||||
private void addInternalRealms(List<Realm> 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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> versionMap = new ObjectLongHashMap<>();
|
||||
private final Hasher hasher = Hasher.BCRYPT;
|
||||
private final List<ChangeListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final AtomicReference<State> 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<Void> 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<UpdateResponse>() {
|
||||
@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<Void> listener) {
|
||||
client.prepareIndex(ShieldTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username)
|
||||
.setSource(Fields.PASSWORD.getPreferredName(), String.valueOf(passwordHash))
|
||||
.setRefresh(refresh)
|
||||
.execute(new ActionListener<IndexResponse>() {
|
||||
@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<Boolean> 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<char[]> passwordHash = new AtomicReference<>();
|
||||
final AtomicReference<Throwable> 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<GetResponse>() {
|
||||
@Override
|
||||
public void onResponse(GetResponse getResponse) {
|
||||
if (getResponse.isExists()) {
|
||||
Map<String, Object> 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<ClearScrollResponse>() {
|
||||
|
@ -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<String> userVersionMap = new ObjectLongHashMap<>();
|
||||
private final ObjectLongHashMap<String> 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<String> knownUsers = new ObjectHashSet<>(versionMap.keys());
|
||||
List<String> changedUsers = new ArrayList<>();
|
||||
|
||||
ObjectLongMap<String> currentUsersMap = collectUsersAndVersions(client);
|
||||
Iterator<ObjectLongCursor<String>> iterator = currentUsersMap.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ObjectLongCursor<String> 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<String> 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<String> scrollForModifiedUsers(Client client, String docType, ObjectLongMap<String> usersMap) {
|
||||
// create a copy of all known users
|
||||
ObjectHashSet<String> knownUsers = new ObjectHashSet<>(usersMap.keys());
|
||||
List<String> 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<ObjectCursor<String>> 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<String> 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<String> collectUsersAndVersions(Client client) {
|
||||
final ObjectLongMap<String> 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;
|
||||
|
|
|
@ -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 <em>always</em> 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<String> 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<User> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> users = FileUserPasswdStore.parseFile(userFilePath, null).keySet();
|
||||
|
||||
Path rolesFilePath = FileRolesStore.resolveFile(env.settings(), env);
|
||||
Set<String> knownRoles = FileRolesStore.parseFileForRoleNames(rolesFilePath, null);
|
||||
Set<String> 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<String> knownRoles = FileRolesStore.parseFileForRoleNames(rolesFile, null);
|
||||
Set<String> knownRoles = Sets.union(FileRolesStore.parseFileForRoleNames(rolesFile, null), ReservedRolesStore.names());
|
||||
Set<String> 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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Boolean> 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<String> authorizedIndicesAndAliases(User user, String action) {
|
||||
Predicate<String> 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<Predicate<String>> predicates = new ArrayList<>();
|
||||
for (String roleName : rolesNames) {
|
||||
Role role = rolesStore.role(roleName);
|
||||
if (role != null) {
|
||||
predicates.add(role.indices().allowedIndicesMatcher(action));
|
||||
}
|
||||
List<Predicate<String>> 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<String> predicate = predicates.stream().reduce(s -> false, (p1, p2) -> p1.or(p2));
|
||||
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,6 +61,10 @@ public interface IndicesPermission extends Permission, Iterable<IndicesPermissio
|
|||
|
||||
private final Group[] groups;
|
||||
|
||||
public Core(List<Group> groups) {
|
||||
this(groups.toArray(new Group[groups.size()]));
|
||||
}
|
||||
|
||||
public Core(Group... groups) {
|
||||
this.groups = groups;
|
||||
loadingFunction = (action) -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<IndicesPermission.Group> convertFromIndicesPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges) {
|
||||
List<IndicesPermission.Group> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import dk.brics.automaton.BasicAutomata;
|
|||
public class GeneralPrivilege extends AbstractAutomatonPrivilege<GeneralPrivilege> {
|
||||
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<RolesStore> 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 {
|
||||
|
|
|
@ -433,7 +433,7 @@ public class NativeRolesStore extends AbstractComponent implements RolesStore, C
|
|||
}
|
||||
|
||||
private <Response> void clearRoleCache(final String role, ActionListener<Response> listener, Response response) {
|
||||
ClearRolesCacheRequest request = new ClearRolesCacheRequest().roles(role);
|
||||
ClearRolesCacheRequest request = new ClearRolesCacheRequest().names(role);
|
||||
securityClient.clearRolesCache(request, new ActionListener<ClearRolesCacheResponse>() {
|
||||
@Override
|
||||
public void onResponse(ClearRolesCacheResponse nodes) {
|
||||
|
|
|
@ -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<RoleDescriptor> 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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ChangePasswordResponse> listener) {
|
||||
client.execute(ChangePasswordAction.INSTANCE, request, listener);
|
||||
}
|
||||
|
||||
/** Role Management */
|
||||
|
||||
public GetRolesRequestBuilder prepareGetRoles(String... names) {
|
||||
|
|
|
@ -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<AuthenticateResponse>(channel) {
|
||||
@Override
|
||||
public RestResponse buildResponse(AuthenticateResponse authenticateResponse, XContentBuilder builder) throws Exception {
|
||||
authenticateResponse.user().toXContent(builder, ToXContent.EMPTY_PARAMS);
|
||||
return new BytesRestResponse(RestStatus.OK, builder);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ClearRolesCacheResponse>(channel) {
|
||||
@Override
|
||||
|
|
|
@ -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<ChangePasswordResponse>(channel) {
|
||||
@Override
|
||||
public RestResponse buildResponse(ChangePasswordResponse changePasswordResponse, XContentBuilder builder) throws
|
||||
Exception {
|
||||
return new BytesRestResponse(RestStatus.OK, channel.newBuilder().startObject().endObject());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <code>initialize</code> 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<String> USERNAME_SETTING =
|
||||
new Setting<>(setting("authc.anonymous.username"), DEFAULT_ANONYMOUS_USERNAME, s -> s, Property.NodeScope);
|
||||
public static final Setting<List<String>> 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<String> 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue