Allow configuring cluster & indices permissions on specific actions
- It is now possible to assign index & cluster permission on an action level (not just the fixed privileges we defined). - also added a test to check that all the actions elasticsearch has are indeed known to shield. So whenever a new action is introduced in elasticsearch, and shield is not aware of it, the build will fail. This will help us ensure that all actions in elasticsearch are 1) well formatted/categorized, 2) secured and "permissible" Closes elastic/elasticsearch#19 Original commit: elastic/x-pack-elasticsearch@170c3b9185
This commit is contained in:
parent
0d5c83d0f0
commit
1154f13345
|
@ -7,6 +7,8 @@ package org.elasticsearch.shield.authz;
|
|||
|
||||
import org.apache.lucene.util.automaton.Automaton;
|
||||
import org.apache.lucene.util.automaton.BasicAutomata;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||
import org.elasticsearch.action.get.GetAction;
|
||||
import org.elasticsearch.action.search.SearchAction;
|
||||
import org.elasticsearch.common.Strings;
|
||||
|
@ -16,6 +18,7 @@ import org.elasticsearch.common.cache.CacheLoader;
|
|||
import org.elasticsearch.common.cache.LoadingCache;
|
||||
import org.elasticsearch.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.collect.Sets;
|
||||
import org.elasticsearch.common.util.concurrent.UncheckedExecutionException;
|
||||
import org.elasticsearch.shield.support.AutomatonPredicate;
|
||||
import org.elasticsearch.shield.support.Automatons;
|
||||
|
||||
|
@ -27,6 +30,8 @@ import java.util.Set;
|
|||
*/
|
||||
public abstract class Privilege<P extends Privilege<P>> {
|
||||
|
||||
static final String SUB_ACTION_SUFFIX_PATTERN = ".*";
|
||||
|
||||
public static final Internal INTERNAL = new Internal();
|
||||
|
||||
protected final Name name;
|
||||
|
@ -128,12 +133,16 @@ public abstract class Privilege<P extends Privilege<P>> {
|
|||
return NONE;
|
||||
}
|
||||
|
||||
public static Index get(Name name) {
|
||||
return cache.getUnchecked(name);
|
||||
public static Index action(String action) {
|
||||
return new Index(action, actionToPattern(action));
|
||||
}
|
||||
|
||||
public static Index action(String action) {
|
||||
return new Index(action, action);
|
||||
public static Index get(Name name) {
|
||||
try {
|
||||
return cache.getUnchecked(name);
|
||||
} catch (UncheckedExecutionException e) {
|
||||
throw (ElasticsearchException) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
public static Index union(Index... indices) {
|
||||
|
@ -146,12 +155,17 @@ public abstract class Privilege<P extends Privilege<P>> {
|
|||
|
||||
private static Index resolve(String name) {
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
if (name.startsWith("indices:")) {
|
||||
return action(name);
|
||||
}
|
||||
for (Index index : values) {
|
||||
if (name.toLowerCase(Locale.ROOT).equals(index.name.toString())) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown index privilege [" + name + "]");
|
||||
throw new ElasticsearchIllegalArgumentException("Unknown index privilege [" + name + "]. A privilege must either " +
|
||||
"on of the predefined fixed indices privileges [" + Strings.arrayToCommaDelimitedString(values) +
|
||||
"] or a pattern over one of the available index actions (the pattern must begin with \"indices:\" prefix)");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -205,21 +219,39 @@ public abstract class Privilege<P extends Privilege<P>> {
|
|||
return NONE;
|
||||
}
|
||||
|
||||
public static Cluster action(String action) {
|
||||
String pattern = actionToPattern(action);
|
||||
return new Cluster(action, pattern);
|
||||
}
|
||||
|
||||
public static Cluster get(Name name) {
|
||||
return cache.getUnchecked(name);
|
||||
try {
|
||||
return cache.getUnchecked(name);
|
||||
} catch (UncheckedExecutionException e) {
|
||||
throw (ElasticsearchException) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private static Cluster resolve(String name) {
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
if (name.startsWith("cluster:")) {
|
||||
return action(name);
|
||||
}
|
||||
for (Cluster cluster : values) {
|
||||
if (name.equals(cluster.name.toString())) {
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown cluster privilege [" + name + "]");
|
||||
throw new ElasticsearchIllegalArgumentException("Unknown cluster privilege [" + name + "]. A privilege must either " +
|
||||
"on of the predefined fixed cluster privileges [" + Strings.arrayToCommaDelimitedString(values) +
|
||||
"] or a pattern over one of the available cluster actions (the pattern must begin with \"cluster:\" prefix)");
|
||||
}
|
||||
}
|
||||
|
||||
static String actionToPattern(String text) {
|
||||
return text.replace(":", "\\:") + SUB_ACTION_SUFFIX_PATTERN;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static abstract class AutomatonPrivilege<P extends AutomatonPrivilege<P>> extends Privilege<P> {
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.elasticsearch.ElasticsearchIllegalStateException;
|
||||
import org.elasticsearch.action.Action;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.util.Callback;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import static org.apache.lucene.util.LuceneTestCase.AwaitsFix;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class KnownActionsSanityCheckTests extends ElasticsearchTestCase {
|
||||
|
||||
private ImmutableSet<String> knownActions;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
final ImmutableSet.Builder<String> builder = ImmutableSet.builder();
|
||||
try (InputStream input = KnownActionsSanityCheckTests.class.getResourceAsStream("actions")) {
|
||||
Streams.readAllLines(input, new Callback<String>() {
|
||||
@Override
|
||||
public void handle(String action) {
|
||||
builder.add(action);
|
||||
}
|
||||
});
|
||||
} catch (IOException ioe) {
|
||||
throw new ElasticsearchIllegalStateException("Could not load known actions", ioe);
|
||||
}
|
||||
knownActions = builder.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllShieldActionsAreKnown() throws Exception {
|
||||
ClassPath classPath = ClassPath.from(Action.class.getClassLoader());
|
||||
ImmutableSet<ClassPath.ClassInfo> infos = classPath.getTopLevelClassesRecursive(Action.class.getPackage().getName());
|
||||
for (ClassPath.ClassInfo info : infos) {
|
||||
Class clazz = info.load();
|
||||
if (Action.class.isAssignableFrom(clazz)) {
|
||||
String name = extractActionName(clazz);
|
||||
if (name != null) {
|
||||
assertThat("elasticsearch core action [" + name + "] is unknown to shield", knownActions, hasItem(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test @AwaitsFix(bugUrl = "waiting for core to change the action names")
|
||||
public void testIndexTemplateActionIsIndicesAction() throws Exception {
|
||||
assertThat(knownActions.contains("indices:admin/template/delete"), is(true));
|
||||
assertThat(knownActions.contains("indices:admin/template/get"), is(true));
|
||||
assertThat(knownActions.contains("indices:admin/template/put"), is(true));
|
||||
}
|
||||
|
||||
private String extractActionName(Class clazz) throws Exception {
|
||||
if (Modifier.isAbstract(clazz.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
Field field = getField(clazz, "INSTANCE");
|
||||
if (field == null || !Modifier.isStatic(field.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
Action action = (Action) field.get(null);
|
||||
return action.name();
|
||||
|
||||
}
|
||||
|
||||
private static Field getField(Class clazz, String name) {
|
||||
try {
|
||||
return clazz.getField(name);
|
||||
} catch (NoSuchFieldException nsfe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -6,17 +6,24 @@
|
|||
package org.elasticsearch.shield.authz;
|
||||
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||
import org.elasticsearch.shield.support.AutomatonPredicate;
|
||||
import org.elasticsearch.shield.support.Automatons;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class PrivilegeTests extends ElasticsearchTestCase {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void testName() throws Exception {
|
||||
Privilege.Name name12 = new Privilege.Name("name1", "name2");
|
||||
|
@ -34,6 +41,14 @@ public class PrivilegeTests extends ElasticsearchTestCase {
|
|||
assertThat(none, is(Privilege.Name.NONE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubActionPattern() throws Exception {
|
||||
AutomatonPredicate predicate = new AutomatonPredicate(Automatons.patterns("foo" + Privilege.SUB_ACTION_SUFFIX_PATTERN));
|
||||
assertThat(predicate.apply("foo[n][nodes]"), is(true));
|
||||
assertThat(predicate.apply("foo[n]"), is(true));
|
||||
assertThat(predicate.apply("bar[n][nodes]"), is(false));
|
||||
assertThat(predicate.apply("[n][nodes]"), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCluster() throws Exception {
|
||||
|
@ -56,6 +71,31 @@ public class PrivilegeTests extends ElasticsearchTestCase {
|
|||
assertThat(cluster, is(cluster2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCluster_InvalidNaem() throws Exception {
|
||||
thrown.expect(ElasticsearchIllegalArgumentException.class);
|
||||
Privilege.Name actionName = new Privilege.Name("foobar");
|
||||
Privilege.Cluster.get(actionName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClusterAction() throws Exception {
|
||||
Privilege.Name actionName = new Privilege.Name("cluster:admin/snapshot/delete");
|
||||
Privilege.Cluster cluster = Privilege.Cluster.get(actionName);
|
||||
assertThat(cluster, notNullValue());
|
||||
assertThat(cluster.predicate().apply("cluster:admin/snapshot/delete"), is(true));
|
||||
assertThat(cluster.predicate().apply("cluster:admin/snapshot/dele"), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIndexAction() throws Exception {
|
||||
Privilege.Name actionName = new Privilege.Name("indices:admin/mapping/delete");
|
||||
Privilege.Index index = Privilege.Index.get(actionName);
|
||||
assertThat(index, notNullValue());
|
||||
assertThat(index.predicate().apply("indices:admin/mapping/delete"), is(true));
|
||||
assertThat(index.predicate().apply("indices:admin/mapping/dele"), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIndex_Collapse() throws Exception {
|
||||
Privilege.Index[] values = Privilege.Index.values();
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
cluster:admin/nodes/restart
|
||||
cluster:admin/nodes/shutdown
|
||||
cluster:admin/repository/delete
|
||||
cluster:admin/repository/get
|
||||
cluster:admin/repository/put
|
||||
cluster:admin/reroute
|
||||
cluster:admin/settings/update
|
||||
cluster:admin/snapshot/create
|
||||
cluster:admin/snapshot/delete
|
||||
cluster:admin/snapshot/get
|
||||
cluster:admin/snapshot/restore
|
||||
cluster:admin/snapshot/status
|
||||
cluster:monitor/health
|
||||
cluster:monitor/nodes/hot_threads
|
||||
cluster:monitor/nodes/info
|
||||
cluster:monitor/nodes/stats
|
||||
cluster:monitor/state
|
||||
cluster:monitor/stats
|
||||
cluster:monitor/task
|
||||
indices:admin/aliases
|
||||
indices:admin/aliases/exists
|
||||
indices:admin/aliases/get
|
||||
indices:admin/analyze
|
||||
indices:admin/cache/clear
|
||||
indices:admin/close
|
||||
indices:admin/create
|
||||
indices:admin/delete
|
||||
indices:admin/exists
|
||||
indices:admin/flush
|
||||
indices:admin/mapping/delete
|
||||
indices:admin/mapping/put
|
||||
indices:admin/mappings/fields/get
|
||||
indices:admin/mappings/get
|
||||
indices:admin/open
|
||||
indices:admin/optimize
|
||||
indices:admin/refresh
|
||||
indices:admin/settings/update
|
||||
indices:admin/shards/search_shards
|
||||
cluster:admin/template/delete
|
||||
cluster:admin/template/get
|
||||
cluster:admin/template/put
|
||||
indices:admin/types/exists
|
||||
indices:admin/validate/query
|
||||
indices:admin/warmers/delete
|
||||
indices:admin/warmers/get
|
||||
indices:admin/warmers/put
|
||||
indices:monitor/recovery
|
||||
indices:monitor/segments
|
||||
indices:monitor/settings/get
|
||||
indices:monitor/stats
|
||||
indices:monitor/status
|
||||
indices:data/benchmark/abort
|
||||
indices:data/benchmark/start
|
||||
indices:data/benchmark/status
|
||||
indices:data/read/count
|
||||
indices:data/read/exists
|
||||
indices:data/read/explain
|
||||
indices:data/read/get
|
||||
indices:data/read/mget
|
||||
indices:data/read/mlt
|
||||
indices:data/read/mpercolate
|
||||
indices:data/read/msearch
|
||||
indices:data/read/mtv
|
||||
indices:data/read/percolate
|
||||
indices:data/read/script/get
|
||||
indices:data/read/scroll
|
||||
indices:data/read/scroll/clear
|
||||
indices:data/read/search
|
||||
indices:data/read/suggest
|
||||
indices:data/read/tv
|
||||
indices:data/write/bulk
|
||||
indices:data/write/delete
|
||||
indices:data/write/delete/by_query
|
||||
indices:data/write/index
|
||||
indices:data/write/script/delete
|
||||
indices:data/write/script/put
|
||||
indices:data/write/update
|
Loading…
Reference in New Issue