Skip to content

Commit 4e1031b

Browse files
authored
Support wider range of application names (#31752)
This extends the validation for application names to allow an optional suffix of "-" (or "_") followed by any number of "filename safe characters" (excluding '*') The purpose of this is to support multiple kibana instances against a single ES cluster where the name of each kibana application is "kibana-${kibana-index}", assuming some reasonable limits on the Kibana index name. The change also retricts the wildcard handling of application names to only support a trailing wildcard: e.g `*`, `kibana-*`, etc.
1 parent 9d61760 commit 4e1031b

File tree

5 files changed

+115
-61
lines changed

5 files changed

+115
-61
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
*/
3030
public final class ApplicationPrivilege extends Privilege {
3131

32-
private static final Pattern VALID_APPLICATION = Pattern.compile("^[a-z][A-Za-z0-9_-]{2,}$");
33-
private static final Pattern VALID_APPLICATION_OR_WILDCARD = Pattern.compile("^[a-z*][A-Za-z0-9_*-]*");
32+
private static final Pattern VALID_APPLICATION_PREFIX = Pattern.compile("^[a-z][A-Za-z0-9]*$");
33+
private static final Pattern WHITESPACE = Pattern.compile("[\\v\\h]");
3434
private static final Pattern VALID_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$");
3535

3636
/**
@@ -69,7 +69,7 @@ String[] getPatterns() {
6969
* @throws IllegalArgumentException if the name is not valid
7070
*/
7171
public static void validateApplicationName(String application) {
72-
validateApplicationName(application, VALID_APPLICATION);
72+
validateApplicationName(application, false);
7373
}
7474

7575
/**
@@ -78,13 +78,61 @@ public static void validateApplicationName(String application) {
7878
* @throws IllegalArgumentException if the name is not valid
7979
*/
8080
public static void validateApplicationNameOrWildcard(String application) {
81-
validateApplicationName(application, VALID_APPLICATION_OR_WILDCARD);
81+
validateApplicationName(application, true);
8282
}
8383

84-
private static void validateApplicationName(String application, Pattern pattern) {
85-
if (pattern.matcher(application).matches() == false) {
86-
throw new IllegalArgumentException("Application names must match the pattern " + pattern.pattern()
87-
+ " (but was '" + application + "')");
84+
/**
85+
* Validates that an application name matches the following rules:
86+
* - consist of a "prefix", optionally followed by either "-" or "_" and a suffix
87+
* - the prefix must begin with a lowercase ASCII letter
88+
* - the prefix only contain ASCII letter or digits
89+
* - the prefix must be at least 3 characters long
90+
* - the suffix must only contain {@link Strings#validFileName valid filename} characters
91+
* - no part of the name may contain whitespace
92+
* If {@code allowWildcard} is true, then the names that end with a '*', and would match a valid
93+
* application name are also accepted.
94+
*/
95+
private static void validateApplicationName(String application, boolean allowWildcard) {
96+
if (Strings.isEmpty(application)) {
97+
throw new IllegalArgumentException("Application names cannot be blank");
98+
}
99+
final int asterisk = application.indexOf('*');
100+
if (asterisk != -1) {
101+
if (allowWildcard == false) {
102+
throw new IllegalArgumentException("Application names may not contain '*' (found '" + application + "')");
103+
}
104+
if(application.equals("*")) {
105+
// this is allowed and short-circuiting here makes the later validation simpler
106+
return;
107+
}
108+
if (asterisk != application.length() - 1) {
109+
throw new IllegalArgumentException("Application name patterns only support trailing wildcards (found '" + application
110+
+ "')");
111+
}
112+
}
113+
if (WHITESPACE.matcher(application).find()) {
114+
throw new IllegalArgumentException("Application names may not contain whitespace (found '" + application + "')");
115+
}
116+
117+
final String[] parts = application.split("[_-]", 2);
118+
String prefix = parts[0];
119+
if (prefix.endsWith("*")) {
120+
prefix = prefix.substring(0, prefix.length() - 1);
121+
}
122+
if (VALID_APPLICATION_PREFIX.matcher(prefix).matches() == false) {
123+
throw new IllegalArgumentException("An application name prefix must match the pattern " + VALID_APPLICATION_PREFIX.pattern()
124+
+ " (found '" + prefix + "')");
125+
}
126+
if (prefix.length() < 3 && asterisk == -1) {
127+
throw new IllegalArgumentException("An application name prefix must be at least 3 characters long (found '" + prefix + "')");
128+
}
129+
130+
if (parts.length > 1) {
131+
final String suffix = parts[1];
132+
if (Strings.validFileName(suffix) == false) {
133+
throw new IllegalArgumentException("An application name suffix may not contain any of the characters '" +
134+
Strings.collectionToDelimitedString(Strings.INVALID_FILENAME_CHARS, "") + "' (found '" + suffix + "')");
135+
}
88136
}
89137
}
90138

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void testSerialization() throws IOException {
4949
public void testValidation() {
5050
// wildcard app name
5151
final ApplicationPrivilegeDescriptor wildcardApp = descriptor("*", "all", "*");
52-
assertValidationFailure(request(wildcardApp), "Application names must match");
52+
assertValidationFailure(request(wildcardApp), "Application names may not contain");
5353

5454
// invalid priv names
5555
final ApplicationPrivilegeDescriptor spaceName = descriptor("app", "r e a d", "read/*");
@@ -68,7 +68,8 @@ public void testValidation() {
6868

6969
// mixed
7070
assertValidationFailure(request(wildcardApp, numericName, reservedMetadata, badAction),
71-
"Application names must match", "Application privilege names must match", "metadata keys may not start", "must contain one of");
71+
"Application names may not contain", "Application privilege names must match", "metadata keys may not start",
72+
"must contain one of");
7273
}
7374

7475
private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) {

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ public void testValidationOfApplicationPrivileges() {
3535
// Fail
3636
assertValidationError("privilege names and actions must match the pattern",
3737
buildRequestWithApplicationPrivilege("app", new String[]{"in valid"}, new String[]{"*"}));
38-
assertValidationError("Application names must match the pattern",
38+
assertValidationError("An application name prefix must match the pattern",
3939
buildRequestWithApplicationPrivilege("000", new String[]{"all"}, new String[]{"*"}));
40-
assertValidationError("Application names must match the pattern",
40+
assertValidationError("An application name prefix must match the pattern",
4141
buildRequestWithApplicationPrivilege("%*", new String[]{"all"}, new String[]{"*"}));
4242
}
4343

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ public void testValidateNoWildcardApplicationPrivileges() {
7676
});
7777
final ActionRequestValidationException exception = request.validate();
7878
assertThat(exception, notNullValue());
79-
assertThat(exception.validationErrors(), hasItem("Application names must match the pattern ^[a-z][A-Za-z0-9_-]{2,}$" +
80-
" (but was '*')"));
79+
assertThat(exception.validationErrors(), hasItem("Application names may not contain '*' (found '*')"));
8180
}
8281

8382
private HasPrivilegesRequest serializeAndDeserialize(HasPrivilegesRequest original, Version version) throws IOException {

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.elasticsearch.xpack.core.security.authz.privilege;
77

8+
import junit.framework.AssertionFailedError;
89
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
910
import org.elasticsearch.common.util.set.Sets;
1011
import org.elasticsearch.test.ESTestCase;
@@ -17,6 +18,7 @@
1718
import java.util.Locale;
1819
import java.util.Map;
1920
import java.util.Set;
21+
import java.util.function.Supplier;
2022

2123
import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
2224
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
@@ -27,62 +29,61 @@
2729
public class ApplicationPrivilegeTests extends ESTestCase {
2830

2931
public void testValidationOfApplicationName() {
30-
// too short
31-
assertValidationFailure("Application names", () -> ApplicationPrivilege.validateApplicationName("ap"));
32-
// must start with lowercase
33-
assertValidationFailure("Application names", () -> ApplicationPrivilege.validateApplicationName("App"));
34-
// must start with letter
35-
assertValidationFailure("Application names", () -> ApplicationPrivilege.validateApplicationName("1app"));
36-
// cannot contain special characters
37-
assertValidationFailure("Application names",
38-
() -> ApplicationPrivilege.validateApplicationName("app" + randomFrom(":;$#%()+=/'.,".toCharArray())));
32+
final String specialCharacters = ":;$#%()+='.{}[]!@^&'";
33+
final Supplier<Character> specialCharacter = () -> specialCharacters.charAt(randomInt(specialCharacters.length() - 1));
34+
35+
assertValidationFailure("a p p", "application name", () -> ApplicationPrivilege.validateApplicationName("a p p"));
36+
assertValidationFailure("ap", "application name", () -> ApplicationPrivilege.validateApplicationName("ap"));
37+
for (String app : Arrays.asList(
38+
"App",// must start with lowercase
39+
"1app", // must start with letter
40+
"app" + specialCharacter.get() // cannot contain special characters unless preceded by a "-" or "_"
41+
)) {
42+
assertValidationFailure(app, "application name", () -> ApplicationPrivilege.validateApplicationName(app));
43+
assertValidationFailure(app, "application name", () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app));
44+
}
3945

4046
// no wildcards
41-
assertValidationFailure("Application names", () -> ApplicationPrivilege.validateApplicationName("app*"));
47+
assertValidationFailure("app*", "application names", () -> ApplicationPrivilege.validateApplicationName("app*"));
4248
// no special characters with wildcards
43-
assertValidationFailure("Application names",
44-
() -> ApplicationPrivilege.validateApplicationNameOrWildcard("app" + randomFrom((":;$#%()+=/'.,").toCharArray()) + "*"));
49+
final String appNameWithSpecialCharAndWildcard = "app" + specialCharacter.get() + "*";
50+
assertValidationFailure(appNameWithSpecialCharAndWildcard, "application name",
51+
() -> ApplicationPrivilege.validateApplicationNameOrWildcard(appNameWithSpecialCharAndWildcard));
4552

53+
String appNameWithSpecialChars = "myapp" + randomFrom('-', '_');
54+
for (int i = randomIntBetween(1, 12); i > 0; i--) {
55+
appNameWithSpecialChars = appNameWithSpecialChars + specialCharacter.get();
56+
}
4657
// these should all be OK
47-
assertNoException(() -> ApplicationPrivilege.validateApplicationName("app"));
48-
assertNoException(() -> ApplicationPrivilege.validateApplicationName("app1"));
49-
assertNoException(() -> ApplicationPrivilege.validateApplicationName("myApp"));
50-
assertNoException(() -> ApplicationPrivilege.validateApplicationName("my-App"));
51-
assertNoException(() -> ApplicationPrivilege.validateApplicationName("my_App"));
52-
assertNoException(() -> ApplicationPrivilege.validateApplicationNameOrWildcard("app*"));
58+
for (String app : Arrays.asList("app", "app1", "myApp", "myApp-:;$#%()+='.", "myApp_:;$#%()+='.", appNameWithSpecialChars)) {
59+
assertNoException(app, () -> ApplicationPrivilege.validateApplicationName(app));
60+
assertNoException(app, () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app));
61+
}
5362
}
5463

5564
public void testValidationOfPrivilegeName() {
5665
// must start with lowercase
57-
assertValidationFailure("privilege names", () -> ApplicationPrivilege.validatePrivilegeName("Read"));
66+
assertValidationFailure("Read", "privilege names", () -> ApplicationPrivilege.validatePrivilegeName("Read"));
5867
// must start with letter
59-
assertValidationFailure("privilege names", () -> ApplicationPrivilege.validatePrivilegeName("1read"));
68+
assertValidationFailure("1read", "privilege names", () -> ApplicationPrivilege.validatePrivilegeName("1read"));
6069
// cannot contain special characters
61-
final String withSpecialChar = "read" + randomFrom(":;$#%()+=/',".toCharArray());
62-
assertValidationFailure("privilege names", () -> ApplicationPrivilege.validatePrivilegeName(withSpecialChar));
70+
final String specialChars = ":;$#%()+=/',";
71+
final String withSpecialChar = "read" + specialChars.charAt(randomInt(specialChars.length()-1));
72+
assertValidationFailure(withSpecialChar, "privilege names", () -> ApplicationPrivilege.validatePrivilegeName(withSpecialChar));
6373

6474
// these should all be OK
65-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeName("read"));
66-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeName("read1"));
67-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeName("readData"));
68-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeName("read-data"));
69-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeName("read.data"));
70-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeName("read_data"));
71-
72-
assertValidationFailure("privilege names and action", () -> ApplicationPrivilege.validatePrivilegeOrActionName("r e a d"));
73-
assertValidationFailure("privilege names and action", () -> ApplicationPrivilege.validatePrivilegeOrActionName("read\n"));
74-
assertValidationFailure("privilege names and action", () -> ApplicationPrivilege.validatePrivilegeOrActionName("copy®"));
75-
76-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read"));
77-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read1"));
78-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("readData"));
79-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read-data"));
80-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read.data"));
81-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read_data"));
82-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read:*"));
83-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read/*"));
84-
assertNoException(() -> ApplicationPrivilege.validatePrivilegeOrActionName("read/a_b.c-d+e%f#(g)"));
75+
for (String priv : Arrays.asList("read", "read1", "readData", "read-data", "read.data", "read_data")) {
76+
assertNoException(priv, () -> ApplicationPrivilege.validatePrivilegeName(priv));
77+
assertNoException(priv, () -> ApplicationPrivilege.validatePrivilegeOrActionName(priv));
78+
}
79+
80+
for (String priv : Arrays.asList("r e a d", "read\n", "copy®")) {
81+
assertValidationFailure(priv, "privilege names and action", () -> ApplicationPrivilege.validatePrivilegeOrActionName(priv));
82+
}
8583

84+
for (String priv : Arrays.asList("read:*", "read/*", "read/a_b.c-d+e%f#(g)")) {
85+
assertNoException(priv, () -> ApplicationPrivilege.validatePrivilegeOrActionName(priv));
86+
}
8687
}
8788

8889
public void testGetPrivilegeByName() {
@@ -144,17 +145,22 @@ private String getPrivilegeName(ApplicationPrivilege privilege) {
144145
}
145146
}
146147

147-
private void assertValidationFailure(String messageContent, ThrowingRunnable body) {
148-
final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, body);
149-
assertThat(exception.getMessage(), containsString(messageContent));
148+
private void assertValidationFailure(String reason,String messageContent, ThrowingRunnable body) {
149+
final IllegalArgumentException exception;
150+
try {
151+
exception = expectThrows(IllegalArgumentException.class, body);
152+
assertThat(exception.getMessage().toLowerCase(Locale.ROOT), containsString(messageContent.toLowerCase(Locale.ROOT)));
153+
} catch (AssertionFailedError e) {
154+
fail(reason + " - " + e.getMessage());
155+
}
150156
}
151157

152-
private void assertNoException(ThrowingRunnable body) {
158+
private void assertNoException(String reason, ThrowingRunnable body) {
153159
try {
154160
body.run();
155161
// pass
156162
} catch (Throwable e) {
157-
Assert.fail("Expected no exception, but got: " + e);
163+
Assert.fail(reason + " - Expected no exception, but got: " + e);
158164
}
159165
}
160166

0 commit comments

Comments
 (0)