Introduced an API to clear realms caches

Since both LDAP and AD realms are caching users. If the groups of the users change on the LDAP side, these changes will not be visible in shield until the relevant cached users will be evicted from cache. This poses a problem, specially when degrading users in terms of their permission (e.g. after degrading them on LDAP, they still have higher privileges until they're evicted from cache). The default cache timeout today is 1 hour. For this reason, a new API is introduced which will enable administrators to force cache evictions.

- Changed the default cache timeout to 20 minute
- `ClearRealmCacheAction was introduced (along with the relevant request and response constructs). This is a cluster action
- the corresponding rest action was introduced as well, under the `_shield/realm/{realm}/cache/clear` URI (where `{realm}` enables clearing specific realms, or all realms when passing `_all`.
- With the introduction of an action, the `ActionModule` now is no longer a node module - it's bound on both node and transport client.
- Added a new Cluster permission - `manage_shield`
- Also cleaned up the `Permission` and `AuthorizationService` class

Original commit: elastic/x-pack-elasticsearch@c59e244435
This commit is contained in:
uboness 2015-01-13 19:27:45 +01:00
parent be768d5a44
commit 2f373f692f
21 changed files with 857 additions and 46 deletions

View File

@ -35,6 +35,7 @@ public class ShieldModule extends AbstractShieldModule.Spawn {
// spawn needed parts in client mode
if (clientMode) {
return ImmutableList.<Module>of(
new ShieldActionModule(settings),
new SecuredTransportModule(settings),
new SSLModule(settings));
}

View File

@ -9,12 +9,14 @@ import org.elasticsearch.action.ActionModule;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.inject.PreProcessModule;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheAction;
import org.elasticsearch.shield.action.authc.cache.TransportClearRealmCacheAction;
import org.elasticsearch.shield.support.AbstractShieldModule;
/**
*
*/
public class ShieldActionModule extends AbstractShieldModule.Node implements PreProcessModule {
public class ShieldActionModule extends AbstractShieldModule implements PreProcessModule {
public ShieldActionModule(Settings settings) {
super(settings);
@ -22,14 +24,23 @@ public class ShieldActionModule extends AbstractShieldModule.Node implements Pre
@Override
public void processModule(Module module) {
if (!clientMode && module instanceof ActionModule) {
((ActionModule) module).registerFilter(ShieldActionFilter.class);
if (module instanceof ActionModule) {
// registering the security filter only for nodes
if (!clientMode) {
((ActionModule) module).registerFilter(ShieldActionFilter.class);
}
// registering all shield actions
((ActionModule) module).registerAction(ClearRealmCacheAction.INSTANCE, TransportClearRealmCacheAction.class);
}
}
@Override
protected void configureNode() {
// we need to ensure that there's only a single instance of this filter.
bind(ShieldActionFilter.class).asEagerSingleton();
protected void configure(boolean clientMode) {
if (!clientMode) {
// we need to ensure that there's only a single instance of this filter.
bind(ShieldActionFilter.class).asEagerSingleton();
}
}
}

View File

@ -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.authc.cache;
import org.elasticsearch.action.admin.cluster.ClusterAction;
import org.elasticsearch.client.ClusterAdminClient;
/**
*
*/
public class ClearRealmCacheAction extends ClusterAction<ClearRealmCacheRequest, ClearRealmCacheResponse, ClearRealmCacheRequestBuilder> {
public static final ClearRealmCacheAction INSTANCE = new ClearRealmCacheAction();
public static final String NAME = "cluster:admin/shield/realm/cache/clear";
protected ClearRealmCacheAction() {
super(NAME);
}
@Override
public ClearRealmCacheRequestBuilder newRequestBuilder(ClusterAdminClient client) {
return new ClearRealmCacheRequestBuilder(client);
}
@Override
public ClearRealmCacheResponse newResponse() {
return new ClearRealmCacheResponse();
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.authc.cache;
import org.elasticsearch.action.support.nodes.NodeOperationRequest;
import org.elasticsearch.action.support.nodes.NodesOperationRequest;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
/**
*
*/
public class ClearRealmCacheRequest extends NodesOperationRequest<ClearRealmCacheRequest> {
String[] realms;
String[] usernames;
/**
* @return {@code true} if this request targets realms, {@code false} otherwise.
*/
public boolean allRealms() {
return realms == null || realms.length == 0;
}
/**
* @return The realms that should be evicted. Empty array indicates all realms.
*/
public String[] realms() {
return realms;
}
/**
* Sets the realms for which caches will be evicted. When not set all the caches of all realms will be
* evicted.
*
* @param realms The realm names
*/
public ClearRealmCacheRequest realms(String... realms) {
this.realms = realms;
return this;
}
/**
* @return {@code true} if this request targets users, {@code false} otherwise.
*/
public boolean allUsernames() {
return usernames == null || usernames.length == 0;
}
/**
* @return The usernames of the users that should be evicted. Empty array indicates all users.
*/
public String[] usernames() {
return usernames;
}
/**
* Sets the usernames of the users that should be evicted from the caches. When not set, all users
* will be evicted.
*
* @param usernames The usernames
*/
public ClearRealmCacheRequest usernames(String... usernames) {
this.usernames = usernames;
return this;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
realms = in.readStringArray();
usernames = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringArrayNullable(realms);
out.writeStringArrayNullable(usernames);
}
static class Node extends NodeOperationRequest {
String[] realms;
String[] usernames;
Node() {
}
Node(ClearRealmCacheRequest request, String nodeId) {
super(request, nodeId);
this.realms = request.realms;
this.usernames = request.usernames;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
realms = in.readStringArray();
usernames = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringArrayNullable(realms);
out.writeStringArrayNullable(usernames);
}
}
}

View File

@ -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.action.authc.cache;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.nodes.NodesOperationRequestBuilder;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.ClusterAdminClient;
import org.elasticsearch.shield.client.ShieldAuthcClient;
import org.elasticsearch.shield.client.ShieldClient;
/**
*
*/
public class ClearRealmCacheRequestBuilder extends NodesOperationRequestBuilder<ClearRealmCacheRequest, ClearRealmCacheResponse, ClearRealmCacheRequestBuilder> {
private final ShieldAuthcClient authcClient;
public ClearRealmCacheRequestBuilder(Client client) {
this(client.admin().cluster());
}
public ClearRealmCacheRequestBuilder(ClusterAdminClient client) {
super(client, new ClearRealmCacheRequest());
authcClient = new ShieldClient(client).authc();
}
/**
* Sets the realms for which caches will be evicted. When not set all the caches of all realms will be
* evicted.
*
* @param realms The realm names
*/
public ClearRealmCacheRequestBuilder realms(String... realms) {
request.realms(realms);
return this;
}
/**
* Sets the usernames of the users that should be evicted from the caches. When not set, all users
* will be evicted.
*
* @param usernames The usernames
*/
public ClearRealmCacheRequestBuilder usernames(String... usernames) {
request.usernames(usernames);
return this;
}
@Override
protected void doExecute(ActionListener<ClearRealmCacheResponse> listener) {
authcClient.clearRealmCache(request, listener);
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.action.authc.cache;
import org.elasticsearch.action.support.nodes.NodeOperationResponse;
import org.elasticsearch.action.support.nodes.NodesOperationResponse;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import java.io.IOException;
/**
*
*/
public class ClearRealmCacheResponse extends NodesOperationResponse<ClearRealmCacheResponse.Node> implements ToXContent {
public ClearRealmCacheResponse() {
}
public ClearRealmCacheResponse(ClusterName clusterName, Node[] nodes) {
super(clusterName, nodes);
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
nodes = new Node[in.readVInt()];
for (int i = 0; i < nodes.length; i++) {
nodes[i] = Node.readNodeResponse(in);
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(nodes.length);
for (Node node : nodes) {
node.writeTo(out);
}
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.field("cluster_name", getClusterName().value());
builder.startObject("nodes");
for (ClearRealmCacheResponse.Node node: getNodes()) {
builder.startObject(node.getNode().id());
builder.field("name", node.getNode().name());
builder.endObject();
}
return builder.endObject();
}
@Override
public String toString() {
try {
XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint();
builder.startObject();
toXContent(builder, EMPTY_PARAMS);
builder.endObject();
return builder.string();
} catch (IOException e) {
return "{ \"error\" : \"" + e.getMessage() + "\"}";
}
}
public static class Node extends NodeOperationResponse {
Node() {
}
Node(DiscoveryNode node) {
super(node);
}
public static Node readNodeResponse(StreamInput in) throws IOException {
Node node = new Node();
node.readFrom(in);
return node;
}
}
}

View File

@ -0,0 +1,117 @@
/*
* 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.authc.cache;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.nodes.TransportNodesOperationAction;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.common.collect.Lists;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.RealmMissingException;
import org.elasticsearch.shield.authc.Realms;
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import java.util.List;
import java.util.concurrent.atomic.AtomicReferenceArray;
/**
*
*/
public class TransportClearRealmCacheAction extends TransportNodesOperationAction<ClearRealmCacheRequest, ClearRealmCacheResponse, ClearRealmCacheRequest.Node, ClearRealmCacheResponse.Node> {
private final Realms realms;
@Inject
public TransportClearRealmCacheAction(Settings settings, ClusterName clusterName, ThreadPool threadPool,
ClusterService clusterService, TransportService transportService,
ActionFilters actionFilters, Realms realms) {
super(settings, ClearRealmCacheAction.NAME, clusterName, threadPool, clusterService, transportService, actionFilters);
this.realms = realms;
}
@Override
protected String executor() {
return ThreadPool.Names.MANAGEMENT;
}
@Override
protected ClearRealmCacheRequest newRequest() {
return new ClearRealmCacheRequest();
}
@Override
protected ClearRealmCacheResponse newResponse(ClearRealmCacheRequest request, AtomicReferenceArray responses) {
final List<ClearRealmCacheResponse.Node> nodes = Lists.newArrayList();
for (int i = 0; i < responses.length(); i++) {
Object resp = responses.get(i);
if (resp instanceof ClearRealmCacheResponse.Node) {
nodes.add((ClearRealmCacheResponse.Node) resp);
}
}
return new ClearRealmCacheResponse(clusterName, nodes.toArray(new ClearRealmCacheResponse.Node[nodes.size()]));
}
@Override
protected ClearRealmCacheRequest.Node newNodeRequest() {
return new ClearRealmCacheRequest.Node();
}
@Override
protected ClearRealmCacheRequest.Node newNodeRequest(String nodeId, ClearRealmCacheRequest request) {
return new ClearRealmCacheRequest.Node(request, nodeId);
}
@Override
protected ClearRealmCacheResponse.Node newNodeResponse() {
return new ClearRealmCacheResponse.Node();
}
@Override
protected ClearRealmCacheResponse.Node nodeOperation(ClearRealmCacheRequest.Node nodeRequest) throws ElasticsearchException {
if (nodeRequest.realms == null || nodeRequest.realms.length == 0) {
for (Realm realm : realms) {
clearCache(realm, nodeRequest.usernames);
}
return new ClearRealmCacheResponse.Node(clusterService.localNode());
}
for (String realmName : nodeRequest.realms) {
Realm realm = realms.realm(realmName);
if (realm == null) {
throw new RealmMissingException("Could not find active realm [" + realmName + "]");
}
clearCache(realm, nodeRequest.usernames);
}
return new ClearRealmCacheResponse.Node(clusterService.localNode());
}
private void clearCache(Realm realm, String[] usernames) {
if (!(realm instanceof CachingUsernamePasswordRealm)) {
return;
}
CachingUsernamePasswordRealm cachingRealm = (CachingUsernamePasswordRealm) realm;
if (usernames != null && usernames.length != 0) {
for (String username : usernames) {
cachingRealm.expire(username);
}
} else {
cachingRealm.expireAll();
}
}
@Override
protected boolean accumulateExceptions() {
return false;
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.rest.RestStatus;
import org.elasticsearch.shield.ShieldException;
/**
*
*/
public class RealmMissingException extends ShieldException {
public RealmMissingException(String msg) {
super(msg);
}
@Override
public RestStatus status() {
return RestStatus.NOT_FOUND;
}
}

View File

@ -9,7 +9,6 @@ import org.apache.lucene.util.CollectionUtil;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.collect.Lists;
import org.elasticsearch.common.collect.Sets;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.ImmutableSettings;
@ -26,7 +25,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class Realms extends AbstractLifecycleComponent<Realms> implements Iterable<Realm> {
private final Map<String, Realm.Factory> factories;
private List<Realm> realms = Collections.EMPTY_LIST;
private List<Realm> realms = Collections.emptyList();
@Inject
public Realms(Settings settings, Map<String, Realm.Factory> factories) {
@ -50,6 +49,15 @@ public class Realms extends AbstractLifecycleComponent<Realms> implements Iterab
return realms.iterator();
}
public Realm realm(String name) {
for (Realm realm : realms) {
if (name.equals(realm.name)) {
return realm;
}
}
return null;
}
public Realm.Factory realmFactory(String type) {
return factories.get(type);
}

View File

@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit;
public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm {
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueHours(1);
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20);
private static final int DEFAULT_MAX_USERS = 100000; //100k users
public static final String CACHE_TTL = "cache.ttl";
public static final String CACHE_MAX_USERS = "cache.max_users";
@ -41,13 +41,13 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm
}
}
protected final void expire(String username) {
public final void expire(String username) {
if (cache != null) {
cache.invalidate(username);
}
}
protected final void expireAll() {
public final void expireAll() {
if (cache != null) {
cache.invalidateAll();
}

View File

@ -92,27 +92,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A
throw denial(user, action, request);
}
String[] roleNames = user.roles();
if (roleNames.length == 0) {
throw denial(user, action, request);
}
Permission.Global permission;
if (roleNames.length == 1) {
permission = rolesStore.role(roleNames[0]);
} else {
// we'll take all the roles and combine their associated permissions
Permission.Global.Compound.Builder roles = Permission.Global.Compound.builder();
for (String roleName : roleNames) {
Permission.Global role = rolesStore.role(roleName);
if (role != null) {
roles.add(role);
}
}
permission = roles.build();
}
Permission.Global permission = permission(user);
// permission can be null as it might be that the user's role
// is unknown
@ -168,6 +148,29 @@ public class InternalAuthorizationService extends AbstractComponent implements A
grant(user, action, request);
}
private Permission.Global permission(User user) {
String[] roleNames = user.roles();
if (roleNames.length == 0) {
return Permission.Global.NONE;
}
if (roleNames.length == 1) {
Permission.Global.Role role = rolesStore.role(roleNames[0]);
return role == null ? Permission.Global.NONE : role;
}
// we'll take all the roles and combine their associated permissions
Permission.Global.Compound.Builder roles = Permission.Global.Compound.builder();
for (String roleName : roleNames) {
Permission.Global role = rolesStore.role(roleName);
if (role != null) {
roles.add(role);
}
}
return roles.build();
}
private AuthorizationException denial(User user, String action, TransportRequest request) {
auditTrail.accessDenied(user, action, request);
return new AuthorizationException("Action [" + action + "] is unauthorized for user [" + user.principal() + "]");

View File

@ -38,7 +38,9 @@ public interface Permission {
boolean isEmpty();
static abstract class Global implements Permission {
static class Global implements Permission {
public static Global NONE = new Global(Cluster.Core.NONE, Indices.Core.NONE);
private final Cluster cluster;
private final Indices indices;

View File

@ -204,13 +204,14 @@ public abstract class Privilege<P extends Privilege<P>> {
public static class Cluster extends AutomatonPrivilege<Cluster> {
public static final Cluster NONE = new Cluster(Name.NONE, Automata.makeEmpty());
public static final Cluster ALL = new Cluster(Name.ALL, "cluster:*", "indices:admin/template/*");
public static final Cluster MONITOR = new Cluster("monitor", "cluster:monitor/*");
public static final Cluster NONE = new Cluster(Name.NONE, Automata.makeEmpty());
public static final Cluster ALL = new Cluster(Name.ALL, "cluster:*", "indices:admin/template/*");
public static final Cluster MONITOR = new Cluster("monitor", "cluster:monitor/*");
public static final Cluster MANAGE_SHIELD = new Cluster("manage_shield", "cluster:admin/shield/*");
final static Predicate<String> ACTION_MATCHER = Privilege.Cluster.ALL.predicate();
private static final Cluster[] values = new Cluster[] { NONE, ALL, MONITOR };
private static final Cluster[] values = new Cluster[] { NONE, ALL, MONITOR, MANAGE_SHIELD };
static Cluster[] values() {
return values;

View File

@ -0,0 +1,54 @@
/*
* 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.client;
import org.elasticsearch.action.ActionFuture;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ClusterAdminClient;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheAction;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheRequest;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheRequestBuilder;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheResponse;
/**
* A client to manage Shield's authentication
*/
public class ShieldAuthcClient {
private final ClusterAdminClient client;
ShieldAuthcClient(ClusterAdminClient client) {
this.client = client;
}
/**
* Clears the realm caches. It's possible to clear all user entries from all realms in the cluster or alternatively
* select the realms (by their unique names) and/or users (by their usernames) that should be evicted.
*/
@SuppressWarnings("unchecked")
public ClearRealmCacheRequestBuilder prepareClearRealmCache() {
return new ClearRealmCacheRequestBuilder(client);
}
/**
* Clears the realm caches. It's possible to clear all user entries from all realms in the cluster or alternatively
* select the realms (by their unique names) and/or users (by their usernames) that should be evicted.
*/
@SuppressWarnings("unchecked")
public void clearRealmCache(ClearRealmCacheRequest request, ActionListener<ClearRealmCacheResponse> listener) {
client.execute(ClearRealmCacheAction.INSTANCE, request, listener);
}
/**
* Clears the realm caches. It's possible to clear all user entries from all realms in the cluster or alternatively
* select the realms (by their unique names) and/or users (by their usernames) that should be evicted.
*/
@SuppressWarnings("unchecked")
public ActionFuture<ClearRealmCacheResponse> clearRealmCache(ClearRealmCacheRequest request) {
return client.execute(ClearRealmCacheAction.INSTANCE, request);
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.client;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.ClusterAdminClient;
/**
* A wrapper to elasticsearch clients that exposes all Shield related APIs
*/
public class ShieldClient {
private final ShieldAuthcClient authcClient;
public ShieldClient(Client client) {
this(client.admin().cluster());
}
public ShieldClient(ClusterAdminClient client) {
this.authcClient = new ShieldAuthcClient(client);
}
/**
* @return The Shield authenticatin client.
*/
public ShieldAuthcClient authc() {
return authcClient;
}
}

View File

@ -10,6 +10,7 @@ import org.elasticsearch.common.inject.PreProcessModule;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestModule;
import org.elasticsearch.shield.rest.action.RestShieldInfoAction;
import org.elasticsearch.shield.rest.action.authc.cache.RestClearRealmCacheAction;
import org.elasticsearch.shield.support.AbstractShieldModule;
/**
@ -30,6 +31,7 @@ public class ShieldRestModule extends AbstractShieldModule.Node implements PrePr
public void processModule(Module module) {
if (module instanceof RestModule) {
((RestModule) module).addRestAction(RestShieldInfoAction.class);
((RestModule) module).addRestAction(RestClearRealmCacheAction.class);
}
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.authc.cache;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.*;
import org.elasticsearch.rest.action.support.RestBuilderListener;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheRequest;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheResponse;
import org.elasticsearch.shield.client.ShieldClient;
import static org.elasticsearch.rest.RestRequest.Method.POST;
public class RestClearRealmCacheAction extends BaseRestHandler {
@Inject
public RestClearRealmCacheAction(Settings settings, RestController controller, Client client) {
super(settings, controller, client);
controller.registerHandler(POST, "/_shield/realm/{realms}/_cache/clear", this);
}
@Override
protected void handleRequest(RestRequest request, final RestChannel channel, Client client) throws Exception {
String[] realms = request.paramAsStringArrayOrEmptyIfAll("realms");
String[] usernames = request.paramAsStringArrayOrEmptyIfAll("usernames");
ClearRealmCacheRequest req = new ClearRealmCacheRequest().realms(realms).usernames(usernames);
new ShieldClient(client).authc().clearRealmCache(req, new RestBuilderListener<ClearRealmCacheResponse>(channel) {
@Override
public RestResponse buildResponse(ClearRealmCacheResponse response, XContentBuilder builder) throws Exception {
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
return new BytesRestResponse(RestStatus.OK, builder);
}
});
}
}

View File

@ -0,0 +1,206 @@
/*
* 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.integration;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.Strings;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheRequest;
import org.elasticsearch.shield.action.authc.cache.ClearRealmCacheResponse;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.Realms;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.client.ShieldClient;
import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import org.elasticsearch.test.ShieldIntegrationTest;
import org.elasticsearch.test.ShieldSettingsSource;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope.SUITE;
import static org.hamcrest.Matchers.*;
/**
*
*/
@ClusterScope(scope = SUITE)
public class ClearRealmsCacheTests extends ShieldIntegrationTest {
private static String[] usernames;
@BeforeClass
public static void init() throws Exception {
usernames = new String[randomIntBetween(5, 10)];
for (int i = 0; i < usernames.length; i++) {
usernames[i] = "user_" + i;
}
}
enum Scenario {
EVICT_ALL() {
@Override
public ClearRealmCacheRequest createRequest() {
return new ClearRealmCacheRequest();
}
@Override
public void assertEviction(User prevUser, User newUser) {
assertThat(prevUser, not(sameInstance(newUser)));
}
},
EVICT_SOME() {
private final String[] evicted_usernames = randomSelection(usernames);
@Override
public ClearRealmCacheRequest createRequest() {
return new ClearRealmCacheRequest().usernames(evicted_usernames);
}
@Override
public void assertEviction(User prevUser, User newUser) {
if (Arrays.binarySearch(evicted_usernames, prevUser.principal()) >= 0) {
assertThat(prevUser, not(sameInstance(newUser)));
} else {
assertThat(prevUser, sameInstance(newUser));
}
}
};
public abstract ClearRealmCacheRequest createRequest();
public abstract void assertEviction(User prevUser, User newUser);
}
@Override
protected String configRoles() {
return ShieldSettingsSource.CONFIG_ROLE_ALLOW_ALL + "\n" +
"r1:\n" +
" cluster: all\n";
}
@Override
protected String configUsers() {
StringBuilder builder = new StringBuilder(ShieldSettingsSource.CONFIG_STANDARD_USER);
for (String username : usernames) {
builder.append(username).append(":{plain}passwd\n");
}
return builder.toString();
}
@Override
protected String configUsersRoles() {
return ShieldSettingsSource.CONFIG_STANDARD_USER_ROLES +
"r1:" + Strings.arrayToCommaDelimitedString(usernames);
}
@Test
public void testEvictAll() throws Exception {
testScenario(Scenario.EVICT_ALL);
}
@Test
public void testEvictSome() throws Exception {
testScenario(Scenario.EVICT_SOME);
}
private void testScenario(Scenario scenario) throws Exception {
Map<String, UsernamePasswordToken> tokens = new HashMap<>();
for (String user : usernames) {
tokens.put(user, new UsernamePasswordToken(user, SecuredStringTests.build("passwd")));
}
List<Realm> realms = new ArrayList<>();
for (Realms nodeRealms : internalCluster().getInstances(Realms.class)) {
realms.add(nodeRealms.realm("esusers"));
}
// we authenticate each user on each of the realms to make sure they're all cached
Map<String, Map<Realm, User>> users = new HashMap<>();
for (Realm realm : realms) {
for (String username : usernames) {
User user = realm.authenticate(tokens.get(username));
assertThat(user, notNullValue());
Map<Realm, User> realmToUser = users.get(username);
if (realmToUser == null) {
realmToUser = new HashMap<>();
users.put(username, realmToUser);
}
realmToUser.put(realm, user);
}
}
// all users should be cached now on all realms, lets verify
for (String username : usernames) {
for (Realm realm : realms) {
assertThat(realm.authenticate(tokens.get(username)), sameInstance(users.get(username).get(realm)));
}
}
// now, lets run the scenario
ShieldClient client = new ShieldClient(client());
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Throwable> error = new AtomicReference<>();
client.authc().clearRealmCache(scenario.createRequest(), new ActionListener<ClearRealmCacheResponse>() {
@Override
public void onResponse(ClearRealmCacheResponse response) {
assertThat(response.getNodes().length, equalTo(internalCluster().getNodeNames().length));
latch.countDown();
}
@Override
public void onFailure(Throwable e) {
error.set(e);
latch.countDown();
}
});
if (!latch.await(5, TimeUnit.SECONDS)) {
fail("waiting for clear realms cache request too long");
}
if (error.get() != null) {
logger.error("Failed to clear realm caches", error.get());
fail("failed to clear realm caches");
}
// now, user_a should have been evicted, but user_b should still be cached
for (String username : usernames) {
for (Realm realm : realms) {
User user = realm.authenticate(tokens.get(username));
assertThat(user, notNullValue());
scenario.assertEviction(users.get(username).get(realm), user);
}
}
}
// selects a random sub-set of the give values
private static String[] randomSelection(String[] values) {
double base = randomDouble();
List<String> list = new ArrayList<>();
for (String value : values) {
if (randomDouble() < base) {
list.add(value);
}
}
return list.toArray(new String[list.size()]);
}
}

View File

@ -11,6 +11,7 @@ import org.elasticsearch.ElasticsearchIllegalStateException;
import org.elasticsearch.action.Action;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.util.Callback;
import org.elasticsearch.shield.action.ShieldActionModule;
import org.elasticsearch.test.ShieldIntegrationTest;
import org.elasticsearch.license.plugin.LicensePlugin;
import org.junit.BeforeClass;
@ -29,17 +30,17 @@ public class KnownActionsTests extends ShieldIntegrationTest {
private static ImmutableSet<String> knownActions;
private static ImmutableSet<String> knownHandlers;
private static ImmutableSet<String> externalActions;
private static ImmutableSet<String> codeActions;
@BeforeClass
public static void init() throws Exception {
knownActions = loadKnownActions();
knownHandlers = loadKnownHandlers();
externalActions = loadExternalActions();
codeActions = loadCodeActions();
}
@Test
public void testAllExternalTransportHandlersAreKnown() {
public void testAllTransportHandlersAreKnown() {
TransportService transportService = internalCluster().getDataNodeInstance(TransportService.class);
for (String handler : transportService.serverHandlers.keySet()) {
if (!knownActions.contains(handler)) {
@ -49,8 +50,8 @@ public class KnownActionsTests extends ShieldIntegrationTest {
}
@Test
public void testAllExternalActionsAreKnown() throws Exception {
for (String action : externalActions) {
public void testAllCodeActionsAreKnown() throws Exception {
for (String action : codeActions) {
assertThat("elasticsearch core action [" + action + "] is unknown to shield", knownActions, hasItem(action));
}
}
@ -58,7 +59,7 @@ public class KnownActionsTests extends ShieldIntegrationTest {
@Test
public void testAllKnownActionsAreValid() {
for (String knownAction : knownActions) {
assertThat("shield known action [" + knownAction + "] is unknown to core", externalActions, hasItems(knownAction));
assertThat("shield known action [" + knownAction + "] is unknown to core", codeActions, hasItems(knownAction));
}
}
@ -100,13 +101,17 @@ public class KnownActionsTests extends ShieldIntegrationTest {
return knownHandlersBuilder.build();
}
private static ImmutableSet<String> loadExternalActions() throws IOException, IllegalAccessException {
private static ImmutableSet<String> loadCodeActions() throws IOException, IllegalAccessException {
ImmutableSet.Builder<String> actions = ImmutableSet.builder();
// loading es core actions
ClassPath classPath = ClassPath.from(Action.class.getClassLoader());
loadActions(classPath, Action.class.getPackage().getName(), actions);
// loading shield actions
classPath = ClassPath.from(ShieldActionModule.class.getClassLoader());
loadActions(classPath, ShieldActionModule.class.getPackage().getName(), actions);
// also loading all actions from the licensing plugin
classPath = ClassPath.from(LicensePlugin.class.getClassLoader());
loadActions(classPath, LicensePlugin.class.getPackage().getName(), actions);

View File

@ -76,4 +76,5 @@ indices:data/write/script/put
indices:data/write/update
cluster:admin/plugin/license/get
cluster:admin/plugin/license/delete
cluster:admin/plugin/license/put
cluster:admin/plugin/license/put
cluster:admin/shield/realm/cache/clear

View File

@ -6,6 +6,8 @@ cluster:monitor/nodes/hot_threads[n]
cluster:monitor/nodes/info[n]
cluster:monitor/nodes/stats[n]
cluster:monitor/stats[n]
cluster:admin/shield/realm/cache/clear
cluster:admin/shield/realm/cache/clear[n]
indices:admin/analyze[s]
indices:admin/cache/clear[s]
indices:admin/flush[s]
@ -83,4 +85,4 @@ internal:index/shard/recovery/start_recovery
internal:index/shard/recovery/translog_ops
internal:river/state/publish
internal:admin/repository/verify
internal:plugin/license/cluster/register_trial_license
internal:plugin/license/cluster/register_trial_license