Enhanced integration with other plugins

- Added an option for other plugins to define reserved roles. These roles will be reserved for the plugins and will be used by the plugin when executing actions. The reserved roles cannot be overridden by the `roles.yml` file. While at it, also made sure the system role cannot be defined in the `roles.yml` file. The roles can be registered via the `AuthorizationModule.registerReservedRole` method.

- Enable plugins to add their own (new) cluster & index privileges. The can be done by simply calling `Cluster.addCustom` and `Index.addCustom` static methods.

Original commit: elastic/x-pack-elasticsearch@11f795bebd
This commit is contained in:
uboness 2015-03-26 13:16:57 +01:00
parent a5e33b1aec
commit 0abef51d80
6 changed files with 214 additions and 25 deletions

View File

@ -5,22 +5,38 @@
*/
package org.elasticsearch.shield.authz;
import org.elasticsearch.common.inject.multibindings.Multibinder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authz.store.FileRolesStore;
import org.elasticsearch.shield.authz.store.RolesStore;
import org.elasticsearch.shield.support.AbstractShieldModule;
import java.util.HashSet;
import java.util.Set;
/**
*
*/
public class AuthorizationModule extends AbstractShieldModule.Node {
private final Set<Permission.Global.Role> reservedRoles = new HashSet<>();
public AuthorizationModule(Settings settings) {
super(settings);
}
public void registerReservedRole(Permission.Global.Role role) {
reservedRoles.add(role);
}
@Override
protected void configureNode() {
Multibinder<Permission.Global.Role> reservedRolesBinder = Multibinder.newSetBinder(binder(), Permission.Global.Role.class);
for (Permission.Global.Role reservedRole : reservedRoles) {
reservedRolesBinder.addBinding().toInstance(reservedRole);
}
bind(FileRolesStore.class).asEagerSingleton();
bind(RolesStore.class).to(FileRolesStore.class).asEagerSingleton();
bind(AuthorizationService.class).to(InternalAuthorizationService.class).asEagerSingleton();

View File

@ -8,7 +8,6 @@ package org.elasticsearch.shield.authz;
import dk.brics.automaton.Automaton;
import dk.brics.automaton.BasicAutomata;
import dk.brics.automaton.BasicOperations;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
@ -25,11 +24,13 @@ 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.ShieldException;
import org.elasticsearch.shield.support.AutomatonPredicate;
import org.elasticsearch.shield.support.Automatons;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import static org.elasticsearch.shield.support.Automatons.patterns;
@ -158,14 +159,29 @@ public abstract class Privilege<P extends Privilege<P>> {
public static final Index DELETE = new Index("delete", "indices:data/write/delete*");
public static final Index WRITE = new Index("write", "indices:data/write/*");
private static final Index[] values = new Index[] {
NONE, ALL, MANAGE, CREATE_INDEX, MANAGE_ALIASES, MONITOR, DATA_ACCESS, CRUD, READ, SEARCH, GET, SUGGEST, INDEX, DELETE, WRITE
};
private static final Set<Index> values = new CopyOnWriteArraySet<>();
static {
values.add(NONE);
values.add(ALL);
values.add(MANAGE);
values.add(CREATE_INDEX);
values.add(MANAGE_ALIASES);
values.add(MONITOR);
values.add(DATA_ACCESS);
values.add(CRUD);
values.add(READ);
values.add(SEARCH);
values.add(GET);
values.add(SUGGEST);
values.add(INDEX);
values.add(DELETE);
values.add(WRITE);
}
public static final Predicate<String> ACTION_MATCHER = ALL.predicate();
public static final Predicate<String> CREATE_INDEX_MATCHER = CREATE_INDEX.predicate();
static Index[] values() {
static Set<Index> values() {
return values;
}
@ -193,6 +209,19 @@ public abstract class Privilege<P extends Privilege<P>> {
super(name, automaton);
}
public static void addCustom(String name, String... actionPatterns) {
for (String pattern : actionPatterns) {
if (!Index.ACTION_MATCHER.apply(pattern)) {
throw new ShieldException("cannot register custom index privilege [" + name + "]. index aciton must follow the 'indices:*' format");
}
}
Index custom = new Index(name, actionPatterns);
if (values.contains(custom)) {
throw new ShieldException("cannot register custom index privilege [" + name + "] as it already exists.");
}
values.add(custom);
}
@Override
protected Index create(Name name, Automaton automaton) {
if (name == Name.NONE) {
@ -237,7 +266,7 @@ public abstract class Privilege<P extends Privilege<P>> {
}
}
throw new ElasticsearchIllegalArgumentException("unknown index privilege [" + name + "]. a privilege must be either " +
"one of the predefined fixed indices privileges [" + Strings.arrayToCommaDelimitedString(values) +
"one of the predefined fixed indices privileges [" + Strings.collectionToCommaDelimitedString(values) +
"] or a pattern over one of the available index actions");
}
@ -252,9 +281,15 @@ public abstract class Privilege<P extends Privilege<P>> {
final static Predicate<String> ACTION_MATCHER = Privilege.Cluster.ALL.predicate();
private static final Cluster[] values = new Cluster[] { NONE, ALL, MONITOR, MANAGE_SHIELD };
private static final Set<Cluster> values = new CopyOnWriteArraySet<>();
static {
values.add(NONE);
values.add(ALL);
values.add(MONITOR);
values.add(MANAGE_SHIELD);
}
static Cluster[] values() {
static Set<Cluster> values() {
return values;
}
@ -282,9 +317,21 @@ public abstract class Privilege<P extends Privilege<P>> {
super(name, automaton);
}
public static void addCustom(String name, String... actionPatterns) {
for (String pattern : actionPatterns) {
if (!Cluster.ACTION_MATCHER.apply(pattern)) {
throw new ShieldException("cannot register custom cluster privilege [" + name + "]. cluster aciton must follow the 'cluster:*' format");
}
}
Cluster custom = new Cluster(name, actionPatterns);
if (values.contains(custom)) {
throw new ShieldException("cannot register custom cluster privilege [" + name + "] as it already exists.");
}
values.add(custom);
}
@Override
protected Cluster create(Name name, Automaton automaton) {
return new Cluster(name, automaton);
}
@ -317,7 +364,7 @@ public abstract class Privilege<P extends Privilege<P>> {
}
}
throw new ElasticsearchIllegalArgumentException("unknown cluster privilege [" + name + "]. a privilege must be either " +
"one of the predefined fixed cluster privileges [" + Strings.arrayToCommaDelimitedString(values) +
"one of the predefined fixed cluster privileges [" + Strings.collectionToCommaDelimitedString(values) +
"] or a pattern over one of the available cluster actions");
}
}
@ -383,6 +430,8 @@ public abstract class Privilege<P extends Privilege<P>> {
protected abstract P create(Name name, Automaton automaton);
protected abstract P none();
}
public static class Name {

View File

@ -10,6 +10,7 @@ import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.jackson.dataformat.yaml.snakeyaml.error.YAMLException;
@ -22,6 +23,7 @@ import org.elasticsearch.shield.ShieldPlugin;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authz.Permission;
import org.elasticsearch.shield.authz.Privilege;
import org.elasticsearch.shield.authz.SystemRole;
import org.elasticsearch.shield.support.NoOpLogger;
import org.elasticsearch.shield.support.Validation;
import org.elasticsearch.watcher.FileChangesListener;
@ -33,10 +35,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.regex.Pattern;
/**
@ -50,20 +49,23 @@ public class FileRolesStore extends AbstractLifecycleComponent<RolesStore> imple
private final Path file;
private final RefreshListener listener;
private final ImmutableSet<Permission.Global.Role> reservedRoles;
private volatile ImmutableMap<String, Permission.Global.Role> permissions;
@Inject
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, RefreshListener.NOOP);
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Set<Permission.Global.Role> reservedRoles) {
this(settings, env, watcherService, reservedRoles, RefreshListener.NOOP);
}
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, RefreshListener listener) {
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Set<Permission.Global.Role> reservedRoles, RefreshListener listener) {
super(settings);
this.file = resolveFile(settings, env);
this.listener = listener;
this.reservedRoles = ImmutableSet.copyOf(reservedRoles);
permissions = ImmutableMap.of();
FileWatcher watcher = new FileWatcher(file.getParent().toFile());
watcher.addListener(new FileListener());
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
@ -71,7 +73,7 @@ public class FileRolesStore extends AbstractLifecycleComponent<RolesStore> imple
@Override
protected void doStart() throws ElasticsearchException {
permissions = parseFile(file, logger);
permissions = parseFile(file, reservedRoles, logger);
}
@Override
@ -97,6 +99,10 @@ public class FileRolesStore extends AbstractLifecycleComponent<RolesStore> imple
}
public static ImmutableMap<String, Permission.Global.Role> parseFile(Path path, ESLogger logger) {
return parseFile(path, Collections.<Permission.Global.Role>emptySet(), logger);
}
public static ImmutableMap<String, Permission.Global.Role> parseFile(Path path, Set<Permission.Global.Role> reservedRoles, ESLogger logger) {
if (logger == null) {
logger = NoOpLogger.INSTANCE;
}
@ -107,7 +113,7 @@ public class FileRolesStore extends AbstractLifecycleComponent<RolesStore> imple
return ImmutableMap.of();
}
ImmutableMap.Builder<String, Permission.Global.Role> roles = ImmutableMap.builder();
Map<String, Permission.Global.Role> roles = new HashMap<>();
try {
@ -115,14 +121,27 @@ public class FileRolesStore extends AbstractLifecycleComponent<RolesStore> imple
for (String segment : roleSegments) {
Permission.Global.Role role = parseRole(segment, path, logger);
if (role != null) {
roles.put(role.name(), role);
if (SystemRole.NAME.equals(role.name())) {
logger.warn("role [{}] is reserved to the system. the relevant role definition in the mapping file will be ignored", SystemRole.NAME);
} else {
roles.put(role.name(), role);
}
}
}
} catch (IOException ioe) {
logger.error("failed to read roles file [{}]. skipping all roles...", ioe, path.toAbsolutePath());
}
return roles.build();
// we now add all the fixed roles (overriding any attempts to override the fixed roles in the file)
for (Permission.Global.Role reservedRole : reservedRoles) {
if (roles.containsKey(reservedRole.name())) {
logger.warn("role [{}] is reserved to the system. the relevant role definition in the mapping file will be ignored", reservedRole.name());
}
roles.put(reservedRole.name(), reservedRole);
}
return ImmutableMap.copyOf(roles);
}
private static Permission.Global.Role parseRole(String segment, Path path, ESLogger logger) {
@ -283,7 +302,7 @@ public class FileRolesStore extends AbstractLifecycleComponent<RolesStore> imple
public void onFileChanged(File file) {
if (file.equals(FileRolesStore.this.file.toFile())) {
try {
permissions = parseFile(file.toPath(), logger);
permissions = parseFile(file.toPath(), reservedRoles, logger);
logger.info("updated roles (roles file [{}] changed)", file.getAbsolutePath());
} catch (Throwable t) {
logger.error("could not reload roles file [{}]. Current roles remain unmodified", t, file.getAbsolutePath());

View File

@ -12,6 +12,7 @@ import org.elasticsearch.action.search.MultiSearchAction;
import org.elasticsearch.action.search.SearchAction;
import org.elasticsearch.action.suggest.SuggestAction;
import org.elasticsearch.common.base.Predicate;
import org.elasticsearch.shield.ShieldException;
import org.elasticsearch.shield.support.AutomatonPredicate;
import org.elasticsearch.shield.support.Automatons;
import org.elasticsearch.test.ElasticsearchTestCase;
@ -111,6 +112,33 @@ public class PrivilegeTests extends ElasticsearchTestCase {
assertThat(cluster.predicate().apply("cluster:admin/snapshot/dele"), is(false));
}
@Test
public void testCluster_AddCustom() throws Exception {
Privilege.Cluster.addCustom("foo", "cluster:bar");
boolean found = false;
for (Privilege.Cluster cluster : Privilege.Cluster.values()) {
if ("foo".equals(cluster.name.toString())) {
found = true;
assertThat(cluster.predicate().apply("cluster:bar"), is(true));
}
}
assertThat(found, is(true));
Privilege.Cluster cluster = Privilege.Cluster.get(new Privilege.Name("foo"));
assertThat(cluster, notNullValue());
assertThat(cluster.name().toString(), is("foo"));
assertThat(cluster.predicate().apply("cluster:bar"), is(true));
}
@Test(expected = ShieldException.class)
public void testCluster_AddCustom_InvalidPattern() throws Exception {
Privilege.Cluster.addCustom("foo", "bar");
}
@Test(expected = ShieldException.class)
public void testCluster_AddCustom_AlreadyExists() throws Exception {
Privilege.Cluster.addCustom("all", "bar");
}
@Test
public void testIndexAction() throws Exception {
Privilege.Name actionName = new Privilege.Name("indices:admin/mapping/delete");
@ -122,7 +150,7 @@ public class PrivilegeTests extends ElasticsearchTestCase {
@Test
public void testIndex_Collapse() throws Exception {
Privilege.Index[] values = Privilege.Index.values();
Privilege.Index[] values = Privilege.Index.values().toArray(new Privilege.Index[Privilege.Index.values().size()]);
Privilege.Index first = values[randomIntBetween(0, values.length-1)];
Privilege.Index second = values[randomIntBetween(0, values.length-1)];
@ -140,7 +168,7 @@ public class PrivilegeTests extends ElasticsearchTestCase {
@Test
public void testIndex_Implies() throws Exception {
Privilege.Index[] values = Privilege.Index.values();
Privilege.Index[] values = Privilege.Index.values().toArray(new Privilege.Index[Privilege.Index.values().size()]);
Privilege.Index first = values[randomIntBetween(0, values.length-1)];
Privilege.Index second = values[randomIntBetween(0, values.length-1)];
@ -171,6 +199,33 @@ public class PrivilegeTests extends ElasticsearchTestCase {
}
}
@Test
public void testIndex_AddCustom() throws Exception {
Privilege.Index.addCustom("foo", "indices:bar");
boolean found = false;
for (Privilege.Index index : Privilege.Index.values()) {
if ("foo".equals(index.name.toString())) {
found = true;
assertThat(index.predicate().apply("indices:bar"), is(true));
}
}
assertThat(found, is(true));
Privilege.Index index = Privilege.Index.get(new Privilege.Name("foo"));
assertThat(index, notNullValue());
assertThat(index.name().toString(), is("foo"));
assertThat(index.predicate().apply("indices:bar"), is(true));
}
@Test(expected = ShieldException.class)
public void testIndex_AddCustom_InvalidPattern() throws Exception {
Privilege.Index.addCustom("foo", "bar");
}
@Test(expected = ShieldException.class)
public void testIndex_AddCustom_AlreadyExists() throws Exception {
Privilege.Index.addCustom("all", "bar");
}
@Test
public void testSystem() throws Exception {
Predicate<String> predicate = Privilege.SYSTEM.predicate();

View File

@ -6,6 +6,7 @@
package org.elasticsearch.shield.authz.store;
import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
@ -24,8 +25,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -137,7 +140,7 @@ public class FileRolesStoreTests extends ElasticsearchTestCase {
threadPool = new ThreadPool("test");
watcherService = new ResourceWatcherService(settings, threadPool);
final CountDownLatch latch = new CountDownLatch(1);
FileRolesStore store = new FileRolesStore(settings, env, watcherService, new RefreshListener() {
FileRolesStore store = new FileRolesStore(settings, env, watcherService, Collections.<Permission.Global.Role>emptySet(), new RefreshListener() {
@Override
public void onRefresh() {
latch.countDown();
@ -205,4 +208,37 @@ public class FileRolesStoreTests extends ElasticsearchTestCase {
assertThat(entries.get(3).text, startsWith("invalid role definition [role3] in roles file [" + path.toAbsolutePath() + "]. [indices] field value must be an array"));
assertThat(entries.get(4).text, startsWith("invalid role definition [role4] in roles file [" + path.toAbsolutePath() + "]. could not resolve indices privileges [al;kjdlkj;lkj]"));
}
@Test
public void testReservedRoles() throws Exception {
Set<Permission.Global.Role> reservedRoles = ImmutableSet.<Permission.Global.Role>builder()
.add(Permission.Global.Role.builder("reserved")
.set(Privilege.Cluster.ALL)
.build())
.build();
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
Path path = Paths.get(getClass().getResource("reserved_roles.yml").toURI());
Map<String, Permission.Global.Role> roles = FileRolesStore.parseFile(path, reservedRoles, logger);
assertThat(roles, notNullValue());
assertThat(roles.size(), is(2));
assertThat(roles, hasKey("admin"));
assertThat(roles, hasKey("reserved"));
Permission.Global.Role reserved = roles.get("reserved");
List<CapturingLogger.Msg> messages = logger.output(CapturingLogger.Level.WARN);
assertThat(messages, notNullValue());
assertThat(messages, hasSize(2));
// the system role will always be checked first
assertThat(messages.get(0).text, containsString("role [__es_system_role] is reserved"));
assertThat(messages.get(1).text, containsString("role [reserved] is reserved"));
// we overriden the configured reserved role with ALL cluster priv. (was configured to be "monitor" only)
assertThat(reserved.cluster().check("cluster:admin/test"), is(true));
// we overriden the configured reserved role without index privs. (was configured with index priv on "index_a_*" indices)
assertThat(reserved.indices().isEmpty(), is(true));
}
}

View File

@ -0,0 +1,14 @@
admin:
cluster: all
indices:
'*': all
reserved:
cluster: monitor
indices:
'index_a_*' : all
__es_system_role:
cluster: all
indices:
'*' : all