From d53f1ef77ecd4b4168585a63d626b26fd0ba5f75 Mon Sep 17 00:00:00 2001 From: Nadir K Amra Date: Tue, 24 Sep 2024 17:10:32 -0500 Subject: [PATCH] User class MFA support, and AS400 class setStayAlive refinement Signed-off-by: Nadir K Amra --- src/main/java/com/ibm/as400/access/AS400.java | 85 +++++-- src/main/java/com/ibm/as400/access/User.java | 231 +++++++++++++++++- 2 files changed, 288 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/ibm/as400/access/AS400.java b/src/main/java/com/ibm/as400/access/AS400.java index f473cc24..cfb17690 100644 --- a/src/main/java/com/ibm/as400/access/AS400.java +++ b/src/main/java/com/ibm/as400/access/AS400.java @@ -4089,18 +4089,23 @@ public void removeVetoableChangeListener(VetoableChangeListener listener) /** * Disconnects all services, and clears the sign-on information. This intent of this method is to "wipe the slate * clean" for this AS400 object, enabling connection properties to be subsequently changed. + *

+ * Note: A call to resetAllServices() results in the stay-alive seconds value to be reset to zero. You will + * need to invoke the setStayAlive() method to re-enable the stay-alive functionality. * * @see #disconnectAllServices + * @see #setStayAlive(long) **/ public synchronized void resetAllServices() { if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Resetting all services."); + setStayAlive(0); + disconnectAllServices(); disconnectService(AS400.HOSTCNN); signonInfo_ = null; propertiesFrozen_ = false; ccsid_ = 0; - stayAliveMilliSeconds_ = 0; // This will end the thread if there is one. } // Resolves the proxy server name. If it is not specified, then look it up in the system properties. Returns empty string if not set. @@ -4998,28 +5003,52 @@ private class StayAliveThread extends Thread @Override public void run() { + // Want to always wake up every 30 seconds if say-alive time greater than that. + // This to ensure that we end thread if stay-alive time set to 0. + // So when whenToPing is zero or less, we actually ping. Otherwise, we loop. + long stayAliveMilliSeconds_original = stayAliveMilliSeconds_; + long whenToPing = stayAliveMilliSeconds_original; + long sleepTime = (stayAliveMilliSeconds_original > 30000) ? 30000 : stayAliveMilliSeconds_original; + while (stayAliveMilliSeconds_ > 0) { try { - sleep(stayAliveMilliSeconds_); + sleep(sleepTime); + whenToPing -= sleepTime; + + // If stay-alive time changed to zero, let thread end. + if (stayAliveMilliSeconds_ == 0) + break; - boolean bDATABASE = isConnectionAlive(AS400.DATABASE); - boolean bCOMMAND = isConnectionAlive(AS400.COMMAND); - boolean bDATAQUEUE = isConnectionAlive(AS400.DATAQUEUE); - boolean bFILE = isConnectionAlive(AS400.FILE); - boolean bPRINT = isConnectionAlive(AS400.PRINT); - boolean bHOSTCNN = isConnectionAlive(AS400.HOSTCNN); + if (whenToPing <= 0) + { + whenToPing = stayAliveMilliSeconds_original; + + boolean bDATABASE = isConnectionAlive(AS400.DATABASE); + boolean bCOMMAND = isConnectionAlive(AS400.COMMAND); + boolean bDATAQUEUE = isConnectionAlive(AS400.DATAQUEUE); + boolean bFILE = isConnectionAlive(AS400.FILE); + boolean bPRINT = isConnectionAlive(AS400.PRINT); + boolean bHOSTCNN = isConnectionAlive(AS400.HOSTCNN); + + if (Trace.traceOn_) + { + Trace.log(Trace.DIAGNOSTIC, "Stayalive status of services: " + + "DATABASE=" + bDATABASE + ", " + + "COMMAND=" + bCOMMAND + ", " + + "DATAQUEUE=" + bDATAQUEUE + ", " + + "FILE=" + bFILE + ", " + + "PRINT=" + bPRINT + ", " + + "HOSTCNN=" + bHOSTCNN); + } + } - if (Trace.traceOn_) + if (stayAliveMilliSeconds_original != stayAliveMilliSeconds_) { - Trace.log(Trace.DIAGNOSTIC, "Stayalive status of services: " - + "DATABASE=" + bDATABASE + ", " - + "COMMAND=" + bCOMMAND + ", " - + "DATAQUEUE=" + bDATAQUEUE + ", " - + "FILE=" + bFILE + ", " - + "PRINT=" + bPRINT + ", " - + "HOSTCNN=" + bHOSTCNN); + stayAliveMilliSeconds_original = stayAliveMilliSeconds_; + whenToPing = stayAliveMilliSeconds_original; + sleepTime = (stayAliveMilliSeconds_original > 30000) ? 30000 : stayAliveMilliSeconds_original; } } catch (Exception e) { @@ -5031,7 +5060,7 @@ public void run() private StayAliveThread stayAliveThread_; /** - * Set the stay-alve interval. When enabled, a request is sent at the specified milliseconds interval to all + * Set the stay-alve interval. When enabled, a request is sent at the specified seconds interval to all * currently opened connections to help keep the connections alive. This is sometimes needed to prevent firewalls * from dropping stale connections. *

@@ -5043,28 +5072,30 @@ public void run() *

* - * @param milliseconds The number of milliseconds between requests to the server. If set to zero, then this + * @param seconds The number of seconds between requests to the server. If set to zero, then this * stay-alive capability will not be used. * - * @exception ExtendedIllegalArgumentException if the value of milliseconds is negative. + * @exception ExtendedIllegalArgumentException if the value of seconds is negative. **/ - synchronized public void setStayAlive(long milliseconds) + synchronized public void setStayAlive(long seconds) { - if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Setting stay-alive: " + milliseconds); + if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Setting stay-alive: " + seconds); // Validate parameter. - if (milliseconds < 0) - throw new ExtendedIllegalArgumentException("milliseconds (" + milliseconds + ")", ExtendedIllegalArgumentException.PARAMETER_VALUE_NOT_VALID); + if (seconds < 0) + throw new ExtendedIllegalArgumentException("seconds (" + seconds + ")", ExtendedIllegalArgumentException.PARAMETER_VALUE_NOT_VALID); - stayAliveMilliSeconds_ = milliseconds; + stayAliveMilliSeconds_ = seconds*1000; - if (stayAliveMilliSeconds_ > 0 && (stayAliveThread_ == null || !stayAliveThread_.isAlive())) + if (stayAliveMilliSeconds_ == 0) + stayAliveThread_ = null; + else if (stayAliveThread_ == null || !stayAliveThread_.isAlive()) { - if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Creating stayalive thread."); + if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Creating stay-alive thread."); stayAliveThread_ = new StayAliveThread(); stayAliveThread_.setDaemon(true); diff --git a/src/main/java/com/ibm/as400/access/User.java b/src/main/java/com/ibm/as400/access/User.java index 03f0a247..37cb0af9 100644 --- a/src/main/java/com/ibm/as400/access/User.java +++ b/src/main/java/com/ibm/as400/access/User.java @@ -65,6 +65,7 @@ public class User implements Serializable /** * Constant value representing the String "*NONE". * + * @see #getAuthenticationMethods() * @see #getGroupProfileName * @see #getAttentionKeyHandlingProgram * @see #getGroupAuthority @@ -124,7 +125,7 @@ public class User implements Serializable * @see #getSpecialAuthority **/ public static final String SPECIAL_AUTHORITY_SERVICE = SPECIAL_AUTHORITIES[5]; - + /** * Constant value representing a special authority of "*SPLCTL". * @@ -307,6 +308,27 @@ public class User implements Serializable private String[] DLOObjectTypesInSTRAUTCOL_; private String[] fileSystemObjectTypesInSTRAUTCOL_; private String[] omitLibNamesInSTRAUTCOL_; + + // MFA-related fields + + /** + * Constant value representing an authentication method of "*TOTP". + * + * @see #getAuthenticationMethods + **/ + public static final String AUTHENTICATION_METHOD_TOTP = "*TOTP"; + /** + * Constant value representing an authentication method of "*REGFAC". + * + * @see #getAuthenticationMethods + **/ + public static final String AUTHENTICATION_METHOD_REGFAC = "*REGFAC"; + + private String[] authenticationMethods_; + private int totpOptionalInterval_; + private int totpOptionalIntervalRemaining_; + private boolean totpKeyExistsIndicator_; + private Date totpKeyLastChangedDate_; /** * Constructs a User object. @@ -520,6 +542,24 @@ public String getAttentionKeyHandlingProgram() return attentionKeyHandlingProgram_; } + /** + * Retrieves the authentication methods for this user. + * + * @return The authentication methods for this user. Possible values are: + * + */ + public String[] getAuthenticationMethods() + { + if (!loaded_) loadUserInformation_SwallowExceptions(); + return authenticationMethods_; + } + /** * Retrieves the character code set ID to be used by the system for this user. * @@ -1508,6 +1548,56 @@ public AS400 getSystem() return system_; } + /** + * Returns the amount of time, in minutes, a time-based one-time password (TOTP) is optional on an authentication + * request for the user. This is the amount of time after a valid user ID and password authentication using a TOTP + * value is performed, that subsequent authentications will be allowed without having to specifying a new TOTP value. + * When this value is zero, a TOTP is required for every authentication for the user if the user has *TOTP as an + * authentication method. + * + * @return the TOTP optional interval + */ + public int getTOTPOptionalInterval() + { + if (!loaded_) loadUserInformation_SwallowExceptions(); + return totpOptionalInterval_; + } + + /** + * Returns the The number of minutes remaining in the TOTP optional interval in which an authentication without + * specifying a new TOTP value is valid. When this value is zero, subsequent authentications will require a valid TOTP + * value be specified. + * + * @return the TOTP optional interval remaining + */ + public int getTOTPOptionalIntervalRemaining() + { + if (!loaded_) loadUserInformation_SwallowExceptions(); + return totpOptionalIntervalRemaining_; + } + + /** + * Returns whether the user has a TOTP key specified. This key is only used when the additional authentication methods include *TOTP. + * + * @return true if TOTP key exists; otherwise, false. + */ + public boolean getTOTPKeyExistsIndicator() + { + if (!loaded_) loadUserInformation_SwallowExceptions(); + return totpKeyExistsIndicator_; + } + + /** + * Returns the date and time the TOTP key was last changed using the Change TOTP Key (CHGTOTPKEY) command. + * + * @return the TOTP key last changed date, or null if unable to retrieve value. + */ + public Date getTOTPKeyLastChangedDate() + { + if (!loaded_) loadUserInformation_SwallowExceptions(); + return totpKeyLastChangedDate_; + } + /** * Retrieves a list of action audit levels for the user. * @@ -2334,6 +2424,47 @@ public void loadUserInformation() throws AS400SecurityException, ErrorCompleting omitLibNamesInSTRAUTCOL_[i] = conv.byteArrayToString(data, omitLibNamesOffset + i * 20, 20).trim(); } } + + if (vrm > 0x00070500) + { + boolean totpB = (data[812] == (byte)0xE8); + boolean regfac = (data[813] == (byte)0xE8); + String[] authMethods = new String[(totpB && regfac) ? 2 : 1]; + + if (totpB || regfac) + { + int idx=0; + if (totpB) + authMethods[idx++] = AUTHENTICATION_METHOD_TOTP; + if (regfac) + authMethods[idx++] = AUTHENTICATION_METHOD_REGFAC; + } + else + authMethods[0] = NONE; + authenticationMethods_ = authMethods; + + totpOptionalInterval_ = BinaryConverter.byteArrayToInt(data, 824); + totpOptionalIntervalRemaining_ = BinaryConverter.byteArrayToInt(data, 828); + totpKeyExistsIndicator_ = (data[832] == (byte)0xF1) ? true : false; + + String totpKeyLastChangedDate = conv.byteArrayToString(data, 833, 13); // Note: This time value is relative to the system's local time zone, not UTC. + if (totpKeyLastChangedDate.trim().length() > 0) + { + Calendar cal = AS400Calendar.getGregorianInstance(); + cal.clear(); + cal.set(Calendar.YEAR, 1900 + Integer.parseInt(totpKeyLastChangedDate.substring(0, 3))); + cal.set(Calendar.MONTH, Integer.parseInt(totpKeyLastChangedDate.substring(3, 5)) - 1); + cal.set(Calendar.DATE, Integer.parseInt(totpKeyLastChangedDate.substring(5, 7))); + cal.set(Calendar.HOUR, Integer.parseInt(totpKeyLastChangedDate.substring(7, 9))); + cal.set(Calendar.MINUTE, Integer.parseInt(totpKeyLastChangedDate.substring(9, 11))); + cal.set(Calendar.SECOND, Integer.parseInt(totpKeyLastChangedDate.substring(11, 13))); + // Set the correct time zone (in case client is in a different zone than server). + cal.setTimeZone(system_.getTimeZone()); + totpKeyLastChangedDate_ = cal.getTime(); + } + else + totpKeyLastChangedDate_ = null; + } // vrm > 0x00070500 } // vrm >= 0x00070300 } // vrm >= 0x00070200 } // vrm >= 0x00070100 @@ -2603,6 +2734,62 @@ public void setAttentionKeyHandlingProgram(String attentionKeyHandlingProgram) t } } + /** + * Set the additional authentication methods used when authenticating a user. + *

+ * Note: This method should not be used when running to IBM i 7.5 or earlier releases. + * + * @param authenticationMethods String array containing the authentication methods to set. Possible values: + *

+ *

+ * The authentication method {@link #NONE NONE}, if specified, must be the only value + * in the array. Passing a zero-length array is equivalent to passing an array with + * {@link #NONE NONE}. + * + * @throws AS400SecurityException If a security or authority error occurs. + * @throws ErrorCompletingRequestException If an error occurs before the request is completed. + * @throws InterruptedException If this thread is interrupted. + * @throws IOException If an error occurs while communicating with the system. + * @throws RequestNotSupportedException If the request is not supported. + * @throws ObjectDoesNotExistException If the object does not exist. + */ + public void setAuthenticationMethods(String[] authenticationMethods) + throws AS400SecurityException, ErrorCompletingRequestException, InterruptedException, IOException, ObjectDoesNotExistException, RequestNotSupportedException + { + if (authenticationMethods == null) + { + Trace.log(Trace.ERROR, "Parameter 'authenticationMethods' is null."); + throw new NullPointerException("authenticationMethods"); + } + + if (system_.getVRM() <= 0x00070500) + { + String currentRelease = system_.getVersion() + "." + system_.getRelease(); + throw new RequestNotSupportedException(currentRelease, RequestNotSupportedException.SYSTEM_LEVEL_NOT_CORRECT); + } + + String authMethods = NONE; + + if (authenticationMethods.length > 0) + { + authMethods = ""; + for (int i=0; i @@ -4239,6 +4426,48 @@ public void setUserExpirationDate(Date expirationDate) runCommand("USREXPDATE(" + expDate + ")"); } + + /** + * Set the amount of time, in minutes, a time-base one-time password (TOTP) is optional on an authentication request + * for the user. This is the amount of time after a valid user ID and password authentication using a TOTP value is + * performed, that subsequent authentications will be allowed without having to specifying a TOTP value. This value + * is only valid when Authentication method includes *TOTP. + *

+ * Note: This method should not be used when running to IBM i 7.5 or earlier releases. + * + * @param totpOptionalInterval the TOTP optional interval to set. Valid values include 0-720. A value of zero + * indicates that there is no optional interval, the time-based one-time password must + * be entered for every authentication. + * + * @throws AS400SecurityException If a security or authority error occurs. + * @throws ErrorCompletingRequestException If an error occurs before the request is completed. + * @throws ExtendedIllegalArgumentException If TOTP optional interval value is not valid. + * @throws InterruptedException If this thread is interrupted. + * @throws IOException If an error occurs while communicating with the system. + * @throws RequestNotSupportedException If the request is not supported. + * @throws ObjectDoesNotExistException If the object does not exist. + */ + public void setTOTPOptionalInterval(int totpOptionalInterval) + throws AS400SecurityException, ErrorCompletingRequestException, InterruptedException, IOException, RequestNotSupportedException, ObjectDoesNotExistException + { + if (system_.getVRM() <= 0x00070500) + { + String currentRelease = system_.getVersion() + "." + system_.getRelease(); + throw new RequestNotSupportedException(currentRelease, RequestNotSupportedException.SYSTEM_LEVEL_NOT_CORRECT); + } + + if (totpOptionalInterval < 0 || totpOptionalInterval > 720) + throw new ExtendedIllegalArgumentException("totpOptionalInterval (" + totpOptionalInterval + ")", ExtendedIllegalArgumentException.RANGE_NOT_VALID); + + + String totpOptionalIntervalString = null; + if (totpOptionalInterval == 0) + totpOptionalIntervalString = NONE; + + runCommand("TOTPOPTITV(" + ((totpOptionalIntervalString != null) ? totpOptionalIntervalString : totpOptionalInterval) + ")"); + + totpOptionalInterval_ = totpOptionalInterval; + } // Utility method. Returns this object's internal DateTimeConverter object. // If we haven't already created dateConverter_, creates it now.