Skip to content

Commit d7c0ae5

Browse files
committed
v0.3.3 update
when configuring an authenticator app, the user is required to enter a valid code before changes are saved various minor frontend improvements (e.g, removed suggestions to use email-to-text, included link to 2FAS authenticator) when a user is deleted in WebCTRL, correponding MFA mappings are also deleted when MFA mappings are deleted, corresponding cached IP addresses are also deleted
1 parent 6a0da2f commit d7c0ae5

File tree

13 files changed

+343
-80
lines changed

13 files changed

+343
-80
lines changed

config/BUILD_DETAILS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ commonbaseutils-2.0.5
2222
commonexceptions-9.0.002
2323
core-9.0.002
2424
core-api-9.0.002
25+
datatable-9.0.002
2526
extensionsupport-api-9.0.002
2627
javax.mail-1.5.6
2728
webaccess-api-9.0.002

config/RUNTIME_DEPS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ file:commonexceptions:modules\commonexceptions
55
file:commonbaseutils:bin\lib
66
file:webaccess-api:modules\webaccess
77
file:extensionsupport-api:modules\extensionsupport
8-
file:javax.mail:bin\lib
8+
file:javax.mail:bin\lib
9+
file:datatable:modules\datatable

root/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<extension version="1">
22
<name>MFA</name>
33
<description>Provides MFA for WebCTRL logins through an authenticator app or emailed security codes.</description>
4-
<version>0.3.2</version>
4+
<version>0.3.3</version>
55
<vendor>Automatic Controls Equipment Systems, Inc.</vendor>
66
<web-operator-provider>aces.webctrl.mfa.core.MFAProvider</web-operator-provider>
77
<system-menu-provider>aces.webctrl.mfa.web.SystemMenuEditor</system-menu-provider>

root/webapp/main.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ button:disabled {
6262
.buttonCopy:hover {
6363
color:darkgoldenrod;
6464
}
65-
a {
65+
a:not(.regularLink) {
6666
border:1px solid white;
6767
border-radius: 8px;
6868
padding-left:6px;

src/aces/webctrl/mfa/core/Config.java

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.util.*;
88
import java.util.concurrent.*;
99
import java.util.concurrent.locks.*;
10+
import java.util.function.Predicate;
11+
import java.util.regex.*;
1012
import java.nio.*;
1113
import java.nio.file.*;
1214
import java.nio.channels.*;
@@ -90,6 +92,47 @@ public static void init(Path mainFile, Path urlFile, Path cookieFile){
9092
Config.cookieFile = cookieFile;
9193
loadData();
9294
}
95+
public static boolean deleteUnused(Set<String> users){
96+
final Predicate<String> pred = new Predicate<>(){
97+
@Override public boolean test(String o){
98+
return !users.contains(o);
99+
}
100+
};
101+
final HashSet<String> mfa = new HashSet<String>((int)Math.ceil(users.size()/0.75));
102+
boolean changed = false;
103+
whitelistLock.writeLock().lock();
104+
try{
105+
changed|=whitelist.removeIf(pred);
106+
}finally{
107+
whitelistLock.writeLock().unlock();
108+
}
109+
mapLock.writeLock().lock();
110+
try{
111+
changed|=usernameEmailMappings.keySet().removeIf(pred);
112+
mfa.addAll(usernameEmailMappings.keySet());
113+
}finally{
114+
mapLock.writeLock().unlock();
115+
}
116+
otpLock.writeLock().lock();
117+
try{
118+
changed|=usernameOTPMappings.keySet().removeIf(pred);
119+
mfa.addAll(usernameOTPMappings.keySet());
120+
}finally{
121+
otpLock.writeLock().unlock();
122+
}
123+
cookieLock.writeLock().lock();
124+
try{
125+
changed|=cookieMappings.keySet().removeIf(new Predicate<String>(){
126+
@Override public boolean test(String s){
127+
final Matcher m = IPCookie.USERNAME_PATTERN.matcher(s);
128+
return !m.find() || !mfa.contains(m.group());
129+
}
130+
});
131+
}finally{
132+
cookieLock.writeLock().unlock();
133+
}
134+
return changed;
135+
}
93136
private static <T> T get(CompletableFuture<T> x, long timeout) throws InterruptedException, ExecutionException, CancellationException {
94137
try{
95138
return x.get(timeout, TimeUnit.MILLISECONDS);
@@ -262,7 +305,7 @@ public static boolean submitToAPI(String user, String code, String ip, boolean c
262305
if (cache && result){
263306
final long cur = System.currentTimeMillis();
264307
if (apiResponseCache.size()>32){
265-
apiResponseCache.values().removeIf(new java.util.function.Predicate<Long>(){
308+
apiResponseCache.values().removeIf(new Predicate<Long>(){
266309
@Override public boolean test(Long o){
267310
return o<=cur;
268311
}
@@ -298,7 +341,7 @@ public static void insertCookie(String user, String ip){
298341
cookieLock.writeLock().lock();
299342
try{
300343
final long cur = System.currentTimeMillis();
301-
cookieMappings.values().removeIf(new java.util.function.Predicate<IPCookie>(){
344+
cookieMappings.values().removeIf(new Predicate<IPCookie>(){
302345
@Override public boolean test(IPCookie o){
303346
return o.expiry<=cur;
304347
}
@@ -326,7 +369,7 @@ public static boolean codeAlreadySubmitted(String user, String code){
326369
l = null;
327370
}
328371
if (submittedCodeCache.size()>32){
329-
submittedCodeCache.values().removeIf(new java.util.function.Predicate<Long>(){
372+
submittedCodeCache.values().removeIf(new Predicate<Long>(){
330373
@Override public boolean test(Long o){
331374
return o<=cur;
332375
}
@@ -359,7 +402,7 @@ public static boolean isRateLimited(String user){
359402
private static int getAttempts(String user){
360403
final long lim = System.currentTimeMillis()-90000L;
361404
final Container<Integer> count = new Container<>(0);
362-
attempts.removeIf(new java.util.function.Predicate<Attempt>(){
405+
attempts.removeIf(new Predicate<Attempt>(){
363406
@Override public boolean test(Attempt o) {
364407
if (o.time<lim){
365408
return true;
@@ -446,6 +489,39 @@ public static void printOTPs(StringBuilder sb){
446489
}
447490
sb.append(']');
448491
}
492+
public static void checkCookies(Set<String> users){
493+
final HashSet<String> set = new HashSet<>(Math.max((int)(users.size()/0.75), 16));
494+
mapLock.readLock().lock();
495+
try{
496+
set.addAll(usernameEmailMappings.keySet());
497+
}finally{
498+
mapLock.readLock().unlock();
499+
}
500+
set.removeAll(users);
501+
if (set.isEmpty()){
502+
return;
503+
}
504+
otpLock.readLock().lock();
505+
try{
506+
set.removeAll(usernameOTPMappings.keySet());
507+
}finally{
508+
otpLock.readLock().unlock();
509+
}
510+
if (set.isEmpty()){
511+
return;
512+
}
513+
cookieLock.writeLock().lock();
514+
try{
515+
cookieMappings.keySet().removeIf(new Predicate<String>(){
516+
@Override public boolean test(String s){
517+
final Matcher m = IPCookie.USERNAME_PATTERN.matcher(s);
518+
return !m.find() || set.contains(m.group());
519+
}
520+
});
521+
}finally{
522+
cookieLock.writeLock().unlock();
523+
}
524+
}
449525
public static void setEmails(Map<String,String> map){
450526
mapLock.writeLock().lock();
451527
try{
@@ -493,6 +569,19 @@ public static void setOTPs(Map<String,String> map){
493569
}
494570
}
495571
public static String setOTP(String username, String otp){
572+
if (otp==null && !containsEmailFor(username)){
573+
cookieLock.writeLock().lock();
574+
try{
575+
cookieMappings.keySet().removeIf(new Predicate<String>(){
576+
@Override public boolean test(String s){
577+
final Matcher m = IPCookie.USERNAME_PATTERN.matcher(s);
578+
return !m.find() || username.equalsIgnoreCase(m.group());
579+
}
580+
});
581+
}finally{
582+
cookieLock.writeLock().unlock();
583+
}
584+
}
496585
otpLock.writeLock().lock();
497586
try{
498587
if (otp==null){

src/aces/webctrl/mfa/core/HelperAPI.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
Contributors: Cameron Vogt (@cvogt729)
55
*/
66
package aces.webctrl.mfa.core;
7+
import java.util.*;
78
import com.controlj.green.addonsupport.web.auth.AuthenticationManager;
89
import com.controlj.green.extensionsupport.Extension;
910
import com.controlj.green.core.ui.UserSession;
1011
import com.controlj.green.core.data.*;
1112
import com.controlj.green.common.policy.*;
13+
import com.controlj.green.datatable.util.CoreHelper;
1214
/**
1315
* Namespace which contains methods to access small sections of a few internal WebCTRL APIs.
1416
*/
@@ -17,6 +19,17 @@ public class HelperAPI {
1719
* Specifies whether methods of this API should log stack traces generated from errors.
1820
*/
1921
private final static boolean logErrors = true;
22+
/**
23+
* @return a collection of all local WebCTRL operators where usernames are mapped to display names, or {@code null} if an error occurs.
24+
*/
25+
public static Map<String,String> getLocalOperators(){
26+
try{
27+
return new CoreHelper().getOperatorList();
28+
}catch(Throwable t){
29+
Initializer.log(t);
30+
return null;
31+
}
32+
}
2033
/**
2134
* @return Whether the specified user exists in the system, case insensitive, and that the password validates.
2235
* If {@code pass} is {@code null}, the password is not validated.

src/aces/webctrl/mfa/core/IPCookie.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package aces.webctrl.mfa.core;
2+
import java.util.regex.Pattern;
23
public class IPCookie {
4+
public final static Pattern USERNAME_PATTERN = Pattern.compile("^.*?(?=_[^_]++$)");
35
public final static long MAX_EXPIRY = 604800000L; // 1 week
46
public volatile String user_ip;
57
public volatile long expiry;

src/aces/webctrl/mfa/core/Initializer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class Initializer implements ServletContextListener {
5454
HelperAPI.logoutAllForeign();
5555
HelperAPI.activateWebOperatorProvider(name);
5656
}
57+
deleteGhosts();
5758
}
5859
/**
5960
* Releases resources.
@@ -65,6 +66,20 @@ public class Initializer implements ServletContextListener {
6566
}
6667
Config.saveData();
6768
}
69+
public static void deleteGhosts(){
70+
final Map<String,String> map = HelperAPI.getLocalOperators();
71+
if (map!=null){
72+
Set<String> set = map.keySet();
73+
//HashSet<String> users = HashSet.newHashSet(set.size());
74+
HashSet<String> users = new HashSet<>((int)Math.ceil(set.size()/0.75));
75+
for (String s: set){
76+
users.add(s.toLowerCase());
77+
}
78+
if (Config.deleteUnused(users)){
79+
Config.saveData();
80+
}
81+
}
82+
}
6883
/**
6984
* Generate a new email token for the specified user.
7085
*/

0 commit comments

Comments
 (0)