Skip to content

Commit

Permalink
Merge branch 'devel' into CB-3952-navigator-forbidden-symbols-can-be-…
Browse files Browse the repository at this point in the history
…used-for-rename-folder
  • Loading branch information
dariamarutkina authored Jan 3, 2024
2 parents 86bc82f + 41861ef commit 534c465
Show file tree
Hide file tree
Showing 48 changed files with 642 additions and 92 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"request": "launch",
"mainClass": "org.eclipse.equinox.launcher.Main",
"classPaths": [
"${workspaceFolder}/../eclipse/eclipse/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar"
"${workspaceFolder}/../eclipse/eclipse/plugins/org.eclipse.equinox.launcher_1.6.600.v20231106-1826.jar"
],
"args": [
"-product",
Expand Down Expand Up @@ -65,7 +65,7 @@
"request": "launch",
"mainClass": "org.eclipse.equinox.launcher.Main",
"classPaths": [
"${workspaceFolder}/../eclipse/Eclipse.app/Contents/Eclipse/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar"
"${workspaceFolder}/../eclipse/Eclipse.app/Contents/Eclipse/plugins/org.eclipse.equinox.launcher_1.6.600.v20231106-1826.jar"
],
"args": [
"-product",
Expand Down
7 changes: 7 additions & 0 deletions config/sample-databases/DefaultConfiguration/cloudbeaver.conf
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@

enableSecurityManager: false,

sm: {
enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:true}",
maxFailedLogin: "${CLOUDBEAVER_MAX_FAILED_LOGINS:10}",
minimumLoginTimeout: "${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}",
blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}"
},

database: {
driver="h2_embedded_v2",
url: "jdbc:h2:${workspace}/.data/cb.h2v2.dat",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* 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.cloudbeaver.auth;

import org.jkiss.code.NotNull;

import java.util.Map;

public interface SMBruteForceProtected {
Object getInputUsername(@NotNull Map<String, Object> cred);
}
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,8 @@ protected Gson getGson() {

protected abstract GsonBuilder getGsonBuilder();

@Override
public boolean isEnvironmentVariablesAccessible() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT
SESSION_TYPE VARCHAR(64) NOT NULL,
APP_SESSION_STATE TEXT NOT NULL,
IS_MAIN_AUTH CHAR(1) DEFAULT 'Y' NOT NULL,
AUTH_USERNAME VARCHAR(128) NULL,
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,

PRIMARY KEY (AUTH_ID),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT
ADD COLUMN AUTH_USERNAME VARCHAR(128) NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package io.cloudbeaver.auth.provider.local;

import io.cloudbeaver.auth.SMBruteForceProtected;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebAuthProviderRegistry;
Expand All @@ -35,7 +36,7 @@
/**
* Local auth provider
*/
public class LocalAuthProvider implements SMAuthProvider<LocalAuthSession> {
public class LocalAuthProvider implements SMAuthProvider<LocalAuthSession>, SMBruteForceProtected {

public static final String PROVIDER_ID = LocalAuthProviderConstants.PROVIDER_ID;
public static final String CRED_USER = LocalAuthProviderConstants.CRED_USER;
Expand Down Expand Up @@ -126,4 +127,8 @@ public static boolean changeUserPassword(@NotNull WebSession webSession, @NotNul
return true;
}

@Override
public Object getInputUsername(@NotNull Map<String, Object> cred) {
return cred.get("user");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@
*/
package io.cloudbeaver.service.security;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import io.cloudbeaver.auth.SMAuthProviderAssigner;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.auth.SMAuthProviderFederated;
import io.cloudbeaver.auth.SMAutoAssign;
import io.cloudbeaver.auth.*;
import io.cloudbeaver.model.app.WebAppConfiguration;
import io.cloudbeaver.model.app.WebAuthApplication;
import io.cloudbeaver.model.app.WebAuthConfiguration;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebAuthProviderRegistry;
import io.cloudbeaver.registry.WebMetaParametersRegistry;
import io.cloudbeaver.service.security.bruteforce.BruteForceUtils;
import io.cloudbeaver.service.security.bruteforce.UserLoginRecord;
import io.cloudbeaver.service.security.db.CBDatabase;
import io.cloudbeaver.service.security.internal.AuthAttemptSessionInfo;
import io.cloudbeaver.service.security.internal.SMTokenInfo;
Expand Down Expand Up @@ -348,7 +346,7 @@ public SMUser getCurrentUser() throws DBException {
public int countUsers(@NotNull SMUserFilter filter) throws DBCException {
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(
"SELECT COUNT(*) FROM {table_prefix}CB_USER" + buildUsersFilter(filter)))) {
"SELECT COUNT(*) FROM {table_prefix}CB_USER" + buildUsersFilter(filter)))) {
setUsersFilterValues(dbStat, filter, 1);
try (ResultSet dbResult = dbStat.executeQuery()) {
if (dbResult.next()) {
Expand Down Expand Up @@ -484,7 +482,7 @@ private void readSubjectMetas(Connection dbCon, SMSubject subject) throws SQLExc
}

private void readSubjectsMetas(Connection dbCon, SMSubjectType subjectType, String userIdMask,
Map<String, ? extends SMSubject> result) throws SQLException {
Map<String, ? extends SMSubject> result) throws SQLException {
// Read metas
try (PreparedStatement dbStat = dbCon.prepareStatement(
database.normalizeTableNames("SELECT m.SUBJECT_ID,m.META_ID,m.META_VALUE FROM {table_prefix}CB_AUTH_SUBJECT s, " +
Expand Down Expand Up @@ -921,8 +919,8 @@ public SMTeam[] readAllTeams() throws DBCException {
}
try (ResultSet dbResult = dbStat.executeQuery(
database.normalizeTableNames("SELECT SUBJECT_ID,PERMISSION_ID\n" +
"FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_TEAM R\n" +
"WHERE AP.SUBJECT_ID=R.TEAM_ID\n"))) {
"FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_TEAM R\n" +
"WHERE AP.SUBJECT_ID=R.TEAM_ID\n"))) {
while (dbResult.next()) {
SMTeam team = teams.get(dbResult.getString(1));
if (team != null) {
Expand Down Expand Up @@ -1303,21 +1301,36 @@ public SMAuthInfo authenticate(
? null
: application.getAuthConfiguration().getAuthProviderConfiguration(authProviderConfigurationId);

if (SMAuthProviderExternal.class.isAssignableFrom(authProviderInstance.getClass())) {
var authProviderExternal = (SMAuthProviderExternal<?>) authProviderInstance;
securedUserIdentifyingCredentials = authProviderExternal.authExternalUser(
authProgressMonitor,
providerConfig,
userCredentials
);
}

var filteredUserCreds = filterSecuredUserData(
securedUserIdentifyingCredentials,
authProviderDescriptor
);
String authAttemptId;
if (SMAuthProviderExternal.class.isAssignableFrom(authProviderInstance.getClass())) {
var authProviderExternal = (SMAuthProviderExternal<?>) authProviderInstance;
try {
securedUserIdentifyingCredentials = authProviderExternal.authExternalUser(
authProgressMonitor,
providerConfig,
userCredentials
);
} catch (DBException e) {
createNewAuthAttempt(
SMAuthStatus.ERROR,
authProviderId,
authProviderConfigurationId,
filteredUserCreds,
appSessionId,
previousSmSessionId,
sessionType,
sessionParameters,
isMainSession
);
throw e;
}
}

var authAttemptId = createNewAuthAttempt(
authAttemptId = createNewAuthAttempt(
SMAuthStatus.IN_PROGRESS,
authProviderId,
authProviderConfigurationId,
Expand Down Expand Up @@ -1386,12 +1399,17 @@ private String createNewAuthAttempt(
String authAttemptId = UUID.randomUUID().toString();
try (Connection dbCon = database.openConnection()) {
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
if (smConfig.isCheckBruteforce()
&& this.getAuthProvider(authProviderId).getInstance() instanceof SMBruteForceProtected bruteforceProtected) {
BruteForceUtils.checkBruteforce(smConfig,
getLatestUserLogins(dbCon, authProviderId, bruteforceProtected.getInputUsername(authData).toString()));
}
try (PreparedStatement dbStat = dbCon.prepareStatement(
database.normalizeTableNames(
"INSERT INTO {table_prefix}CB_AUTH_ATTEMPT" +
"(AUTH_ID,AUTH_STATUS,APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE," +
"SESSION_ID,IS_MAIN_AUTH) " +
"VALUES(?,?,?,?,?,?,?)"
"SESSION_ID,IS_MAIN_AUTH, AUTH_USERNAME) " +
"VALUES(?,?,?,?,?,?,?,?)"
)
)) {
dbStat.setString(1, authAttemptId);
Expand All @@ -1405,6 +1423,11 @@ private String createNewAuthAttempt(
dbStat.setNull(6, Types.VARCHAR);
}
dbStat.setString(7, isMainSession ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE);
if (this.getAuthProvider(authProviderId).getInstance() instanceof SMBruteForceProtected bruteforceProtected) {
dbStat.setString(8, bruteforceProtected.getInputUsername(authData).toString());
} else {
dbStat.setString(8, null);
}
dbStat.execute();
}

Expand All @@ -1429,6 +1452,37 @@ private String createNewAuthAttempt(
}
}

private List<UserLoginRecord> getLatestUserLogins(Connection dbCon, String authProviderId, String inputLogin) throws SQLException {
List<UserLoginRecord> userLoginRecords = new ArrayList<>();
try (PreparedStatement dbStat = dbCon.prepareStatement(
database.normalizeTableNames(
"SELECT" +
" attempt.AUTH_STATUS," +
" attempt.CREATE_TIME" +
" FROM" +
" {table_prefix}CB_AUTH_ATTEMPT attempt" +
" JOIN" +
" {table_prefix}CB_AUTH_ATTEMPT_INFO info ON attempt.AUTH_ID = info.AUTH_ID" +
" WHERE AUTH_PROVIDER_ID = ? AND AUTH_USERNAME = ?" +
" ORDER BY attempt.CREATE_TIME DESC " +
database.getDialect().getOffsetLimitQueryPart(0, smConfig.getMaxFailedLogin())
)
)) {
dbStat.setString(1, authProviderId);
dbStat.setString(2, inputLogin);
try (ResultSet dbResult = dbStat.executeQuery()) {
while (dbResult.next()) {
UserLoginRecord loginDto = new UserLoginRecord(
SMAuthStatus.valueOf(dbResult.getString(1)),
dbResult.getTimestamp(2).toLocalDateTime()
);
userLoginRecords.add(loginDto);
}
}
}
return userLoginRecords;
}

private boolean isSmSessionNotExpired(String prevSessionId) {
//TODO: implement after we start tracking user logout
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ public class SMControllerConfiguration {
private int refreshTokenTtl = DEFAULT_REFRESH_TOKEN_TTL;
private int expiredAuthAttemptInfoTtl = DEFAULT_EXPIRED_AUTH_ATTEMPT_INFO_TTL;

private boolean enableBruteForceProtection = true;

//in seconds
public static final int DEFAULT_MAX_FAILED_LOGIN = 10;
public static final int DEFAULT_MINIMUM_LOGIN_TIMEOUT = 1; //1sec
public static final int DEFAULT_BLOCK_LOGIN_PERIOD = 300; //5min
private int maxFailedLogin = DEFAULT_MAX_FAILED_LOGIN;
private int minimumLoginTimeout = DEFAULT_MINIMUM_LOGIN_TIMEOUT;
private int blockLoginPeriod = DEFAULT_BLOCK_LOGIN_PERIOD;

public int getAccessTokenTtl() {
return accessTokenTtl;
}
Expand All @@ -50,4 +60,36 @@ public int getExpiredAuthAttemptInfoTtl() {
public void setExpiredAuthAttemptInfoTtl(int expiredAuthAttemptInfoTtl) {
this.expiredAuthAttemptInfoTtl = expiredAuthAttemptInfoTtl;
}

public void setCheckBruteforce(boolean checkBruteforce) {
this.enableBruteForceProtection = checkBruteforce;
}

public boolean isCheckBruteforce() {
return enableBruteForceProtection;
}

public int getMaxFailedLogin() {
return maxFailedLogin;
}

public int getMinimumLoginTimeout() {
return minimumLoginTimeout;
}

public int getBlockLoginPeriod() {
return blockLoginPeriod;
}

public void setMaxFailedLogin(int maxFailed) {
this.maxFailedLogin = maxFailed;
}

public void setMinimumLoginTimeout(int minimumTimeout) {
this.minimumLoginTimeout = minimumTimeout;
}

public void setBlockLoginPeriod(int blockPeriod) {
this.blockLoginPeriod = blockPeriod;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* 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.cloudbeaver.service.security.bruteforce;

import io.cloudbeaver.service.security.SMControllerConfiguration;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.auth.SMAuthStatus;
import org.jkiss.dbeaver.model.security.exception.SMException;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;

public class BruteForceUtils {

private static final Log log = Log.getLog(BruteForceUtils.class);

public static void checkBruteforce(SMControllerConfiguration smConfig, List<UserLoginRecord> latestLogins) throws DBException {
if (latestLogins.isEmpty()) {
return;
}

var latestLogin = latestLogins.get(0);
checkLoginInterval(latestLogin.time(), smConfig.getMinimumLoginTimeout());

long errorsCount = latestLogins.stream()
.filter(authAttemptSessionInfo -> authAttemptSessionInfo.smAuthStatus() == SMAuthStatus.ERROR).count();

boolean shouldBlock = errorsCount >= smConfig.getMaxFailedLogin();
if (shouldBlock) {
int blockPeriod = smConfig.getBlockLoginPeriod();
LocalDateTime unblockTime = latestLogin.time().plusSeconds(blockPeriod);

LocalDateTime now = LocalDateTime.now();
shouldBlock = unblockTime.isAfter(now);

if (shouldBlock) {
log.error("User login is blocked due to exceeding the limit of incorrect password entry");
Duration lockDuration = Duration.ofSeconds(smConfig.getBlockLoginPeriod());

throw new SMException("User blocked for " +
lockDuration.minus(Duration.between(latestLogin.time(), now)).getSeconds() + " seconds");
}
}
}

private static void checkLoginInterval(LocalDateTime createTime, int timeout) throws DBException {
if (createTime != null && Duration.between(createTime, LocalDateTime.now()).getSeconds() < timeout) {
throw new DBException("You are trying to log in too fast");
}
}
}
Loading

0 comments on commit 534c465

Please sign in to comment.