From 4ec584601f8fcfeb0256c5aa7eafea4e2b2ef283 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 5 May 2026 10:40:25 -0700 Subject: [PATCH 1/9] Move "limit unsuccessful logins" settings to core --- .../api/security/AuthenticationManager.java | 53 +++++++++- core/module.properties | 2 +- .../postgresql/core-26.004-26.005.sql | 2 + .../sqlserver/core-26.004-26.005.sql | 2 + core/src/client/components/GlobalSettings.tsx | 99 ++++++++++++++++--- core/src/client/components/models.ts | 4 + core/src/org/labkey/core/CoreUpgradeCode.java | 44 +++++++++ .../labkey/core/login/LoginController.java | 59 ++++++++++- 8 files changed, 249 insertions(+), 16 deletions(-) create mode 100644 core/resources/schemas/dbscripts/postgresql/core-26.004-26.005.sql create mode 100644 core/resources/schemas/dbscripts/sqlserver/core-26.004-26.005.sql diff --git a/api/src/org/labkey/api/security/AuthenticationManager.java b/api/src/org/labkey/api/security/AuthenticationManager.java index 4d498671541..f03dd2f3411 100644 --- a/api/src/org/labkey/api/security/AuthenticationManager.java +++ b/api/src/org/labkey/api/security/AuthenticationManager.java @@ -263,6 +263,32 @@ public static boolean isAutoCreateAccountsEnabled() public static boolean isSelfServiceEmailChangesEnabled() { return getAuthSetting(SELF_SERVICE_EMAIL_CHANGES_KEY, false);} + public static boolean isLoginAttemptControlEnabled() + { + return getAuthSetting(LOGIN_ATTEMPT_ENABLED_KEY, false); + } + + public static int getLoginAttemptLimit() + { + Map props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY); + String value = props.get(LOGIN_ATTEMPT_LIMIT_KEY); + return value == null ? 3 : Integer.parseInt(value); + } + + public static int getLoginAttemptPeriod() + { + Map props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY); + String value = props.get(LOGIN_ATTEMPT_PERIOD_KEY); + return value == null ? 30 : Integer.parseInt(value); + } + + public static int getLoginAttemptResetTime() + { + Map props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY); + String value = props.get(LOGIN_ATTEMPT_RESET_TIME_KEY); + return value == null ? 5 : Integer.parseInt(value); + } + public static @NotNull String getDefaultDomain() { Map props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY); @@ -308,6 +334,22 @@ public static void saveAuthSettings(User user, Map map) .forEach(e->saveAuthSetting(user, e.getKey(), e.getValue())); } + public static void addLoginAttemptSettingsListener(Runnable listener) + { + _loginAttemptSettingsListeners.add(listener); + } + + public static void saveLoginAttemptSettings(User user, boolean enabled, int limit, int period, int resetTime) + { + WritablePropertyMap props = PropertyManager.getWritableProperties(AUTHENTICATION_CATEGORY, true); + props.put(LOGIN_ATTEMPT_ENABLED_KEY, String.valueOf(enabled)); + props.put(LOGIN_ATTEMPT_LIMIT_KEY, String.valueOf(limit)); + props.put(LOGIN_ATTEMPT_PERIOD_KEY, String.valueOf(period)); + props.put(LOGIN_ATTEMPT_RESET_TIME_KEY, String.valueOf(resetTime)); + props.save(); + _loginAttemptSettingsListeners.forEach(Runnable::run); + } + public static void reorderConfigurations(User user, String name, int[] rowIds) { if (null != rowIds && rowIds.length != 0) @@ -635,8 +677,8 @@ public static void setAcceptOnlyFicamProviders(User user, boolean enable) AuthenticationConfigurationCache.clear(); } - // Used by start-up properties - private static final String AUTHENTICATION_CATEGORY = "Authentication"; + // Used by start-up properties and upgrade code + public static final String AUTHENTICATION_CATEGORY = "Authentication"; public static final String SELF_REGISTRATION_KEY = "SelfRegistration"; public static final String AUTO_CREATE_ACCOUNTS_KEY = "AutoCreateAccounts"; @@ -644,6 +686,13 @@ public static void setAcceptOnlyFicamProviders(User user, boolean enable) public static final String SELF_SERVICE_EMAIL_CHANGES_KEY = "SelfServiceEmailChanges"; public static final String ACCEPT_ONLY_FICAM_PROVIDERS_KEY = "AcceptOnlyFicamProviders"; + public static final String LOGIN_ATTEMPT_ENABLED_KEY = "LoginAttemptEnabled"; + public static final String LOGIN_ATTEMPT_LIMIT_KEY = "LoginAttemptLimit"; + public static final String LOGIN_ATTEMPT_PERIOD_KEY = "LoginAttemptPeriod"; + public static final String LOGIN_ATTEMPT_RESET_TIME_KEY = "LoginAttemptResetTime"; + + private static final List _loginAttemptSettingsListeners = new CopyOnWriteArrayList<>(); + public enum AuthenticationSettings implements StartupProperty { SelfRegistration("Allow self sign up"), diff --git a/core/module.properties b/core/module.properties index 756c5aea7e0..d4c7dec2745 100644 --- a/core/module.properties +++ b/core/module.properties @@ -1,6 +1,6 @@ Name: Core ModuleClass: org.labkey.core.CoreModule -SchemaVersion: 26.004 +SchemaVersion: 26.005 Label: Administration and Essential Services Description: The Core module provides central services such as login, \ security, administration, folder management, user management, \ diff --git a/core/resources/schemas/dbscripts/postgresql/core-26.004-26.005.sql b/core/resources/schemas/dbscripts/postgresql/core-26.004-26.005.sql new file mode 100644 index 00000000000..fbeac64a91e --- /dev/null +++ b/core/resources/schemas/dbscripts/postgresql/core-26.004-26.005.sql @@ -0,0 +1,2 @@ +-- Migrate login attempt settings from the compliance module's property store to core authentication settings. +SELECT core.executeJavaUpgradeCode('migrateLoginAttemptSettings'); diff --git a/core/resources/schemas/dbscripts/sqlserver/core-26.004-26.005.sql b/core/resources/schemas/dbscripts/sqlserver/core-26.004-26.005.sql new file mode 100644 index 00000000000..5e512f7015f --- /dev/null +++ b/core/resources/schemas/dbscripts/sqlserver/core-26.004-26.005.sql @@ -0,0 +1,2 @@ +-- Migrate login attempt settings from the compliance module's property store to core authentication settings. +EXEC core.executeJavaUpgradeCode 'migrateLoginAttemptSettings'; diff --git a/core/src/client/components/GlobalSettings.tsx b/core/src/client/components/GlobalSettings.tsx index 312775ae408..1d6a7b04e17 100644 --- a/core/src/client/components/GlobalSettings.tsx +++ b/core/src/client/components/GlobalSettings.tsx @@ -9,6 +9,10 @@ interface GlobalSettingFieldData { tip: string; } +const LOGIN_ATTEMPT_LIMIT_OPTIONS = ['3', '5', '10', '100']; +const LOGIN_ATTEMPT_PERIOD_OPTIONS = ['5', '15', '30', '60']; +const LOGIN_ATTEMPT_RESET_TIME_OPTIONS = ['5', '10', '30', '60']; + const FIELD_DATA: GlobalSettingFieldData[] = [ { id: 'SelfRegistration', @@ -76,6 +80,25 @@ export const GlobalSettings: FC = memo(({ canEdit, authCount, onChange, g [onChange] ); + const onLoginAttemptEnabledChange = useCallback( + (event: ChangeEvent) => { + onChange('LoginAttemptEnabled', event.target.checked); + }, + [onChange] + ); + + const onLoginAttemptSelectChange = useCallback( + (event: ChangeEvent) => { + onChange(event.target.name, event.target.value); + }, + [onChange] + ); + + const loginAttemptEnabled = !!globalSettings?.LoginAttemptEnabled; + const loginAttemptLimit = globalSettings?.LoginAttemptLimit ?? '3'; + const loginAttemptPeriod = globalSettings?.LoginAttemptPeriod ?? '30'; + const loginAttemptResetTime = globalSettings?.LoginAttemptResetTime ?? '5'; + return (
@@ -83,18 +106,6 @@ export const GlobalSettings: FC = memo(({ canEdit, authCount, onChange, g
- {fieldData.map(data => ( - - ))} -
System Default Domain @@ -117,6 +128,70 @@ export const GlobalSettings: FC = memo(({ canEdit, authCount, onChange, g />
+ +
+ + {fieldData.map(data => ( + + ))} + +
+ +
+ +
+
+ Disable user login if + + consecutive invalid logins are attempted in a + + second period. Automatically allow users to login again after + + minutes. +
+
); diff --git a/core/src/client/components/models.ts b/core/src/client/components/models.ts index af64099db69..15bc8712a54 100644 --- a/core/src/client/components/models.ts +++ b/core/src/client/components/models.ts @@ -33,6 +33,10 @@ export interface AuthConfigProvider { export interface GlobalSettingsOptions { AutoCreateAccounts?: boolean; DefaultDomain?: string; + LoginAttemptEnabled?: boolean; + LoginAttemptLimit?: string; + LoginAttemptPeriod?: string; + LoginAttemptResetTime?: string; SelfRegistration?: boolean; SelfServiceEmailChanges?: boolean; } diff --git a/core/src/org/labkey/core/CoreUpgradeCode.java b/core/src/org/labkey/core/CoreUpgradeCode.java index 4001279f522..91f6219970b 100644 --- a/core/src/org/labkey/core/CoreUpgradeCode.java +++ b/core/src/org/labkey/core/CoreUpgradeCode.java @@ -19,8 +19,10 @@ import org.apache.logging.log4j.Logger; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.attachments.AttachmentService; +import org.labkey.api.data.ContainerManager; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DeferredUpgrade; +import org.labkey.api.data.PropertyManager; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SqlExecutor; import org.labkey.api.data.SqlSelector; @@ -28,6 +30,7 @@ import org.labkey.api.data.dialect.TestUpgradeCodeCounter; import org.labkey.api.module.ModuleContext; import org.labkey.api.module.ModuleLoader; +import org.labkey.api.security.AuthenticationManager; import org.labkey.api.security.Directive; import org.labkey.api.settings.AppProps; import org.labkey.api.util.ContextListener; @@ -36,8 +39,11 @@ import org.labkey.core.security.AllowedExternalResourceHosts; import org.labkey.core.security.AllowedExternalResourceHosts.AllowedHost; +import java.util.Map; import java.util.List; +import static org.labkey.api.settings.AbstractSettingsGroup.SITE_CONFIG_USER; + public class CoreUpgradeCode implements UpgradeCode { private static final Logger LOG = LogHelper.getLogger(CoreUpgradeCode.class, "Custom core upgrade steps"); @@ -146,4 +152,42 @@ public static void deleteOrphanedAttachments(ModuleContext context) svc.deleteOrphanedAttachments(); } } + + /** + * Called from core-26.004-26.005.sql. Migrates login attempt settings that were previously stored in a + * compliance module property group to the core authentication property category. + */ + @SuppressWarnings("unused") + @DeferredUpgrade // Make sure property schema is up-to-date before migrating settings + public static void migrateLoginAttemptSettings(ModuleContext context) + { + if (context.isNewInstall()) + return; + + String complianceCategory = "complianceSettingPropLoginAttempt"; + String keyPrefix = complianceCategory + "/"; + Map complianceProps = PropertyManager.getProperties(SITE_CONFIG_USER, ContainerManager.getRoot(), complianceCategory); + + String enabledVal = complianceProps.get(keyPrefix + "attemptEnabled"); + String limitVal = complianceProps.get(keyPrefix + "attemptLimit"); + String periodVal = complianceProps.get(keyPrefix + "attemptPeriod"); + String resetVal = complianceProps.get(keyPrefix + "resetTime"); + + if (enabledVal == null && limitVal == null && periodVal == null && resetVal == null) + return; // Nothing to migrate + + PropertyManager.WritablePropertyMap authProps = + PropertyManager.getWritableProperties(AuthenticationManager.AUTHENTICATION_CATEGORY, true); + + if (enabledVal != null) + authProps.put(AuthenticationManager.LOGIN_ATTEMPT_ENABLED_KEY, enabledVal); + if (limitVal != null) + authProps.put(AuthenticationManager.LOGIN_ATTEMPT_LIMIT_KEY, limitVal); + if (periodVal != null) + authProps.put(AuthenticationManager.LOGIN_ATTEMPT_PERIOD_KEY, periodVal); + if (resetVal != null) + authProps.put(AuthenticationManager.LOGIN_ATTEMPT_RESET_TIME_KEY, resetVal); + + authProps.save(); + } } diff --git a/core/src/org/labkey/core/login/LoginController.java b/core/src/org/labkey/core/login/LoginController.java index d9f0349a3bf..771537aa9a8 100644 --- a/core/src/org/labkey/core/login/LoginController.java +++ b/core/src/org/labkey/core/login/LoginController.java @@ -135,6 +135,10 @@ import static org.labkey.api.security.AuthenticationManager.AUTO_CREATE_ACCOUNTS_KEY; import static org.labkey.api.security.AuthenticationManager.AuthenticationStatus.Success; import static org.labkey.api.security.AuthenticationManager.DEFAULT_DOMAIN; +import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_ENABLED_KEY; +import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_LIMIT_KEY; +import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_PERIOD_KEY; +import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_RESET_TIME_KEY; import static org.labkey.api.security.AuthenticationManager.SELF_REGISTRATION_KEY; import static org.labkey.api.security.AuthenticationManager.SELF_SERVICE_EMAIL_CHANGES_KEY; @@ -2244,6 +2248,7 @@ public Object execute(SaveSettingsForm form, BindException errors) throws Except )); AuthenticationManager.setDefaultDomain(getUser(), form.getDefaultDomain()); + AuthenticationManager.saveLoginAttemptSettings(getUser(), form.isLoginAttemptEnabled(), form.getLoginAttemptLimit(), form.getLoginAttemptPeriod(), form.getLoginAttemptResetTime()); // rowId arrays will be posted only if they are dirty AuthenticationManager.reorderConfigurations(getUser(), "LDAP", form.getFormConfigurations()); @@ -2260,6 +2265,10 @@ public static class SaveSettingsForm private boolean _selfServiceEmailChanges; private boolean _autoCreateAccounts; private String _defaultDomain; + private boolean _loginAttemptEnabled; + private int _loginAttemptLimit = 3; + private int _loginAttemptPeriod = 30; + private int _loginAttemptResetTime = 5; private int[] _formConfigurations; private int[] _ssoConfigurations; private int[] _secondaryConfigurations; @@ -2308,6 +2317,50 @@ public void setDefaultDomain(String defaultDomain) _defaultDomain = defaultDomain; } + public boolean isLoginAttemptEnabled() + { + return _loginAttemptEnabled; + } + + @SuppressWarnings("unused") + public void setLoginAttemptEnabled(boolean loginAttemptEnabled) + { + _loginAttemptEnabled = loginAttemptEnabled; + } + + public int getLoginAttemptLimit() + { + return _loginAttemptLimit; + } + + @SuppressWarnings("unused") + public void setLoginAttemptLimit(int loginAttemptLimit) + { + _loginAttemptLimit = loginAttemptLimit; + } + + public int getLoginAttemptPeriod() + { + return _loginAttemptPeriod; + } + + @SuppressWarnings("unused") + public void setLoginAttemptPeriod(int loginAttemptPeriod) + { + _loginAttemptPeriod = loginAttemptPeriod; + } + + public int getLoginAttemptResetTime() + { + return _loginAttemptResetTime; + } + + @SuppressWarnings("unused") + public void setLoginAttemptResetTime(int loginAttemptResetTime) + { + _loginAttemptResetTime = loginAttemptResetTime; + } + public int[] getFormConfigurations() { return _formConfigurations; @@ -2529,7 +2582,11 @@ public ApiResponse execute(Object o, BindException errors) SELF_REGISTRATION_KEY, AuthenticationManager.isRegistrationEnabled(), SELF_SERVICE_EMAIL_CHANGES_KEY, AuthenticationManager.isSelfServiceEmailChangesEnabled(), AUTO_CREATE_ACCOUNTS_KEY, AuthenticationManager.isAutoCreateAccountsEnabled(), - DEFAULT_DOMAIN, AuthenticationManager.getDefaultDomain() + DEFAULT_DOMAIN, AuthenticationManager.getDefaultDomain(), + LOGIN_ATTEMPT_ENABLED_KEY, AuthenticationManager.isLoginAttemptControlEnabled(), + LOGIN_ATTEMPT_LIMIT_KEY, String.valueOf(AuthenticationManager.getLoginAttemptLimit()), + LOGIN_ATTEMPT_PERIOD_KEY, String.valueOf(AuthenticationManager.getLoginAttemptPeriod()), + LOGIN_ATTEMPT_RESET_TIME_KEY, String.valueOf(AuthenticationManager.getLoginAttemptResetTime()) ); // Primary providers From 41889d2a9f409b93e262f8362757498034d57bc1 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 5 May 2026 12:55:58 -0700 Subject: [PATCH 2/9] Move ComplianceDisableLoginProvider to core (as LoginAttemptDisableLoginProvider) --- core/src/org/labkey/core/CoreModule.java | 7 +- .../LoginAttemptDisableLoginProvider.java | 166 ++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 core/src/org/labkey/core/login/LoginAttemptDisableLoginProvider.java diff --git a/core/src/org/labkey/core/CoreModule.java b/core/src/org/labkey/core/CoreModule.java index 803404e97ce..4aa8e3a6c88 100644 --- a/core/src/org/labkey/core/CoreModule.java +++ b/core/src/org/labkey/core/CoreModule.java @@ -253,6 +253,7 @@ import org.labkey.core.dialect.PostgreSqlVersion; import org.labkey.core.junit.JunitController; import org.labkey.core.login.DbLoginAuthenticationProvider; +import org.labkey.core.login.LoginAttemptDisableLoginProvider; import org.labkey.core.login.DbLoginManager; import org.labkey.core.login.LoginController; import org.labkey.core.metrics.SimpleMetricsServiceImpl; @@ -446,15 +447,17 @@ protected void init() addController("notification", NotificationController.class); addController("product", ProductController.class); + WarningService.setInstance(new WarningServiceImpl()); + AuthenticationManager.registerProvider(new DbLoginAuthenticationProvider(), Priority.Low); + AuthenticationManager.registerProvider(new LoginAttemptDisableLoginProvider()); + AuthenticationManager.addLoginAttemptSettingsListener(LoginAttemptDisableLoginProvider::reloadCache); AttachmentService.setInstance(new AttachmentServiceImpl()); AnalyticsService.setInstance(new AnalyticsServiceImpl()); RhinoService.register(); CacheManager.addListener(RhinoService::clearCaches); NotificationService.setInstance(NotificationServiceImpl.getInstance()); - WarningService.setInstance(new WarningServiceImpl()); - ViewService.setInstance(ViewServiceImpl.getInstance()); OptionalFeatureService.setInstance(new OptionalFeatureServiceImpl()); ThumbnailService.setInstance(new ThumbnailServiceImpl()); diff --git a/core/src/org/labkey/core/login/LoginAttemptDisableLoginProvider.java b/core/src/org/labkey/core/login/LoginAttemptDisableLoginProvider.java new file mode 100644 index 00000000000..87654448f8e --- /dev/null +++ b/core/src/org/labkey/core/login/LoginAttemptDisableLoginProvider.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-2026 LabKey Corporation + * + * 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 org.labkey.core.login; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.labkey.api.cache.Cache; +import org.labkey.api.cache.CacheLoader; +import org.labkey.api.cache.CacheManager; +import org.labkey.api.security.AuthenticationManager; +import org.labkey.api.security.AuthenticationProvider; +import org.labkey.api.security.LoginDisabledException; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.security.ValidEmail; +import org.labkey.api.security.ValidEmail.InvalidEmailException; +import org.labkey.api.util.CountLimiter; +import org.labkey.api.util.StringUtilsLabKey; +import org.labkey.api.util.logging.LogHelper; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.concurrent.TimeUnit; + +public class LoginAttemptDisableLoginProvider implements AuthenticationProvider.DisableLoginProvider +{ + private static final Logger _log = LogHelper.getLogger(LoginAttemptDisableLoginProvider.class, "Warnings about disabled logins due to too many failures"); + + private static final String NAME = "loginAttemptDisableLogin"; + private static final String DESCRIPTION = "Disable unsuccessful login provider"; + private static final Cache userLimiter = CacheManager.getCache(CacheManager.UNLIMITED, CacheManager.DAY, "User login attempt limiter"); + + private static CacheLoader userLoader; + + static + { + reloadCache(); + } + + @NotNull + @Override + public String getName() + { + return NAME; + } + + @NotNull + @Override + public String getDescription() + { + return DESCRIPTION; + } + + private static Integer _toKey(String s) + { + return null == s ? 0 : s.toLowerCase().hashCode() % 1000; + } + + @Override + public boolean isEnabledForUser(String id) + { + User user = getUserFromEmailStr(id); + if (user == null || user.hasRootAdminPermission()) + return false; + + return AuthenticationManager.isLoginAttemptControlEnabled(); + } + + @Override + public long getUserDelay(String id) throws LoginDisabledException + { + CountLimiter rl = userLimiter.get(_toKey(id)); + if (rl != null && isLoginDisabled(rl.getLimitReachedTimeStamp())) + { + int resetTime = AuthenticationManager.getLoginAttemptResetTime(); + User user = getUserFromEmailStr(id); + String errorMessage = getUserLoginDisabledMsg(user.getEmail()); + AuthenticationManager.addAuditEvent(user, null, errorMessage); + _log.warn(errorMessage); + throw new LoginDisabledException("Your login has been disabled. Please try again in " + StringUtilsLabKey.pluralize(resetTime, "minute") + "."); + } + return 0; + } + + public boolean isLoginDisabled(long lastLimitReachedTimestamp) + { + long resetMinutes = AuthenticationManager.getLoginAttemptResetTime(); + long now = System.currentTimeMillis(); + return lastLimitReachedTimestamp > 0 && (now - lastLimitReachedTimestamp) < TimeUnit.MINUTES.toMillis(resetMinutes); + } + + @Override + public void addUserDelay(HttpServletRequest request, String id, int add) + { + CountLimiter rl = userLimiter.get(_toKey(id), request, userLoader); + rl.add(add); + } + + @Override + public void resetUserDelay(String id) + { + CountLimiter rl = userLimiter.get(_toKey(id)); + if (rl != null) + rl.reset(); + } + + public static void reloadCache() + { + if (!AuthenticationManager.isLoginAttemptControlEnabled()) + { + userLimiter.clear(); + return; + } + + long attemptSeconds = AuthenticationManager.getLoginAttemptPeriod(); + long attemptCount = AuthenticationManager.getLoginAttemptLimit(); + + userLimiter.clear(); + userLoader = (key, request) -> new CountLimiter("User login attempt limiter: " + key, TimeUnit.SECONDS.toMillis(attemptSeconds), 0, attemptCount); + } + + private User getUserFromEmailStr(String emailStr) + { + if (StringUtils.isBlank(emailStr)) + return null; + ValidEmail email = null; + + try + { + email = new ValidEmail(emailStr); + } + catch (InvalidEmailException _) + { + } + + if (null == email) + return null; + + return UserManager.getUser(email); + } + + private String getUserLoginDisabledMsg(String email) + { + int resetTime = AuthenticationManager.getLoginAttemptResetTime(); + int attemptLimit = AuthenticationManager.getLoginAttemptLimit(); + int attemptPeriod = AuthenticationManager.getLoginAttemptPeriod(); + + return email + + " disabled from login for " + StringUtilsLabKey.pluralize(resetTime, "minute") + ": " + + "incorrect password entered " + StringUtilsLabKey.pluralize(attemptLimit, "time") + " in " + + StringUtilsLabKey.pluralize(attemptPeriod, "second") + "."; + } +} From 386611fa50e8f9c6d730beef702c89127b42229e Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 5 May 2026 14:55:39 -0700 Subject: [PATCH 3/9] Alignment --- core/src/client/components/GlobalSettings.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/client/components/GlobalSettings.tsx b/core/src/client/components/GlobalSettings.tsx index 1d6a7b04e17..d74e76cb509 100644 --- a/core/src/client/components/GlobalSettings.tsx +++ b/core/src/client/components/GlobalSettings.tsx @@ -158,10 +158,7 @@ export const GlobalSettings: FC = memo(({ canEdit, authCount, onChange, g - - -
-
+
Disable user login if