Skip to content

Commit

Permalink
feat: userId mapping fixes (#489)
Browse files Browse the repository at this point in the history
* delete and get user apis now work with userIdMapping

* updates emailpassword apis to use userIdMapping

* adds remaining userIdMapping changes to thirdParty and passwordless APIs

* implements more feedback

* adds more tests implement more feedback

* adds userPagination test with userIdMapping

* API fixes

* adds passwordResetFlow test

* finishes EmailPassword tests

* starts ThirdPartAPI tests

* adds some thirdParty tests

* implements feedback for delete API

* implements some changes

* adds test for new EmailVerification function

* adds checks to deleteUserIdMapping function

* updates Create UserIdMapping and DelteUserIdMapping API to have optional force option

* implements feedback changes

* implements changes to deleteUser

* adds additional check in createUserIdMapping function

* adds get user tests for ThirdParty recipe

* fixes due to plugin interface change

* adds deleteUser tests for UserIdMapping

* implements more feedback

* adds comments explaining changes to deleteUser queries

* implements more feedback

* updates tests to now ignore jwt recipe checking for create and deleteUserIdMapping tests

* comment fixes

* cleans up code

* Update src/main/java/io/supertokens/authRecipe/AuthRecipe.java

* adds more tests

* removes unnecessary static function

* implements feedback

* implements more feedback

* fixes

* comment fixes

* adds more passwordless tests

* bug fix, and test update

* updates deleteUserIdMapping behavior

* fixes

* test fixes

* test fixes

* fixes

* adds userIdMapping changes

* adds test for deleteingUserIdMapping with force

* adds tests for intermediate state in deleteAPI

* adds forbidden state tests

* adds more tests

* removes unused import

* fixes bug in auth getUserAPIs

* adds additional test

* removes unnecessary checks

* updates version information

* gst

* fixes

* fixes

* fixes

Co-authored-by: Rishabh Poddar <[email protected]>
  • Loading branch information
jscyo and rishabhpoddar authored Aug 10, 2022
1 parent 3843c38 commit 451986a
Show file tree
Hide file tree
Showing 35 changed files with 2,254 additions and 48 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [unreleased]

## [3.15.1] - 2022-08-10

- Updates UserIdMapping recipe to resolve UserId Mappings for Auth recipes in the core itself

## [3.15.0] - 2022-07-25

- Adds UserIdMapping recipe
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}

version = "3.15.0"
version = "3.15.1"


repositories {
Expand Down
2 changes: 1 addition & 1 deletion pluginInterfaceSupported.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "contains a list of plugin interfaces branch names that this core supports",
"versions": [
"2.16"
"2.17"
]
}
35 changes: 35 additions & 0 deletions src/main/java/io/supertokens/authRecipe/AuthRecipe.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.useridmapping.UserIdType;

import javax.annotation.Nullable;

/*This files contains functions that are common for all auth recipes*/

public class AuthRecipe {

public static final int USER_PAGINATION_LIMIT = 500;

public static long getUsersCount(Main main, RECIPE_ID[] includeRecipeIds) throws StorageQueryException {
return StorageLayer.getAuthRecipeStorage(main).getUsersCount(includeRecipeIds);
}
Expand Down Expand Up @@ -65,12 +69,43 @@ public static void deleteUser(Main main, String userId) throws StorageQueryExcep
// - session: the session will expire anyway
// - email verification: email verification tokens can be created for any userId anyway

// If userId mapping exists then delete entries with superTokensUserId from auth related tables and
// externalUserid from non-auth tables
UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(main, userId,
UserIdType.ANY);
if (userIdMapping != null) {
// We check if the mapped externalId is another SuperTokens UserId, this could come up when migrating
// recipes.
// in reference to
// https://docs.google.com/spreadsheets/d/17hYV32B0aDCeLnSxbZhfRN2Y9b0LC2xUF44vV88RNAA/edit?usp=sharing
// we want to check which state the db is in
if (StorageLayer.getAuthRecipeStorage(main).doesUserIdExist(userIdMapping.externalUserId)) {
// db is in state A4
// delete only from auth tables
deleteAuthRecipeUser(main, userId);
} else {
// db is in state A3
// delete user from non-auth tables with externalUserId
deleteNonAuthRecipeUser(main, userIdMapping.externalUserId);
// delete user from auth tables with superTokensUserId
deleteAuthRecipeUser(main, userIdMapping.superTokensUserId);
}
} else {
deleteNonAuthRecipeUser(main, userId);
deleteAuthRecipeUser(main, userId);
}

}

private static void deleteNonAuthRecipeUser(Main main, String userId) throws StorageQueryException {
// non auth recipe deletion
StorageLayer.getUserMetadataStorage(main).deleteUserMetadata(userId);
StorageLayer.getSessionStorage(main).deleteSessionsOfUser(userId);
StorageLayer.getEmailVerificationStorage(main).deleteEmailVerificationUserInfo(userId);
StorageLayer.getUserRolesStorage(main).deleteAllRolesForUser(userId);
}

private static void deleteAuthRecipeUser(Main main, String userId) throws StorageQueryException {
// auth recipe deletions here only
StorageLayer.getEmailPasswordStorage(main).deleteEmailPasswordUser(userId);
StorageLayer.getThirdPartyStorage(main).deleteThirdPartyUser(userId);
Expand Down
97 changes: 97 additions & 0 deletions src/main/java/io/supertokens/inmemorydb/Start.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import io.supertokens.Main;
import io.supertokens.ProcessState;
import io.supertokens.ResourceDistributor;
import io.supertokens.emailpassword.EmailPassword;
import io.supertokens.emailverification.EmailVerification;
import io.supertokens.emailverification.exception.EmailAlreadyVerifiedException;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.inmemorydb.queries.*;
import io.supertokens.pluginInterface.KeyValueInfo;
Expand All @@ -34,20 +37,24 @@
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage;
import io.supertokens.pluginInterface.emailverification.EmailVerificationStorage;
import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo;
import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException;
import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage;
import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.jwt.JWTRecipeStorage;
import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo;
import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException;
import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage;
import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage;
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
import io.supertokens.pluginInterface.passwordless.exception.*;
import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage;
import io.supertokens.pluginInterface.session.SessionInfo;
import io.supertokens.pluginInterface.session.SessionStorage;
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException;
Expand All @@ -56,17 +63,28 @@
import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage;
import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException;
import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException;
import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage;
import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage;
import io.supertokens.pluginInterface.userroles.UserRolesStorage;
import io.supertokens.pluginInterface.userroles.exception.DuplicateUserRoleMappingException;
import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException;
import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage;
import io.supertokens.session.Session;
import io.supertokens.usermetadata.UserMetadata;
import io.supertokens.userroles.UserRoles;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLTransactionRollbackException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

public class Start
Expand Down Expand Up @@ -1524,4 +1542,83 @@ public boolean updateOrDeleteExternalUserIdInfo(String userId, boolean isSuperTo
}

}

@Override
public HashMap<String, String> getUserIdMappingForSuperTokensIds(ArrayList<String> userIds)
throws StorageQueryException {
try {
return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public boolean isUserIdBeingUsedInNonAuthRecipe(String className, String userId) throws StorageQueryException {

// check if the input userId is being used in nonAuthRecipes.
if (className.equals(SessionStorage.class.getName())) {
String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(userId);
return sessionHandlesForUser.length > 0;
} else if (className.equals(UserRolesStorage.class.getName())) {
String[] roles = getRolesForUser(userId);
return roles.length > 0;
} else if (className.equals(UserMetadataStorage.class.getName())) {
JsonObject userMetadata = getUserMetadata(userId);
return userMetadata != null;
} else if (className.equals(EmailVerificationStorage.class.getName())) {
try {
return EmailVerificationQueries.isUserIdBeingUsedForEmailVerification(this, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
} else if (className.equals(JWTRecipeStorage.class.getName())) {
return false;
} else {
throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage");
}
}

@TestOnly
@Override
public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId) throws StorageQueryException {
// add entries to nonAuthRecipe tables with input userId
if (className.equals(SessionStorage.class.getName())) {
try {
Session.createNewSession(this.main, userId, new JsonObject(), new JsonObject());
} catch (Exception e) {
throw new StorageQueryException(e);
}
} else if (className.equals(UserRolesStorage.class.getName())) {
try {
String role = "testRole";
UserRoles.createNewRoleOrModifyItsPermissions(this.main, role, null);
UserRoles.addRoleToUser(this.main, userId, role);
} catch (StorageTransactionLogicException e) {
throw new StorageQueryException(e.actualException);
} catch (UnknownRoleException e) {
throw new StorageQueryException(e);
}
} else if (className.equals(EmailVerificationStorage.class.getName())) {
try {
EmailVerification.generateEmailVerificationToken(this.main, userId, "[email protected]");
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new StorageQueryException(e);
} catch (EmailAlreadyVerifiedException e) {
/* do nothing cause the userId already exists in the table */
}
} else if (className.equals(UserMetadataStorage.class.getName())) {
JsonObject data = new JsonObject();
data.addProperty("test", "testData");
try {
UserMetadata.updateUserMetadata(this.main, userId, data);
} catch (StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
} else if (className.equals(JWTRecipeStorage.class.getName())) {
/* Since JWT recipe tables do not store userId we do not add any data to them */
} else {
throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ public static void deleteUser(Start start, String userId)
pst.setString(2, EMAIL_PASSWORD.toString());
});
}
// Since SQLite does not enforce foreign key constraints we have to manually delete the mapping for the
// user.
{
String QUERY = "DELETE FROM " + getConfig(start).getUserIdMappingTable()
+ " WHERE supertokens_user_id = ?";

update(sqlCon, QUERY, pst -> {
pst.setString(1, userId);
});
}

{
String QUERY = "DELETE FROM " + getConfig(start).getEmailPasswordUsersTable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ public static boolean isEmailVerified(Start start, String userId, String email)
}, ResultSet::next);
}

public static boolean isUserIdBeingUsedForEmailVerification(Start start, String userId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTokensTable() + " WHERE user_id = ?";

return execute(start, QUERY, pst -> {
pst.setString(1, userId);
}, ResultSet::next);
}

public static void unverifyEmail(Start start, String userId, String email)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,17 @@ public static void deleteUser(Start start, String userId)
});
}

// Since SQLite does not enforce foreign key constraints we have to manually delete the mapping for the
// user.
{
String QUERY = "DELETE FROM " + getConfig(start).getUserIdMappingTable()
+ " WHERE supertokens_user_id = ?";

update(sqlCon, QUERY, pst -> {
pst.setString(1, userId);
});
}

// Even if the user is changed after we read it here (which is unlikely),
// we'd only leave devices that will be cleaned up later automatically when they expire.
UserInfo user = getUserById(start, userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ public static void deleteUser(Start start, String userId)
pst.setString(2, THIRD_PARTY.toString());
});
}
// Since SQLite does not enforce foreign key constraints we have to manually delete the mapping for the
// user.
{
String QUERY = "DELETE FROM " + getConfig(start).getUserIdMappingTable()
+ " WHERE supertokens_user_id = ?";

update(sqlCon, QUERY, pst -> {
pst.setString(1, userId);
});
}

{
String QUERY = "DELETE FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id = ? ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;

import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
Expand Down Expand Up @@ -149,6 +150,35 @@ public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start s
return rowUpdated > 0;
}

public static HashMap<String, String> getUserIdMappingWithUserIds(Start start, ArrayList<String> userIds)
throws SQLException, StorageQueryException {

StringBuilder QUERY = new StringBuilder(
"SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN (");
for (int i = 0; i < userIds.size(); i++) {
QUERY.append("?");
if (i != userIds.size() - 1) {
// not the last element
QUERY.append(",");
}
}
QUERY.append(")");

return execute(start, QUERY.toString(), pst -> {
for (int i = 0; i < userIds.size(); i++) {
// i+1 cause this starts with 1 and not 0
pst.setString(i + 1, userIds.get(i));
}
}, result -> {
HashMap<String, String> userIdMappings = new HashMap<>();
while (result.next()) {
UserIdMapping temp = UserIdMappingRowMapper.getInstance().mapOrThrow(result);
userIdMappings.put(temp.superTokensUserId, temp.externalUserId);
}
return userIdMappings;
});
}

private static class UserIdMappingRowMapper implements RowMapper<UserIdMapping, ResultSet> {
private static final UserIdMappingRowMapper INSTANCE = new UserIdMappingRowMapper();

Expand Down
Loading

0 comments on commit 451986a

Please sign in to comment.