Skip to content

Commit 6742957

Browse files
Move "limit unsuccessful logins" settings to core (#7645)
#### Rationale Some IT security teams think this is a very important setting to have. LabKey/internal-issues#907 #### Related Pull Request - LabKey/premiumModules#546 - LabKey/testAutomation#2979 --------- Co-authored-by: labkey-tchad <[email protected]>
1 parent 55044e5 commit 6742957

13 files changed

Lines changed: 675 additions & 105 deletions

File tree

api/src/org/labkey/api/security/AuthenticationManager.java

Lines changed: 107 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
import org.labkey.api.data.Container;
4646
import org.labkey.api.data.ContainerManager;
4747
import org.labkey.api.data.CoreSchema;
48+
import org.labkey.api.data.DbScope;
49+
import org.labkey.api.data.DbScope.Transaction;
4850
import org.labkey.api.data.Project;
4951
import org.labkey.api.data.PropertyManager;
5052
import org.labkey.api.data.PropertyManager.PropertyMap;
@@ -263,6 +265,41 @@ public static boolean isAutoCreateAccountsEnabled()
263265

264266
public static boolean isSelfServiceEmailChangesEnabled() { return getAuthSetting(SELF_SERVICE_EMAIL_CHANGES_KEY, false);}
265267

268+
public static boolean isLoginAttemptControlEnabled()
269+
{
270+
return getAuthSetting(LOGIN_ATTEMPT_ENABLED_KEY, false);
271+
}
272+
273+
public static int getLoginAttemptLimit()
274+
{
275+
return getAuthenticationProperty(LOGIN_ATTEMPT_LIMIT_KEY, 3);
276+
}
277+
278+
public static int getLoginAttemptPeriod()
279+
{
280+
return getAuthenticationProperty(LOGIN_ATTEMPT_PERIOD_KEY, 30);
281+
}
282+
283+
public static int getLoginAttemptResetTime()
284+
{
285+
return getAuthenticationProperty(LOGIN_ATTEMPT_RESET_TIME_KEY, 5);
286+
}
287+
288+
// Convenience method that returns the default value on missing or bad value
289+
private static int getAuthenticationProperty(@NotNull String key, int defaultValue)
290+
{
291+
Map<String, String> props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY);
292+
String value = props.get(key);
293+
try
294+
{
295+
return value == null ? defaultValue : Integer.parseInt(value);
296+
}
297+
catch (NumberFormatException e)
298+
{
299+
return defaultValue;
300+
}
301+
}
302+
266303
public static @NotNull String getDefaultDomain()
267304
{
268305
Map<String, String> props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY);
@@ -291,7 +328,7 @@ public static void saveAuthSetting(User user, String key, boolean value)
291328
saveAuthSetting(user, key, Boolean.toString(value), value ? "enabled" : "disabled");
292329
}
293330

294-
private static void saveAuthSetting(User user, String key, String value, String action)
331+
public static void saveAuthSetting(User user, String key, String value, String action)
295332
{
296333
WritablePropertyMap props = PropertyManager.getWritableProperties(AUTHENTICATION_CATEGORY, true);
297334
props.put(key, value);
@@ -308,6 +345,41 @@ public static void saveAuthSettings(User user, Map<String, Boolean> map)
308345
.forEach(e->saveAuthSetting(user, e.getKey(), e.getValue()));
309346
}
310347

348+
// Returns true if any setting changed
349+
public static boolean saveLoginAttemptSettings(User user, boolean enabled, int limit, int period, int resetTime)
350+
{
351+
if (limit < 1 || period < 1 || resetTime < 1)
352+
throw new IllegalArgumentException("limit, period, and resetTime values must be positive!");
353+
354+
// Use standard saveAuthSetting() methods to ensure audit logging
355+
boolean changed = false;
356+
try (Transaction t = DbScope.getLabKeyScope().beginTransaction())
357+
{
358+
if (enabled != isLoginAttemptControlEnabled())
359+
{
360+
saveAuthSetting(user, LOGIN_ATTEMPT_ENABLED_KEY, enabled);
361+
changed = true;
362+
}
363+
if (limit != getLoginAttemptLimit())
364+
{
365+
saveAuthSetting(user, LOGIN_ATTEMPT_LIMIT_KEY, String.valueOf(limit), "set to " + limit);
366+
changed = true;
367+
}
368+
if (period != getLoginAttemptPeriod())
369+
{
370+
saveAuthSetting(user, LOGIN_ATTEMPT_PERIOD_KEY, String.valueOf(period), "set to " + period);
371+
changed = true;
372+
}
373+
if (resetTime != getLoginAttemptResetTime())
374+
{
375+
saveAuthSetting(user, LOGIN_ATTEMPT_RESET_TIME_KEY, String.valueOf(resetTime), "set to " + resetTime);
376+
changed = true;
377+
}
378+
t.commit();
379+
}
380+
return changed;
381+
}
382+
311383
public static void reorderConfigurations(User user, String name, int[] rowIds)
312384
{
313385
if (null != rowIds && rowIds.length != 0)
@@ -588,7 +660,8 @@ private static void addAuthSettingAuditEvent(User user, String name, String acti
588660
return AuthenticationProviderCache.getProvider(ResetPasswordProvider.class, name);
589661
}
590662

591-
public static @Nullable DisableLoginProvider getEnabledDisableLoginProviderForUser(String id)
663+
// Return a DisableLoginProvider if it's enabled and applicable to this user
664+
public static @Nullable DisableLoginProvider getDisableLoginProviderForUser(String id)
592665
{
593666
for (DisableLoginProvider provider : AuthenticationProviderCache.getProviders(DisableLoginProvider.class))
594667
if (provider.isEnabledForUser(id))
@@ -631,19 +704,27 @@ public static boolean isAcceptOnlyFicamProviders()
631704

632705
public static void setAcceptOnlyFicamProviders(User user, boolean enable)
633706
{
634-
saveAuthSetting(user, ACCEPT_ONLY_FICAM_PROVIDERS_KEY, enable);
635-
AuthenticationConfigurationCache.clear();
707+
if (isAcceptOnlyFicamProviders() != enable)
708+
{
709+
saveAuthSetting(user, ACCEPT_ONLY_FICAM_PROVIDERS_KEY, enable);
710+
AuthenticationConfigurationCache.clear();
711+
}
636712
}
637713

638-
// Used by start-up properties
639-
private static final String AUTHENTICATION_CATEGORY = "Authentication";
714+
// Used by start-up properties and upgrade code
715+
public static final String AUTHENTICATION_CATEGORY = "Authentication";
640716

641717
public static final String SELF_REGISTRATION_KEY = "SelfRegistration";
642718
public static final String AUTO_CREATE_ACCOUNTS_KEY = "AutoCreateAccounts";
643719
public static final String DEFAULT_DOMAIN = "DefaultDomain";
644720
public static final String SELF_SERVICE_EMAIL_CHANGES_KEY = "SelfServiceEmailChanges";
645721
public static final String ACCEPT_ONLY_FICAM_PROVIDERS_KEY = "AcceptOnlyFicamProviders";
646722

723+
public static final String LOGIN_ATTEMPT_ENABLED_KEY = "LoginAttemptEnabled";
724+
public static final String LOGIN_ATTEMPT_LIMIT_KEY = "LoginAttemptLimit";
725+
public static final String LOGIN_ATTEMPT_PERIOD_KEY = "LoginAttemptPeriod";
726+
public static final String LOGIN_ATTEMPT_RESET_TIME_KEY = "LoginAttemptResetTime";
727+
647728
public enum AuthenticationSettings implements StartupProperty
648729
{
649730
SelfRegistration("Allow self sign up"),
@@ -1165,17 +1246,23 @@ public static PrimaryAuthenticationResult finalizePrimaryAuthentication(HttpServ
11651246

11661247
// limit one bad login per second averaged out over 60sec
11671248
private static final Cache<Integer, RateLimiter> addrLimiter = CacheManager.getCache(1001, TimeUnit.MINUTES.toMillis(5), "Login limiter");
1168-
private static final Cache<Integer, RateLimiter> userLimiter = CacheManager.getCache(1001, TimeUnit.MINUTES.toMillis(5), "User limiter");
11691249
private static final Cache<Integer, RateLimiter> pwdLimiter = CacheManager.getCache(1001, TimeUnit.MINUTES.toMillis(5), "Password limiter");
1170-
private static final CacheLoader<Integer, RateLimiter> addrLoader = (key, request) -> new RateLimiter("Addr limiter: " + key, new Rate(60, TimeUnit.MINUTES));
1171-
private static final CacheLoader<Integer, RateLimiter> pwdLoader = (key, request) -> new RateLimiter("Pwd limiter: " + key, new Rate(20, TimeUnit.MINUTES));
1172-
private static final CacheLoader<Integer, RateLimiter> userLoader = (key, request) -> new RateLimiter("User limiter: " + key, new Rate(20, TimeUnit.MINUTES));
1250+
private static final CacheLoader<Integer, RateLimiter> addrLoader = (key, _) -> new RateLimiter("Addr limiter: " + key, new Rate(60, TimeUnit.MINUTES));
1251+
private static final CacheLoader<Integer, RateLimiter> pwdLoader = (key, _) -> new RateLimiter("Pwd limiter: " + key, new Rate(20, TimeUnit.MINUTES));
1252+
1253+
private static final Cache<String, RateLimiter> userLimiter = CacheManager.getCache(10000, TimeUnit.MINUTES.toMillis(5), "User limiter");
1254+
private static final CacheLoader<String, RateLimiter> userLoader = (key, _) -> new RateLimiter("User limiter: " + key, new Rate(20, TimeUnit.MINUTES));
11731255

1174-
private static Integer _toKey(String s)
1256+
private static Integer getIntCacheKey(String s)
11751257
{
11761258
return null==s ? 0 : s.toLowerCase().hashCode() % 1000;
11771259
}
11781260

1261+
public static String getEmailCacheKey(String s)
1262+
{
1263+
return StringUtils.trimToEmpty(s).toLowerCase();
1264+
}
1265+
11791266
private static PrimaryAuthenticationResult _beforeAuthenticate(HttpServletRequest request, String id, String pwd)
11801267
{
11811268
if (null == id || null == pwd)
@@ -1184,10 +1271,10 @@ private static PrimaryAuthenticationResult _beforeAuthenticate(HttpServletReques
11841271
long delay = 0;
11851272

11861273
// slow down login attempts when we detect more than 20/minute bad attempts per user, password, or ip address
1187-
rl = addrLimiter.get(_toKey(request == null ? null : request.getRemoteAddr()));
1274+
rl = addrLimiter.get(getIntCacheKey(request == null ? null : request.getRemoteAddr()));
11881275
if (null != rl)
11891276
delay = Math.max(delay,rl.add(0, false));
1190-
rl = pwdLimiter.get(_toKey(pwd));
1277+
rl = pwdLimiter.get(getIntCacheKey(pwd));
11911278
if (null != rl)
11921279
delay = Math.max(delay, rl.add(0, false));
11931280

@@ -1209,15 +1296,15 @@ private static PrimaryAuthenticationResult _beforeAuthenticate(HttpServletReques
12091296

12101297
private static long getUserLoginDelay(String id) throws LoginDisabledException
12111298
{
1212-
DisableLoginProvider provider = AuthenticationManager.getEnabledDisableLoginProviderForUser(id);
1299+
DisableLoginProvider provider = AuthenticationManager.getDisableLoginProviderForUser(id);
12131300
if (provider != null)
12141301
return provider.getUserDelay(id);
12151302
return getDefaultUserLoginDelay(id);
12161303
}
12171304

12181305
private static long getDefaultUserLoginDelay(String id)
12191306
{
1220-
RateLimiter rl = userLimiter.get(_toKey(id));
1307+
RateLimiter rl = userLimiter.get(getEmailCacheKey(id));
12211308
if (null != rl)
12221309
return rl.add(0, false);
12231310
return 0;
@@ -1230,9 +1317,9 @@ private static void _afterAuthenticate(HttpServletRequest request, String id, St
12301317
if (result.getStatus() == AuthenticationStatus.BadCredentials || result.getStatus() == AuthenticationStatus.InactiveUser)
12311318
{
12321319
RateLimiter rl;
1233-
rl = addrLimiter.get(_toKey(request.getRemoteAddr()),request, addrLoader);
1320+
rl = addrLimiter.get(getIntCacheKey(request.getRemoteAddr()),request, addrLoader);
12341321
rl.add(1, false);
1235-
rl = pwdLimiter.get(_toKey(pwd),request, pwdLoader);
1322+
rl = pwdLimiter.get(getIntCacheKey(pwd),request, pwdLoader);
12361323
rl.add(1, false);
12371324

12381325
addUserLoginDelay(request, id);
@@ -1245,14 +1332,14 @@ else if (result.getStatus() == AuthenticationStatus.Success)
12451332

12461333
private static void resetModuleUserLoginDelay(String id)
12471334
{
1248-
DisableLoginProvider provider = AuthenticationManager.getEnabledDisableLoginProviderForUser(id);
1335+
DisableLoginProvider provider = AuthenticationManager.getDisableLoginProviderForUser(id);
12491336
if (provider != null)
12501337
provider.resetUserDelay(id);
12511338
}
12521339

12531340
private static void addUserLoginDelay(HttpServletRequest request, String id)
12541341
{
1255-
DisableLoginProvider provider = AuthenticationManager.getEnabledDisableLoginProviderForUser(id);
1342+
DisableLoginProvider provider = AuthenticationManager.getDisableLoginProviderForUser(id);
12561343
if (provider != null)
12571344
provider.addUserDelay(request, id, 1);
12581345
else
@@ -1261,7 +1348,7 @@ private static void addUserLoginDelay(HttpServletRequest request, String id)
12611348

12621349
private static void addDefaultUserLoginDelay(HttpServletRequest request, String id)
12631350
{
1264-
RateLimiter rl = userLimiter.get(_toKey(id),request, userLoader);
1351+
RateLimiter rl = userLimiter.get(getEmailCacheKey(id),request, userLoader);
12651352
rl.add(1, false);
12661353
}
12671354

core/module.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Name: Core
22
ModuleClass: org.labkey.core.CoreModule
3-
SchemaVersion: 26.004
3+
SchemaVersion: 26.005
44
Label: Administration and Essential Services
55
Description: The Core module provides central services such as login, \
66
security, administration, folder management, user management, \
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Migrate login attempt settings from the compliance module's property store to core authentication settings.
2+
SELECT core.executeJavaUpgradeCode('migrateLoginAttemptSettings');
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Migrate login attempt settings from the compliance module's property store to core authentication settings.
2+
EXEC core.executeJavaUpgradeCode 'migrateLoginAttemptSettings';

core/src/client/AuthenticationConfiguration/authenticationConfiguration.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
margin-bottom: 14px;
2828
}
2929

30+
.global-settings__text-row-section {
31+
margin-left: 17px;
32+
margin-top: 8px;
33+
}
34+
35+
.global-settings__text-row-section.disabled {
36+
opacity: 0.5;
37+
}
38+
3039
.global-settings__text {
3140
margin-left: 14px;
3241
}

core/src/client/components/GlobalSettings.test.tsx

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,12 @@ import { GLOBAL_SETTINGS } from '../../../test/data';
66

77
import { GlobalSettings } from './GlobalSettings';
88

9-
describe('<GlobalSettings/>', () => {
9+
describe('GlobalSettings', () => {
1010
test('Clicking a checkbox toggles the checkbox', async () => {
1111
const checkGlobalAuthBox = jest.fn();
12-
render(
13-
<GlobalSettings
14-
globalSettings={GLOBAL_SETTINGS}
15-
canEdit={true}
16-
authCount={3}
17-
onChange={checkGlobalAuthBox}
18-
/>
19-
);
12+
render(<GlobalSettings authCount={3} canEdit globalSettings={GLOBAL_SETTINGS} onChange={checkGlobalAuthBox} />);
2013

21-
// Click self registration checkbox
14+
// Click self-registration checkbox
2215
const firstCheckBox = document.querySelector('input[type="checkbox"]');
2316
await userEvent.click(firstCheckBox);
2417
expect(checkGlobalAuthBox).toHaveBeenCalled();
@@ -27,32 +20,20 @@ describe('<GlobalSettings/>', () => {
2720
test('An authCount of 1 eliminates the option to auto-create authenticated users', () => {
2821
const checkGlobalAuthBox = jest.fn();
2922
const { rerender } = render(
30-
<GlobalSettings
31-
authCount={3}
32-
canEdit={true}
33-
globalSettings={GLOBAL_SETTINGS}
34-
onChange={checkGlobalAuthBox}
35-
/>
23+
<GlobalSettings authCount={3} canEdit globalSettings={GLOBAL_SETTINGS} onChange={checkGlobalAuthBox} />
3624
);
3725

38-
expect(document.querySelectorAll('input[type="checkbox"]').length).toBe(3);
26+
expect(document.querySelectorAll('input[type="checkbox"]')).toHaveLength(4);
3927
rerender(
40-
<GlobalSettings
41-
authCount={1}
42-
canEdit={true}
43-
globalSettings={GLOBAL_SETTINGS}
44-
onChange={checkGlobalAuthBox}
45-
/>
28+
<GlobalSettings authCount={1} canEdit globalSettings={GLOBAL_SETTINGS} onChange={checkGlobalAuthBox} />
4629
);
47-
expect(document.querySelectorAll('input[type="checkbox"]').length).toBe(2);
30+
expect(document.querySelectorAll('input[type="checkbox"]')).toHaveLength(3);
4831
expect(document.querySelector('.panel-body').innerHTML).not.toMatch(/Auto-create authenticated users/);
4932
});
5033

5134
test('view-only mode', () => {
52-
render(
53-
<GlobalSettings globalSettings={GLOBAL_SETTINGS} canEdit={false} authCount={3} onChange={jest.fn()} />
54-
);
35+
render(<GlobalSettings authCount={3} canEdit={false} globalSettings={GLOBAL_SETTINGS} onChange={jest.fn()} />);
5536

56-
expect(document.querySelectorAll('input[disabled=""]')).toHaveLength(4);
37+
expect(document.querySelectorAll('input[disabled=""]')).toHaveLength(5);
5738
});
5839
});

0 commit comments

Comments
 (0)