Bugfix: Prevent invalid privileges in manage roles privilege (#128532)

This PR addresses the bug reported in
[#127496](https://github.com/elastic/elasticsearch/issues/127496)

**Changes:** - Added validation logic in `ConfigurableClusterPrivileges`
to ensure privileges defined for a global cluster manage role privilege
are valid  - Added unit test to `ManageRolePrivilegesTest` to ensure
invalid privilege is caught during role creation - Updated
`BulkPutRoleRestIT` to assert that an error is thrown and that the role
is not created.

Both existing and new unit/integration tests passed locally.
This commit is contained in:
Graeme Mjehovich 2025-05-29 12:14:54 -04:00 committed by GitHub
parent 68d5cc6973
commit 57d4e15b62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 114 additions and 1 deletions

View file

@ -0,0 +1,5 @@
pr: 128532
summary: "Prevent invalid privileges in manage roles privilege"
area: "Authorization"
type: bug
issues: [127496]

View file

@ -562,6 +562,11 @@ public final class ConfigurableClusterPrivileges {
}
for (String privilege : indexPrivilege.privileges) {
IndexPrivilege namedPrivilege = IndexPrivilege.getNamedOrNull(privilege);
// Use resolveBySelectorAccess to determine whether the passed privilege is valid.
// IllegalArgumentException is thrown here when an invalid permission is encountered.
IndexPrivilege.resolveBySelectorAccess(Set.of(privilege));
if (namedPrivilege != null && namedPrivilege.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) {
throw new IllegalArgumentException(
"Failure store related privileges are not supported as targets of manage roles but found [" + privilege + "]"

View file

@ -34,10 +34,13 @@ import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
@ -257,6 +260,70 @@ public class ManageRolesPrivilegesTests extends AbstractNamedWriteableTestCase<C
assertThat(permissionCheck(permission, "cluster:admin/xpack/security/role/put", putRoleRequest), is(false));
}
public void testParseInvalidPrivilege() throws Exception {
final String unknownPrivilege = randomValueOtherThanMany(
i -> IndexPrivilege.values().containsKey(i),
() -> randomAlphaOfLength(10).toLowerCase(Locale.ROOT)
);
final String invalidJsonString = String.format(Locale.ROOT, """
{
"manage": {
"indices": [
{
"names": ["test-*"],
"privileges": ["%s"]
}
]
}
}""", unknownPrivilege);
assertInvalidPrivilegeParsing(invalidJsonString, unknownPrivilege);
}
public void testParseMixedValidAndInvalidPrivileges() throws Exception {
final String unknownPrivilege = randomValueOtherThanMany(
i -> IndexPrivilege.values().containsKey(i),
() -> randomAlphaOfLength(10).toLowerCase(Locale.ROOT)
);
final String validPrivilege = "read";
final String mixedPrivilegesJson = String.format(Locale.ROOT, """
{
"manage": {
"indices": [
{
"names": ["test-*"],
"privileges": ["%s", "%s"]
}
]
}
}""", validPrivilege, unknownPrivilege);
assertInvalidPrivilegeParsing(mixedPrivilegesJson, unknownPrivilege);
}
/**
* Helper method to assert that parsing the given JSON payload results in an
* IllegalArgumentException due to an unknown privilege.
*
* @param jsonPayload The JSON string containing the privilege data.
* @param expectedErrorDetail The specific unknown privilege name expected in the error message.
*/
private static void assertInvalidPrivilegeParsing(final String jsonPayload, final String expectedErrorDetail) throws Exception {
final XContent xContent = XContentType.JSON.xContent();
try (
XContentParser parser = xContent.createParser(XContentParserConfiguration.EMPTY, jsonPayload.getBytes(StandardCharsets.UTF_8))
) {
assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT));
assertThat(parser.nextToken(), equalTo(XContentParser.Token.FIELD_NAME));
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> ManageRolesPrivilege.parse(parser));
assertThat(exception.getMessage(), containsString("unknown index privilege [" + expectedErrorDetail + "]"));
}
}
private static boolean permissionCheck(ClusterPermission permission, String action, ActionRequest request) {
final Authentication authentication = AuthenticationTestHelper.builder().build();
assertThat(request.validate(), nullValue());

View file

@ -21,7 +21,7 @@ import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
public class BulkPutRoleRestIT extends SecurityOnTrialLicenseRestTestCase {
public class PutRoleRestIT extends SecurityOnTrialLicenseRestTestCase {
public void testPutManyValidRoles() throws Exception {
Map<String, Object> responseMap = upsertRoles("""
{"roles": {"test1": {"cluster": ["all"],"indices": [{"names": ["*"],"privileges": ["all"]}]}, "test2":
@ -312,4 +312,40 @@ public class BulkPutRoleRestIT extends SecurityOnTrialLicenseRestTestCase {
);
}
}
public void testPutRoleWithInvalidManageRolesPrivilege() throws Exception {
final String badRoleName = "bad-role";
final ResponseException exception = expectThrows(ResponseException.class, () -> upsertRoles(String.format("""
{
"roles": {
"%s": {
"global": {
"role": {
"manage": {
"indices": [
{
"names": ["allowed-index-prefix-*"],
"privileges": ["foobar"]
}
]
}
}
}
}
}
}""", badRoleName)));
assertThat(exception.getMessage(), containsString("unknown index privilege [foobar]"));
assertEquals(400, exception.getResponse().getStatusLine().getStatusCode());
assertRoleDoesNotExist(badRoleName);
}
private void assertRoleDoesNotExist(final String roleName) throws Exception {
final ResponseException roleNotFound = expectThrows(
ResponseException.class,
() -> adminClient().performRequest(new Request("GET", "/_security/role/" + roleName))
);
assertEquals(404, roleNotFound.getResponse().getStatusLine().getStatusCode());
}
}