diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java index 16822ec600..78a87c8fcb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java @@ -55,6 +55,7 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor { private final boolean trusted; private final boolean isPrivate; private final boolean isAuthHidden; + private final boolean isCaseInsensitive; private final String[] requiredFeatures; private final boolean isRequired; private final String[] types; @@ -69,6 +70,7 @@ public WebAuthProviderDescriptor(IConfigurationElement cfg) { this.isPrivate = CommonUtils.toBoolean(cfg.getAttribute("private")); this.isRequired = CommonUtils.toBoolean(cfg.getAttribute("required")); this.isAuthHidden = CommonUtils.toBoolean(cfg.getAttribute("authHidden")); + this.isCaseInsensitive = CommonUtils.toBoolean(cfg.getAttribute("caseInsensitive")); for (IConfigurationElement cfgElement : cfg.getChildren("configuration")) { List properties = WebAuthProviderRegistry.readProperties(cfgElement); @@ -132,6 +134,10 @@ public boolean isAuthHidden() { return isAuthHidden; } + public boolean isCaseInsensitive() { + return isCaseInsensitive; + } + public List getConfigurationParameters() { return new ArrayList<>(configurationParameters.values()); } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 48e78992a3..51c959e209 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -161,12 +161,13 @@ public AdminUserInfo createUser( if (userName.isEmpty()) { throw new DBWebException("Empty user name"); } - webSession.addInfoMessage("Create new user - " + userName); + String userId = userName.toLowerCase(); + webSession.addInfoMessage("Create new user - " + userId); try { var securityController = webSession.getAdminSecurityController(); - securityController.createUser(userName, Map.of(), enabled, authRole); - var smUser = securityController.getUserById(userName); + securityController.createUser(userId, Map.of(), enabled, authRole); + var smUser = securityController.getUserById(userId); return new AdminUserInfo(webSession, new WebUser(smUser)); } catch (Exception e) { throw new DBWebException("Error creating new user", e); diff --git a/server/bundles/io.cloudbeaver.service.security/plugin.xml b/server/bundles/io.cloudbeaver.service.security/plugin.xml index e9ef310594..e4b77aab29 100644 --- a/server/bundles/io.cloudbeaver.service.security/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.security/plugin.xml @@ -4,6 +4,7 @@ diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java index fce0269e82..60cf8809cc 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java @@ -68,7 +68,9 @@ public String validateLocalAuth(@NotNull DBRProgressMonitor monitor, throw new DBException("No user password provided"); } String clientPasswordHash = AuthPropertyEncryption.hash.encrypt(userName, clientPassword); - if (!storedPasswordHash.equals(clientPasswordHash)) { + // we also need to check a hash with lower case (CB-5833) + String clientPasswordHashLowerCase = AuthPropertyEncryption.hash.encrypt(userName.toLowerCase(), clientPassword); + if (!storedPasswordHash.equals(clientPasswordHash) && !clientPasswordHashLowerCase.equals(storedPasswordHash)) { throw new DBException("Invalid user name or password"); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 3827bfa13a..f801ae4f21 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -116,6 +116,9 @@ private boolean isSubjectExists(String subjectId) throws DBCException { /////////////////////////////////////////// // Users + /** + * Creates user. Saves user id in database in lower-case. + */ @Override public void createUser( @NotNull String userId, @@ -126,6 +129,7 @@ public void createUser( if (CommonUtils.isEmpty(userId)) { throw new DBCException("Empty user name is not allowed"); } + userId = userId.toLowerCase(); // creating new users only with lowercase if (isSubjectExists(userId)) { throw new DBCException("User or team '" + userId + "' already exists"); } @@ -140,6 +144,9 @@ public void createUser( } } + /** + * Creates user. Saves user id in database as it is. + */ public void createUser( @NotNull Connection dbCon, @NotNull String userId, @@ -910,20 +917,38 @@ private String findUserByCredentials( @NotNull WebAuthProviderDescriptor authProvider, @NotNull Map authParameters, boolean onlyActive // throws exception if user is inactive + ) throws DBException { + String userId = findUserByCredentials(authProvider, authParameters, onlyActive, false); + if (userId == null && authProvider.isCaseInsensitive()) { + // try to find user id with lower case is auth provider is case-insensitive + return findUserByCredentials(authProvider, authParameters, onlyActive, true); + } + return userId; + } + + @Nullable + private String findUserByCredentials( + @NotNull WebAuthProviderDescriptor authProvider, + @NotNull Map authParameters, + boolean onlyActive, + boolean isCaseInsensitive ) throws DBCException { - Map identCredentials = new LinkedHashMap<>(); + Map identCredentials = new LinkedHashMap<>(); String[] propNames = authParameters.keySet().toArray(new String[0]); for (AuthPropertyDescriptor prop : authProvider.getCredentialParameters(propNames)) { if (prop.isIdentifying()) { String propId = CommonUtils.toString(prop.getId()); - Object paramValue = authParameters.get(propId); - if (paramValue == null) { + if (authParameters.get(propId) == null) { throw new DBCException("Authentication parameter '" + prop.getId() + "' is missing"); } if (prop.getEncryption() == AuthPropertyEncryption.hash) { throw new DBCException("Hash encryption can't be used in identifying credentials"); } - identCredentials.put(propId, paramValue); + String paramValue = CommonUtils.toString(authParameters.get(propId)); + identCredentials.put( + propId, + isCaseInsensitive ? paramValue.toLowerCase() : paramValue + ); } } if (identCredentials.isEmpty()) { @@ -947,9 +972,9 @@ private String findUserByCredentials( try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sql.toString()))) { dbStat.setString(1, authProvider.getId()); int param = 2; - for (Map.Entry credEntry : identCredentials.entrySet()) { + for (Map.Entry credEntry : identCredentials.entrySet()) { dbStat.setString(param++, credEntry.getKey()); - dbStat.setString(param++, CommonUtils.toString(credEntry.getValue())); + dbStat.setString(param++, credEntry.getValue()); } try (ResultSet dbResult = dbStat.executeQuery()) { @@ -980,6 +1005,15 @@ private String findUserByCredentials( @Override public Map getUserCredentials(String userId, String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId); + Map creds = getUserCredentials(authProvider, userId); + if (creds.isEmpty() && authProvider.isCaseInsensitive()) { + return getUserCredentials(authProvider, userId.toLowerCase()); + } + return creds; + } + + @NotNull + private Map getUserCredentials(WebAuthProviderDescriptor authProvider, String userId) throws DBCException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("SELECT CRED_ID,CRED_VALUE FROM {table_prefix}CB_USER_CREDENTIALS\n" + @@ -990,7 +1024,6 @@ public Map getUserCredentials(String userId, String authProvider try (ResultSet dbResult = dbStat.executeQuery()) { Map credentials = new LinkedHashMap<>(); - while (dbResult.next()) { credentials.put(dbResult.getString(1), dbResult.getString(2)); } @@ -1182,6 +1215,7 @@ public void createTeam(String teamId, String name, String description, String gr if (CommonUtils.isEmpty(teamId)) { throw new DBCException("Empty team name is not allowed"); } + teamId = teamId.toLowerCase(); if (isSubjectExists(teamId)) { throw new DBCException("User or team '" + teamId + "' already exists"); } @@ -2427,13 +2461,20 @@ private String findOrCreateExternalUserByCredentials( return null; } - userId = userIdFromCredentials; + userId = authProvider.isCaseInsensitive() ? userIdFromCredentials.toLowerCase() : userIdFromCredentials; if (!isSubjectExists(userId)) { - createUser(userId, - Map.of(), - true, - resolveUserAuthRole(null, authRole) - ); + log.debug("Create user: " + userId); + try (Connection dbCon = database.openConnection()) { + createUser( + dbCon, + userId, + Map.of(), + true, + resolveUserAuthRole(null, authRole) + ); + } catch (SQLException e) { + throw new DBException("Error saving user in database", e); + } } setUserCredentials(userId, authProvider.getId(), userCredentials); } else if (userId == null) { diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java index d642b44e34..e2bebd4db8 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java @@ -16,15 +16,19 @@ */ package io.cloudbeaver.test.platform; +import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.auth.provider.rp.RPAuthProvider; import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.auth.SMAuthStatus; import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.SecurityUtils; import org.junit.Assert; import org.junit.Test; import java.util.List; import java.util.Map; +import java.util.Set; public class AuthenticationTest { private static final String GQL_OPEN_SESSION = """ @@ -39,6 +43,12 @@ mutation openSession($defaultLocale: String) { userId } }"""; + private static final String GQL_AUTH_LOGOUT = """ + query authLogoutExtended($provider: ID, $configuration: ID) { + result: authLogoutExtended(provider: $provider, configuration: $configuration) { + redirectLinks + } + }"""; @Test public void testLoginUser() throws Exception { @@ -47,6 +57,34 @@ public void testLoginUser() throws Exception { Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); } + + @Test + public void testLoginUserWithCamelCase() throws Exception { + WebGQLClient client = CEServerTestSuite.createClient(); + for (String userId : Set.of("Test", "tESt", "tesT", "TEST")) { + Map credsWithCamelCase = getUserCredentials(userId); + // authenticating with user + Map authInfo = CEServerTestSuite.authenticateTestUser(client, credsWithCamelCase); + Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + Map activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertEquals(userId.toLowerCase(), JSONUtils.getString(activeUser, "userId")); + // making logout + client.sendQuery(GQL_AUTH_LOGOUT, Map.of("provider", "local")); + + activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertNotEquals(userId.toLowerCase(), JSONUtils.getString(activeUser, "userId")); + } + } + + @NotNull + private Map getUserCredentials(@NotNull String userId) throws Exception { + return Map.of( + LocalAuthProvider.CRED_USER, userId, + LocalAuthProvider.CRED_PASSWORD, SecurityUtils.makeDigest("test") + ); + } + + @Test public void testReverseProxyAnonymousModeLogin() throws Exception { WebGQLClient client = CEServerTestSuite.createClient(); diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index 3ca615b5ea..b133f1baa5 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -101,11 +101,18 @@ public static WebGQLClient createClient(@NotNull HttpClient httpClient) { } public static Map authenticateTestUser(@NotNull WebGQLClient client) throws Exception { + return authenticateTestUser(client, TEST_CREDENTIALS); + } + + public static Map authenticateTestUser( + @NotNull WebGQLClient client, + @NotNull Map credentials + ) throws Exception { return client.sendQuery( WebGQLClient.GQL_AUTHENTICATE, Map.of( "provider", LocalAuthProvider.PROVIDER_ID, - "credentials", TEST_CREDENTIALS + "credentials", credentials ) ); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx index ac13ee727f..c5136b7564 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx @@ -54,7 +54,16 @@ export const UserFormInfoCredentials = observer(function UserFormInfoCred return ( {translate('authentication_user_credentials')} - + {translate('authentication_user_name')} {local && ( diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts index 19b4cb1663..240cd8e90b 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts @@ -104,6 +104,8 @@ export class UserFormInfoPart extends FormPart