Skip to content

Commit

Permalink
[8.2] Fix resolution of wildcard application privileges (#87359)
Browse files Browse the repository at this point in the history
Previously, a role with application privileges of

    {
      "application": "*",
      "privileges": ["*"]
      "resources": ["*"]
    }

would be resolved to mean every _defined_ privilege in every _defined_
application (subject to condition 1 described below).

This implementation was based on the assumption that every action that
would ever be checked by _has_privileges would be explicitly defined
as an application privilege (via PUT /_security/privilege)

That assumption would have been fine if not for 2 discrepancies

1. The resolved privileges were then filtered by privilege name and
   wildcards were not respected. So the role shown above would
   actually filter down to nothing. However if the user had another
   role with named privileges (not wildcards) then the filtering would
   include that privilege _if it was defined_.

2. The logic to construct a runtime ApplicationPrivilege from the
   resolved ApplicationPrivilegeDescriptor instances had special case
   logic to handle 0 matching descriptors. This was needed so that
   superuser always had every privilege, even if the descriptors had
   not yet been defined, or where unavailable for some reason. However
   because this logic only applied if there were no matching
   descriptors the behaviour was inconsistent. For the most part it
   would look like wildcard application privileges functioned
   correctly even when no application privileges were defined, but
   this behaviour could not be relied on if the user had additional
   roles that references defined application privileges (per point 1).

This change solves point 2, by always including the wildcard
permission even if there are matching descriptors. It does nothing to
solve point 1, and it is likely that we need additional commits to
cleanup the logic there.

Backport of: #87293
  • Loading branch information
tvernum committed Jun 6, 2022
1 parent 0ef67a3 commit 44a3c97
Show file tree
Hide file tree
Showing 5 changed files with 331 additions and 12 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/87293.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 87293
summary: Fix resolution of wildcard application privileges
area: Authorization
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.core.security.authz.privilege;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.xpack.core.security.support.Automatons;

import java.util.Arrays;
Expand Down Expand Up @@ -192,18 +193,15 @@ public static Set<ApplicationPrivilege> get(String application, Set<String> name
if (name.isEmpty()) {
return Collections.singleton(NONE.apply(application));
} else if (application.contains("*")) {
Predicate<String> predicate = Automatons.predicate(application);
final Set<ApplicationPrivilege> result = stored.stream()
final Set<ApplicationPrivilege> result = Sets.newHashSet(resolve(application, name, Collections.emptyMap()));
final Predicate<String> predicate = Automatons.predicate(application);
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;
}
.forEach(result::add);
return result;
} else {
return Collections.singleton(resolve(application, name, stored));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Assert;

Expand All @@ -25,10 +27,10 @@

import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.iterableWithSize;

public class ApplicationPrivilegeTests extends ESTestCase {
Expand Down Expand Up @@ -145,10 +147,51 @@ public void testGetPrivilegeByName() {
}
}

public void testGetPrivilegeByWildcard() {
final ApplicationPrivilegeDescriptor apmRead = descriptor("apm", "read", "action:read/*");
final ApplicationPrivilegeDescriptor apmWrite = descriptor("apm", "write", "action:write/*");
final ApplicationPrivilegeDescriptor kibanaRead = descriptor("kibana", "read", "data:read/*", "action:read:*");
final ApplicationPrivilegeDescriptor kibanaWrite = descriptor("kibana", "write", "data:write/*", "action:w*");
final Set<ApplicationPrivilegeDescriptor> stored = Sets.newHashSet(apmRead, apmWrite, kibanaRead, kibanaWrite);

{
final Set<ApplicationPrivilege> everyThing = ApplicationPrivilege.get("*", Set.of("*"), stored);
assertThat(everyThing, hasItem(privilegeEquals("*", "*", Set.of("*"))));
assertThat(everyThing, hasItem(privilegeEquals("apm", "*", Set.of("*"))));
assertThat(everyThing, hasItem(privilegeEquals("kibana", "*", Set.of("*"))));
assertThat(everyThing, iterableWithSize(3));
}
{
final Set<ApplicationPrivilege> allKibana = ApplicationPrivilege.get("kibana", Set.of("*"), stored);
assertThat(allKibana, hasItem(privilegeEquals("kibana", "*", Set.of("*"))));
assertThat(allKibana, iterableWithSize(1));
}
{
final Set<ApplicationPrivilege> allRead = ApplicationPrivilege.get("*", Set.of("read"), stored);
assertThat(allRead, hasItem(privilegeEquals(kibanaRead)));
assertThat(allRead, hasItem(privilegeEquals(apmRead)));
assertThat(allRead, hasItem(privilegeEquals("*", "read", Set.of())));
assertThat(allRead, iterableWithSize(3));
}
}

private void assertPrivilegeEquals(ApplicationPrivilege privilege, ApplicationPrivilegeDescriptor descriptor) {
assertThat(privilege.getApplication(), equalTo(descriptor.getApplication()));
assertThat(privilege.name(), contains(descriptor.getName()));
assertThat(Sets.newHashSet(privilege.getPatterns()), equalTo(descriptor.getActions()));
assertThat(privilege, privilegeEquals(descriptor));
}

private Matcher<ApplicationPrivilege> privilegeEquals(ApplicationPrivilegeDescriptor descriptor) {
return privilegeEquals(descriptor.getApplication(), descriptor.getName(), descriptor.getActions());
}

private Matcher<ApplicationPrivilege> privilegeEquals(String application, String name, Set<String> actions) {
return new CustomTypeSafeMatcher<>("equals(" + application + ";" + name + ";" + actions + ")") {
@Override
protected boolean matchesSafely(ApplicationPrivilege item) {
return item.getApplication().equals(application)
&& item.name().equals(Set.of(name))
&& Set.of(item.getPatterns()).equals(actions);
}
};
}

private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
package org.elasticsearch.xpack.security;

import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.test.TestSecurityClient;
import org.elasticsearch.test.XContentTestUtils;
import org.elasticsearch.xcontent.ObjectPath;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
import org.elasticsearch.xpack.core.security.user.User;
import org.hamcrest.Matchers;
import org.junit.Before;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.equalTo;

public class HasApplicationPrivilegesIT extends SecurityInBasicRestTestCase {

private TestSecurityClient securityClient;

@Before
public void setupClient() {
securityClient = new TestSecurityClient(adminClient());
}

public void testUserWithWildcardPrivileges() throws Exception {
var mainApplication = randomApplicationName();

// Privilege names that are defined application privileges, possible on the "main" app, possibly for another app.
final Set<String> definedPrivilegeNames = new HashSet<>();

// All applications for which application privileges have been defined
final Set<String> allApplications = new HashSet<>();
allApplications.add(mainApplication);
CheckedConsumer<String, IOException> createApplicationPrivilege = (app) -> {
final String privilegeName;
// If this is the first privilege for this app, then maybe (randomly) reuse a privilege name that was defined for another app
if (allApplications.contains(app) == false && definedPrivilegeNames.size() > 0 && randomBoolean()) {
privilegeName = randomFrom(definedPrivilegeNames);
} else {
privilegeName = randomValueOtherThanMany(definedPrivilegeNames::contains, this::randomPrivilegeName);
}

createApplicationPrivilege(app, privilegeName, randomArray(1, 4, String[]::new, () -> randomActionName()));
allApplications.add(app);
definedPrivilegeNames.add(privilegeName);
};

// Create 0 or more application privileges for this application
for (int i = randomIntBetween(0, 2); i > 0; i--) {
createApplicationPrivilege.accept(mainApplication);
}

// Create 0 or more application privileges for other applications
for (int i = randomIntBetween(0, 3); i > 0; i--) {
createApplicationPrivilege.accept(randomValueOtherThan(mainApplication, this::randomApplicationName));
}

// Define a role with all privileges (by wildcard) for this application
var roleName = randomAlphaOfLengthBetween(6, 10);
var singleAppOnly = randomBoolean();
createRole(roleName, singleAppOnly ? mainApplication : "*", new String[] { "*" }, new String[] { "*" });

final Set<String> allRoles = new HashSet<>();
allRoles.add(roleName);

// Create 0 or more additional roles with privileges for one of the applications
for (int i = randomIntBetween(0, 3); i > 0; i--) {
var extraRoleName = randomValueOtherThanMany(allRoles::contains, () -> randomAlphaOfLengthBetween(8, 16));
final String privilegeName = definedPrivilegeNames.size() > 0 && randomBoolean()
? randomFrom(definedPrivilegeNames) // This may or may not correspond to the application we pick. Both are valid tests
: randomPrivilegeName();
createRole(
extraRoleName,
randomFrom(allApplications),
new String[] { privilegeName },
new String[] { "data/" + randomAlphaOfLength(6) }
);
allRoles.add(extraRoleName);
}

// Create a user with all (might be 1 or more) of the roles
var username = randomAlphaOfLengthBetween(8, 12);
var password = new SecureString(randomAlphaOfLength(12).toCharArray());
createUser(username, password, allRoles);

// Assert that has_privileges returns true for any arbitrary privilege or action in that application
var reqOptions = RequestOptions.DEFAULT.toBuilder()
.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(username, password))
.build();

final String testPrivilege;
if (randomBoolean() && definedPrivilegeNames.size() > 0) {
testPrivilege = randomFrom(definedPrivilegeNames);
} else if (randomBoolean()) {
testPrivilege = randomPrivilegeName();
} else {
testPrivilege = randomActionName();
}
var testResource = randomAlphaOfLengthBetween(4, 12);

{
final List<ResourcePrivileges> shouldHavePrivileges = hasPrivilege(
reqOptions,
mainApplication,
new String[] { testPrivilege },
new String[] { testResource }
);

assertSinglePrivilege(shouldHavePrivileges, testResource, testPrivilege, true);
}

if (singleAppOnly) {
List<ResourcePrivileges> shouldNotHavePrivileges = hasPrivilege(
reqOptions,
randomValueOtherThanMany(allApplications::contains, this::randomApplicationName),
new String[] { testPrivilege },
new String[] { testResource }
);
assertSinglePrivilege(shouldNotHavePrivileges, testResource, testPrivilege, false);

if (allApplications.size() > 1) { // there is an app other than the main app
shouldNotHavePrivileges = hasPrivilege(
reqOptions,
randomValueOtherThan(mainApplication, () -> randomFrom(allApplications)),
new String[] { testPrivilege },
new String[] { testResource }
);
assertSinglePrivilege(shouldNotHavePrivileges, testResource, testPrivilege, false);
}
}
}

private void assertSinglePrivilege(
List<ResourcePrivileges> hasPrivilegesResult,
String expectedResource,
String expectedPrivilegeName,
boolean shoudHavePrivilege
) {
assertThat(hasPrivilegesResult, Matchers.hasSize(1));
assertThat(hasPrivilegesResult.get(0).getResource(), equalTo(expectedResource));
assertThat(hasPrivilegesResult.get(0).getPrivileges(), Matchers.hasEntry(expectedPrivilegeName, shoudHavePrivilege));
assertThat(hasPrivilegesResult.get(0).getPrivileges(), aMapWithSize(1));
}

private List<ResourcePrivileges> hasPrivilege(RequestOptions requestOptions, String appName, String[] privileges, String[] resources)
throws IOException {
logger.info("Checking privileges: App=[{}] Privileges=[{}] Resources=[{}]", appName, privileges, resources);
Request req = new Request("POST", "/_security/user/_has_privileges");
req.setOptions(requestOptions);
Map<String, Object> body = Map.ofEntries(
Map.entry(
"application",
List.of(
Map.ofEntries(
Map.entry("application", appName),
Map.entry("privileges", List.of(privileges)),
Map.entry("resources", List.of(resources))
)
)
)
);
req.setJsonEntity(XContentTestUtils.convertToXContent(body, XContentType.JSON).utf8ToString());
final Map<String, Object> response = responseAsMap(client().performRequest(req));
logger.info("Has privileges: [{}]", response);
final Map<String, Object> privilegesByResource = ObjectPath.eval("application." + appName, response);
return Stream.of(resources).map(res -> {
Map<String, Boolean> priv = ObjectPath.eval(res, privilegesByResource);
return ResourcePrivileges.builder(res).addPrivileges(priv).build();
}).collect(Collectors.toList());
}

private void createUser(String username, SecureString password, Set<String> roles) throws IOException {
logger.info("Create User [{}] with roles [{}]", username, roles);
securityClient.putUser(new User(username, roles.toArray(String[]::new)), password);
}

private void createRole(String roleName, String applicationName, String[] privileges, String[] resources) throws IOException {
logger.info(
"Create role [{}] with privileges App=[{}] Privileges=[{}] Resources=[{}]",
roleName,
applicationName,
privileges,
resources
);
securityClient.putRole(
new RoleDescriptor(
roleName,
new String[0], // cluster
new RoleDescriptor.IndicesPrivileges[0],
new RoleDescriptor.ApplicationResourcePrivileges[] {
RoleDescriptor.ApplicationResourcePrivileges.builder()
.application(applicationName)
.privileges(privileges)
.resources(resources)
.build() },
new ConfigurableClusterPrivilege[0],
new String[0],// run-as
Map.of(), // metadata
Map.of() // transient metadata
)
);
}

private void createApplicationPrivilege(String applicationName, String privilegeName, String[] actions) {
logger.info("Create app privilege App=[{}] Privilege=[{}] Actions=[{}]", applicationName, privilegeName, actions);
try {
securityClient.putApplicationPrivilege(applicationName, privilegeName, actions);
} catch (IOException e) {
throw new AssertionError(
"Failed to create application privilege app=["
+ applicationName
+ "], privilege=["
+ privilegeName
+ "], actions=["
+ String.join(",", actions)
+ "]",
e
);
}
}

private String randomApplicationName() {
return randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 7);
}

private String randomPrivilegeName() {
if (randomBoolean()) {
return randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 7);
} else {
return randomAlphaOfLengthBetween(2, 4).toLowerCase(Locale.ROOT) + randomFrom(".", "_", "-") + randomAlphaOfLengthBetween(2, 6);
}
}

private String randomActionName() {
return randomAlphaOfLengthBetween(3, 5) + ":" + randomAlphaOfLengthBetween(3, 5);
}

}

0 comments on commit 44a3c97

Please sign in to comment.