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:
parent
dfc70e6ef0
commit
7bdd41399d
|
@ -104,15 +104,18 @@ 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 checkPrivilege.name().equals(nameSet) : "Privilege " + checkPrivilege + " should have name " + nameSet;
|
assert Automatons.predicate(applicationName).test(checkPrivilege.getApplication()) : "Privilege " + checkPrivilege +
|
||||||
|
" should have application " + applicationName;
|
||||||
|
assert checkPrivilege.name().equals(nameSet) : "Privilege " + checkPrivilege + " should have name " + nameSet;
|
||||||
|
|
||||||
if (grants(checkPrivilege, checkResource)) {
|
if (grants(checkPrivilege, checkResource)) {
|
||||||
resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.TRUE);
|
resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.TRUE);
|
||||||
} else {
|
} else {
|
||||||
resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.FALSE);
|
resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.FALSE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -101,7 +103,7 @@ public final class ApplicationPrivilege extends Privilege {
|
||||||
if (allowWildcard == false) {
|
if (allowWildcard == false) {
|
||||||
throw new IllegalArgumentException("Application names may not contain '*' (found '" + application + "')");
|
throw new IllegalArgumentException("Application names may not contain '*' (found '" + application + "')");
|
||||||
}
|
}
|
||||||
if(application.equals("*")) {
|
if (application.equals("*")) {
|
||||||
// this is allowed and short-circuiting here makes the later validation simpler
|
// this is allowed and short-circuiting here makes the later validation simpler
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -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,20 +170,38 @@ 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 {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Map<String, ApplicationPrivilegeDescriptor> lookup = stored.stream()
|
return Collections.singleton(resolve(application, name, stored));
|
||||||
.filter(apd -> apd.getApplication().equals(application))
|
|
||||||
.collect(Collectors.toMap(ApplicationPrivilegeDescriptor::getName, Function.identity()));
|
|
||||||
return resolve(application, name, lookup);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
.collect(Collectors.toMap(ApplicationPrivilegeDescriptor::getName, Function.identity()));
|
||||||
|
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();
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
|
Loading…
Reference in New Issue