Authorization: split analyze api into cluster level action and original indices action

The analyze api allows to specify an index, to retrieve analyzers or token filters from a specific index. That is why it is categorized as indices level action. That said the index is optional and when not specified the action is executed at the cluster level. We have to remap the name of the action in that case, to make sure that it requires a different privilege under cluster: cluster:admin/analyze instead of indices:admin/analyze .

Closes elastic/elasticsearch#566
Closes elastic/elasticsearch#565
Closes elastic/elasticsearch#592

Original commit: elastic/x-pack-elasticsearch@9073b30d08
This commit is contained in:
Luca Cavanna 2015-01-20 14:18:30 +01:00
parent 166514651a
commit f29cc62829
7 changed files with 210 additions and 20 deletions

View File

@ -38,8 +38,6 @@ import java.util.List;
*/
public class ShieldActionFilter extends AbstractComponent implements ActionFilter {
public static final String CLUSTER_PERMISSION_SCROLL_CLEAR_ALL_NAME = "cluster:admin/indices/scroll/clear_all";
private static final Predicate<String> READ_ACTION_MATCHER = Privilege.Index.READ.predicate();
private final AuthenticationService authcService;

View File

@ -5,6 +5,8 @@
*/
package org.elasticsearch.shield.action;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeAction;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeRequest;
import org.elasticsearch.action.search.ClearScrollAction;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.transport.TransportRequest;
@ -17,16 +19,28 @@ import org.elasticsearch.transport.TransportRequest;
*/
public class ShieldActionMapper {
static final String CLUSTER_PERMISSION_SCROLL_CLEAR_ALL_NAME = "cluster:admin/indices/scroll/clear_all";
static final String CLUSTER_PERMISSION_ANALYZE = "cluster:admin/analyze";
/**
* Returns the shield specific action name given the incoming action name and request
*/
public String action(String action, TransportRequest request) {
if (action.equals(ClearScrollAction.NAME)) {
assert request instanceof ClearScrollRequest;
boolean isClearAllScrollRequest = ((ClearScrollRequest) request).scrollIds().contains("_all");
if (isClearAllScrollRequest) {
return ShieldActionFilter.CLUSTER_PERMISSION_SCROLL_CLEAR_ALL_NAME;
}
switch (action) {
case ClearScrollAction.NAME:
assert request instanceof ClearScrollRequest;
boolean isClearAllScrollRequest = ((ClearScrollRequest) request).scrollIds().contains("_all");
if (isClearAllScrollRequest) {
return CLUSTER_PERMISSION_SCROLL_CLEAR_ALL_NAME;
}
break;
case AnalyzeAction.NAME:
assert request instanceof AnalyzeRequest;
String[] indices = ((AnalyzeRequest) request).indices();
if (indices == null || indices.length == 0) {
return CLUSTER_PERMISSION_ANALYZE;
}
break;
}
return action;
}

View File

@ -7,12 +7,11 @@ package org.elasticsearch.shield.authz;
import org.elasticsearch.action.CompositeIndicesRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeRequest;
import org.elasticsearch.action.search.ClearScrollAction;
import org.elasticsearch.action.search.SearchScrollAction;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestHelper;
import org.elasticsearch.action.search.ClearScrollAction;
import org.elasticsearch.action.search.SearchScrollAction;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.base.Predicate;
@ -144,10 +143,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A
}
Set<String> indexNames = resolveIndices(user, action, request);
//Note: some APIs (e.g. analyze) are categorized under indices, but their indices are optional.
//In that case the resolved indices set is empty (for now). See https://github.com/elasticsearch/elasticsearch-shield/issues/566
assert !indexNames.isEmpty() || request instanceof AnalyzeRequest
: "no indices request other than the analyze api has optional indices thus the resolved indices must not be empty";
assert !indexNames.isEmpty() : "every indices request needs to have its indices set thus the resolved indices must not be empty";
if (!authorizeIndices(action, indexNames, permission.indices())) {
throw denial(user, action, request);

View File

@ -216,7 +216,7 @@ public abstract class Privilege<P extends Privilege<P>> {
static Cluster[] values() {
return values;
};
}
private static final LoadingCache<Name, Cluster> cache = CacheBuilder.newBuilder().build(
new CacheLoader<Name, Cluster>() {

View File

@ -0,0 +1,92 @@
/*
* 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;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeAction;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeRequest;
import org.elasticsearch.action.search.ClearScrollAction;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.transport.KnownActionsTests;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
public class ShieldActionMapperTests extends ElasticsearchTestCase {
@Test
public void testThatAllOrdinaryActionsRemainTheSame() {
List<String> actions = new ArrayList<>();
actions.addAll(KnownActionsTests.loadKnownActions());
actions.addAll(KnownActionsTests.loadKnownHandlers());
ShieldActionMapper shieldActionMapper = new ShieldActionMapper();
int iterations = randomIntBetween(10, 100);
for (int i = 0; i < iterations; i++) {
String randomAction;
do {
if (randomBoolean()) {
randomAction = randomFrom(actions);
} else {
randomAction = randomAsciiOfLength(randomIntBetween(1, 30));
}
} while (randomAction.equals(ClearScrollAction.NAME) ||
randomAction.equals(AnalyzeAction.NAME));
assertThat(shieldActionMapper.action(randomAction, null), equalTo(randomAction));
}
}
@Test
public void testClearScroll() {
ShieldActionMapper shieldActionMapper = new ShieldActionMapper();
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
int scrollIds = randomIntBetween(1, 10);
for (int i = 0; i < scrollIds; i++) {
clearScrollRequest.addScrollId(randomAsciiOfLength(randomIntBetween(1, 30)));
}
assertThat(shieldActionMapper.action(ClearScrollAction.NAME, clearScrollRequest), equalTo(ClearScrollAction.NAME));
}
@Test
public void testClearScrollAll() {
ShieldActionMapper shieldActionMapper = new ShieldActionMapper();
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
int scrollIds = randomIntBetween(0, 10);
for (int i = 0; i < scrollIds; i++) {
clearScrollRequest.addScrollId(randomAsciiOfLength(randomIntBetween(1, 30)));
}
clearScrollRequest.addScrollId("_all");
//make sure that wherever the _all is among the scroll ids the action name gets translated
Collections.shuffle(clearScrollRequest.getScrollIds(), getRandom());
assertThat(shieldActionMapper.action(ClearScrollAction.NAME, clearScrollRequest), equalTo(ShieldActionMapper.CLUSTER_PERMISSION_SCROLL_CLEAR_ALL_NAME));
}
@Test
public void testIndicesAnalyze() {
ShieldActionMapper shieldActionMapper = new ShieldActionMapper();
AnalyzeRequest analyzeRequest;
if (randomBoolean()) {
analyzeRequest = new AnalyzeRequest(randomAsciiOfLength(randomIntBetween(1, 30)), "text");
} else {
analyzeRequest = new AnalyzeRequest("text");
analyzeRequest.index(randomAsciiOfLength(randomIntBetween(1, 30)));
}
assertThat(shieldActionMapper.action(AnalyzeAction.NAME, analyzeRequest), equalTo(AnalyzeAction.NAME));
}
@Test
public void testClusterAnalyze() {
ShieldActionMapper shieldActionMapper = new ShieldActionMapper();
AnalyzeRequest analyzeRequest = new AnalyzeRequest("text");
assertThat(shieldActionMapper.action(AnalyzeAction.NAME, analyzeRequest), equalTo(ShieldActionMapper.CLUSTER_PERMISSION_ANALYZE));
}
}

View File

@ -0,0 +1,90 @@
/*
* 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 org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.test.ShieldIntegrationTest;
import org.junit.Test;
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.BASIC_AUTH_HEADER;
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
import static org.hamcrest.CoreMatchers.containsString;
@ClusterScope(scope = Scope.SUITE)
public class AnalyzeTests extends ShieldIntegrationTest {
@Override
protected String configUsers() {
return super.configUsers() +
"analyze_indices:{plain}test123\n" +
"analyze_cluster:{plain}test123\n";
}
@Override
protected String configUsersRoles() {
return super.configUsersRoles() +
"analyze_indices:analyze_indices\n" +
"analyze_cluster:analyze_cluster\n";
}
@Override
protected String configRoles() {
return super.configRoles()+ "\n" +
//role that has analyze indices privileges only
"analyze_indices:\n" +
" indices:\n" +
" 'test_*': indices:admin/analyze\n" +
"analyze_cluster:\n" +
" cluster:\n" +
" - cluster:admin/analyze\n";
}
@Test
public void testAnalyzeWithIndices() {
//this test tries to execute different analyze api variants from a user that has analyze privileges only on a specific index namespace
createIndex("test_1");
ensureGreen();
//ok: user has permissions for analyze on test_*
client().admin().indices().prepareAnalyze("this is my text").setIndex("test_1").setAnalyzer("standard")
.putHeader(BASIC_AUTH_HEADER, basicAuthHeaderValue("analyze_indices", new SecuredString("test123".toCharArray()))).get();
try {
//fails: user doesn't have permissions for analyze on index non_authorized
client().admin().indices().prepareAnalyze("this is my text").setIndex("non_authorized").setAnalyzer("standard")
.putHeader(BASIC_AUTH_HEADER, basicAuthHeaderValue("analyze_indices", new SecuredString("test123".toCharArray()))).get();
} catch(AuthorizationException e) {
assertThat(e.getMessage(), containsString("action [indices:admin/analyze] is unauthorized for user [analyze_indices]"));
}
try {
//fails: user doesn't have permissions for cluster level analyze
client().admin().indices().prepareAnalyze("this is my text").setAnalyzer("standard")
.putHeader(BASIC_AUTH_HEADER, basicAuthHeaderValue("analyze_indices", new SecuredString("test123".toCharArray()))).get();
} catch(AuthorizationException e) {
assertThat(e.getMessage(), containsString("action [cluster:admin/analyze] is unauthorized for user [analyze_indices]"));
}
}
@Test
public void testAnalyzeWithoutIndices() {
//this test tries to execute different analyze api variants from a user that has analyze privileges only at cluster level
try {
//fails: user doesn't have permissions for analyze on index test_1
client().admin().indices().prepareAnalyze("this is my text").setIndex("test_1").setAnalyzer("standard")
.putHeader(BASIC_AUTH_HEADER, basicAuthHeaderValue("analyze_cluster", new SecuredString("test123".toCharArray()))).get();
} catch(AuthorizationException e) {
assertThat(e.getMessage(), containsString("action [indices:admin/analyze] is unauthorized for user [analyze_cluster]"));
}
client().admin().indices().prepareAnalyze("this is my text").setAnalyzer("standard")
.putHeader(BASIC_AUTH_HEADER, basicAuthHeaderValue("analyze_cluster", new SecuredString("test123".toCharArray()))).get();
}
}

View File

@ -52,14 +52,14 @@ public class KnownActionsTests extends ShieldIntegrationTest {
@Test
public void testAllCodeActionsAreKnown() throws Exception {
for (String action : codeActions) {
assertThat("elasticsearch core action [" + action + "] is unknown to shield", knownActions, hasItem(action));
assertThat("classpath action [" + action + "] is unknown to shield", knownActions, hasItem(action));
}
}
@Test
public void testAllKnownActionsAreValid() {
for (String knownAction : knownActions) {
assertThat("shield known action [" + knownAction + "] is unknown to core", codeActions, hasItems(knownAction));
assertThat("shield known action [" + knownAction + "] is not among the classpath actions", codeActions, hasItems(knownAction));
}
}
@ -71,7 +71,7 @@ public class KnownActionsTests extends ShieldIntegrationTest {
}
}
private static ImmutableSet<String> loadKnownActions() {
public static ImmutableSet<String> loadKnownActions() {
final ImmutableSet.Builder<String> knownActionsBuilder = ImmutableSet.builder();
try (InputStream input = KnownActionsTests.class.getResourceAsStream("actions")) {
Streams.readAllLines(input, new Callback<String>() {
@ -86,7 +86,7 @@ public class KnownActionsTests extends ShieldIntegrationTest {
return knownActionsBuilder.build();
}
private static ImmutableSet<String> loadKnownHandlers() {
public static ImmutableSet<String> loadKnownHandlers() {
final ImmutableSet.Builder<String> knownHandlersBuilder = ImmutableSet.builder();
try (InputStream input = KnownActionsTests.class.getResourceAsStream("handlers")) {
Streams.readAllLines(input, new Callback<String>() {