Browse Source

Merge remote-tracking branch 'origin/master' into ng-helpdesk

jalbr74 7 năm trước cách đây
mục cha
commit
f62adf2ffb
26 tập tin đã thay đổi với 339 bổ sung257 xóa
  1. 1 1
      onejar/pom.xml
  2. 9 25
      server/pom.xml
  3. 1 0
      server/src/main/java/password/pwm/AppProperty.java
  4. 16 0
      server/src/main/java/password/pwm/config/Configuration.java
  5. 19 15
      server/src/main/java/password/pwm/config/PwmSetting.java
  6. 4 1
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  7. 2 1
      server/src/main/java/password/pwm/config/profile/ProfileType.java
  8. 59 0
      server/src/main/java/password/pwm/config/profile/SetupOtpProfile.java
  9. 6 0
      server/src/main/java/password/pwm/http/SessionManager.java
  10. 84 97
      server/src/main/java/password/pwm/http/servlet/SetupOtpServlet.java
  11. 2 5
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java
  12. 17 1
      server/src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java
  13. 23 12
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  14. 0 1
      server/src/main/java/password/pwm/ldap/ViewableUserInfoDisplayReader.java
  15. 3 3
      server/src/main/java/password/pwm/svc/stats/Statistic.java
  16. 0 9
      server/src/main/java/password/pwm/util/LDAPPermissionCalculator.java
  17. 5 1
      server/src/main/java/password/pwm/util/db/DBConfiguration.java
  18. 11 2
      server/src/main/java/password/pwm/util/db/DatabaseUtil.java
  19. 7 53
      server/src/main/java/password/pwm/util/operations/OtpService.java
  20. 13 4
      server/src/main/java/password/pwm/util/operations/otp/LdapOtpOperator.java
  21. 1 0
      server/src/main/resources/password/pwm/AppProperty.properties
  22. 16 2
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  23. 13 7
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  24. 5 2
      server/src/main/webapp/WEB-INF/jsp/setupotpsecret.jsp
  25. 13 15
      server/src/main/webapp/private/index.jsp
  26. 9 0
      server/src/test/java/password/pwm/config/PwmSettingCategoryTest.java

+ 1 - 1
onejar/pom.xml

@@ -16,7 +16,7 @@
     <name>PWM Password Self Service: Executable Jar</name>
 
     <properties>
-        <tomcat.version>9.0.4</tomcat.version>
+        <tomcat.version>9.0.5</tomcat.version>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
 

+ 9 - 25
server/pom.xml

@@ -538,22 +538,6 @@
     </build>
 
     <reporting>
-        <!--
-        Note: to run these reports, you can execute the maven command: "mvn site",
-        then you can view the results by opening the file: target/site/project-reports.html in your browser.
-         -->
-        <plugins>
-            <plugin>
-                <groupId>com.github.spotbugs</groupId>
-                <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.1</version>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-pmd-plugin</artifactId>
-                <version>3.6</version>
-            </plugin>
-        </plugins>
     </reporting>
 
     <dependencies>
@@ -581,25 +565,25 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>2.13.0</version>
+            <version>2.15.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
-            <version>3.9.0</version>
+            <version>3.9.1</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock</artifactId>
-            <version>2.14.0</version>
+            <version>2.15.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.reflections</groupId>
             <artifactId>reflections</artifactId>
-            <version>0.9.10</version>
+            <version>0.9.11</version>
             <scope>test</scope>
         </dependency>
 
@@ -679,7 +663,7 @@
         <dependency>
             <groupId>com.sun.mail</groupId>
             <artifactId>javax.mail</artifactId>
-            <version>1.6.0</version>
+            <version>1.6.1</version>
         </dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
@@ -759,7 +743,7 @@
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.1.0</version>
+            <version>1.2.0</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
@@ -797,17 +781,17 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.webjars.bower</groupId>
+            <groupId>org.webjars.npm</groupId>
             <artifactId>dojo</artifactId>
             <version>1.13.0</version>
         </dependency>
         <dependency>
-            <groupId>org.webjars.bower</groupId>
+            <groupId>org.webjars.npm</groupId>
             <artifactId>dijit</artifactId>
             <version>1.13.0</version>
         </dependency>
         <dependency>
-            <groupId>org.webjars.bower</groupId>
+            <groupId>org.webjars.npm</groupId>
             <artifactId>dojox</artifactId>
             <version>1.13.0</version>
         </dependency>

+ 1 - 0
server/src/main/java/password/pwm/AppProperty.java

@@ -84,6 +84,7 @@ public enum AppProperty
     DB_CONNECTIONS_MAX                              ( "db.connections.max" ),
     DB_CONNECTIONS_TIMEOUT_MS                       ( "db.connections.timeoutMs" ),
     DB_CONNECTIONS_WATCHDOG_FREQUENCY_SECONDS       ( "db.connections.watchdogFrequencySeconds" ),
+    DB_INIT_HALT_ON_INDEX_CREATE_ERROR              ( "db.init.haltOnIndexCreateError" ),
     DB_SCHEMA_KEY_LENGTH                            ( "db.schema.keyLength" ),
     DOWNLOAD_FILENAME_STATISTICS_CSV                ( "download.filename.statistics.csv" ),
     DOWNLOAD_FILENAME_USER_REPORT_SUMMARY_CSV       ( "download.filename.reportSummary.csv" ),

+ 16 - 0
server/src/main/java/password/pwm/config/Configuration.java

@@ -42,6 +42,7 @@ import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileUtility;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.UpdateAttributesProfile;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.StoredConfigurationImpl;
@@ -1069,6 +1070,17 @@ public class Configuration implements SettingReader
         return returnMap;
     }
 
+    public Map<String, SetupOtpProfile> getSetupOTPProfiles( )
+    {
+        final Map<String, SetupOtpProfile> returnMap = new LinkedHashMap<>();
+        final Map<String, Profile> profileMap = profileMap( ProfileType.SetupOTPProfile );
+        for ( final Map.Entry<String, Profile> entry : profileMap.entrySet() )
+        {
+            returnMap.put( entry.getKey(), ( SetupOtpProfile ) entry.getValue() );
+        }
+        return returnMap;
+    }
+
     public Map<String, UpdateAttributesProfile> getUpdateAttributesProfile( )
     {
         final Map<String, UpdateAttributesProfile> returnMap = new LinkedHashMap<>();
@@ -1130,6 +1142,10 @@ public class Configuration implements SettingReader
                 newProfile = DeleteAccountProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
                 break;
 
+            case SetupOTPProfile:
+                newProfile = SetupOtpProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
+                break;
+
             default:
                 throw new IllegalArgumentException( "unknown profile type: " + profileType.toString() );
         }

+ 19 - 15
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -231,6 +231,7 @@ public enum PwmSetting
     LDAP_PROFILE_DISPLAY_NAME(
             "ldap.profile.displayName", PwmSettingSyntax.LOCALIZED_STRING, PwmSettingCategory.LDAP_LOGIN ),
 
+    // ldap attributes
     LDAP_USERNAME_ATTRIBUTE(
             "ldap.username.attr", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     LDAP_GUID_ATTRIBUTE(
@@ -253,6 +254,8 @@ public enum PwmSetting
             "events.ldap.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     CACHED_USER_ATTRIBUTES(
             "webservice.userAttributes", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.LDAP_ATTRIBUTES ),
+    OTP_SECRET_LDAP_ATTRIBUTE(
+            "otp.secret.ldap.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     AUTO_ADD_OBJECT_CLASSES(
             "ldap.addObjectClasses", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.LDAP_ATTRIBUTES ),
 
@@ -602,26 +605,27 @@ public enum PwmSetting
             "token.ldap.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.TOKEN ),
 
     // OTP
-    OTP_ENABLED(
-            "otp.enabled", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.OTP ),
+    OTP_PROFILE_LIST(
+            "otp.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL ),
+    OTP_SETUP_USER_PERMISSION(
+            "otp.secret.allowSetup.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.OTP_PROFILE ),
+    OTP_ALLOW_SETUP(
+            "otp.enabled", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.OTP_PROFILE ),
     OTP_FORCE_SETUP(
-            "otp.forceSetup", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP ),
+            "otp.forceSetup", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP_PROFILE ),
+    OTP_SECRET_IDENTIFIER(
+            "otp.secret.identifier", PwmSettingSyntax.STRING, PwmSettingCategory.OTP_PROFILE ),
+    OTP_RECOVERY_CODES(
+            "otp.secret.recoveryCodes", PwmSettingSyntax.NUMERIC, PwmSettingCategory.OTP_PROFILE ),
+
     OTP_SECRET_READ_PREFERENCE(
-            "otp.secret.readPreference", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP ),
+            "otp.secret.readPreference", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP_SETTINGS ),
     OTP_SECRET_WRITE_PREFERENCE(
-            "otp.secret.writePreference", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP ),
+            "otp.secret.writePreference", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP_SETTINGS ),
     OTP_SECRET_STORAGEFORMAT(
-            "otp.secret.storageFormat", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP ),
+            "otp.secret.storageFormat", PwmSettingSyntax.SELECT, PwmSettingCategory.OTP_SETTINGS ),
     OTP_SECRET_ENCRYPT(
-            "otp.secret.encrypt", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.OTP ),
-    OTP_SECRET_LDAP_ATTRIBUTE(
-            "otp.secret.ldap.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.OTP ),
-    OTP_SETUP_USER_PERMISSION(
-            "otp.secret.allowSetup.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.OTP ),
-    OTP_SECRET_IDENTIFIER(
-            "otp.secret.identifier", PwmSettingSyntax.STRING, PwmSettingCategory.OTP ),
-    OTP_RECOVERY_CODES(
-            "otp.secret.recoveryCodes", PwmSettingSyntax.NUMERIC, PwmSettingCategory.OTP ),
+            "otp.secret.encrypt", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.OTP_SETTINGS ),
 
     // logger settings
     EVENTS_JAVA_STDOUT_LEVEL(

+ 4 - 1
server/src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -106,7 +106,6 @@ public enum PwmSettingCategory
     PASSWORD_GLOBAL( SETTINGS ),
 
     TOKEN( SETTINGS ),
-    OTP( SETTINGS ),
     LOGGING( SETTINGS ),
 
     DATABASE( SETTINGS ),
@@ -168,6 +167,10 @@ public enum PwmSettingCategory
 
     HELPDESK_SETTINGS( HELPDESK ),
 
+    OTP_SETUP( MODULES_PRIVATE ),
+    OTP_PROFILE( OTP_SETUP ),
+    OTP_SETTINGS( OTP_SETUP ),
+
     DELETE_ACCOUNT( MODULES_PRIVATE ),
     DELETE_ACCOUNT_SETTINGS( DELETE_ACCOUNT ),
     DELETE_ACCOUNT_PROFILE( DELETE_ACCOUNT ),

+ 2 - 1
server/src/main/java/password/pwm/config/profile/ProfileType.java

@@ -31,7 +31,8 @@ public enum ProfileType
     ForgottenPassword( false, PwmSettingCategory.RECOVERY_PROFILE, PwmSetting.RECOVERY_PROFILE_QUERY_MATCH ),
     NewUser( false, PwmSettingCategory.NEWUSER_PROFILE, null ),
     UpdateAttributes( true, PwmSettingCategory.UPDATE_PROFILE, PwmSetting.UPDATE_PROFILE_QUERY_MATCH ),
-    DeleteAccount( true, PwmSettingCategory.DELETE_ACCOUNT_PROFILE, PwmSetting.DELETE_ACCOUNT_PERMISSION ),;
+    DeleteAccount( true, PwmSettingCategory.DELETE_ACCOUNT_PROFILE, PwmSetting.DELETE_ACCOUNT_PERMISSION ),
+    SetupOTPProfile( true, PwmSettingCategory.OTP_PROFILE, PwmSetting.OTP_SETUP_USER_PERMISSION ),;
 
     private final boolean authenticated;
     private final PwmSettingCategory category;

+ 59 - 0
server/src/main/java/password/pwm/config/profile/SetupOtpProfile.java

@@ -0,0 +1,59 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.config.profile;
+
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfiguration;
+
+import java.util.Locale;
+import java.util.Map;
+
+public class SetupOtpProfile extends AbstractProfile
+{
+    private static final ProfileType PROFILE_TYPE = ProfileType.SetupOTPProfile;
+
+    protected SetupOtpProfile( final String identifier, final Map<PwmSetting, StoredValue> storedValueMap )
+    {
+        super( identifier, storedValueMap );
+    }
+
+    public static SetupOtpProfile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final String identifier )
+    {
+        final Map<PwmSetting, StoredValue> valueMap = makeValueMap( storedConfiguration, identifier, PROFILE_TYPE.getCategory() );
+        return new SetupOtpProfile( identifier, valueMap );
+    }
+
+    @Override
+    public String getDisplayName( final Locale locale )
+    {
+        return this.getIdentifier();
+    }
+
+    @Override
+    public ProfileType profileType( )
+    {
+        return PROFILE_TYPE;
+    }
+
+}

+ 6 - 0
server/src/main/java/password/pwm/http/SessionManager.java

@@ -33,6 +33,7 @@ import password.pwm.config.profile.DeleteAccountProfile;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.Profile;
 import password.pwm.config.profile.ProfileType;
+import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.UpdateAttributesProfile;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
@@ -254,6 +255,11 @@ public class SessionManager
         return ( HelpdeskProfile ) getProfile( pwmApplication, ProfileType.Helpdesk );
     }
 
+    public SetupOtpProfile getSetupOTPProfile( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
+    {
+        return ( SetupOtpProfile ) getProfile( pwmApplication, ProfileType.SetupOTPProfile );
+    }
+
     public UpdateAttributesProfile getUpdateAttributeProfile( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
     {
         return ( UpdateAttributesProfile ) getProfile( pwmApplication, ProfileType.UpdateAttributes );

+ 84 - 97
server/src/main/java/password/pwm/http/servlet/SetupOtpServlet.java

@@ -25,7 +25,6 @@ package password.pwm.http.servlet;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import net.glxn.qrgen.QRCode;
 import password.pwm.AppProperty;
-import password.pwm.Permission;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.LoginInfoBean;
@@ -33,6 +32,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.ForceSetupPolicy;
+import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
@@ -40,11 +40,11 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.SetupOtpBean;
-import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.event.AuditEvent;
@@ -52,7 +52,6 @@ import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.UserAuditRecord;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.Validator;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -80,11 +79,11 @@ import java.util.Map;
                 PwmConstants.URL_PREFIX_PRIVATE + "/SetupOtp"
         }
 )
-public class SetupOtpServlet extends AbstractPwmServlet
+public class SetupOtpServlet extends ControlledPwmServlet
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( SetupOtpServlet.class );
 
-    enum SetupOtpAction implements AbstractPwmServlet.ProcessAction
+    public enum SetupOtpAction implements AbstractPwmServlet.ProcessAction
     {
         clearOtp( HttpMethod.POST ),
         testOtpSecret( HttpMethod.POST ),
@@ -106,45 +105,39 @@ public class SetupOtpServlet extends AbstractPwmServlet
         }
     }
 
-    protected SetupOtpAction readProcessAction( final PwmRequest request )
-            throws PwmUnrecoverableException
+    public Class<? extends ProcessAction> getProcessActionsClass( )
     {
-        try
-        {
-            return SetupOtpAction.valueOf( request.readParameterAsString( PwmConstants.PARAM_ACTION_REQUEST ) );
-        }
-        catch ( IllegalArgumentException e )
-        {
-            return null;
-        }
+        return SetupOtpAction.class;
+    }
+
+
+    private SetupOtpBean getSetupOtpBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, SetupOtpBean.class );
     }
 
-    protected void processAction( final PwmRequest pwmRequest )
-            throws ServletException, ChaiUnavailableException, IOException, PwmUnrecoverableException
+    public static SetupOtpProfile getSetupOtpProfile( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        return pwmRequest.getPwmSession().getSessionManager().getSetupOTPProfile( pwmRequest.getPwmApplication() );
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ServletException
     {
         // fetch the required beans / managers
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final UserInfo userInfo = pwmSession.getUserInfo();
         final Configuration config = pwmApplication.getConfig();
 
-        if ( !config.readSettingAsBoolean( PwmSetting.OTP_ENABLED ) )
+        final SetupOtpProfile setupOtpProfile = getSetupOtpProfile( pwmRequest );
+        if ( setupOtpProfile == null || !setupOtpProfile.readSettingAsBoolean( PwmSetting.OTP_ALLOW_SETUP ) )
         {
-            final String errorMsg = "setup OTP Secret service is not enabled";
+            final String errorMsg = "setup OTP is not enabled";
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, errorMsg );
             LOGGER.error( pwmRequest, errorInformation );
             pwmRequest.respondWithError( errorInformation );
-            return;
-        }
-
-        // check to see if the user is permitted to setup OTP
-        if ( !pwmSession.getSessionManager().checkPermission( pwmApplication, Permission.SETUP_OTP_SECRET ) )
-        {
-            final String errorMsg = String.format( "user %s does not have permission to setup an OTP secret", userInfo.getUserIdentity() );
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, errorMsg );
-            LOGGER.error( pwmRequest, errorInformation );
-            pwmRequest.respondWithError( errorInformation );
-            return;
+            return ProcessStatus.Halt;
         }
 
         // check whether the setup can be stored
@@ -152,7 +145,7 @@ public class SetupOtpServlet extends AbstractPwmServlet
         {
             LOGGER.error( pwmSession, "OTP Secret cannot be setup" );
             pwmRequest.respondWithError( PwmError.ERROR_INVALID_CONFIG.toInfo() );
-            return;
+            return ProcessStatus.Halt;
         }
 
         if ( pwmSession.getLoginInfoBean().getType() == AuthenticationType.AUTH_WITHOUT_PASSWORD )
@@ -161,54 +154,16 @@ public class SetupOtpServlet extends AbstractPwmServlet
             throw new PwmUnrecoverableException( PwmError.ERROR_PASSWORD_REQUIRED );
         }
 
-        final SetupOtpBean otpBean = pwmApplication.getSessionStateService().getBean( pwmRequest, SetupOtpBean.class );
-
+        final SetupOtpBean otpBean = getSetupOtpBean( pwmRequest );
         initializeBean( pwmRequest, otpBean );
-
-        final SetupOtpAction action = readProcessAction( pwmRequest );
-        if ( action != null )
-        {
-            pwmRequest.validatePwmFormID();
-
-            switch ( action )
-            {
-                case clearOtp:
-                    handleClearOtpSecret( pwmRequest, otpBean );
-                    break;
-
-                case testOtpSecret:
-                    handleTestOtpSecret( pwmRequest, otpBean );
-                    break;
-
-                case toggleSeen:
-                    otpBean.setCodeSeen( !otpBean.isCodeSeen() );
-                    break;
-
-                case restValidateCode:
-                    handleRestValidateCode( pwmRequest );
-                    return;
-
-                case complete:
-                    handleComplete( pwmRequest );
-                    return;
-
-                case skip:
-                    handleSkip( pwmRequest, otpBean );
-                    return;
-
-                default:
-                    JavaHelper.unhandledSwitchStatement( action );
-            }
-        }
-        this.advanceToNextStage( pwmRequest, otpBean );
+        return ProcessStatus.Continue;
     }
 
-    private void advanceToNextStage(
-            final PwmRequest pwmRequest,
-            final SetupOtpBean otpBean
-    )
-            throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
+    @Override
+    protected void nextStep( final  PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ServletException
     {
+        final SetupOtpBean otpBean = getSetupOtpBean( pwmRequest );
         if ( otpBean.isHasPreExistingOtp() )
         {
             pwmRequest.forwardToJsp( JspUrl.SETUP_OTP_SECRET_EXISTING );
@@ -284,12 +239,13 @@ public class SetupOtpServlet extends AbstractPwmServlet
     }
 
 
-    private void handleSkip(
-            final PwmRequest pwmRequest,
-            final SetupOtpBean otpBean
+    @ActionHandler( action = "skip" )
+    private ProcessStatus handleSkipRequest(
+            final PwmRequest pwmRequest
     )
             throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
     {
+
         boolean allowSkip = false;
         if ( !pwmRequest.isForcedPageView() )
         {
@@ -297,7 +253,8 @@ public class SetupOtpServlet extends AbstractPwmServlet
         }
         else
         {
-            final ForceSetupPolicy policy = pwmRequest.getConfig().readSettingAsEnum( PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class );
+            final SetupOtpProfile setupOtpProfile = getSetupOtpProfile( pwmRequest );
+            final ForceSetupPolicy policy = setupOtpProfile.readSettingAsEnum( PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class );
             if ( policy == ForceSetupPolicy.FORCE_ALLOW_SKIP )
             {
                 allowSkip = true;
@@ -308,13 +265,14 @@ public class SetupOtpServlet extends AbstractPwmServlet
         {
             pwmRequest.getPwmSession().getLoginInfoBean().getLoginFlags().add( LoginInfoBean.LoginFlag.skipOtp );
             pwmRequest.sendRedirectToContinue();
-            return;
+            return ProcessStatus.Halt;
         }
 
-        this.advanceToNextStage( pwmRequest, otpBean );
+        return ProcessStatus.Continue;
     }
 
-    private void handleComplete(
+    @ActionHandler( action = "complete" )
+    private ProcessStatus handleComplete(
             final PwmRequest pwmRequest
     )
             throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
@@ -324,10 +282,11 @@ public class SetupOtpServlet extends AbstractPwmServlet
         pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, SetupOtpBean.class );
 
         pwmRequest.sendRedirectToContinue();
+        return ProcessStatus.Halt;
     }
 
-
-    private void handleRestValidateCode(
+    @ActionHandler( action = "restValidateCode" )
+    private ProcessStatus handleRestValidateCode(
             final PwmRequest pwmRequest
     )
             throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
@@ -363,14 +322,18 @@ public class SetupOtpServlet extends AbstractPwmServlet
             LOGGER.error( pwmSession, errorMsg );
             pwmRequest.outputJsonResult( RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg ), pwmRequest ) );
         }
+
+        return ProcessStatus.Continue;
     }
 
-    private void handleClearOtpSecret(
-            final PwmRequest pwmRequest,
-            final SetupOtpBean otpBean
+    @ActionHandler( action = "clearOtp" )
+    private ProcessStatus handleClearOtpSecret(
+            final PwmRequest pwmRequest
     )
-            throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
+            throws PwmUnrecoverableException, ChaiUnavailableException
     {
+        final SetupOtpBean otpBean = getSetupOtpBean( pwmRequest );
+
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final OtpService service = pwmApplication.getOtpService();
@@ -383,19 +346,22 @@ public class SetupOtpServlet extends AbstractPwmServlet
         {
             setLastError( pwmRequest, e.getErrorInformation() );
             LOGGER.error( pwmRequest, e.getErrorInformation() );
-            return;
+            return ProcessStatus.Halt;
         }
 
         otpBean.setHasPreExistingOtp( false );
         initializeBean( pwmRequest, otpBean );
+        return ProcessStatus.Continue;
     }
 
-    private void handleTestOtpSecret(
-            final PwmRequest pwmRequest,
-            final SetupOtpBean otpBean
+    @ActionHandler( action = "testOtpSecret" )
+    private ProcessStatus handleTestOtpSecret(
+            final PwmRequest pwmRequest
     )
             throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
     {
+        final SetupOtpBean otpBean = getSetupOtpBean( pwmRequest );
+
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
 
@@ -435,14 +401,14 @@ public class SetupOtpServlet extends AbstractPwmServlet
             }
         }
 
-        //@todo: handle case to HOTP
+        return ProcessStatus.Continue;
     }
 
     private void initializeBean(
             final PwmRequest pwmRequest,
             final SetupOtpBean otpBean
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException
+            throws PwmUnrecoverableException
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
@@ -459,7 +425,17 @@ public class SetupOtpServlet extends AbstractPwmServlet
         // first time here
         if ( otpBean.getOtpUserRecord() == null )
         {
-            final OTPUserRecord existingUserRecord = service.readOTPUserConfiguration( pwmRequest.getSessionLabel(), theUser );
+
+            final OTPUserRecord existingUserRecord;
+            try
+            {
+                existingUserRecord = service.readOTPUserConfiguration( pwmRequest.getSessionLabel(), theUser );
+            }
+            catch ( ChaiUnavailableException e )
+            {
+                throw PwmUnrecoverableException.fromChaiException( e );
+            }
+
             if ( existingUserRecord != null )
             {
                 otpBean.setHasPreExistingOtp( true );
@@ -474,10 +450,12 @@ public class SetupOtpServlet extends AbstractPwmServlet
             try
             {
                 final Configuration config = pwmApplication.getConfig();
-                final String identifierConfigValue = config.readSettingAsString( PwmSetting.OTP_SECRET_IDENTIFIER );
+                final SetupOtpProfile setupOtpProfile = getSetupOtpProfile( pwmRequest );
+                final String identifierConfigValue = setupOtpProfile.readSettingAsString( PwmSetting.OTP_SECRET_IDENTIFIER );
                 final String identifier = pwmSession.getSessionManager().getMacroMachine( pwmApplication ).expandMacros( identifierConfigValue );
                 final OTPUserRecord otpUserRecord = new OTPUserRecord();
                 final List<String> rawRecoveryCodes = pwmApplication.getOtpService().initializeUserRecord(
+                        setupOtpProfile,
                         otpUserRecord,
                         pwmRequest.getSessionLabel(),
                         identifier
@@ -499,6 +477,15 @@ public class SetupOtpServlet extends AbstractPwmServlet
         }
     }
 
+    @ActionHandler( action = "toggleSeen" )
+    private ProcessStatus processToggleSeen( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final SetupOtpBean otpBean = getSetupOtpBean( pwmRequest );
+        otpBean.setCodeSeen( !otpBean.isCodeSeen() );
+        return ProcessStatus.Continue;
+    }
+
     private boolean canSetupOtpSecret( final Configuration config )
     {
         /* TODO */

+ 2 - 5
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java

@@ -325,12 +325,9 @@ public class HelpdeskDetailInfoBean implements Serializable
             buttons.add( StandardButton.clearResponses );
         }
 
-        if ( pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.OTP_ENABLED ) )
+        if ( helpdeskProfile.readSettingAsBoolean( PwmSetting.HELPDESK_CLEAR_OTP_BUTTON ) )
         {
-            if ( helpdeskProfile.readSettingAsBoolean( PwmSetting.HELPDESK_CLEAR_OTP_BUTTON ) )
-            {
-                buttons.add( StandardButton.clearOtpSecret );
-            }
+            buttons.add( StandardButton.clearOtpSecret );
         }
 
         if ( !helpdeskProfile.readOptionalVerificationMethods().isEmpty() )

+ 17 - 1
server/src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java

@@ -31,6 +31,7 @@ import password.pwm.PwmEnvironment;
 import password.pwm.bean.PasswordStatus;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.ProfileType;
+import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthMonitor;
 import password.pwm.health.HealthStatus;
@@ -58,7 +59,7 @@ public enum PwmIfTest
     showHeaderMenu( new ShowHeaderMenuTest() ),
     showVersionHeader( new BooleanAppPropertyTest( AppProperty.HTTP_HEADER_SEND_XVERSION ) ),
     permission( new BooleanPermissionTest() ),
-    otpEnabled( new BooleanPwmSettingTest( PwmSetting.OTP_ENABLED ) ),
+    otpSetupEnabled( new SetupOTPEnabled() ),
     hasStoredOtpTimestamp( new HasStoredOtpTimestamp() ),
     setupChallengeEnabled( new BooleanPwmSettingTest( PwmSetting.CHALLENGE_ENABLE ) ),
     shortcutsEnabled( new BooleanPwmSettingTest( PwmSetting.SHORTCUT_ENABLE ) ),
@@ -495,4 +496,19 @@ public enum PwmIfTest
             return passwordStatus.isExpired() || passwordStatus.isPreExpired() || passwordStatus.isViolatesPolicy();
         }
     }
+
+    private static class SetupOTPEnabled implements Test
+    {
+        @Override
+        public boolean test( final PwmRequest pwmRequest, final PwmIfOptions options ) throws ChaiUnavailableException, PwmUnrecoverableException
+        {
+            if ( !pwmRequest.isAuthenticated() )
+            {
+                return false;
+            }
+
+            final SetupOtpProfile setupOtpProfile = pwmRequest.getPwmSession().getSessionManager().getSetupOTPProfile( pwmRequest.getPwmApplication() );
+            return setupOtpProfile != null && setupOtpProfile.readSettingAsBoolean( PwmSetting.OTP_ALLOW_SETUP );
+        }
+    }
 }

+ 23 - 12
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -43,6 +43,7 @@ import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileUtility;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.UpdateAttributesProfile;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.UserPermission;
@@ -422,31 +423,41 @@ public class UserInfoReader implements UserInfo
     {
         LOGGER.trace( sessionLabel, "checkOtp: beginning process to check if user OTP setup is required" );
 
-        final UserIdentity userIdentity = getUserIdentity();
+        SetupOtpProfile setupOtpProfile = null;
+        final Map<ProfileType, String> profileIDs = selfCachedReference.getProfileIDs();
+        if ( profileIDs.containsKey( ProfileType.UpdateAttributes ) )
+        {
+            setupOtpProfile = pwmApplication.getConfig().getSetupOTPProfiles().get( profileIDs.get( ProfileType.SetupOTPProfile ) );
+        }
 
-        if ( !pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.OTP_ENABLED ) )
+        if ( setupOtpProfile == null )
         {
-            LOGGER.trace( sessionLabel, "checkOtp: OTP is not enabled, user OTP setup is not required" );
+            LOGGER.trace( sessionLabel, "checkOtp: no otp setup profile assigned, user OTP setup is not required" );
             return false;
         }
 
-        final OTPUserRecord otpUserRecord = selfCachedReference.getOtpUserRecord();
-        final boolean hasStoredOtp = otpUserRecord != null && otpUserRecord.getSecret() != null;
-
-        if ( hasStoredOtp )
+        if ( !setupOtpProfile.readSettingAsBoolean( PwmSetting.OTP_ALLOW_SETUP ) )
         {
-            LOGGER.trace( sessionLabel, "checkOtp: user has existing valid otp record, user OTP setup is not required" );
+            LOGGER.trace( sessionLabel, "checkOtp: OTP allow setup is not enabled" );
             return false;
         }
 
-        final List<UserPermission> setupOtpPermission = pwmApplication.getConfig().readSettingAsUserPermission( PwmSetting.OTP_SETUP_USER_PERMISSION );
-        if ( !LdapPermissionTester.testUserPermissions( pwmApplication, sessionLabel, userIdentity, setupOtpPermission ) )
+        final ForceSetupPolicy policy = setupOtpProfile.readSettingAsEnum( PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class );
+
+        if ( policy == ForceSetupPolicy.SKIP )
         {
-            LOGGER.trace( sessionLabel, "checkOtp: " + userIdentity.toString() + " is not eligible for checkOtp due to query match" );
+            LOGGER.trace( sessionLabel, "checkOtp: OTP force setup policy is set to SKIP, user OTP setup is not required" );
             return false;
         }
 
-        final ForceSetupPolicy policy = pwmApplication.getConfig().readSettingAsEnum( PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class );
+        final OTPUserRecord otpUserRecord = selfCachedReference.getOtpUserRecord();
+        final boolean hasStoredOtp = otpUserRecord != null && otpUserRecord.getSecret() != null;
+
+        if ( hasStoredOtp )
+        {
+            LOGGER.trace( sessionLabel, "checkOtp: user has existing valid otp record, user OTP setup is not required" );
+            return false;
+        }
 
         // hasStoredOtp is always true at this point, so if forced then update needed
         LOGGER.debug( sessionLabel, "checkOtp: user does not have existing valid otp record, user OTP setup is required" );

+ 0 - 1
server/src/main/java/password/pwm/ldap/ViewableUserInfoDisplayReader.java

@@ -225,7 +225,6 @@ public final class ViewableUserInfoDisplayReader
             );
         }
 
-        if ( config.readSettingAsBoolean( PwmSetting.OTP_ENABLED ) )
         {
             maker.add(
                     ViewStatusFields.OTPStored,

+ 3 - 3
server/src/main/java/password/pwm/svc/stats/Statistic.java

@@ -74,7 +74,7 @@ public enum Statistic
     LDAP_UNAVAILABLE_COUNT( Type.INCREMENTOR, "LdapUnavailableCount", null ),
     DB_UNAVAILABLE_COUNT( Type.INCREMENTOR, "DatabaseUnavailableCount", null ),
     SETUP_RESPONSES( Type.INCREMENTOR, "SetupResponses", null ),
-    SETUP_OTP_SECRET( Type.INCREMENTOR, "SetupOtpSecret", new ConfigSettingDetail( PwmSetting.OTP_ENABLED ) ),
+    SETUP_OTP_SECRET( Type.INCREMENTOR, "SetupOtpSecret", null ),
     UPDATE_ATTRIBUTES( Type.INCREMENTOR, "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
     SHORTCUTS_SELECTED( Type.INCREMENTOR, "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
     GENERATED_PASSWORDS( Type.INCREMENTOR, "GeneratedPasswords", null ),
@@ -85,8 +85,8 @@ public enum Statistic
     RECOVERY_TOKENS_SENT( Type.INCREMENTOR, "RecoveryTokensSent", null ),
     RECOVERY_TOKENS_PASSED( Type.INCREMENTOR, "RecoveryTokensPassed", null ),
     RECOVERY_TOKENS_FAILED( Type.INCREMENTOR, "RecoveryTokensFailed", null ),
-    RECOVERY_OTP_PASSED( Type.INCREMENTOR, "RecoveryOTPPassed", new ConfigSettingDetail( PwmSetting.OTP_ENABLED ) ),
-    RECOVERY_OTP_FAILED( Type.INCREMENTOR, "RecoveryOTPFailed", new ConfigSettingDetail( PwmSetting.OTP_ENABLED ) ),
+    RECOVERY_OTP_PASSED( Type.INCREMENTOR, "RecoveryOTPPassed", null ),
+    RECOVERY_OTP_FAILED( Type.INCREMENTOR, "RecoveryOTPFailed", null ),
     PEOPLESEARCH_CACHE_HITS( Type.INCREMENTOR, "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
     PEOPLESEARCH_CACHE_MISSES( Type.INCREMENTOR, "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
     PEOPLESEARCH_SEARCHES( Type.INCREMENTOR, "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),

+ 0 - 9
server/src/main/java/password/pwm/util/LDAPPermissionCalculator.java

@@ -356,15 +356,6 @@ public class LDAPPermissionCalculator implements Serializable
             }
             break;
 
-            case OTP_SECRET_LDAP_ATTRIBUTE:
-            {
-                if ( !configuration.readSettingAsBoolean( PwmSetting.OTP_ENABLED ) )
-                {
-                    return Collections.emptyList();
-                }
-            }
-            break;
-
             case SMS_USER_PHONE_ATTRIBUTE:
             {
                 if ( !SmsQueueManager.smsIsConfigured( configuration ) )

+ 5 - 1
server/src/main/java/password/pwm/util/db/DBConfiguration.java

@@ -53,6 +53,7 @@ public class DBConfiguration implements Serializable
     private final int maxConnections;
     private final int connectionTimeout;
     private final int keyColumnLength;
+    private final boolean failOnIndexCreation;
 
     public byte[] getJdbcDriver( )
     {
@@ -94,6 +95,8 @@ public class DBConfiguration implements Serializable
 
         final int keyColumnLength = Integer.parseInt( config.readAppProperty( AppProperty.DB_SCHEMA_KEY_LENGTH ) );
 
+        final boolean haltOnIndexCreateError = Boolean.parseBoolean( config.readAppProperty( AppProperty.DB_INIT_HALT_ON_INDEX_CREATE_ERROR ) );
+
         return new DBConfiguration(
                 config.readSettingAsString( PwmSetting.DATABASE_CLASS ),
                 config.readSettingAsString( PwmSetting.DATABASE_URL ),
@@ -105,7 +108,8 @@ public class DBConfiguration implements Serializable
                 strategies,
                 maxConnections,
                 connectionTimeout,
-                keyColumnLength
+                keyColumnLength,
+                haltOnIndexCreateError
         );
     }
 }

+ 11 - 2
server/src/main/java/password/pwm/util/db/DatabaseUtil.java

@@ -42,6 +42,8 @@ class DatabaseUtil
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( DatabaseUtil.class );
 
+    private static final String INDEX_NAME_SUFFIX = "_IDX";
+
     private DatabaseUtil( )
     {
     }
@@ -166,7 +168,7 @@ class DatabaseUtil
         }
 
         {
-            final String indexName = table.toString() + "_IDX";
+            final String indexName = table.toString() + INDEX_NAME_SUFFIX;
             final String sqlString = "CREATE index " + indexName
                     + " ON " + table.toString()
                     + " (" + DatabaseService.KEY_COLUMN + ")";
@@ -186,7 +188,14 @@ class DatabaseUtil
             {
                 final String errorMsg = "error creating new index " + indexName + ": " + ex.getMessage();
                 final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, errorMsg );
-                throw new DatabaseException( errorInformation );
+                if ( dbConfiguration.isFailOnIndexCreation() )
+                {
+                    throw new DatabaseException ( errorInformation );
+                }
+                else
+                {
+                    LOGGER.warn( errorInformation.toDebugStr() );
+                }
             }
             finally
             {

+ 7 - 53
server/src/main/java/password/pwm/util/operations/OtpService.java

@@ -23,6 +23,7 @@
 package password.pwm.util.operations;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
+import lombok.Getter;
 import org.apache.commons.codec.binary.Base32;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
@@ -33,6 +34,7 @@ import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.OTPStorageFormat;
+import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
@@ -176,6 +178,7 @@ public class OtpService implements PwmService
     }
 
     public List<String> initializeUserRecord(
+            final SetupOtpProfile otpProfile,
             final OTPUserRecord otpUserRecord,
             final SessionLabel sessionLabel,
             final String identifier
@@ -205,7 +208,8 @@ public class OtpService implements PwmService
         final List<String> rawRecoveryCodes;
         if ( settings.getOtpStorageFormat().supportsRecoveryCodes() )
         {
-            rawRecoveryCodes = createRawRecoveryCodes( settings.getRecoveryCodesCount(), sessionLabel );
+            final int recoveryCodesCount = ( int ) otpProfile.readSettingAsLong( PwmSetting.OTP_RECOVERY_CODES );
+            rawRecoveryCodes = createRawRecoveryCodes( recoveryCodesCount, sessionLabel );
             final List<OTPUserRecord.RecoveryCode> recoveryCodeList = new ArrayList<>();
             final OTPUserRecord.RecoveryInfo recoveryInfo = new OTPUserRecord.RecoveryInfo();
             if ( settings.getOtpStorageFormat().supportsHashedRecoveryCodes() )
@@ -509,11 +513,11 @@ public class OtpService implements PwmService
         return userGUID;
     }
 
+    @Getter
     public static class OtpSettings implements Serializable
     {
         private OTPStorageFormat otpStorageFormat;
         private OTPUserRecord.Type otpType = OTPUserRecord.Type.TOTP;
-        private int recoveryCodesCount;
         private int totpPastIntervals;
         private int totpFutureIntervals;
         private int totpIntervalSeconds;
@@ -522,62 +526,12 @@ public class OtpService implements PwmService
         private int recoveryHashIterations;
         private String recoveryHashMethod;
 
-        public OTPStorageFormat getOtpStorageFormat( )
-        {
-            return otpStorageFormat;
-        }
-
-        public OTPUserRecord.Type getOtpType( )
-        {
-            return otpType;
-        }
-
-        public int getRecoveryCodesCount( )
-        {
-            return recoveryCodesCount;
-        }
-
-        public int getTotpPastIntervals( )
-        {
-            return totpPastIntervals;
-        }
-
-        public int getTotpFutureIntervals( )
-        {
-            return totpFutureIntervals;
-        }
-
-        public int getTotpIntervalSeconds( )
-        {
-            return totpIntervalSeconds;
-        }
-
-        public int getOtpTokenLength( )
-        {
-            return otpTokenLength;
-        }
-
-        public String getRecoveryTokenMacro( )
-        {
-            return recoveryTokenMacro;
-        }
-
-        public int getRecoveryHashIterations( )
-        {
-            return recoveryHashIterations;
-        }
-
-        public String getRecoveryHashMethod( )
-        {
-            return recoveryHashMethod;
-        }
 
-        public static OtpSettings fromConfig( final Configuration config )
+        static OtpSettings fromConfig( final Configuration config )
         {
             final OtpSettings otpSettings = new OtpSettings();
 
             otpSettings.otpStorageFormat = config.readSettingAsEnum( PwmSetting.OTP_SECRET_STORAGEFORMAT, OTPStorageFormat.class );
-            otpSettings.recoveryCodesCount = ( int ) config.readSettingAsLong( PwmSetting.OTP_RECOVERY_CODES );
             otpSettings.totpPastIntervals = Integer.parseInt( config.readAppProperty( AppProperty.TOTP_PAST_INTERVALS ) );
             otpSettings.totpFutureIntervals = Integer.parseInt( config.readAppProperty( AppProperty.TOTP_FUTURE_INTERVALS ) );
             otpSettings.totpIntervalSeconds = Integer.parseInt( config.readAppProperty( AppProperty.TOTP_INTERVAL ) );

+ 13 - 4
server/src/main/java/password/pwm/util/operations/otp/LdapOtpOperator.java

@@ -31,6 +31,7 @@ import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -55,10 +56,15 @@ public class LdapOtpOperator extends AbstractOtpOperator
      * Read OTP secret and instantiate a OTP User Configuration object.
      */
     @Override
-    public OTPUserRecord readOtpUserConfiguration( final UserIdentity userIdentity, final String userGUID ) throws PwmUnrecoverableException
+    public OTPUserRecord readOtpUserConfiguration(
+            final UserIdentity userIdentity,
+            final String userGUID
+    )
+            throws PwmUnrecoverableException
     {
         final Configuration config = getPwmApplication().getConfig();
-        final String ldapStorageAttribute = config.readSettingAsString( PwmSetting.OTP_SECRET_LDAP_ATTRIBUTE );
+        final LdapProfile ldapProfile = config.getLdapProfiles().get( userIdentity.getLdapProfileID() );
+        final String ldapStorageAttribute = ldapProfile.readSettingAsString( PwmSetting.OTP_SECRET_LDAP_ATTRIBUTE );
         if ( ldapStorageAttribute == null || ldapStorageAttribute.length() < 1 )
         {
             final String errorMsg = "ldap storage attribute is not configured, unable to read OTP secret";
@@ -109,7 +115,8 @@ public class LdapOtpOperator extends AbstractOtpOperator
     ) throws PwmUnrecoverableException
     {
         final Configuration config = pwmApplication.getConfig();
-        final String ldapStorageAttribute = config.readSettingAsString( PwmSetting.OTP_SECRET_LDAP_ATTRIBUTE );
+        final LdapProfile ldapProfile = config.getLdapProfiles().get( userIdentity.getLdapProfileID() );
+        final String ldapStorageAttribute = ldapProfile.readSettingAsString( PwmSetting.OTP_SECRET_LDAP_ATTRIBUTE );
         if ( ldapStorageAttribute == null || ldapStorageAttribute.length() < 1 )
         {
             final String errorMsg = "ldap storage attribute is not configured, unable to write OTP secret";
@@ -170,7 +177,9 @@ public class LdapOtpOperator extends AbstractOtpOperator
     ) throws PwmUnrecoverableException
     {
         final Configuration config = pwmApplication.getConfig();
-        final String ldapStorageAttribute = config.readSettingAsString( PwmSetting.OTP_SECRET_LDAP_ATTRIBUTE );
+
+        final LdapProfile ldapProfile = config.getLdapProfiles().get( userIdentity.getLdapProfileID() );
+        final String ldapStorageAttribute = ldapProfile.readSettingAsString( PwmSetting.OTP_SECRET_LDAP_ATTRIBUTE );
         if ( ldapStorageAttribute == null || ldapStorageAttribute.length() < 1 )
         {
             final String errorMsg = "ldap storage attribute is not configured, unable to clear OTP secret";

+ 1 - 0
server/src/main/resources/password/pwm/AppProperty.properties

@@ -75,6 +75,7 @@ db.jdbcLoadStrategy=AppPathFileLoader,Classpath
 db.connections.max=5
 db.connections.timeoutMs=30000
 db.connections.watchdogFrequencySeconds=30
+db.init.haltOnIndexCreateError=false
 db.schema.keyLength=128
 download.filename.statistics.csv=Statistics.csv
 download.filename.reportSummary.csv=UserReportSummary.csv

+ 16 - 2
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -1692,6 +1692,15 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="true" key="otp.profile.list" level="1" required="false">
+        <regex>^(?!.*all.*)([a-zA-Z][a-zA-Z0-9-]{2,15})$</regex>
+        <properties>
+            <property key="Minimum">1</property>
+        </properties>
+        <default>
+            <value>default</value>
+        </default>
+    </setting>
     <setting hidden="false" key="otp.forceSetup" level="1" required="true">
         <default>
             <value><![CDATA[SKIP]]></value>
@@ -1755,7 +1764,7 @@
             <value><![CDATA[PWM]]></value>
         </default>
         <options>
-            <option value="PWM">PWM JSON</option>
+            <option value="PWM">JSON</option>
             <option value="BASE32SECRET">Base32 secret</option>
             <option value="OTPURL">OTP URL</option>
             <option value="PAM">PAM text</option>
@@ -3900,7 +3909,12 @@
     </category>
     <category hidden="false" key="TOKEN">
     </category>
-    <category hidden="false" key="OTP">
+    <category hidden="false" key="OTP_SETUP">
+    </category>
+    <category hidden="false" key="OTP_SETTINGS">
+    </category>
+    <category hidden="false" key="OTP_PROFILE" profiles="true">
+        <profile setting="otp.profile.list"/>
     </category>
     <category hidden="false" key="LOGGING">
     </category>

+ 13 - 7
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -81,7 +81,9 @@ Category_Description_NEWUSER_SETTINGS=New user self-registration settings.  The
 Category_Description_NOTES=Configuration Notes
 Category_Description_OAUTH=Integration with an OAuth identity server for SSO to this application.
 Category_Description_ORACLE_DS=Oracle Directory Server-specific settings
-Category_Description_OTP=Options for time-based one time passwords.
+Category_Description_OTP_PROFILE=Options for time-based one time passwords.
+Category_Description_OTP_SETTINGS=Options for time-based one time passwords.
+Category_Description_OTP_SETUP=Options for time-based one time passwords.
 Category_Description_PASSWORD_GLOBAL=Password related settings that apply to all users regardless of the password policy or profile appear here.  For profile-specific password settings, see Profiles -> Password Policy Profiles.
 Category_Description_PASSWORD_POLICY=Settings that define the LDAP directories that are used by the application.  If the user identities are in multiple LDAP directories, configure each directory as an LDAP Directory Profile.  Within each LDAP directory profile definition, you can control the individual servers and other settings for each LDAP directory.
 Category_Description_PEOPLE_SEARCH=The people search module provides basic white pages or directory lookup functionality to your users.  Customizations allow easy searching and display quick detailed information about your users' colleagues.
@@ -175,7 +177,9 @@ Category_Label_NEWUSER_SETTINGS=New User Settings
 Category_Label_NOTES=Configuration Notes
 Category_Label_OAUTH=OAuth
 Category_Label_ORACLE_DS=Oracle DS
-Category_Label_OTP=One Time Password
+Category_Label_OTP_PROFILE=OTP Profile
+Category_Label_OTP_SETTINGS=OTP Settings
+Category_Label_OTP_SETUP=Setup OTP
 Category_Label_PASSWORD_GLOBAL=Password Settings
 Category_Label_PASSWORD_POLICY=Password Policies
 Category_Label_PEOPLE_SEARCH=People Search
@@ -488,10 +492,11 @@ Setting_Description_oauth.idserver.dnAttributeName=Specify the attribute to requ
 Setting_Description_oauth.idserver.loginUrl=Specify the OAuth server login URL. This is the URL to redirect the user to for authentication.
 Setting_Description_oauth.idserver.secret=Specify the OAuth shared secret. The OAuth identity service provider (IdP) provides this value.
 Setting_Description_oauth.idserver.serverCerts=Import the certificate for the OAuth web service server.
-Setting_Description_otp.enabled=Enable this option to enable the configuration and use of a one-time password.
+Setting_Description_otp.enabled=Enable this option to allow the user to configure and save an one time password.
 Setting_Description_otp.forceSetup=Enable this option and enabled one-time passwords to have @PwmAppName@ direct the user to configure a one-time password secret when logging in. @PwmAppName@ forces the user to configure one-time password if they do not have a current valid secret stored.
-Setting_Description_otp.secret.allowSetup.queryMatch=Specify the permission used to determine if @PwmAppName@ permits a user to setup a one-time password secret.
-Setting_Description_otp.secret.encrypt=Enable this option to have @PwmAppName@ uses the Security Key to encrypt and decrypt token information, to make sure it is not readable as plain text. Multiple application instances must use the same Security Key.  If you change the Security Key, stored OTP passwords are no longer usable.
+Setting_Description_otp.profile.list=List of OTP Profiles
+Setting_Description_otp.secret.allowSetup.queryMatch=Specify the set of users that this OTP Setup profile will include.
+Setting_Description_otp.secret.encrypt=Enable this option to have @PwmAppName@ use the Security Key to encrypt and decrypt token information, to make sure it is not readable as plain text. Multiple application instances must use the same Security Key.  If you change the Security Key, stored OTP passwords are no longer usable.
 Setting_Description_otp.secret.identifier=Specify the User Identifier for OTP.  Macros are available.
 Setting_Description_otp.secret.ldap.attribute=Specify the LDAP attribute for storing the OTP secret. @PwmAppName@ only uses this setting when the storage method is set to LDAP.
 Setting_Description_otp.secret.readPreference=Select the location where to read the OTP secret. If you select an option with multiple values, @PwmAppName@ reads each location in turn until it finds a stored response.
@@ -971,9 +976,10 @@ Setting_Label_oauth.idserver.dnAttributeName=OAuth User Name/DN Login Attribute
 Setting_Label_oauth.idserver.loginUrl=OAuth Login URL
 Setting_Label_oauth.idserver.secret=OAuth Shared Secret
 Setting_Label_oauth.idserver.serverCerts=OAUTH Web Service Server Certificate
-Setting_Label_otp.enabled=Enable One Time Passwords
+Setting_Label_otp.enabled=Allow Saving One Time Passwords
 Setting_Label_otp.forceSetup=Force Setup of One Time Passwords
-Setting_Label_otp.secret.allowSetup.queryMatch=OTP Secret Setup Permission
+Setting_Label_otp.profile.list=OTP Profiles
+Setting_Label_otp.secret.allowSetup.queryMatch=One Time Password Profile Match
 Setting_Label_otp.secret.encrypt=Encrypt OTP secret
 Setting_Label_otp.secret.identifier=OTP Secret Identifier
 Setting_Label_otp.secret.ldap.attribute=OTP Secret LDAP Attribute

+ 5 - 2
server/src/main/webapp/WEB-INF/jsp/setupotpsecret.jsp

@@ -25,6 +25,8 @@
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.util.operations.otp.OTPUserRecord" %>
 <%@ page import="password.pwm.http.PwmRequestAttribute" %>
+<%@ page import="password.pwm.config.profile.SetupOtpProfile" %>
+<%@ page import="password.pwm.http.servlet.SetupOtpServlet" %>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true"
          contentType="text/html" %>
@@ -34,10 +36,11 @@
     boolean allowSkip = false;
     boolean forcedPageView = false;
     try {
-        final PwmRequest pwmRequest = PwmRequest.forRequest(request, response);
+        final PwmRequest pwmRequest = JspUtility.getPwmRequest( pageContext );
         final SetupOtpBean setupOtpBean = JspUtility.getSessionBean(pageContext, SetupOtpBean.class);
+        final SetupOtpProfile setupOtpProfile = SetupOtpServlet.getSetupOtpProfile( pwmRequest );
         otpUserRecord = setupOtpBean.getOtpUserRecord();
-        allowSkip = pwmRequest.getConfig().readSettingAsEnum(PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class) == ForceSetupPolicy.FORCE_ALLOW_SKIP;
+        allowSkip = setupOtpProfile.readSettingAsEnum(PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class) == ForceSetupPolicy.FORCE_ALLOW_SKIP;
         forcedPageView = pwmRequest.isForcedPageView();
     } catch (PwmUnrecoverableException e) {
         /* application must be unavailable */

+ 13 - 15
server/src/main/webapp/private/index.jsp

@@ -38,10 +38,10 @@
     </jsp:include>
 
     <div id="centerbody" class="tile-centerbody">
-    <pwm:if test="<%=PwmIfTest.endUserFunctionalityAvailable%>" negate="true">
-        <p><pwm:display key="Warning_NoEndUserModules" bundle="Config"/></p>
-        <br/>
-    </pwm:if>
+        <pwm:if test="<%=PwmIfTest.endUserFunctionalityAvailable%>" negate="true">
+            <p><pwm:display key="Warning_NoEndUserModules" bundle="Config"/></p>
+            <br/>
+        </pwm:if>
         <pwm:if test="<%=PwmIfTest.endUserFunctionalityAvailable%>">
             <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.CHANGE_PASSWORD%>">
                 <a id="button_ChangePassword" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.PrivateChangePassword.servletUrl()%>'/>">
@@ -97,18 +97,16 @@
                 </pwm:if>
             </pwm:if>
 
-            <pwm:if test="<%=PwmIfTest.otpEnabled%>">
-                <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.SETUP_OTP_SECRET%>">
-                    <a id="button_SetupOtpSecret" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.SetupOtp.servletUrl()%>'/>">
-                        <div class="tile">
-                            <div class="tile-content">
-                                <div class="tile-image mobile-image"></div>
-                                <div class="tile-title" title="<pwm:display key='Title_SetupOtpSecret'/>"><pwm:display key="Title_SetupOtpSecret"/></div>
-                                <div class="tile-subtitle" title="<pwm:display key='Long_Title_SetupOtpSecret'/>"><pwm:display key="Long_Title_SetupOtpSecret"/></div>
-                            </div>
+            <pwm:if test="<%=PwmIfTest.otpSetupEnabled%>">
+                <a id="button_SetupOtpSecret" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.SetupOtp.servletUrl()%>'/>">
+                    <div class="tile">
+                        <div class="tile-content">
+                            <div class="tile-image mobile-image"></div>
+                            <div class="tile-title" title="<pwm:display key='Title_SetupOtpSecret'/>"><pwm:display key="Title_SetupOtpSecret"/></div>
+                            <div class="tile-subtitle" title="<pwm:display key='Long_Title_SetupOtpSecret'/>"><pwm:display key="Long_Title_SetupOtpSecret"/></div>
                         </div>
-                    </a>
-                </pwm:if>
+                    </div>
+                </a>
             </pwm:if>
 
             <pwm:if test="<%=PwmIfTest.updateProfileAvailable%>">

+ 9 - 0
server/src/test/java/password/pwm/config/PwmSettingCategoryTest.java

@@ -53,6 +53,15 @@ public class PwmSettingCategoryTest {
         }
     }
 
+    @Test
+    public void testProfileSettingSyntax() {
+        for (final PwmSettingCategory category : PwmSettingCategory.values()) {
+            if (category.hasProfiles()) {
+                final PwmSetting pwmSetting = category.getProfileSetting();
+                Assert.assertEquals( pwmSetting.getSyntax(), PwmSettingSyntax.PROFILE );
+            }
+        }
+    }
 
     @Test
     public void testProfileCategoryHasSettingsOrChildren() {