Support roles with application privileges against wildcard applications (#40675)

This commit introduces 2 changes to application privileges:

- The validation rules now accept a wildcard in the "suffix" of an application name.
  Wildcards were always accepted in the application name, but the "valid filename" check
  for the suffix incorrectly prevented the use of wildcards there.

- A role may now be defined against a wildcard application (e.g. kibana-*) and this will
  be correctly treated as granting the named privileges against all named applications.
  This does not allow wildcard application names in the body of a "has-privileges" check, but the
  "has-privileges" check can test concrete application names against roles with wildcards.

Backport of: #40398
This commit is contained in:
Tim Vernum 2019-04-02 14:48:39 +11:00 committed by GitHub
parent dfc70e6ef0
commit 7bdd41399d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 301 additions and 37 deletions

View File

@ -104,9 +104,11 @@ public final class ApplicationPermission {
for (String checkResource : checkForResources) { for (String checkResource : checkForResources) {
for (String checkPrivilegeName : checkForPrivilegeNames) { for (String checkPrivilegeName : checkForPrivilegeNames) {
final Set<String> nameSet = Collections.singleton(checkPrivilegeName); final Set<String> nameSet = Collections.singleton(checkPrivilegeName);
final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(applicationName, nameSet, storedPrivileges); final Set<ApplicationPrivilege> checkPrivileges = ApplicationPrivilege.get(applicationName, nameSet, storedPrivileges);
assert checkPrivilege.getApplication().equals(applicationName) : "Privilege " + checkPrivilege + " should have application " logger.trace("Resolved privileges [{}] for [{},{}]", checkPrivileges, applicationName, nameSet);
+ applicationName; for (ApplicationPrivilege checkPrivilege : checkPrivileges) {
assert Automatons.predicate(applicationName).test(checkPrivilege.getApplication()) : "Privilege " + checkPrivilege +
" should have application " + applicationName;
assert checkPrivilege.name().equals(nameSet) : "Privilege " + checkPrivilege + " should have name " + nameSet; assert checkPrivilege.name().equals(nameSet) : "Privilege " + checkPrivilege + " should have name " + nameSet;
if (grants(checkPrivilege, checkResource)) { if (grants(checkPrivilege, checkResource)) {
@ -116,6 +118,7 @@ public final class ApplicationPermission {
} }
} }
} }
}
return resourcePrivilegesMapBuilder.build(); return resourcePrivilegesMapBuilder.build();
} }

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.core.security.authz.privilege; package org.elasticsearch.xpack.core.security.authz.privilege;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.xpack.core.security.support.Automatons;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -15,6 +16,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -128,7 +130,10 @@ public final class ApplicationPrivilege extends Privilege {
} }
if (parts.length > 1) { if (parts.length > 1) {
final String suffix = parts[1]; String suffix = parts[1];
if (allowWildcard && suffix.endsWith("*")) {
suffix = suffix.substring(0, suffix.length() - 1);
}
if (Strings.validFileName(suffix) == false) { if (Strings.validFileName(suffix) == false) {
throw new IllegalArgumentException("An application name suffix may not contain any of the characters '" + throw new IllegalArgumentException("An application name suffix may not contain any of the characters '" +
Strings.collectionToDelimitedString(Strings.INVALID_FILENAME_CHARS, "") + "' (found '" + suffix + "')"); Strings.collectionToDelimitedString(Strings.INVALID_FILENAME_CHARS, "") + "' (found '" + suffix + "')");
@ -165,19 +170,37 @@ public final class ApplicationPrivilege extends Privilege {
} }
/** /**
* Finds or creates an application privileges with the provided names. * Finds or creates a collection of application privileges with the provided names.
* If application is a wildcard, it will be expanded to all matching application names in {@code stored}
* Each element in {@code name} may be the name of a stored privilege (to be resolved from {@code stored}, or a bespoke action pattern. * Each element in {@code name} may be the name of a stored privilege (to be resolved from {@code stored}, or a bespoke action pattern.
*/ */
public static ApplicationPrivilege get(String application, Set<String> name, Collection<ApplicationPrivilegeDescriptor> stored) { public static Set<ApplicationPrivilege> get(String application, Set<String> name, Collection<ApplicationPrivilegeDescriptor> stored) {
if (name.isEmpty()) { if (name.isEmpty()) {
return NONE.apply(application); return Collections.singleton(NONE.apply(application));
} else if (application.contains("*")) {
Predicate<String> predicate = Automatons.predicate(application);
final Set<ApplicationPrivilege> result = stored.stream()
.map(ApplicationPrivilegeDescriptor::getApplication)
.filter(predicate)
.distinct()
.map(appName -> resolve(appName, name, stored))
.collect(Collectors.toSet());
if (result.isEmpty()) {
return Collections.singleton(resolve(application, name, Collections.emptyMap()));
} else { } else {
Map<String, ApplicationPrivilegeDescriptor> lookup = stored.stream() return result;
}
} else {
return Collections.singleton(resolve(application, name, stored));
}
}
private static ApplicationPrivilege resolve(String application, Set<String> name, Collection<ApplicationPrivilegeDescriptor> stored) {
final Map<String, ApplicationPrivilegeDescriptor> lookup = stored.stream()
.filter(apd -> apd.getApplication().equals(application)) .filter(apd -> apd.getApplication().equals(application))
.collect(Collectors.toMap(ApplicationPrivilegeDescriptor::getName, Function.identity())); .collect(Collectors.toMap(ApplicationPrivilegeDescriptor::getName, Function.identity()));
return resolve(application, name, lookup); return resolve(application, name, lookup);
} }
}
private static ApplicationPrivilege resolve(String application, Set<String> names, Map<String, ApplicationPrivilegeDescriptor> lookup) { private static ApplicationPrivilege resolve(String application, Set<String> names, Map<String, ApplicationPrivilegeDescriptor> lookup) {
final int size = names.size(); final int size = names.size();

View File

@ -23,6 +23,8 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
/** /**
* An {@code ApplicationPrivilegeDescriptor} is a representation of a <em>stored</em> {@link ApplicationPrivilege}. * An {@code ApplicationPrivilegeDescriptor} is a representation of a <em>stored</em> {@link ApplicationPrivilege}.
* A user (via a role) can be granted an application privilege by name (e.g. ("myapp", "read"). * A user (via a role) can be granted an application privilege by name (e.g. ("myapp", "read").
@ -104,6 +106,11 @@ public class ApplicationPrivilegeDescriptor implements ToXContentObject, Writeab
return builder.endObject(); return builder.endObject();
} }
@Override
public String toString() {
return getClass().getSimpleName() + "{[" + application + "],[" + name + "],[" + collectionToCommaDelimitedString(actions) + "]}";
}
/** /**
* Construct a new {@link ApplicationPrivilegeDescriptor} from XContent. * Construct a new {@link ApplicationPrivilegeDescriptor} from XContent.
* *

View File

@ -13,15 +13,16 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivileg
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
public class ApplicationPermissionTests extends ESTestCase { public class ApplicationPermissionTests extends ESTestCase {
@ -34,6 +35,7 @@ public class ApplicationPermissionTests extends ESTestCase {
private ApplicationPrivilege app1Delete = storePrivilege("app1", "delete", "write/delete"); private ApplicationPrivilege app1Delete = storePrivilege("app1", "delete", "write/delete");
private ApplicationPrivilege app1Create = storePrivilege("app1", "create", "write/create"); private ApplicationPrivilege app1Create = storePrivilege("app1", "create", "write/create");
private ApplicationPrivilege app2Read = storePrivilege("app2", "read", "read/*"); private ApplicationPrivilege app2Read = storePrivilege("app2", "read", "read/*");
private ApplicationPrivilege otherAppRead = storePrivilege("other-app", "read", "read/*");
private ApplicationPrivilege storePrivilege(String app, String name, String... patterns) { private ApplicationPrivilege storePrivilege(String app, String name, String... patterns) {
store.add(new ApplicationPrivilegeDescriptor(app, name, Sets.newHashSet(patterns), Collections.emptyMap())); store.add(new ApplicationPrivilegeDescriptor(app, name, Sets.newHashSet(patterns), Collections.emptyMap()));
@ -104,6 +106,16 @@ public class ApplicationPermissionTests extends ESTestCase {
assertThat(buildPermission(app1All, "*").grants(app2Read, "123"), equalTo(false)); assertThat(buildPermission(app1All, "*").grants(app2Read, "123"), equalTo(false));
} }
public void testMatchingWithWildcardApplicationNames() {
final Set<ApplicationPrivilege> readAllApp = ApplicationPrivilege.get("app*", Collections.singleton("read"), store);
assertThat(buildPermission(readAllApp, "*").grants(app1Read, "123"), equalTo(true));
assertThat(buildPermission(readAllApp, "foo/*").grants(app2Read, "foo/bar"), equalTo(true));
assertThat(buildPermission(readAllApp, "*").grants(app1Write, "123"), equalTo(false));
assertThat(buildPermission(readAllApp, "foo/*").grants(app2Read, "bar/baz"), equalTo(false));
assertThat(buildPermission(readAllApp, "*").grants(otherAppRead, "abc"), equalTo(false));
}
public void testMergedPermissionChecking() { public void testMergedPermissionChecking() {
final ApplicationPrivilege app1ReadWrite = compositePrivilege("app1", app1Read, app1Write); final ApplicationPrivilege app1ReadWrite = compositePrivilege("app1", app1Read, app1Write);
final ApplicationPermission hasPermission = buildPermission(app1ReadWrite, "allow/*"); final ApplicationPermission hasPermission = buildPermission(app1ReadWrite, "allow/*");
@ -138,16 +150,27 @@ public class ApplicationPermissionTests extends ESTestCase {
} }
private ApplicationPrivilege actionPrivilege(String appName, String... actions) { private ApplicationPrivilege actionPrivilege(String appName, String... actions) {
return ApplicationPrivilege.get(appName, Sets.newHashSet(actions), Collections.emptyList()); final Set<ApplicationPrivilege> privileges = ApplicationPrivilege.get(appName, Sets.newHashSet(actions), Collections.emptyList());
assertThat(privileges, hasSize(1));
return privileges.iterator().next();
} }
private ApplicationPrivilege compositePrivilege(String application, ApplicationPrivilege... children) { private ApplicationPrivilege compositePrivilege(String application, ApplicationPrivilege... children) {
Set<String> names = Stream.of(children).map(ApplicationPrivilege::name).flatMap(Set::stream).collect(Collectors.toSet()); Set<String> names = Stream.of(children).map(ApplicationPrivilege::name).flatMap(Set::stream).collect(Collectors.toSet());
return ApplicationPrivilege.get(application, names, store); final Set<ApplicationPrivilege> privileges = ApplicationPrivilege.get(application, names, store);
assertThat(privileges, hasSize(1));
return privileges.iterator().next();
} }
private ApplicationPermission buildPermission(ApplicationPrivilege privilege, String... resources) { private ApplicationPermission buildPermission(ApplicationPrivilege privilege, String... resources) {
return new ApplicationPermission(singletonList(new Tuple<>(privilege, Sets.newHashSet(resources)))); return buildPermission(Collections.singleton(privilege), resources);
}
private ApplicationPermission buildPermission(Collection<ApplicationPrivilege> privileges, String... resources) {
final Set<String> resourceSet = Sets.newHashSet(resources);
final List<Tuple<ApplicationPrivilege, Set<String>>> privilegesAndResources = privileges.stream()
.map(p -> new Tuple<>(p, resourceSet))
.collect(Collectors.toList());
return new ApplicationPermission(privilegesAndResources);
} }
} }

View File

@ -10,6 +10,7 @@ import org.apache.lucene.util.automaton.CharacterRunAutomaton;
import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils; import org.elasticsearch.test.EqualsHashCodeTestUtils;
import org.hamcrest.Matchers;
import org.junit.Assert; import org.junit.Assert;
import java.util.Arrays; import java.util.Arrays;
@ -22,9 +23,11 @@ import java.util.function.Supplier;
import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString; import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.iterableWithSize;
public class ApplicationPrivilegeTests extends ESTestCase { public class ApplicationPrivilegeTests extends ESTestCase {
@ -59,6 +62,12 @@ public class ApplicationPrivilegeTests extends ESTestCase {
assertNoException(app, () -> ApplicationPrivilege.validateApplicationName(app)); assertNoException(app, () -> ApplicationPrivilege.validateApplicationName(app));
assertNoException(app, () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app)); assertNoException(app, () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app));
} }
// wildcards in the suffix
for (String app : Arrays.asList("app1-*", "app1-foo*", "app1-.*", "app1-.foo.*", appNameWithSpecialChars + "*")) {
assertValidationFailure(app, "application name", () -> ApplicationPrivilege.validateApplicationName(app));
assertNoException(app, () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app));
}
} }
public void testValidationOfPrivilegeName() { public void testValidationOfPrivilegeName() {
@ -101,16 +110,23 @@ public class ApplicationPrivilegeTests extends ESTestCase {
} }
public void testGetPrivilegeByName() { public void testGetPrivilegeByName() {
final ApplicationPrivilegeDescriptor descriptor = descriptor("my-app", "read", "data:read/*", "action:login"); final ApplicationPrivilegeDescriptor myRead = descriptor("my-app", "read", "data:read/*", "action:login");
final ApplicationPrivilegeDescriptor myWrite = descriptor("my-app", "write", "data:write/*", "action:login"); final ApplicationPrivilegeDescriptor myWrite = descriptor("my-app", "write", "data:write/*", "action:login");
final ApplicationPrivilegeDescriptor myAdmin = descriptor("my-app", "admin", "data:read/*", "action:*"); final ApplicationPrivilegeDescriptor myAdmin = descriptor("my-app", "admin", "data:read/*", "action:*");
final ApplicationPrivilegeDescriptor yourRead = descriptor("your-app", "read", "data:read/*", "action:login"); final ApplicationPrivilegeDescriptor yourRead = descriptor("your-app", "read", "data:read/*", "action:login");
final Set<ApplicationPrivilegeDescriptor> stored = Sets.newHashSet(descriptor, myWrite, myAdmin, yourRead); final Set<ApplicationPrivilegeDescriptor> stored = Sets.newHashSet(myRead, myWrite, myAdmin, yourRead);
assertEqual(ApplicationPrivilege.get("my-app", Collections.singleton("read"), stored), descriptor); final Set<ApplicationPrivilege> myAppRead = ApplicationPrivilege.get("my-app", Collections.singleton("read"), stored);
assertEqual(ApplicationPrivilege.get("my-app", Collections.singleton("write"), stored), myWrite); assertThat(myAppRead, iterableWithSize(1));
assertPrivilegeEquals(myAppRead.iterator().next(), myRead);
final ApplicationPrivilege readWrite = ApplicationPrivilege.get("my-app", Sets.newHashSet("read", "write"), stored); final Set<ApplicationPrivilege> myAppWrite = ApplicationPrivilege.get("my-app", Collections.singleton("write"), stored);
assertThat(myAppWrite, iterableWithSize(1));
assertPrivilegeEquals(myAppWrite.iterator().next(), myWrite);
final Set<ApplicationPrivilege> myReadWrite = ApplicationPrivilege.get("my-app", Sets.newHashSet("read", "write"), stored);
assertThat(myReadWrite, Matchers.hasSize(1));
final ApplicationPrivilege readWrite = myReadWrite.iterator().next();
assertThat(readWrite.getApplication(), equalTo("my-app")); assertThat(readWrite.getApplication(), equalTo("my-app"));
assertThat(readWrite.name(), containsInAnyOrder("read", "write")); assertThat(readWrite.name(), containsInAnyOrder("read", "write"));
assertThat(readWrite.getPatterns(), arrayContainingInAnyOrder("data:read/*", "data:write/*", "action:login")); assertThat(readWrite.getPatterns(), arrayContainingInAnyOrder("data:read/*", "data:write/*", "action:login"));
@ -124,10 +140,10 @@ public class ApplicationPrivilegeTests extends ESTestCase {
} }
} }
private void assertEqual(ApplicationPrivilege myReadPriv, ApplicationPrivilegeDescriptor myRead) { private void assertPrivilegeEquals(ApplicationPrivilege privilege, ApplicationPrivilegeDescriptor descriptor) {
assertThat(myReadPriv.getApplication(), equalTo(myRead.getApplication())); assertThat(privilege.getApplication(), equalTo(descriptor.getApplication()));
assertThat(getPrivilegeName(myReadPriv), equalTo(myRead.getName())); assertThat(privilege.name(), contains(descriptor.getName()));
assertThat(Sets.newHashSet(myReadPriv.getPatterns()), equalTo(myRead.getActions())); assertThat(Sets.newHashSet(privilege.getPatterns()), equalTo(descriptor.getActions()));
} }
private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) { private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) {

View File

@ -402,8 +402,8 @@ public class CompositeRolesStore {
.flatMap(Collection::stream) .flatMap(Collection::stream)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
privilegeStore.getPrivileges(applicationNames, applicationPrivilegeNames, ActionListener.wrap(appPrivileges -> { privilegeStore.getPrivileges(applicationNames, applicationPrivilegeNames, ActionListener.wrap(appPrivileges -> {
applicationPrivilegesMap.forEach((key, names) -> applicationPrivilegesMap.forEach((key, names) -> ApplicationPrivilege.get(key.v1(), names, appPrivileges)
builder.addApplicationPrivilege(ApplicationPrivilege.get(key.v1(), names, appPrivileges), key.v2())); .forEach(priv -> builder.addApplicationPrivilege(priv, key.v2())));
listener.onResponse(builder.build()); listener.onResponse(builder.build());
}, listener::onFailure)); }, listener::onFailure));
} }

View File

@ -34,9 +34,11 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParseException; import org.elasticsearch.common.xcontent.XContentParseException;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.query.TermsQueryBuilder;
import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ClientHelper;
import org.elasticsearch.xpack.core.security.ScrollHelper; import org.elasticsearch.xpack.core.security.ScrollHelper;
import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheRequest; import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheRequest;
@ -46,6 +48,7 @@ import org.elasticsearch.xpack.core.security.client.SecurityClient;
import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.support.SecurityIndexManager;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -62,6 +65,7 @@ import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
import static org.elasticsearch.xpack.core.ClientHelper.stashWithOrigin; import static org.elasticsearch.xpack.core.ClientHelper.stashWithOrigin;
import static org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor.DOC_TYPE_VALUE; import static org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor.DOC_TYPE_VALUE;
import static org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor.Fields.APPLICATION;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME;
/** /**
@ -97,7 +101,7 @@ public class NativePrivilegeStore {
listener.onResponse(Collections.emptyList()); listener.onResponse(Collections.emptyList());
} else if (frozenSecurityIndex.isAvailable() == false) { } else if (frozenSecurityIndex.isAvailable() == false) {
listener.onFailure(frozenSecurityIndex.getUnavailableReason()); listener.onFailure(frozenSecurityIndex.getUnavailableReason());
} else if (applications != null && applications.size() == 1 && names != null && names.size() == 1) { } else if (isSinglePrivilegeMatch(applications, names)) {
getPrivilege(Objects.requireNonNull(Iterables.get(applications, 0)), Objects.requireNonNull(Iterables.get(names, 0)), getPrivilege(Objects.requireNonNull(Iterables.get(applications, 0)), Objects.requireNonNull(Iterables.get(names, 0)),
ActionListener.wrap(privilege -> ActionListener.wrap(privilege ->
listener.onResponse(privilege == null ? Collections.emptyList() : Collections.singletonList(privilege)), listener.onResponse(privilege == null ? Collections.emptyList() : Collections.singletonList(privilege)),
@ -110,11 +114,14 @@ public class NativePrivilegeStore {
if (isEmpty(applications) && isEmpty(names)) { if (isEmpty(applications) && isEmpty(names)) {
query = typeQuery; query = typeQuery;
} else if (isEmpty(names)) { } else if (isEmpty(names)) {
query = QueryBuilders.boolQuery().filter(typeQuery).filter( query = QueryBuilders.boolQuery().filter(typeQuery).filter(getApplicationNameQuery(applications));
QueryBuilders.termsQuery(ApplicationPrivilegeDescriptor.Fields.APPLICATION.getPreferredName(), applications));
} else if (isEmpty(applications)) { } else if (isEmpty(applications)) {
query = QueryBuilders.boolQuery().filter(typeQuery) query = QueryBuilders.boolQuery().filter(typeQuery)
.filter(QueryBuilders.termsQuery(ApplicationPrivilegeDescriptor.Fields.NAME.getPreferredName(), names)); .filter(getPrivilegeNameQuery(names));
} else if (hasWildcard(applications)) {
query = QueryBuilders.boolQuery().filter(typeQuery)
.filter(getApplicationNameQuery(applications))
.filter(getPrivilegeNameQuery(names));
} else { } else {
final String[] docIds = applications.stream() final String[] docIds = applications.stream()
.flatMap(a -> names.stream().map(n -> toDocId(a, n))) .flatMap(a -> names.stream().map(n -> toDocId(a, n)))
@ -139,6 +146,49 @@ public class NativePrivilegeStore {
} }
} }
private boolean isSinglePrivilegeMatch(Collection<String> applications, Collection<String> names) {
return applications != null && applications.size() == 1 && hasWildcard(applications) == false && names != null && names.size() == 1;
}
private boolean hasWildcard(Collection<String> applications) {
return applications.stream().anyMatch(n -> n.endsWith("*"));
}
private QueryBuilder getPrivilegeNameQuery(Collection<String> names) {
return QueryBuilders.termsQuery(ApplicationPrivilegeDescriptor.Fields.NAME.getPreferredName(), names);
}
private QueryBuilder getApplicationNameQuery(Collection<String> applications) {
if (applications.contains("*")) {
return QueryBuilders.existsQuery(APPLICATION.getPreferredName());
}
final List<String> rawNames = new ArrayList<>(applications.size());
final List<String> wildcardNames = new ArrayList<>(applications.size());
for (String name : applications) {
if (name.endsWith("*")) {
wildcardNames.add(name);
} else {
rawNames.add(name);
}
}
assert rawNames.isEmpty() == false || wildcardNames.isEmpty() == false;
TermsQueryBuilder termsQuery = rawNames.isEmpty() ? null : QueryBuilders.termsQuery(APPLICATION.getPreferredName(), rawNames);
if (wildcardNames.isEmpty()) {
return termsQuery;
}
final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (termsQuery != null) {
boolQuery.filter(termsQuery);
}
for (String wildcard : wildcardNames) {
final String prefix = wildcard.substring(0, wildcard.length() - 1);
boolQuery.filter(QueryBuilders.prefixQuery(APPLICATION.getPreferredName(), prefix));
}
return boolQuery;
}
private static boolean isEmpty(Collection<String> collection) { private static boolean isEmpty(Collection<String> collection) {
return collection == null || collection.isEmpty(); return collection == null || collection.isEmpty();
} }

View File

@ -181,6 +181,45 @@ public class NativePrivilegeStoreTests extends ESTestCase {
assertResult(sourcePrivileges, future); assertResult(sourcePrivileges, future);
} }
public void testGetPrivilegesByWildcardApplicationName() throws Exception {
final PlainActionFuture<Collection<ApplicationPrivilegeDescriptor>> future = new PlainActionFuture<>();
store.getPrivileges(Arrays.asList("myapp-*", "yourapp"), null, future);
assertThat(requests, iterableWithSize(1));
assertThat(requests.get(0), instanceOf(SearchRequest.class));
SearchRequest request = (SearchRequest) requests.get(0);
assertThat(request.indices(), arrayContaining(SecurityIndexManager.SECURITY_INDEX_NAME));
final String query = Strings.toString(request.source().query());
assertThat(query, containsString("{\"bool\":{\"filter\":[{\"terms\":{\"application\":[\"yourapp\"]"));
assertThat(query, containsString("{\"prefix\":{\"application\":{\"value\":\"myapp-\""));
assertThat(query, containsString("{\"term\":{\"type\":{\"value\":\"application-privilege\""));
final SearchHit[] hits = new SearchHit[0];
listener.get().onResponse(new SearchResponse(new SearchResponseSections(
new SearchHits(hits, new TotalHits(hits.length, TotalHits.Relation.EQUAL_TO), 0f),
null, null, false, false, null, 1),
"_scrollId1", 1, 1, 0, 1, null, null));
}
public void testGetPrivilegesByStarApplicationName() throws Exception {
final PlainActionFuture<Collection<ApplicationPrivilegeDescriptor>> future = new PlainActionFuture<>();
store.getPrivileges(Arrays.asList("*", "anything"), null, future);
assertThat(requests, iterableWithSize(1));
assertThat(requests.get(0), instanceOf(SearchRequest.class));
SearchRequest request = (SearchRequest) requests.get(0);
assertThat(request.indices(), arrayContaining(SecurityIndexManager.SECURITY_INDEX_NAME));
final String query = Strings.toString(request.source().query());
assertThat(query, containsString("{\"exists\":{\"field\":\"application\""));
assertThat(query, containsString("{\"term\":{\"type\":{\"value\":\"application-privilege\""));
final SearchHit[] hits = new SearchHit[0];
listener.get().onResponse(new SearchResponse(new SearchResponseSections(
new SearchHits(hits, new TotalHits(hits.length, TotalHits.Relation.EQUAL_TO), 0f),
null, null, false, false, null, 1),
"_scrollId1", 1, 1, 0, 1, null, null));
}
public void testGetAllPrivileges() throws Exception { public void testGetAllPrivileges() throws Exception {
final List<ApplicationPrivilegeDescriptor> sourcePrivileges = Arrays.asList( final List<ApplicationPrivilegeDescriptor> sourcePrivileges = Arrays.asList(
new ApplicationPrivilegeDescriptor("app1", "admin", newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap()), new ApplicationPrivilegeDescriptor("app1", "admin", newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap()),

View File

@ -28,6 +28,16 @@ setup:
"name": "write", "name": "write",
"actions": [ "data:write/*" ] "actions": [ "data:write/*" ]
} }
},
"yourapp-v1" : {
"read": {
"actions": [ "data:read/*" ]
}
},
"yourapp-v2" : {
"read": {
"actions": [ "data:read/*" ]
}
} }
} }
@ -83,6 +93,21 @@ setup:
} }
] ]
} }
- do:
security.put_role:
name: "yourapp_read_config"
body: >
{
"cluster": [],
"indices": [],
"applications": [
{
"application": "yourapp-*",
"privileges": ["read"],
"resources": ["settings/*"]
}
]
}
# And a user for each role # And a user for each role
- do: - do:
@ -101,6 +126,14 @@ setup:
"password": "p@ssw0rd", "password": "p@ssw0rd",
"roles" : [ "myapp_engineering_write" ] "roles" : [ "myapp_engineering_write" ]
} }
- do:
security.put_user:
username: "your_read"
body: >
{
"password": "p@ssw0rd",
"roles" : [ "yourapp_read_config" ]
}
--- ---
teardown: teardown:
@ -109,6 +142,16 @@ teardown:
application: myapp application: myapp
name: "user,read,write" name: "user,read,write"
ignore: 404 ignore: 404
- do:
security.delete_privileges:
application: yourapp-v1
name: "read"
ignore: 404
- do:
security.delete_privileges:
application: yourapp-v2
name: "read"
ignore: 404
- do: - do:
security.delete_user: security.delete_user:
@ -120,6 +163,11 @@ teardown:
username: "eng_write" username: "eng_write"
ignore: 404 ignore: 404
- do:
security.delete_user:
username: "your_read"
ignore: 404
- do: - do:
security.delete_role: security.delete_role:
name: "myapp_engineering_read" name: "myapp_engineering_read"
@ -129,6 +177,11 @@ teardown:
security.delete_role: security.delete_role:
name: "myapp_engineering_write" name: "myapp_engineering_write"
ignore: 404 ignore: 404
- do:
security.delete_role:
name: "yourapp_read_config"
ignore: 404
--- ---
"Test has_privileges with application-privileges": "Test has_privileges with application-privileges":
- do: - do:
@ -188,3 +241,53 @@ teardown:
} }
} } } }
- do:
headers: { Authorization: "Basic eW91cl9yZWFkOnBAc3N3MHJk" } # your_read
security.has_privileges:
user: null
body: >
{
"application": [
{
"application" : "yourapp-v1",
"resources" : [ "settings/host", "settings/port", "system/key" ],
"privileges" : [ "data:read/settings", "data:write/settings", "read", "write" ]
},
{
"application" : "yourapp-v2",
"resources" : [ "settings/host" ],
"privileges" : [ "data:read/settings", "data:write/settings" ]
}
]
}
- match: { "username" : "your_read" }
- match: { "has_all_requested" : false }
- match: { "application": {
"yourapp-v1": {
"settings/host": {
"data:read/settings": true,
"data:write/settings": false,
"read": true,
"write": false
},
"settings/port": {
"data:read/settings": true,
"data:write/settings": false,
"read": true,
"write": false
},
"system/key": {
"data:read/settings": false,
"data:write/settings": false,
"read": false,
"write": false
}
},
"yourapp-v2": {
"settings/host": {
"data:read/settings": true,
"data:write/settings": false,
}
}
} }