From 32625d4ba2edfc865467034b6a7d02e3eb3de295 Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Tue, 20 Feb 2024 12:09:30 +0530 Subject: [PATCH] fix: PR changes --- .../io/supertokens/bulkimport/BulkImport.java | 29 ++- .../BulkImportUserPaginationContainer.java | 8 +- .../BulkImportUserPaginationToken.java | 16 +- .../bulkimport/BulkImportUserUtils.java | 167 ++++++++++++++++++ .../InvalidBulkImportDataException.java | 33 ++++ .../api/bulkimport/AddBulkImportUsers.java | 42 ++++- .../api/bulkimport/GetBulkImportUsers.java | 30 ++-- .../apis/AddBulkImportUsersTest.java | 51 +++++- .../apis/GetBulkImportUsersTest.java | 4 +- 9 files changed, 333 insertions(+), 47 deletions(-) create mode 100644 src/main/java/io/supertokens/bulkimport/BulkImportUserUtils.java create mode 100644 src/main/java/io/supertokens/bulkimport/exceptions/InvalidBulkImportDataException.java diff --git a/src/main/java/io/supertokens/bulkimport/BulkImport.java b/src/main/java/io/supertokens/bulkimport/BulkImport.java index 4b8f55509..0f95b9cef 100644 --- a/src/main/java/io/supertokens/bulkimport/BulkImport.java +++ b/src/main/java/io/supertokens/bulkimport/BulkImport.java @@ -16,15 +16,16 @@ package io.supertokens.bulkimport; +import io.supertokens.pluginInterface.bulkimport.BulkImportStorage.BulkImportUserStatus; import io.supertokens.pluginInterface.bulkimport.BulkImportUser; +import io.supertokens.pluginInterface.bulkimport.BulkImportUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.utils.Utils; -import com.google.gson.JsonObject; -import java.util.ArrayList; +import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -34,12 +35,12 @@ public class BulkImport { public static final int GET_USERS_PAGINATION_LIMIT = 500; public static final int GET_USERS_DEFAULT_LIMIT = 100; - public static void addUsers(AppIdentifierWithStorage appIdentifierWithStorage, ArrayList users) + public static void addUsers(AppIdentifierWithStorage appIdentifierWithStorage, List users) throws StorageQueryException, TenantOrAppNotFoundException { while (true) { try { - appIdentifierWithStorage.getBulkImportStorage().addBulkImportUsers(appIdentifierWithStorage, users); - break; + appIdentifierWithStorage.getBulkImportStorage().addBulkImportUsers(appIdentifierWithStorage, users); + break; } catch (io.supertokens.pluginInterface.bulkimport.exceptions.DuplicateUserIdException ignored) { // We re-generate the user id for every user and retry for (BulkImportUser user : users) { @@ -50,10 +51,9 @@ public static void addUsers(AppIdentifierWithStorage appIdentifierWithStorage, A } public static BulkImportUserPaginationContainer getUsers(AppIdentifierWithStorage appIdentifierWithStorage, - @Nonnull Integer limit, @Nullable String status, @Nullable String paginationToken) - throws StorageQueryException, BulkImportUserPaginationToken.InvalidTokenException, - TenantOrAppNotFoundException { - JsonObject[] users; + @Nonnull Integer limit, @Nullable BulkImportUserStatus status, @Nullable String paginationToken) + throws StorageQueryException, BulkImportUserPaginationToken.InvalidTokenException { + List users; if (paginationToken == null) { users = appIdentifierWithStorage.getBulkImportStorage() @@ -65,15 +65,14 @@ public static BulkImportUserPaginationContainer getUsers(AppIdentifierWithStorag } String nextPaginationToken = null; - int maxLoop = users.length; - if (users.length == limit + 1) { + int maxLoop = users.size(); + if (users.size() == limit + 1) { maxLoop = limit; - nextPaginationToken = new BulkImportUserPaginationToken(users[limit].get("id").getAsString()) - .generateToken(); + BulkImportUserInfo user = users.get(limit); + nextPaginationToken = new BulkImportUserPaginationToken(user.id, user.createdAt).generateToken(); } - JsonObject[] resultUsers = new JsonObject[maxLoop]; - System.arraycopy(users, 0, resultUsers, 0, maxLoop); + List resultUsers = users.subList(0, maxLoop); return new BulkImportUserPaginationContainer(resultUsers, nextPaginationToken); } } diff --git a/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationContainer.java b/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationContainer.java index a9e4d644c..2993a83a7 100644 --- a/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationContainer.java +++ b/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationContainer.java @@ -16,16 +16,18 @@ package io.supertokens.bulkimport; +import java.util.List; + import javax.annotation.Nonnull; import javax.annotation.Nullable; -import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.bulkimport.BulkImportUserInfo; public class BulkImportUserPaginationContainer { - public final JsonObject[] users; + public final List users; public final String nextPaginationToken; - public BulkImportUserPaginationContainer(@Nonnull JsonObject[] users, @Nullable String nextPaginationToken) { + public BulkImportUserPaginationContainer(@Nonnull List users, @Nullable String nextPaginationToken) { this.users = users; this.nextPaginationToken = nextPaginationToken; } diff --git a/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationToken.java b/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationToken.java index d66d02041..0c24211a9 100644 --- a/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationToken.java +++ b/src/main/java/io/supertokens/bulkimport/BulkImportUserPaginationToken.java @@ -20,22 +20,30 @@ public class BulkImportUserPaginationToken { public final String bulkImportUserId; + public final long createdAt; - public BulkImportUserPaginationToken(String bulkImportUserId) { + public BulkImportUserPaginationToken(String bulkImportUserId, long timeJoined) { this.bulkImportUserId = bulkImportUserId; + this.createdAt = timeJoined; } public static BulkImportUserPaginationToken extractTokenInfo(String token) throws InvalidTokenException { try { - String bulkImportUserId = new String(Base64.getDecoder().decode(token)); - return new BulkImportUserPaginationToken(bulkImportUserId); + String decodedPaginationToken = new String(Base64.getDecoder().decode(token)); + String[] splitDecodedToken = decodedPaginationToken.split(";"); + if (splitDecodedToken.length != 2) { + throw new InvalidTokenException(); + } + String bulkImportUserId = splitDecodedToken[0]; + long timeJoined = Long.parseLong(splitDecodedToken[1]); + return new BulkImportUserPaginationToken(bulkImportUserId, timeJoined); } catch (Exception e) { throw new InvalidTokenException(); } } public String generateToken() { - return new String(Base64.getEncoder().encode((this.bulkImportUserId).getBytes())); + return new String(Base64.getEncoder().encode((this.bulkImportUserId + ";" + this.createdAt).getBytes())); } public static class InvalidTokenException extends Exception { diff --git a/src/main/java/io/supertokens/bulkimport/BulkImportUserUtils.java b/src/main/java/io/supertokens/bulkimport/BulkImportUserUtils.java new file mode 100644 index 000000000..43915743a --- /dev/null +++ b/src/main/java/io/supertokens/bulkimport/BulkImportUserUtils.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.bulkimport; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.bulkimport.exceptions.InvalidBulkImportDataException; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod.EmailPasswordLoginMethod; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod.ThirdPartyLoginMethod; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod.PasswordlessLoginMethod; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser.TotpDevice; +import io.supertokens.pluginInterface.utils.JsonValidatorUtils.ValueType; +import io.supertokens.utils.Utils; + +import static io.supertokens.pluginInterface.utils.JsonValidatorUtils.parseAndValidateField; +import static io.supertokens.pluginInterface.utils.JsonValidatorUtils.validateJsonFieldType; + +public class BulkImportUserUtils { + public static BulkImportUser createBulkImportUserFromJSON(JsonObject userData, String id) throws InvalidBulkImportDataException { + List errors = new ArrayList<>(); + + String externalUserId = parseAndValidateField(userData, "externalUserId", ValueType.STRING, false, String.class, errors, "."); + JsonObject userMetadata = parseAndValidateField(userData, "userMetadata", ValueType.OBJECT, false, JsonObject.class, errors, "."); + List userRoles = getParsedUserRoles(userData, errors); + List totpDevices = getParsedTotpDevices(userData, errors); + List loginMethods = getParsedLoginMethods(userData, errors); + + if (!errors.isEmpty()) { + throw new InvalidBulkImportDataException(errors); + } + return new BulkImportUser(id, externalUserId, userMetadata, userRoles, totpDevices, loginMethods); + } + + private static List getParsedUserRoles(JsonObject userData, List errors) { + JsonArray jsonUserRoles = parseAndValidateField(userData, "roles", ValueType.ARRAY_OF_STRING, + false, + JsonArray.class, errors, "."); + + if (jsonUserRoles == null) { + return null; + } + + List userRoles = new ArrayList<>(); + jsonUserRoles.forEach(role -> userRoles.add(role.getAsString().trim())); + return userRoles; + } + + private static List getParsedTotpDevices(JsonObject userData, List errors) { + JsonArray jsonTotpDevices = parseAndValidateField(userData, "totp", ValueType.ARRAY_OF_OBJECT, false, JsonArray.class, errors, "."); + if (jsonTotpDevices == null) { + return null; + } + + List totpDevices = new ArrayList<>(); + for (JsonElement jsonTotpDeviceEl : jsonTotpDevices) { + JsonObject jsonTotpDevice = jsonTotpDeviceEl.getAsJsonObject(); + + String secretKey = parseAndValidateField(jsonTotpDevice, "secretKey", ValueType.STRING, true, String.class, errors, " for a totp device."); + Number period = parseAndValidateField(jsonTotpDevice, "period", ValueType.NUMBER, true, Number.class, errors, " for a totp device."); + Number skew = parseAndValidateField(jsonTotpDevice, "skew", ValueType.NUMBER, true, Number.class, errors, " for a totp device."); + String deviceName = parseAndValidateField(jsonTotpDevice, "deviceName", ValueType.STRING, false, String.class, errors, " for a totp device."); + + if (period != null && period.intValue() < 1) { + errors.add("period should be > 0 for a totp device."); + } + if (skew != null && skew.intValue() < 0) { + errors.add("skew should be >= 0 for a totp device."); + } + + if(deviceName != null) { + deviceName = deviceName.trim(); + } + totpDevices.add(new TotpDevice(secretKey, period.intValue(), skew.intValue(), deviceName)); + } + return totpDevices; + } + + private static List getParsedLoginMethods(JsonObject userData, List errors) { + JsonArray jsonLoginMethods = parseAndValidateField(userData, "loginMethods", ValueType.ARRAY_OF_OBJECT, true, JsonArray.class, errors, "."); + + if (jsonLoginMethods == null) { + return new ArrayList<>(); + } + + if (jsonLoginMethods.size() == 0) { + errors.add("At least one loginMethod is required."); + return new ArrayList<>(); + } + + Boolean hasPrimaryLoginMethod = false; + + List loginMethods = new ArrayList<>(); + for (JsonElement jsonLoginMethod : jsonLoginMethods) { + JsonObject jsonLoginMethodObj = jsonLoginMethod.getAsJsonObject(); + + if (validateJsonFieldType(jsonLoginMethodObj, "isPrimary", ValueType.BOOLEAN)) { + if (jsonLoginMethodObj.get("isPrimary").getAsBoolean()) { + if (hasPrimaryLoginMethod) { + errors.add("No two loginMethods can have isPrimary as true."); + } + hasPrimaryLoginMethod = true; + } + } + + String recipeId = parseAndValidateField(jsonLoginMethodObj, "recipeId", ValueType.STRING, true, String.class, errors, " for a loginMethod."); + String tenantId = parseAndValidateField(jsonLoginMethodObj, "tenantId", ValueType.STRING, false, String.class, errors, " for a loginMethod."); + Boolean isVerified = parseAndValidateField(jsonLoginMethodObj, "isVerified", ValueType.BOOLEAN, false, Boolean.class, errors, " for a loginMethod."); + Boolean isPrimary = parseAndValidateField(jsonLoginMethodObj, "isPrimary", ValueType.BOOLEAN, false, Boolean.class, errors, " for a loginMethod."); + Number timeJoined = parseAndValidateField(jsonLoginMethodObj, "timeJoinedInMSSinceEpoch", ValueType.NUMBER, false, Number.class, errors, " for a loginMethod"); + Long timeJoinedInMSSinceEpoch = timeJoined != null ? timeJoined.longValue() : 0; + + if ("emailpassword".equals(recipeId)) { + String email = parseAndValidateField(jsonLoginMethodObj, "email", ValueType.STRING, true, String.class, errors, " for an emailpassword recipe."); + String passwordHash = parseAndValidateField(jsonLoginMethodObj, "passwordHash", ValueType.STRING, true, String.class, errors, " for an emailpassword recipe."); + String hashingAlgorithm = parseAndValidateField(jsonLoginMethodObj, "hashingAlgorithm", ValueType.STRING, true, String.class, errors, " for an emailpassword recipe."); + + email = email != null ? Utils.normaliseEmail(email) : null; + hashingAlgorithm = hashingAlgorithm != null ? hashingAlgorithm.trim().toUpperCase() : null; + + EmailPasswordLoginMethod emailPasswordLoginMethod = new EmailPasswordLoginMethod(email, passwordHash, hashingAlgorithm); + loginMethods.add(new LoginMethod(tenantId, recipeId, isVerified, isPrimary, timeJoinedInMSSinceEpoch, emailPasswordLoginMethod, null, null)); + } else if ("thirdparty".equals(recipeId)) { + String email = parseAndValidateField(jsonLoginMethodObj, "email", ValueType.STRING, true, String.class, errors, " for a thirdparty recipe."); + String thirdPartyId = parseAndValidateField(jsonLoginMethodObj, "thirdPartyId", ValueType.STRING, true, String.class, errors, " for a thirdparty recipe."); + String thirdPartyUserId = parseAndValidateField(jsonLoginMethodObj, "thirdPartyUserId", ValueType.STRING, true, String.class, errors, " for a thirdparty recipe."); + + email = email != null ? Utils.normaliseEmail(email) : null; + + ThirdPartyLoginMethod thirdPartyLoginMethod = new ThirdPartyLoginMethod(email, thirdPartyId, thirdPartyUserId); + loginMethods.add(new LoginMethod(tenantId, recipeId, isVerified, isPrimary, timeJoinedInMSSinceEpoch, null, thirdPartyLoginMethod, null)); + } else if ("passwordless".equals(recipeId)) { + String email = parseAndValidateField(jsonLoginMethodObj, "email", ValueType.STRING, false, String.class, errors, " for a passwordless recipe."); + String phoneNumber = parseAndValidateField(jsonLoginMethodObj, "phoneNumber", ValueType.STRING, false, String.class, errors, " for a passwordless recipe."); + + email = email != null ? Utils.normaliseEmail(email) : null; + phoneNumber = Utils.normalizeIfPhoneNumber(phoneNumber); + + PasswordlessLoginMethod passwordlessLoginMethod = new PasswordlessLoginMethod(email, phoneNumber); + loginMethods.add(new LoginMethod(tenantId, recipeId, isVerified, isPrimary, timeJoinedInMSSinceEpoch, null, null, passwordlessLoginMethod)); + } else if (recipeId != null) { + errors.add("Invalid recipeId for loginMethod. Pass one of emailpassword, thirdparty or, passwordless!"); + } + } + return loginMethods; + } +} diff --git a/src/main/java/io/supertokens/bulkimport/exceptions/InvalidBulkImportDataException.java b/src/main/java/io/supertokens/bulkimport/exceptions/InvalidBulkImportDataException.java new file mode 100644 index 000000000..3fbcd8fbd --- /dev/null +++ b/src/main/java/io/supertokens/bulkimport/exceptions/InvalidBulkImportDataException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.bulkimport.exceptions; + +import java.util.List; + +public class InvalidBulkImportDataException extends Exception { + private static final long serialVersionUID = 1L; + public List errors; + + public InvalidBulkImportDataException(List errors) { + super("Data has missing or invalid fields. Please check the errors field for more details."); + this.errors = errors; + } + + public void addError(String error) { + this.errors.add(error); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/bulkimport/AddBulkImportUsers.java b/src/main/java/io/supertokens/webserver/api/bulkimport/AddBulkImportUsers.java index 0e5d3df46..524eb4dc8 100644 --- a/src/main/java/io/supertokens/webserver/api/bulkimport/AddBulkImportUsers.java +++ b/src/main/java/io/supertokens/webserver/api/bulkimport/AddBulkImportUsers.java @@ -18,17 +18,24 @@ import io.supertokens.Main; import io.supertokens.bulkimport.BulkImport; +import io.supertokens.bulkimport.BulkImportUserUtils; +import io.supertokens.bulkimport.exceptions.InvalidBulkImportDataException; +import io.supertokens.config.CoreConfig; +import io.supertokens.emailpassword.PasswordHashingUtils; +import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.pluginInterface.bulkimport.BulkImportUser; -import io.supertokens.pluginInterface.bulkimport.exceptions.InvalidBulkImportDataException; +import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod.EmailPasswordLoginMethod; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantConfig; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -68,20 +75,29 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S throw new ServletException(new WebserverAPI.BadRequestException(errorResponseJson.toString())); } - AppIdentifier appIdentifier = getTenantIdentifierFromRequest(req).toAppIdentifier(); + AppIdentifierWithStorage appIdentifierWithStorage; + try { + appIdentifierWithStorage = getAppIdentifierWithStorageFromRequestAndEnforcePublicTenant(req); + } catch (TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } JsonArray errorsJson = new JsonArray(); ArrayList usersToAdd = new ArrayList<>(); for (int i = 0; i < users.size(); i++) { try { - BulkImportUser user = new BulkImportUser(users.get(i).getAsJsonObject(), null); - usersToAdd.add(user); - + BulkImportUser user = BulkImportUserUtils.createBulkImportUserFromJSON(users.get(i).getAsJsonObject(), Utils.getUUID()); + for (BulkImportUser.LoginMethod loginMethod : user.loginMethods) { - validateTenantId(appIdentifier, loginMethod.tenantId, loginMethod.recipeId); + validateTenantId(appIdentifierWithStorage, loginMethod.tenantId, loginMethod.recipeId); + + if (loginMethod.emailPasswordLoginMethod != null) { + validatePasswordHashingAlgorithm(appIdentifierWithStorage, loginMethod.emailPasswordLoginMethod); + } } - } catch (InvalidBulkImportDataException e) { + usersToAdd.add(user); + } catch (io.supertokens.bulkimport.exceptions.InvalidBulkImportDataException e) { JsonObject errorObj = new JsonObject(); JsonArray errors = e.errors.stream() @@ -103,7 +119,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } try { - AppIdentifierWithStorage appIdentifierWithStorage = getAppIdentifierWithStorage(req); BulkImport.addUsers(appIdentifierWithStorage, usersToAdd); } catch (TenantOrAppNotFoundException | StorageQueryException e) { throw new ServletException(e); @@ -141,4 +156,15 @@ private void validateTenantId(AppIdentifier appIdentifier, String tenantId, Stri new ArrayList<>(Arrays.asList("Invalid tenantId: " + tenantId + " for " + recipeId + " recipe."))); } } + + private void validatePasswordHashingAlgorithm(AppIdentifier appIdentifier, EmailPasswordLoginMethod emailPasswordLoginMethod) throws InvalidBulkImportDataException, ServletException { + try { + CoreConfig.PASSWORD_HASHING_ALG passwordHashingAlgorithm = CoreConfig.PASSWORD_HASHING_ALG.valueOf(emailPasswordLoginMethod.hashingAlgorithm); + PasswordHashingUtils.assertSuperTokensSupportInputPasswordHashFormat(appIdentifier, main, emailPasswordLoginMethod.passwordHash, passwordHashingAlgorithm); + } catch (UnsupportedPasswordHashingFormatException | TenantOrAppNotFoundException e) { + throw new InvalidBulkImportDataException(new ArrayList<>(Arrays.asList(e.getMessage()))); + } catch (IllegalArgumentException e) { + throw new InvalidBulkImportDataException(new ArrayList<>(Arrays.asList("Invalid hashingAlgorithm for emailpassword recipe. Pass one of bcrypt, argon2 or, firebase_scrypt!"))); + } + } } diff --git a/src/main/java/io/supertokens/webserver/api/bulkimport/GetBulkImportUsers.java b/src/main/java/io/supertokens/webserver/api/bulkimport/GetBulkImportUsers.java index f349ac55b..f89fe39c3 100644 --- a/src/main/java/io/supertokens/webserver/api/bulkimport/GetBulkImportUsers.java +++ b/src/main/java/io/supertokens/webserver/api/bulkimport/GetBulkImportUsers.java @@ -17,7 +17,6 @@ package io.supertokens.webserver.api.bulkimport; import java.io.IOException; -import java.util.Arrays; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -26,7 +25,10 @@ import io.supertokens.bulkimport.BulkImport; import io.supertokens.bulkimport.BulkImportUserPaginationContainer; import io.supertokens.bulkimport.BulkImportUserPaginationToken; +import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; +import io.supertokens.pluginInterface.bulkimport.BulkImportStorage.BulkImportUserStatus; +import io.supertokens.pluginInterface.bulkimport.BulkImportUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -49,7 +51,7 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String status = InputParser.getQueryParamOrThrowError(req, "status", true); + String statusString = InputParser.getQueryParamOrThrowError(req, "status", true); String paginationToken = InputParser.getQueryParamOrThrowError(req, "paginationToken", true); Integer limit = InputParser.getIntQueryParamOrThrowError(req, "limit", true); @@ -64,29 +66,31 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se limit = BulkImport.GET_USERS_DEFAULT_LIMIT; } - if (status != null - && !Arrays.asList("NEW", "PROCESSING", "FAILED").contains(status)) { - throw new ServletException(new BadRequestException( - "Invalid value for status. Pass one of NEW, PROCESSING or, FAILED!")); + BulkImportUserStatus status = null; + if (statusString != null) { + try { + status = BulkImportUserStatus.valueOf(statusString); + } catch (IllegalArgumentException e) { + throw new ServletException(new BadRequestException("Invalid value for status. Pass one of NEW, PROCESSING, or FAILED!")); + } } AppIdentifierWithStorage appIdentifierWithStorage = null; try { - appIdentifierWithStorage = this.getAppIdentifierWithStorage(req); - } catch (TenantOrAppNotFoundException e) { + appIdentifierWithStorage = getAppIdentifierWithStorageFromRequestAndEnforcePublicTenant(req); + } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } try { - BulkImportUserPaginationContainer users = BulkImport.getUsers(appIdentifierWithStorage, limit, status, - paginationToken); + BulkImportUserPaginationContainer users = BulkImport.getUsers(appIdentifierWithStorage, limit, status, paginationToken); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); JsonArray usersJson = new JsonArray(); - for (JsonObject user : users.users) { - usersJson.add(user); + for (BulkImportUserInfo user : users.users) { + usersJson.add(user.toJsonObject()); } result.add("users", usersJson); @@ -97,7 +101,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } catch (BulkImportUserPaginationToken.InvalidTokenException e) { Logging.debug(main, null, Utils.exceptionStacktraceToString(e)); throw new ServletException(new BadRequestException("invalid pagination token")); - } catch (StorageQueryException | TenantOrAppNotFoundException e) { + } catch (StorageQueryException e) { throw new ServletException(e); } } diff --git a/src/test/java/io/supertokens/test/bulkimport/apis/AddBulkImportUsersTest.java b/src/test/java/io/supertokens/test/bulkimport/apis/AddBulkImportUsersTest.java index 06fcd7096..f96d5b485 100644 --- a/src/test/java/io/supertokens/test/bulkimport/apis/AddBulkImportUsersTest.java +++ b/src/test/java/io/supertokens/test/bulkimport/apis/AddBulkImportUsersTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.util.HashMap; import java.util.UUID; import org.junit.AfterClass; @@ -382,9 +383,55 @@ public void shouldReturn200Response() throws Exception { JsonObject request = generateUsersJson(10000); JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/bulk-import/add-users", + request, 1000, 10000, null, Utils.getCdiVersionStringLatestForTests(), null); + assertEquals("OK", response.get("status").getAsString()); + + process.kill(); + Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void shouldNormaliseFields() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject request = generateUsersJson(1); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/bulk-import/add-users", request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), null); assertEquals("OK", response.get("status").getAsString()); + JsonObject getResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/bulk-import/users", + new HashMap<>(), 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), null); + + assertEquals("OK", getResponse.get("status").getAsString()); + JsonArray bulkImportUsers = getResponse.get("users").getAsJsonArray(); + assertEquals(1, bulkImportUsers.size()); + + JsonParser parser = new JsonParser(); + JsonObject bulkImportUserJson = bulkImportUsers.get(0).getAsJsonObject(); + JsonArray loginMethods = parser.parse(bulkImportUserJson.get("rawData").getAsString()).getAsJsonObject().getAsJsonArray("loginMethods"); + + for (int i = 0; i < loginMethods.size(); i++) { + JsonObject loginMethod = loginMethods.get(i).getAsJsonObject(); + if (loginMethod.has("email")) { + assertEquals("johndoe+1@gmail.com", loginMethod.get("hashingAlgorithm").getAsString()); + } + if (loginMethod.has("phoneNumber")) { + assertEquals("919999999999", loginMethod.get("phoneNumber").getAsString()); + } + if (loginMethod.has("hashingAlgorithm")) { + assertEquals("ARGON2", loginMethod.get("hashingAlgorithm").getAsString()); + } + } + process.kill(); Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } @@ -400,9 +447,9 @@ public static JsonObject generateUsersJson(int numberOfUsers) { user.addProperty("externalUserId", UUID.randomUUID().toString()); user.add("userMetadata", parser.parse("{\"key1\":\"value1\",\"key2\":{\"key3\":\"value3\"}}")); user.add("roles", parser.parse("[\"role1\", \"role2\"]")); - user.add("totp", parser.parse("[{\"secretKey\":\"secretKey\",\"period\":0,\"skew\":0,\"deviceName\":\"deviceName\"}]")); + user.add("totp", parser.parse("[{\"secretKey\":\"secretKey\",\"period\": 30,\"skew\":1,\"deviceName\":\"deviceName\"}]")); - String email = "johndoe+" + i + "@gmail.com"; + String email = " johndoe+" + i + "@gmail.com "; JsonArray loginMethodsArray = new JsonArray(); loginMethodsArray.add(createEmailLoginMethod(email)); diff --git a/src/test/java/io/supertokens/test/bulkimport/apis/GetBulkImportUsersTest.java b/src/test/java/io/supertokens/test/bulkimport/apis/GetBulkImportUsersTest.java index 7dbba3b04..64539bee8 100644 --- a/src/test/java/io/supertokens/test/bulkimport/apis/GetBulkImportUsersTest.java +++ b/src/test/java/io/supertokens/test/bulkimport/apis/GetBulkImportUsersTest.java @@ -75,7 +75,7 @@ public void shouldReturn400Error() throws Exception { } catch (io.supertokens.test.httpRequest.HttpResponseException e) { assertEquals(400, e.statusCode); assertEquals( - "Http error. Status Code: 400. Message: Invalid value for status. Pass one of NEW, PROCESSING or, FAILED!", + "Http error. Status Code: 400. Message: Invalid value for status. Pass one of NEW, PROCESSING, or FAILED!", e.getMessage()); } @@ -150,7 +150,7 @@ public void shouldReturn200Response() throws Exception { assertEquals(1, bulkImportUsers.size()); JsonObject bulkImportUserJson = bulkImportUsers.get(0).getAsJsonObject(); bulkImportUserJson.get("status").getAsString().equals("NEW"); - bulkImportUserJson.get("raw_data").getAsString().equals(rawData); + bulkImportUserJson.get("rawData").getAsString().equals(rawData); process.kill(); Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));