Skip to content

Commit 64b2ed6

Browse files
authored
Merge pull request #11 from kbase/dev-new_client
Add isValidUsername method
2 parents 2f6cedf + c323ad3 commit 64b2ed6

File tree

5 files changed

+140
-7
lines changed

5 files changed

+140
-7
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ jobs:
5050
- name: Upload coverage to Codecov
5151
uses: codecov/codecov-action@v3
5252
with:
53+
token: ${{ secrets.CODECOV_TOKEN }}
5354
fail_ci_if_error: true

src/main/java/us/kbase/auth/client/AuthClient.java

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
import java.net.URI;
1010
import java.net.URISyntaxException;
1111
import java.nio.charset.StandardCharsets;
12+
import java.util.HashMap;
13+
import java.util.LinkedList;
14+
import java.util.List;
1215
import java.util.Map;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
1318

1419
import org.slf4j.LoggerFactory;
1520

1621
import com.fasterxml.jackson.databind.ObjectMapper;
1722

1823
import us.kbase.auth.AuthException;
1924
import us.kbase.auth.AuthToken;
25+
import us.kbase.auth.client.internal.StringCache;
2026
import us.kbase.auth.client.internal.TokenCache;
2127

2228
/** A client for the KBase Auth2 authentication server (https://github.com/kbase/auth2).
@@ -31,8 +37,11 @@ public class AuthClient {
3137

3238
private static final ObjectMapper MAPPER = new ObjectMapper();
3339

34-
// make this configurable? Add a builder if so
40+
// make these configurable? Add a builder if so
3541
private final TokenCache tokenCache = new TokenCache(1000, 2000); // same as old auth client
42+
private final StringCache userCache = new StringCache(1000, 2000); // same as old auth client
43+
44+
final static Pattern INVALID_USERNAME = Pattern.compile("[^a-z\\d_]+");
3645

3746
private final URI rootURI;
3847

@@ -164,9 +173,7 @@ public String getServerVersion() throws IOException, AuthException {
164173
* @throws AuthException if an auth exception occurs communicating with the auth service.
165174
*/
166175
public AuthToken validateToken(final String token) throws IOException, AuthException {
167-
if (token == null || token.trim().isEmpty()) {
168-
throw new IllegalArgumentException("token must be a non-whitespace string");
169-
}
176+
checkToken(token);
170177
final AuthToken t = tokenCache.getToken(token);
171178
if (t != null) {
172179
return t;
@@ -179,4 +186,53 @@ public AuthToken validateToken(final String token) throws IOException, AuthExcep
179186
return authToken;
180187
}
181188

189+
private void checkToken(final String token) {
190+
if (token == null || token.trim().isEmpty()) {
191+
throw new IllegalArgumentException("token must be a non-whitespace string");
192+
}
193+
}
194+
195+
/** Check if usernames are valid accounts in the auth service.
196+
* @param users the list of usernames to check. If they contain any invalid characters
197+
* (e.g. anything other than a-z, 0-9, or _, an exception will be thrown.
198+
* @param token any valid auth token.
199+
* @return a mapping of each username to whether it's valid or not.
200+
* @throws IOException if an IOException occurs communicating with the auth service.
201+
* @throws AuthException if an auth exception occurs communicating with the auth service.
202+
*/
203+
public Map<String, Boolean> isValidUserName(final List<String> users, final String token)
204+
throws IOException, AuthException {
205+
// theoretically someone could submit hundreds of users and hit the url size limit
206+
// don't worry about that for now.
207+
checkToken(token);
208+
if (users == null || users.isEmpty()) {
209+
throw new IllegalArgumentException("users cannot be null or empty");
210+
}
211+
final List<String> badlist = new LinkedList<String>();
212+
final Map<String, Boolean> result = new HashMap<>();
213+
for (String user: users) {
214+
if (user == null || user.trim().isEmpty()) {
215+
throw new IllegalArgumentException("each user must be a non-whitespace string");
216+
}
217+
user = user.trim();
218+
final Matcher m = INVALID_USERNAME.matcher(user);
219+
if (m.find()) {
220+
// this matches the prior behavior, but later could have an validity enum
221+
// one for bad chars, one for does not exist
222+
throw new IllegalArgumentException(
223+
"username " + user + " has invalid character: " + m.group(0));
224+
}
225+
if (userCache.hasString(user)) {
226+
result.put(user, true);
227+
} else {
228+
badlist.add(user);
229+
}
230+
}
231+
final URI target = rootURI.resolve("api/V2/users/?list=" + String.join(",", badlist));
232+
final Map<String, Object> res = request(target, token.trim());
233+
res.keySet().stream().forEach(u -> userCache.putString(u));
234+
badlist.stream().forEach(u -> result.put(u, res.containsKey(u)));
235+
return result;
236+
}
237+
182238
}

src/main/java/us/kbase/auth/client/internal/TokenCache.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public void putValidToken(AuthToken token) {
116116
}
117117
Collections.sort(dts);
118118
for(int i = size; i < dts.size(); i++) {
119-
cache.remove(dts.get(i).token);
119+
cache.remove(dts.get(i).tokenHash);
120120
}
121121
}
122122
}
@@ -135,11 +135,11 @@ class UserDate {
135135

136136
class DateToken implements Comparable<DateToken>{
137137

138-
final String token;
138+
final String tokenHash;
139139
final Date date;
140140

141141
DateToken(long date, String token) {
142-
this.token = token;
142+
this.tokenHash = token;
143143
this.date = new Date(date);
144144
}
145145

src/test/java/us/kbase/test/auth/client/AuthClientTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import static org.junit.Assert.fail;
66

77
import java.net.URI;
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.LinkedList;
812
import java.util.List;
13+
import java.util.Map;
914

1015
import org.junit.AfterClass;
1116
import org.junit.Before;
@@ -179,4 +184,74 @@ private void validateTokenFail(final URI uri, final String token, final Exceptio
179184
TestCommon.assertExceptionCorrect(got, expected);
180185
}
181186
}
187+
188+
@Test
189+
public void isValidUserName() throws Exception {
190+
final String token1 = TestCommon.getAuthToken1();
191+
final List<String> goodUsers = TestCommon.getGoodUsers();
192+
193+
final AuthClient c = AuthClient.from(new URI(TestCommon.getAuthURI()));
194+
195+
final List<String> badUsers = Arrays.asList(
196+
"superfakeuserthatdoesntexistihope",
197+
"anothersuperfakeuserrighthereimfake");
198+
199+
final List<String> allUsers = new LinkedList<>(badUsers);
200+
goodUsers.stream().forEach(u -> allUsers.add(String.format(" \t %s ", u)));
201+
202+
final Map<String, Boolean> expected = new HashMap<>();
203+
goodUsers.stream().forEach(u -> expected.put(u.trim(), true));
204+
badUsers.stream().forEach(u -> expected.put(u, false));
205+
206+
final Map<String, Boolean> res = c.isValidUserName(allUsers, token1);
207+
208+
assertThat("incorrect users", res, is(expected));
209+
210+
// 2nd time from cache. Again no good way to test this w/o a client mock
211+
final Map<String, Boolean> res2 = c.isValidUserName(allUsers, token1);
212+
213+
assertThat("incorrect users", res2, is(expected));
214+
}
215+
216+
@Test
217+
public void isValidUserNameFailBadArgs() throws Exception {
218+
final URI uri = new URI(TestCommon.getAuthURI());
219+
final List<String> u = Arrays.asList("u");
220+
isValidUserNameFail(uri, null, "foo",
221+
new IllegalArgumentException("users cannot be null or empty"));
222+
isValidUserNameFail(uri, Collections.emptyList(), "foo",
223+
new IllegalArgumentException("users cannot be null or empty"));
224+
isValidUserNameFail(uri, Arrays.asList("a", null), "foo",
225+
new IllegalArgumentException("each user must be a non-whitespace string"));
226+
isValidUserNameFail(uri, Arrays.asList("a", " "), "foo",
227+
new IllegalArgumentException("each user must be a non-whitespace string"));
228+
isValidUserNameFail(uri, Arrays.asList("a", " foo*bar "), "foo",
229+
new IllegalArgumentException("username foo*bar has invalid character: *"));
230+
isValidUserNameFail(uri, u, null,
231+
new IllegalArgumentException("token must be a non-whitespace string"));
232+
isValidUserNameFail(uri, u, " \t ",
233+
new IllegalArgumentException("token must be a non-whitespace string"));
234+
}
235+
236+
@Test
237+
public void isValidUserNameFailBadToken() throws Exception {
238+
final URI uri = new URI(TestCommon.getAuthURI());
239+
final List<String> u = Arrays.asList("u");
240+
isValidUserNameFail(uri, u, "badtoken", new AuthException(
241+
"Auth service returned an error: 10020 Invalid token"));
242+
}
243+
244+
private void isValidUserNameFail(
245+
final URI uri,
246+
final List<String> usernames,
247+
final String token,
248+
final Exception expected) {
249+
try {
250+
AuthClient.from(uri).isValidUserName(usernames, token);
251+
fail("expected exception");
252+
} catch (Exception got) {
253+
TestCommon.assertExceptionCorrect(got, expected);
254+
}
255+
}
256+
182257
}

test.cfg.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ auth_user1 = <user name goes here>
88
auth_token2 = <token goes here>
99
auth_user2 = <user name goes here>
1010

11+
# existing names from the auth server
1112
good_users = <comma separated list of user names>

0 commit comments

Comments
 (0)