Skip to content

Commit f4f368c

Browse files
authored
Merge pull request #439 from kbase/dev-user_search
PTV-1911: Fix user search bugs re underscores + mongo errors
2 parents b93fdd8 + 5527329 commit f4f368c

File tree

11 files changed

+370
-114
lines changed

11 files changed

+370
-114
lines changed

RELEASE_NOTES.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Authentication Service MKII release notes
22

3+
## 0.7.2
4+
5+
* Fixed a bug where usernames with underscores would not be matched in username searches if an
6+
underscore was an interior character of a search prefix.
7+
* Fixed a bug where a MongoDB error would be thrown if a user search prefix resulted in no search
8+
terms if it had no valid characters for the requested search, whether user name or display
9+
name. Now a service error is thrown.
10+
311
## 0.7.1
412

513
* Publishes a shadow jar on jitpack.io for supporting tests in other repos.

src/main/java/us/kbase/auth2/Version.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
public class Version {
66

77
/** The version of the KBase Auth2 service. */
8-
public static final String VERSION = "0.7.1";
8+
public static final String VERSION = "0.7.2";
99

1010
}

src/main/java/us/kbase/auth2/lib/Authentication.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -1181,10 +1181,17 @@ private Optional<UserName> getAvailableUserName(
11811181
* problems. Make this smarter if necessary. E.g. could store username and numeric suffix
11821182
* db side and search and sort db side.
11831183
*/
1184-
final UserSearchSpec spec = UserSearchSpec.getBuilder()
1185-
// checked that this does indeed use an index for the mongo implementation
1186-
.withSearchRegex("^" + Pattern.quote(sugStrip) + "\\d*$")
1187-
.withSearchOnUserName(true).withIncludeDisabled(true).build();
1184+
final UserSearchSpec spec;
1185+
try {
1186+
spec = UserSearchSpec.getBuilder()
1187+
// checked that this does indeed use an index for the mongo implementation
1188+
.withSearchRegex("^" + Pattern.quote(sugStrip) + "\\d*$")
1189+
.withSearchOnUserName(true)
1190+
.withIncludeDisabled(true)
1191+
.build();
1192+
} catch (IllegalParameterException e) {
1193+
throw new RuntimeException("this is impossible", e);
1194+
}
11881195
final Map<UserName, DisplayName> users = storage.getUserDisplayNames(spec, -1);
11891196
final boolean match = users.containsKey(suggestedUserName);
11901197
final boolean hasNumSuffix = sugStrip.length() != sugName.length();

src/main/java/us/kbase/auth2/lib/UserName.java

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package us.kbase.auth2.lib;
22

33
import static java.util.Objects.requireNonNull;
4+
import static us.kbase.auth2.lib.Utils.checkStringNoCheckedException;
45

6+
import java.util.Arrays;
7+
import java.util.List;
58
import java.util.Optional;
69
import java.util.regex.Matcher;
710
import java.util.regex.Pattern;
11+
import java.util.stream.Collectors;
812

913
import us.kbase.auth2.lib.exceptions.ErrorType;
1014
import us.kbase.auth2.lib.exceptions.IllegalParameterException;
@@ -35,8 +39,8 @@ public class UserName extends Name {
3539
}
3640
}
3741

38-
private static final String INVALID_CHARS_REGEX = "[^a-z\\d_]+";
39-
private final static Pattern INVALID_CHARS = Pattern.compile(INVALID_CHARS_REGEX);
42+
private static final Pattern FORCE_ALPHA_FIRST_CHAR = Pattern.compile("^[^a-z]+");
43+
private final static Pattern INVALID_CHARS = Pattern.compile("[^a-z\\d_]+");
4044
public final static int MAX_NAME_LENGTH = 100;
4145

4246
/** Create a new user name.
@@ -75,15 +79,37 @@ public boolean isRoot() {
7579
*/
7680
public static Optional<UserName> sanitizeName(final String suggestedUserName) {
7781
requireNonNull(suggestedUserName, "suggestedUserName");
78-
final String s = suggestedUserName.toLowerCase().replaceAll(INVALID_CHARS_REGEX, "")
79-
.replaceAll("^[^a-z]+", "");
82+
final String s = cleanUserName(suggestedUserName);
8083
try {
8184
return s.isEmpty() ? Optional.empty() : Optional.of(new UserName(s));
8285
} catch (IllegalParameterException | MissingParameterException e) {
8386
throw new RuntimeException("This should be impossible", e);
8487
}
8588
}
8689

90+
private static String cleanUserName(final String putativeName) {
91+
return FORCE_ALPHA_FIRST_CHAR.matcher(
92+
INVALID_CHARS.matcher(
93+
putativeName.toLowerCase())
94+
.replaceAll(""))
95+
.replaceAll("");
96+
}
97+
98+
/** Given a string, splits the string by whitespace, strips all illegal
99+
* characters from the tokens, and returns the resulting strings,
100+
* discarding repeats.
101+
* @param names the names string to process.
102+
* @return the list of canonicalized names.
103+
*/
104+
public static List<String> getCanonicalNames(final String names) {
105+
checkStringNoCheckedException(names, "names");
106+
return Arrays.asList(names.toLowerCase().split("\\s+")).stream()
107+
.map(u -> cleanUserName(u))
108+
.filter(u -> !u.isEmpty())
109+
.distinct()
110+
.collect(Collectors.toList());
111+
}
112+
87113
@Override
88114
public String toString() {
89115
StringBuilder builder = new StringBuilder();

src/main/java/us/kbase/auth2/lib/UserSearchSpec.java

+93-34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.Optional;
1111
import java.util.Set;
1212

13+
import us.kbase.auth2.lib.exceptions.IllegalParameterException;
14+
1315
/** A specification for how a user search should be conducted.
1416
*
1517
* If a search prefix or regex is supplied and neither withSearchOnUserName() nor
@@ -24,7 +26,8 @@ public class UserSearchSpec {
2426

2527
//TODO ZLATER CODE don't expose regex externally. Not sure how best to do this without duplicating a lot of the class. For now setting regex is default access (package only).
2628

27-
private final List<String> prefixes;
29+
private final List<String> userNamePrefixes;
30+
private final List<String> displayPrefixes;
2831
private final String regex;
2932
private final boolean searchUser;
3033
private final boolean searchDisplayName;
@@ -34,15 +37,19 @@ public class UserSearchSpec {
3437
private final boolean includeDisabled;
3538

3639
private UserSearchSpec(
37-
final List<String> prefixes,
40+
final List<String> userNamePrefixes,
41+
final List<String> displayPrefixes,
3842
final String regex,
3943
final boolean searchUser,
4044
final boolean searchDisplayName,
4145
final Set<Role> searchRoles,
4246
final Set<String> searchCustomRoles,
4347
final boolean includeRoot,
4448
final boolean includeDisabled) {
45-
this.prefixes = prefixes == null ? null : Collections.unmodifiableList(prefixes);
49+
this.userNamePrefixes = userNamePrefixes == null ? null :
50+
Collections.unmodifiableList(userNamePrefixes);
51+
this.displayPrefixes = displayPrefixes == null ? null :
52+
Collections.unmodifiableList(displayPrefixes);
4653
this.regex = regex;
4754
this.searchUser = searchUser;
4855
this.searchDisplayName = searchDisplayName;
@@ -52,13 +59,21 @@ private UserSearchSpec(
5259
this.includeDisabled = includeDisabled;
5360
}
5461

55-
/** Returns the user and/or display name prefixes for the search, if any.
56-
* The prefixes match the start of the username or the start of any part of the whitespace
62+
/** Returns the user name prefixes for the search, if any.
63+
* The prefixes match the start of the user name.
64+
* @return the search prefix.
65+
*/
66+
public List<String> getSearchUserNamePrefixes() {
67+
return userNamePrefixes == null ? Collections.emptyList() : userNamePrefixes;
68+
}
69+
70+
/** Returns the display name prefixes for the search, if any.
71+
* The prefixes match the start of any part of the whitespace
5772
* tokenized display name.
5873
* @return the search prefix.
5974
*/
60-
public List<String> getSearchPrefixes() {
61-
return prefixes == null ? Collections.emptyList() : prefixes;
75+
public List<String> getSearchDisplayPrefixes() {
76+
return displayPrefixes == null ? Collections.emptyList() : displayPrefixes;
6277
}
6378

6479
/** Returns the user and/or display name regex for the search, if any.
@@ -80,35 +95,33 @@ public boolean hasSearchRegex() {
8095
* @return true if the search prefixes are set.
8196
*/
8297
public boolean hasSearchPrefixes() {
83-
return prefixes != null;
98+
return displayPrefixes != null;
8499
}
85100

86-
private boolean hasSearchString() {
87-
return prefixes != null || regex != null;
88-
}
89-
90101
/** Returns true if a search should occur on the user's user name.
91102
*
92-
* True when a) a prefix or regex is provided and b) withSearchOnUserName() was called with a
93-
* true argument or neither withSearchOnUserName() nor withSearchOnDisplayName() were called
94-
* with a true argument.
103+
* True when
104+
* a) a prefix with a valid format for a username or regex is provided and
105+
* b) withSearchOnUserName() was called with a true argument or neither or both of
106+
* withSearchOnUserName() and withSearchOnDisplayName() were called with a true argument.
95107
* @return whether the search should occur on the user's user name with the provided prefix or
96108
* regex.
97109
*/
98110
public boolean isUserNameSearch() {
99-
return searchUser || (hasSearchString() && !searchDisplayName);
111+
return (regex != null || userNamePrefixes != null) && (searchUser || !searchDisplayName);
100112
}
101113

102114
/** Returns true if a search should occur on the user's tokenized display name.
103115
*
104-
* True when a) a prefix or regex is provided and b) withSearchOnDisplayName() was called with
105-
* a true argument or neither withSearchOnUserName() nor withSearchOnDisplayName() were
106-
* called with a true argument.
116+
* True when
117+
* a) a prefix or regex is provided and
118+
* b) withSearchOnDisplayName() was called with a true argument or neither or both of
119+
* withSearchOnUserName() and withSearchOnDisplayName() were called with a true argument.
107120
* @return whether the search should occur on the users's display name with the provided
108121
* prefix or regex.
109122
*/
110123
public boolean isDisplayNameSearch() {
111-
return searchDisplayName || (hasSearchString() && !searchUser);
124+
return (regex != null || displayPrefixes != null) && (searchDisplayName || !searchUser);
112125
}
113126

114127
/** Returns true if a search should occur on the user's roles.
@@ -202,8 +215,8 @@ public static Builder getBuilder() {
202215

203216
@Override
204217
public int hashCode() {
205-
return Objects.hash(includeDisabled, includeRoot, prefixes, regex,
206-
searchCustomRoles, searchDisplayName, searchRoles, searchUser);
218+
return Objects.hash(displayPrefixes, includeDisabled, includeRoot, regex,
219+
searchCustomRoles, searchDisplayName, searchRoles, searchUser, userNamePrefixes);
207220
}
208221

209222
@Override
@@ -218,14 +231,15 @@ public boolean equals(Object obj) {
218231
return false;
219232
}
220233
UserSearchSpec other = (UserSearchSpec) obj;
221-
return includeDisabled == other.includeDisabled
234+
return Objects.equals(displayPrefixes, other.displayPrefixes)
235+
&& includeDisabled == other.includeDisabled
222236
&& includeRoot == other.includeRoot
223-
&& Objects.equals(prefixes, other.prefixes)
224237
&& Objects.equals(regex, other.regex)
225238
&& Objects.equals(searchCustomRoles, other.searchCustomRoles)
226239
&& searchDisplayName == other.searchDisplayName
227240
&& Objects.equals(searchRoles, other.searchRoles)
228-
&& searchUser == other.searchUser;
241+
&& searchUser == other.searchUser
242+
&& Objects.equals(userNamePrefixes, other.userNamePrefixes);
229243
}
230244

231245
/** A builder for a UserSearchSpec.
@@ -234,7 +248,7 @@ public boolean equals(Object obj) {
234248
*/
235249
public static class Builder {
236250

237-
private List<String> prefixes = null;
251+
private String prefix;
238252
private String regex = null;
239253
private boolean searchUser = false;
240254
private boolean searchDisplayName = false;
@@ -249,15 +263,16 @@ private Builder() {}
249263
* The prefix will replace the search regex, if any.
250264
* The prefix matches the start of the username or the start of any part of the whitespace
251265
* and hyphen tokenized display name.
252-
* The prefix is always split by whitespace and hyphens, punctuation removed, and
253-
* converted to lower case.
266+
* The user name prefix is split by whitespace and all illegal characters removed.
267+
* The display name prefix is split by whitespace and hyphens, punctuation removed,
268+
* and converted to lower case.
254269
* Once the prefix or search regex is set in this builder it cannot be removed.
255270
* @param prefix the prefix.
256271
* @return this builder.
257272
*/
258273
public Builder withSearchPrefix(final String prefix) {
259274
checkStringNoCheckedException(prefix, "prefix");
260-
this.prefixes = DisplayName.getCanonicalDisplayName(prefix);
275+
this.prefix = prefix;
261276
this.regex = null;
262277
return this;
263278
}
@@ -273,12 +288,14 @@ public Builder withSearchPrefix(final String prefix) {
273288
*/
274289
Builder withSearchRegex(final String regex) {
275290
this.regex = checkStringNoCheckedException(regex, "regex");
276-
this.prefixes = null;
291+
this.prefix = null;
277292
return this;
278293
}
279294

280295
/** Specify whether a search on a users's user name should occur.
281296
* A prefix must be set prior to calling this method.
297+
* If neither a user nor a display search is set (the default) and a prefix is set, then
298+
* the search occurs on both fields.
282299
* @param search whether the search should occur on the user's user name.
283300
* @return this builder.
284301
*/
@@ -290,6 +307,8 @@ public Builder withSearchOnUserName(final boolean search) {
290307

291308
/** Specify whether a search on a users's display name should occur.
292309
* A prefix must be set prior to calling this method.
310+
* If neither a user nor a display search is set (the default) and a prefix is set, then
311+
* the search occurs on both fields.
293312
* @param search whether the search should occur on the user's display name.
294313
* @return this builder.
295314
*/
@@ -300,7 +319,7 @@ public Builder withSearchOnDisplayName(final boolean search) {
300319
}
301320

302321
private void checkSearchPrefix(final boolean search) {
303-
if (search && prefixes == null && regex == null) {
322+
if (search && prefix == null && regex == null) {
304323
throw new IllegalStateException(
305324
"Must provide a prefix or regex if a name search is to occur");
306325
}
@@ -353,10 +372,50 @@ public Builder withIncludeDisabled(final boolean include) {
353372

354373
/** Build a UserSearchSpec instance.
355374
* @return a UserSearchSpec.
375+
* @throws IllegalParameterException if a prefix is set that, after normalizing, contains
376+
* no characters for the requested search(es).
356377
*/
357-
public UserSearchSpec build() {
358-
return new UserSearchSpec(prefixes, regex, searchUser, searchDisplayName, searchRoles,
359-
searchCustomRoles, includeRoot, includeDisabled);
378+
public UserSearchSpec build() throws IllegalParameterException {
379+
List<String> userNamePrefixes = null;
380+
List<String> displayPrefixes = null;
381+
if (this.prefix != null) {
382+
/* UsrSrch DisSrch UsrOK DisOK Throw exception?
383+
* T T Y implies Y
384+
* T T No Y No, just go with display search
385+
* T T No No Display or user exception
386+
*
387+
* T F Y implies Y
388+
* T F No Y User exception
389+
* T F No No User exception
390+
*
391+
* F T Y implies Y
392+
* F T No Y
393+
* F T No No Display exception
394+
*
395+
* Note that:
396+
* * If the user search is ok (UsrOK) the display search must be ok since the
397+
* user search has at least one a-z char.
398+
* * The first block where UsrSrch and DisSrch are all true is equivalent
399+
* to a block where they're all false, and so that block is omitted.
400+
*/
401+
userNamePrefixes = UserName.getCanonicalNames(prefix);
402+
userNamePrefixes = userNamePrefixes.isEmpty() ? null : userNamePrefixes;
403+
displayPrefixes = DisplayName.getCanonicalDisplayName(prefix);
404+
displayPrefixes = displayPrefixes.isEmpty() ? null : displayPrefixes;
405+
if (searchUser && !searchDisplayName && userNamePrefixes == null) {
406+
throw new IllegalParameterException(String.format(
407+
"The search prefix %s contains no valid username prefix "
408+
+ "and a user name search was requested", this.prefix));
409+
}
410+
if (displayPrefixes == null) {
411+
throw new IllegalParameterException(String.format(
412+
"The search prefix %s contains only punctuation and a "
413+
+ "display name search was requested", this.prefix));
414+
}
415+
}
416+
return new UserSearchSpec(userNamePrefixes, displayPrefixes, regex, searchUser,
417+
searchDisplayName, searchRoles, searchCustomRoles,
418+
includeRoot, includeDisabled);
360419
}
361420
}
362421
}

0 commit comments

Comments
 (0)