Browse Source

export orgchart server side functionality

jrivard@gmail.com 6 years ago
parent
commit
03b1b382b0
28 changed files with 833 additions and 274 deletions
  1. 6 2
      server/src/main/java/password/pwm/AppProperty.java
  2. 10 0
      server/src/main/java/password/pwm/PwmApplication.java
  3. 2 2
      server/src/main/java/password/pwm/PwmConstants.java
  4. 18 0
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  5. 5 1
      server/src/main/java/password/pwm/config/PwmSetting.java
  6. 0 1
      server/src/main/java/password/pwm/config/option/IdentityVerificationMethod.java
  7. 2 2
      server/src/main/java/password/pwm/config/profile/ForgottenPasswordProfile.java
  8. 1 1
      server/src/main/java/password/pwm/http/SessionManager.java
  9. 4 0
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java
  10. 5 11
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  11. 208 0
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationOptionsBean.java
  12. 20 22
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchClientConfigBean.java
  13. 56 42
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchConfiguration.java
  14. 266 53
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  15. 76 45
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  16. 6 42
      server/src/main/java/password/pwm/http/servlet/peoplesearch/SearchResultBean.java
  17. 2 1
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  18. 2 14
      server/src/main/java/password/pwm/ldap/PhotoDataBean.java
  19. 31 0
      server/src/main/java/password/pwm/svc/cache/CacheLoader.java
  20. 30 19
      server/src/main/java/password/pwm/svc/cache/CacheService.java
  21. 4 1
      server/src/main/java/password/pwm/svc/cache/CacheStore.java
  22. 47 10
      server/src/main/java/password/pwm/svc/cache/MemoryCacheStore.java
  23. 5 0
      server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java
  24. 1 0
      server/src/main/java/password/pwm/util/java/JsonUtil.java
  25. 6 2
      server/src/main/resources/password/pwm/AppProperty.properties
  26. 15 0
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  27. 5 1
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  28. 0 2
      webapp/src/main/webapp/META-INF/context.xml

+ 6 - 2
server/src/main/java/password/pwm/AppProperty.java

@@ -273,11 +273,15 @@ public enum AppProperty
     PWNOTIFY_BATCH_DELAY_TIME_MULTIPLIER            ( "pwNotify.batch.delayTimeMultiplier" ),
     PWNOTIFY_MAX_LDAP_SEARCH_SIZE                   ( "pwNotify.maxLdapSearchSize" ),
     PWNOTIFY_MAX_SKIP_RERUN_WINDOW_SECONDS          ( "pwNotify.maxSkipRerunWindowSeconds" ),
+    PEOPLESEARCH_EXPORT_CSV_MAX_DEPTH               ( "peoplesearch.export.csv.maxDepth" ),
+    PEOPLESEARCH_EXPORT_CSV_MAX_ITEMS               ( "peoplesearch.export.csv.maxItems" ),
+    PEOPLESEARCH_EXPORT_CSV_MAX_SECONDS             ( "peoplesearch.export.csv.maxSeconds" ),
+    PEOPLESEARCH_EXPORT_CSV_MAX_THREADS             ( "peoplesearch.export.csv.threads" ),
+    PEOPLESEARCH_ORGCHART_ENABLE_CHILD_COUNT        ( "peoplesearch.orgChart.enableChildCount" ),
+    PEOPLESEARCH_ORGCHART_MAX_PARENTS               ( "peoplesearch.orgChart.maxParents" ),
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ( "peoplesearch.values.verifyUserDN" ),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ( "peoplesearch.values.maxCount" ),
     PEOPLESEARCH_VIEW_DETAIL_LINKS                  ( "peoplesearch.view.detail.links" ),
-    PEOPLESEARCH_ORGCHART_ENABLE_CHILD_COUNT        ( "peoplesearch.orgChart.enableChildCount" ),
-    PEOPLESEARCH_ORGCHART_MAX_PARENTS               ( "peoplesearch.orgChart.maxParents" ),
     QUEUE_EMAIL_RETRY_TIMEOUT_MS                    ( "queue.email.retryTimeoutMs" ),
     QUEUE_EMAIL_MAX_COUNT                           ( "queue.email.maxCount" ),
     QUEUE_EMAIL_MAX_THREADS                         ( "queue.email.maxThreads" ),

+ 10 - 0
server/src/main/java/password/pwm/PwmApplication.java

@@ -53,6 +53,7 @@ import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
+import password.pwm.svc.sessiontrack.UserAgentUtils;
 import password.pwm.svc.shorturl.UrlShortenerService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
@@ -401,6 +402,15 @@ public class PwmApplication
 
         MBeanUtility.registerMBean( this );
 
+        try
+        {
+            UserAgentUtils.initializeCache();
+        }
+        catch ( Exception e )
+        {
+            LOGGER.debug( "error initializing UserAgentUtils: " + e.getMessage() );
+        }
+
         LOGGER.trace( "completed post init tasks in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
     }
 

+ 2 - 2
server/src/main/java/password/pwm/PwmConstants.java

@@ -64,8 +64,8 @@ public abstract class PwmConstants
     {
         final String servletVersion =
                 ( BUILD_VERSION.length() > 0 ? "v" + BUILD_VERSION : "" )
-                        + ( BUILD_NUMBER.length() > 0 ? " b" + BUILD_NUMBER : "" )
-                        + ( BUILD_REVISION.length() > 0 ? " r" + BUILD_REVISION : "" ).trim();
+                        + ( BUILD_VERSION.length() > 0 ? " b" + BUILD_NUMBER : "" )
+                        + ( BUILD_NUMBER.length() > 0 ? " r" + BUILD_REVISION : "" ).trim();
 
         SERVLET_VERSION = servletVersion.isEmpty()
                 ? MISSING_VERSION_STRING

+ 18 - 0
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -168,4 +168,22 @@ public class TokenDestinationItem implements Serializable
 
         return Optional.empty();
     }
+
+    public static List<TokenDestinationItem> stripValues( final List<TokenDestinationItem> input )
+    {
+        final List<TokenDestinationItem> returnList = new ArrayList<>();
+        if ( input != null )
+        {
+            for ( final TokenDestinationItem item : input )
+            {
+                final TokenDestinationItem newItem = TokenDestinationItem.builder()
+                        .display( item.display )
+                        .id( item.id )
+                        .type ( item.type )
+                        .build();
+                returnList.add( newItem );
+            }
+        }
+        return returnList;
+    }
 }

+ 5 - 1
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -272,10 +272,12 @@ public enum PwmSetting
             "peopleSearch.photo.urlOverride", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     LDAP_ATTRIBUTE_ORGCHART_PARENT(
             "peopleSearch.orgChart.parentAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
-    LDAP_ATTRIBUTE_ORGCHARD_CHILD(
+    LDAP_ATTRIBUTE_ORGCHART_CHILD(
             "peopleSearch.orgChart.childAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     LDAP_ATTRIBUTE_ORGCHART_ASSISTANT(
             "peopleSearch.orgChart.assistantAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
+    LDAP_ATTRIBUTE_ORGCHART_WORKFORCEID(
+            "peopleSearch.orgChart.workforceIdAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     AUTO_ADD_OBJECT_CLASSES(
             "ldap.addObjectClasses", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.LDAP_ATTRIBUTES ),
 
@@ -963,6 +965,8 @@ public enum PwmSetting
             "peopleSearch.enablePublic", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_ENABLE_ORGCHART(
             "peopleSearch.enableOrgChart", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
+    PEOPLE_SEARCH_ENABLE_EXPORT(
+            "peopleSearch.enableExport", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS(
             "peopleSearch.idleTimeout", PwmSettingSyntax.DURATION, PwmSettingCategory.PEOPLE_SEARCH ),
 

+ 0 - 1
server/src/main/java/password/pwm/config/option/IdentityVerificationMethod.java

@@ -73,5 +73,4 @@ public enum IdentityVerificationMethod implements Serializable, ConfigurationOpt
         values.addAll( Arrays.asList( IdentityVerificationMethod.values() ) );
         return values.toArray( new IdentityVerificationMethod[ values.size() ] );
     }
-
 }

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

@@ -84,9 +84,9 @@ public class ForgottenPasswordProfile extends AbstractProfile
         return optionalRecoveryVerificationMethods;
     }
 
-    private Set<IdentityVerificationMethod> readRecoveryAuthMethods( final VerificationMethodValue.EnabledState enabledState )
+    private Set<IdentityVerificationMethod> readRecoveryAuthMethods( final VerificationMethodValue.EnabledState enforcement )
     {
-        return this.readVerificationMethods( PwmSetting.RECOVERY_VERIFICATION_METHODS, enabledState );
+        return this.readVerificationMethods( PwmSetting.RECOVERY_VERIFICATION_METHODS, enforcement );
     }
 
     public int getMinOptionalRequired( )

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

@@ -70,7 +70,7 @@ public class SessionManager
     }
 
     public ChaiProvider getChaiProvider( )
-            throws ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         if ( chaiProvider == null )
         {

+ 4 - 0
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java

@@ -89,6 +89,8 @@ public class HelpdeskDetailInfoBean implements Serializable
 
     private Set<StandardButton> visibleButtons;
     private Set<StandardButton> enabledButtons;
+
+    private HelpdeskVerificationOptionsBean verificationOptions;
     
     public enum StandardButton
     {
@@ -223,6 +225,8 @@ public class HelpdeskDetailInfoBean implements Serializable
             builder.enabledButtons( determineEnabledButtons( visibleButtons, userInfo ) );
         }
 
+        builder.verificationOptions( HelpdeskVerificationOptionsBean.makeBean( pwmRequest, helpdeskProfile, userIdentity ) );
+
         final HelpdeskDetailInfoBean helpdeskDetailInfoBean = builder.build();
 
         if ( pwmRequest.getConfig().isDevDebugMode() )

+ 5 - 11
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -956,7 +956,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             final HelpdeskProfile helpdeskProfile,
             final UserIdentity userIdentity
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final boolean useProxy = helpdeskProfile.readSettingAsBoolean( PwmSetting.HELPDESK_USE_PROXY );
         return useProxy
@@ -979,8 +979,10 @@ public class HelpdeskServlet extends ControlledPwmServlet
         final String rawVerificationStr = bodyMap.get( HelpdeskVerificationStateBean.PARAMETER_VERIFICATION_STATE_KEY );
         final HelpdeskVerificationStateBean state = HelpdeskVerificationStateBean.fromClientString( pwmRequest, rawVerificationStr );
         final boolean passed = HelpdeskServletUtil.checkIfRequiredVerificationPassed( userIdentity, state, helpdeskProfile );
+        final HelpdeskVerificationOptionsBean optionsBean = HelpdeskVerificationOptionsBean.makeBean( pwmRequest, helpdeskProfile, userIdentity );
         final HashMap<String, Object> results = new HashMap<>();
         results.put( "passed", passed );
+        results.put( "verificationOptions", optionsBean );
         final RestResultBean restResultBean = RestResultBean.withData( results );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
@@ -1035,15 +1037,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             }
 
             final Map<String, String> bodyMap = JsonUtil.deserializeStringMap( bodyString );
-            final ChaiUser chaiUser;
-            try
-            {
-                chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
-            }
-            catch ( ChaiUnavailableException e )
-            {
-                throw new PwmUnrecoverableException( PwmError.forChaiError( e.getErrorCode() ) );
-            }
+            final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
 
             int successCount = 0;
             for ( final FormConfiguration formConfiguration : verificationForms )
@@ -1381,7 +1375,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
             resp.setContentType( photoData.getMimeType() );
 
-            outputStream.write( photoData.getContents() );
+            outputStream.write( photoData.getContents().getBytes() );
         }
         return ProcessStatus.Halt;
     }

+ 208 - 0
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationOptionsBean.java

@@ -0,0 +1,208 @@
+/*
+ * 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.http.servlet.helpdesk;
+
+
+import com.novell.ldapchai.ChaiUser;
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.profile.HelpdeskProfile;
+import password.pwm.config.value.VerificationMethodValue;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
+import password.pwm.svc.token.TokenUtil;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.logging.PwmLogger;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+@Value
+@Builder
+public class HelpdeskVerificationOptionsBean implements Serializable
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( HelpdeskVerificationOptionsBean.class );
+
+    private Map<VerificationMethodValue.EnabledState, Collection<IdentityVerificationMethod>> verificationMethods;
+    private List<HelpdeskClientDataBean.FormInformation> verificationForm;
+    private List<TokenDestinationItem> tokenDestinations;
+
+    static HelpdeskVerificationOptionsBean makeBean(
+            final PwmRequest pwmRequest,
+            final HelpdeskProfile helpdeskProfile,
+            final UserIdentity targetUser
+
+    )
+            throws PwmUnrecoverableException
+    {
+        final ChaiUser theUser = HelpdeskServlet.getChaiUser( pwmRequest, helpdeskProfile, targetUser );
+        final UserInfo userInfo = UserInfoFactory.newUserInfo(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                targetUser,
+                theUser.getChaiProvider() );
+
+        final Locale locale = pwmRequest.getLocale();
+
+        final List<HelpdeskClientDataBean.FormInformation> formInformations;
+        {
+            final List<HelpdeskClientDataBean.FormInformation> returnList = new ArrayList<>();
+            final List<FormConfiguration> attributeVerificationForm = helpdeskProfile.readSettingAsForm( PwmSetting.HELPDESK_VERIFICATION_FORM );
+            if ( attributeVerificationForm != null )
+            {
+                for ( final FormConfiguration formConfiguration : attributeVerificationForm )
+                {
+                    final String name = formConfiguration.getName();
+                    String label = formConfiguration.getLabel( locale );
+                    label = ( label != null && !label.isEmpty() ) ? label : formConfiguration.getName();
+                    final HelpdeskClientDataBean.FormInformation formInformation = new HelpdeskClientDataBean.FormInformation( name, label );
+                    returnList.add( formInformation );
+                }
+            }
+            formInformations = Collections.unmodifiableList( returnList );
+        }
+
+        final List<TokenDestinationItem> tokenDestinations;
+        {
+            final MessageSendMethod testSetting = helpdeskProfile.readSettingAsEnum( PwmSetting.HELPDESK_TOKEN_SEND_METHOD, MessageSendMethod.class );
+            final List<TokenDestinationItem> returnList = new ArrayList<>( );
+
+            if ( testSetting != null && testSetting != MessageSendMethod.NONE )
+            {
+                try
+                {
+                    returnList.addAll( TokenUtil.figureAvailableTokenDestinations(
+                            pwmRequest.getPwmApplication(),
+                            pwmRequest.getSessionLabel(),
+                            pwmRequest.getLocale(),
+                            userInfo,
+                            testSetting
+                    ) );
+                }
+                catch ( PwmUnrecoverableException e )
+                {
+                    LOGGER.trace( "error while calculating available token methods: " + e.getMessage() );
+                }
+            }
+            tokenDestinations = Collections.unmodifiableList( TokenDestinationItem.stripValues( returnList ) );
+        }
+
+        final Set<IdentityVerificationMethod> unavailableMethods;
+        {
+            final Set<IdentityVerificationMethod> returnSet = new HashSet<>();
+            final Set<IdentityVerificationMethod> workSet = new HashSet<>();
+            workSet.addAll( helpdeskProfile.readOptionalVerificationMethods()  );
+            workSet.addAll( helpdeskProfile.readRequiredVerificationMethods()  );
+
+            for ( final IdentityVerificationMethod method : workSet )
+            {
+                switch ( method )
+                {
+                    case ATTRIBUTES:
+                    {
+                        if ( JavaHelper.isEmpty( formInformations ) )
+                        {
+                            returnSet.add( IdentityVerificationMethod.ATTRIBUTES );
+                        }
+                    }
+                    break;
+
+                    case OTP:
+                    {
+                        if ( userInfo.getOtpUserRecord() == null )
+                        {
+                            returnSet.add( IdentityVerificationMethod.OTP );
+                        }
+
+                    }
+                    break;
+
+                    case TOKEN:
+                    {
+                        if ( JavaHelper.isEmpty( tokenDestinations ) )
+                        {
+                            returnSet.add( IdentityVerificationMethod.TOKEN );
+                        }
+                    }
+                    break;
+
+                    default:
+                        break;
+                }
+            }
+
+            unavailableMethods = Collections.unmodifiableSet( returnSet );
+        }
+
+        final Map<VerificationMethodValue.EnabledState, Collection<IdentityVerificationMethod>> verificationMethodsMap;
+        {
+            final Map<VerificationMethodValue.EnabledState, Collection<IdentityVerificationMethod>> returnMap = new HashMap<>();
+            {
+                final Set<IdentityVerificationMethod> optionalMethods = new HashSet<>( helpdeskProfile.readOptionalVerificationMethods() );
+                optionalMethods.removeAll( unavailableMethods );
+                returnMap.put( VerificationMethodValue.EnabledState.optional, optionalMethods );
+            }
+            {
+                final Set<IdentityVerificationMethod> requiredMethods = new HashSet<>( helpdeskProfile.readRequiredVerificationMethods() );
+                requiredMethods.removeAll( unavailableMethods );
+                returnMap.put( VerificationMethodValue.EnabledState.required, requiredMethods );
+            }
+            verificationMethodsMap = Collections.unmodifiableMap( returnMap );
+        }
+
+        if (
+                JavaHelper.isEmpty( verificationMethodsMap.get( VerificationMethodValue.EnabledState.required ) )
+                        && !JavaHelper.isEmpty( helpdeskProfile.readRequiredVerificationMethods() )
+        )
+        {
+            final String msg = "configuration requires verification, but target user has no eligible required verification methods available.";
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_MISSING_CONTACT, msg );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        return HelpdeskVerificationOptionsBean.builder()
+                .tokenDestinations( tokenDestinations )
+                .verificationForm( formInformations )
+                .verificationMethods( verificationMethodsMap )
+                .build();
+    }
+}

+ 20 - 22
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchClientConfigBean.java

@@ -22,24 +22,22 @@
 
 package password.pwm.http.servlet.peoplesearch;
 
-import lombok.Getter;
-import lombok.Setter;
-import password.pwm.PwmApplication;
-import password.pwm.bean.SessionLabel;
+import lombok.Builder;
+import lombok.Value;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
 
 import java.io.Serializable;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 
-@Getter
-@Setter
+@Value
+@Builder
 public class PeopleSearchClientConfigBean implements Serializable
 {
     private Map<String, String> searchColumns;
@@ -47,34 +45,34 @@ public class PeopleSearchClientConfigBean implements Serializable
     private boolean orgChartEnabled;
     private boolean orgChartShowChildCount;
     private int orgChartMaxParents;
+    private boolean enableExport;
+    private int exportMaxDepth;
 
 
     static PeopleSearchClientConfigBean fromConfig(
-            final PwmApplication pwmApplication,
+            final PwmRequest pwmRequest,
             final PeopleSearchConfiguration peopleSearchConfiguration,
-            final Locale locale,
-            final UserIdentity userIdentity,
-            final SessionLabel sessionLabel
+            final UserIdentity userIdentity
     )
             throws PwmUnrecoverableException
     {
-        final Configuration configuration = pwmApplication.getConfig();
+        final Configuration configuration = pwmRequest.getConfig();
         final Map<String, String> searchColumns = new LinkedHashMap<>();
         final List<FormConfiguration> searchForm = configuration.readSettingAsForm( PwmSetting.PEOPLE_SEARCH_RESULT_FORM );
         for ( final FormConfiguration formConfiguration : searchForm )
         {
             searchColumns.put( formConfiguration.getName(),
-                    formConfiguration.getLabel( locale ) );
+                    formConfiguration.getLabel( pwmRequest.getLocale() ) );
         }
 
-        final PeopleSearchClientConfigBean peopleSearchClientConfigBean = new PeopleSearchClientConfigBean();
-        peopleSearchClientConfigBean.setSearchColumns( searchColumns );
-
-        peopleSearchClientConfigBean.setEnablePhoto( peopleSearchConfiguration.isPhotosEnabled( userIdentity, sessionLabel ) );
-        peopleSearchClientConfigBean.setOrgChartEnabled( peopleSearchConfiguration.isOrgChartEnabled() );
-        peopleSearchClientConfigBean.setOrgChartShowChildCount( peopleSearchConfiguration.isOrgChartShowChildCount() );
-        peopleSearchClientConfigBean.setOrgChartMaxParents( peopleSearchConfiguration.getOrgChartMaxParents() );
-
-        return peopleSearchClientConfigBean;
+        return PeopleSearchClientConfigBean.builder()
+                .searchColumns( searchColumns )
+                .enablePhoto( peopleSearchConfiguration.isPhotosEnabled( userIdentity, pwmRequest.getSessionLabel() ) )
+                .orgChartEnabled( peopleSearchConfiguration.isOrgChartEnabled() )
+                .orgChartShowChildCount( peopleSearchConfiguration.isOrgChartShowChildCount() )
+                .orgChartMaxParents( peopleSearchConfiguration.getOrgChartMaxParents() )
+                .enableExport( peopleSearchConfiguration.isEnableExportCsv() )
+                .exportMaxDepth( peopleSearchConfiguration.getExportCsvMaxDepth() )
+                .build();
     }
 }

+ 56 - 42
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchConfiguration.java

@@ -33,24 +33,20 @@ import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.ldap.LdapPermissionTester;
-import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
 
 import java.util.List;
 
 public class PeopleSearchConfiguration
 {
+    private final PwmRequest pwmRequest;
     private final PwmApplication pwmApplication;
 
-    private boolean orgChartEnabled;
-    private String orgChartParentAttr;
-    private String orgChartChildAttr;
-    private String orgChartAssistantAttr;
-    private boolean orgChartShowChildCount;
-    private int orgChartMaxParents;
 
-    private PeopleSearchConfiguration( final PwmApplication pwmApplication )
+    private PeopleSearchConfiguration( final PwmRequest pwmRequest )
     {
-        this.pwmApplication = pwmApplication;
+        this.pwmRequest = pwmRequest;
+        this.pwmApplication = pwmRequest.getPwmApplication();
     }
 
     public String getPhotoAttribute( final UserIdentity userIdentity )
@@ -59,13 +55,13 @@ public class PeopleSearchConfiguration
         return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_PHOTO );
     }
 
-    public String getPhotoUrlOverride( final UserIdentity userIdentity )
+    String getPhotoUrlOverride( final UserIdentity userIdentity )
     {
         final LdapProfile ldapProfile = userIdentity.getLdapProfile( pwmApplication.getConfig() );
         return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_PHOTO_URL_OVERRIDE );
     }
 
-    public boolean isPhotosEnabled( final UserIdentity actor, final SessionLabel sessionLabel )
+    boolean isPhotosEnabled( final UserIdentity actor, final SessionLabel sessionLabel )
             throws PwmUnrecoverableException
     {
         if ( actor == null )
@@ -75,61 +71,79 @@ public class PeopleSearchConfiguration
 
         final List<UserPermission> permissions =  pwmApplication.getConfig().readSettingAsUserPermission( PwmSetting.PEOPLE_SEARCH_PHOTO_QUERY_FILTER );
         return LdapPermissionTester.testUserPermissions( pwmApplication, sessionLabel, actor, permissions );
+    }
 
+    public boolean isOrgChartEnabled()
+    {
+        final Configuration config = pwmApplication.getConfig();
+        return config.readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_ORGCHART );
     }
 
-    public boolean isOrgChartEnabled( )
+    String getOrgChartParentAttr( final UserIdentity userIdentity )
     {
-        return orgChartEnabled;
+        final LdapProfile ldapProfile = userIdentity.getLdapProfile( pwmApplication.getConfig() );
+        return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_PARENT );
     }
 
-    public String getOrgChartParentAttr( )
+    String getOrgChartChildAttr( final UserIdentity userIdentity  )
     {
-        return orgChartParentAttr;
+        final LdapProfile ldapProfile = userIdentity.getLdapProfile( pwmApplication.getConfig() );
+        return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_CHILD );
     }
 
-    public String getOrgChartChildAttr( )
+    String getOrgChartAssistantAttr( final UserIdentity userIdentity  )
     {
-        return orgChartChildAttr;
+        final LdapProfile ldapProfile = userIdentity.getLdapProfile( pwmApplication.getConfig() );
+        return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_ASSISTANT );
     }
 
-    public String getOrgChartAssistantAttr( )
+    String getOrgChartWorkforceIDAttr( final UserIdentity userIdentity  )
     {
-        return orgChartAssistantAttr;
+        final LdapProfile ldapProfile = userIdentity.getLdapProfile( pwmApplication.getConfig() );
+        return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_WORKFORCEID );
     }
 
-    public boolean isOrgChartShowChildCount( )
+    boolean isOrgChartShowChildCount( )
     {
-        return orgChartShowChildCount;
+        return Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_ORGCHART_ENABLE_CHILD_COUNT ) );
     }
 
-    public int getOrgChartMaxParents( )
+    int getOrgChartMaxParents( )
     {
-        return orgChartMaxParents;
+        return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_ORGCHART_MAX_PARENTS ) );
     }
 
-    public static PeopleSearchConfiguration forRequest(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException
+    boolean isEnableExportCsv( )
+    {
+        return pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_EXPORT );
+    }
+
+    int getExportCsvMaxDepth( )
     {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final LdapProfile ldapProfile = pwmRequest.isAuthenticated()
-                ? pwmRequest.getUserInfoIfLoggedIn().getLdapProfile( pwmRequest.getConfig() )
-                : pwmRequest.getConfig().getDefaultLdapProfile();
+        return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_DEPTH ) );
+    }
 
-        final Configuration configuration = pwmApplication.getConfig();
-        final PeopleSearchConfiguration config = new PeopleSearchConfiguration( pwmApplication );
-        config.orgChartAssistantAttr = ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_ASSISTANT );
-        config.orgChartParentAttr = ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_PARENT );
-        config.orgChartChildAttr = ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHARD_CHILD );
-        config.orgChartEnabled = configuration.readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_ORGCHART )
-                && !StringUtil.isEmpty( config.orgChartParentAttr )
-                && !StringUtil.isEmpty( config.orgChartChildAttr );
+    TimeDuration getExportCsvMaxDuration( )
+    {
+        final int seconds = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_SECONDS ) );
+        return TimeDuration.of( seconds, TimeDuration.Unit.SECONDS );
+    }
 
-        config.orgChartShowChildCount = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.PEOPLESEARCH_ORGCHART_ENABLE_CHILD_COUNT ) );
-        config.orgChartMaxParents = Integer.parseInt( configuration.readAppProperty( AppProperty.PEOPLESEARCH_ORGCHART_MAX_PARENTS ) );
+    int getExportCsvMaxThreads( )
+    {
+        return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_THREADS ) );
+    }
 
-        return config;
+    int getExportCsvMaxItems( )
+    {
+        return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_ITEMS ) );
     }
+
+    public static PeopleSearchConfiguration forRequest(
+            final PwmRequest pwmRequest
+    )
+    {
+        return new PeopleSearchConfiguration( pwmRequest );
+    }
+
 }

+ 266 - 53
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -27,6 +27,8 @@ import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
+import lombok.Value;
+import org.apache.commons.csv.CSVPrinter;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
@@ -50,16 +52,19 @@ import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.search.UserSearchResults;
 import password.pwm.svc.cache.CacheKey;
+import password.pwm.svc.cache.CacheLoader;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 
+import java.io.IOException;
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -71,6 +76,12 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 class PeopleSearchDataReader
 {
@@ -79,7 +90,14 @@ class PeopleSearchDataReader
     private final PwmRequest pwmRequest;
     private final PeopleSearchConfiguration peopleSearchConfiguration;
 
-    PeopleSearchDataReader( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    private enum CacheIdentifier
+    {
+        attributeRead,
+        checkIfViewable,
+        searchResultBean,
+    }
+
+    PeopleSearchDataReader( final PwmRequest pwmRequest )
     {
         this.pwmRequest = pwmRequest;
         this.peopleSearchConfiguration = PeopleSearchConfiguration.forRequest( pwmRequest );
@@ -89,7 +107,7 @@ class PeopleSearchDataReader
             final String searchData,
             final boolean includeDisplayName
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException
+            throws PwmUnrecoverableException
     {
         final CacheKey cacheKey = makeCacheKey( SearchResultBean.class.getSimpleName(), searchData + "|" + includeDisplayName );
 
@@ -98,9 +116,9 @@ class PeopleSearchDataReader
             final SearchResultBean cachedResult = pwmRequest.getPwmApplication().getCacheService().get( cacheKey, SearchResultBean.class );
             if ( cachedResult != null )
             {
-                cachedResult.setFromCache( true );
+                final SearchResultBean copyWithCacheSet = cachedResult.toBuilder().fromCache( true ).build();
                 StatisticsManager.incrementStat( pwmRequest, Statistic.PEOPLESEARCH_CACHE_HITS );
-                return cachedResult;
+                return copyWithCacheSet;
             }
             else
             {
@@ -109,8 +127,9 @@ class PeopleSearchDataReader
         }
 
         // if not in cache, build results from ldap
-        final SearchResultBean searchResultBean = makeSearchResultsImpl( pwmRequest, searchData, includeDisplayName );
-        searchResultBean.setFromCache( false );
+        final SearchResultBean searchResultBean = makeSearchResultsImpl( pwmRequest, searchData, includeDisplayName )
+                .toBuilder().fromCache( false ).build();
+
         StatisticsManager.incrementStat( pwmRequest, Statistic.PEOPLESEARCH_SEARCHES );
         storeDataInCache( pwmRequest.getPwmApplication(), cacheKey, searchResultBean );
         LOGGER.trace( pwmRequest, "returning " + searchResultBean.getSearchResults().size() + " results for search request '" + searchData + "'" );
@@ -153,7 +172,7 @@ class PeopleSearchDataReader
 
         {
             // make parent reference
-            final List<UserIdentity> parentIdentities = readUserDNAttributeValues( userIdentity, peopleSearchConfiguration.getOrgChartParentAttr() );
+            final List<UserIdentity> parentIdentities = readUserDNAttributeValues( userIdentity, peopleSearchConfiguration.getOrgChartParentAttr( userIdentity ) );
             if ( parentIdentities != null && !parentIdentities.isEmpty() )
             {
                 final UserIdentity parentIdentity = parentIdentities.iterator().next();
@@ -166,7 +185,7 @@ class PeopleSearchDataReader
         {
             // make children reference
             final Map<String, OrgChartReferenceBean> sortedChildren = new TreeMap<>();
-            final List<UserIdentity> childIdentities = readUserDNAttributeValues( userIdentity, peopleSearchConfiguration.getOrgChartChildAttr() );
+            final List<UserIdentity> childIdentities = readUserDNAttributeValues( userIdentity, peopleSearchConfiguration.getOrgChartChildAttr( userIdentity ) );
             for ( final UserIdentity childIdentity : childIdentities )
             {
                 final OrgChartReferenceBean childReference = makeOrgChartReferenceForIdentity( childIdentity );
@@ -187,9 +206,9 @@ class PeopleSearchDataReader
             orgChartData.setChildren( Collections.unmodifiableList( new ArrayList<>( sortedChildren.values() ) ) );
         }
 
-        if ( !StringUtil.isEmpty( peopleSearchConfiguration.getOrgChartAssistantAttr() ) )
+        if ( !StringUtil.isEmpty( peopleSearchConfiguration.getOrgChartAssistantAttr( userIdentity ) ) )
         {
-            final List<UserIdentity> assistantIdentities = readUserDNAttributeValues( userIdentity, peopleSearchConfiguration.getOrgChartAssistantAttr() );
+            final List<UserIdentity> assistantIdentities = readUserDNAttributeValues( userIdentity, peopleSearchConfiguration.getOrgChartAssistantAttr( userIdentity ) );
             if ( assistantIdentities != null && !assistantIdentities.isEmpty() )
             {
                 final UserIdentity assistantIdentity = assistantIdentities.iterator().next();
@@ -203,17 +222,16 @@ class PeopleSearchDataReader
 
         final TimeDuration totalTime = TimeDuration.fromCurrent( startTime );
         storeDataInCache( pwmRequest.getPwmApplication(), cacheKey, orgChartData );
-        LOGGER.trace( pwmRequest, "completed makeOrgChartData in " + totalTime.asCompactString() + " with " + childCount + " children" );
+        LOGGER.trace( pwmRequest, "completed makeOrgChartData of " + userIdentity.toDisplayString() + " in " + totalTime.asCompactString() + " with " + childCount + " children" );
         return orgChartData;
     }
 
     UserDetailBean makeUserDetailRequest(
-            final String userKey
+            final UserIdentity userIdentity
     )
-            throws PwmUnrecoverableException, PwmOperationalException, ChaiUnavailableException
+            throws PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
-        final UserIdentity userIdentity = UserIdentity.fromKey( userKey, pwmRequest.getPwmApplication() );
 
         final CacheKey cacheKey = makeCacheKey( UserDetailBean.class.getSimpleName(), userIdentity.toDelimitedKey() );
         {
@@ -229,21 +247,13 @@ class PeopleSearchDataReader
             }
         }
 
-        try
-        {
-            checkIfUserIdentityViewable( userIdentity );
-        }
-        catch ( PwmOperationalException e )
-        {
-            LOGGER.error( pwmRequest.getPwmSession(), "error during detail results request while checking if requested userIdentity is within search scope: " + e.getMessage() );
-            throw e;
-        }
+        checkIfUserIdentityViewable( userIdentity );
 
         final UserSearchResults detailResults = doDetailLookup( userIdentity );
         final Map<String, String> searchResults = detailResults.getResults().get( userIdentity );
 
         final UserDetailBean userDetailBean = new UserDetailBean();
-        userDetailBean.setUserKey( userKey );
+        userDetailBean.setUserKey( userIdentity.toObfuscatedKey( pwmRequest.getPwmApplication() ) );
         final List<FormConfiguration> detailFormConfig = pwmRequest.getConfig().readSettingAsForm( PwmSetting.PEOPLE_SEARCH_DETAIL_FORM );
         final Map<String, AttributeDetailBean> attributeBeans = convertResultMapToBeans( pwmRequest, userIdentity, detailFormConfig, searchResults );
 
@@ -415,18 +425,11 @@ class PeopleSearchDataReader
             final UserIdentity loopIdentity = new UserIdentity( userDN, userIdentity.getLdapProfileID() );
             if ( returnObj.size() < maxValues )
             {
-                try
-                {
-                    if ( checkUserDNValues )
-                    {
-                        checkIfUserIdentityViewable( loopIdentity );
-                    }
-                    returnObj.add( loopIdentity );
-                }
-                catch ( PwmOperationalException e )
+                if ( checkUserDNValues )
                 {
-                    LOGGER.debug( pwmRequest, "discarding userDN " + userDN + " from attribute " + attributeName + " because it does not match search filter" );
+                    checkIfUserIdentityViewable( loopIdentity );
                 }
+                returnObj.add( loopIdentity );
             }
             else
             {
@@ -452,6 +455,21 @@ class PeopleSearchDataReader
         }
     }
 
+    private <T extends Serializable> T storeDataInCache(
+            final CacheIdentifier operationIdentifier,
+            final String dataIdentifier,
+            final Class<T> classOfT,
+            final CacheLoader<T> cacheLoader
+    )
+            throws PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final CacheKey cacheKey = makeCacheKey( operationIdentifier.name(), dataIdentifier );
+        final long maxCacheSeconds = pwmApplication.getConfig().readSettingAsLong( PwmSetting.PEOPLE_SEARCH_MAX_CACHE_SECONDS );
+        final CachePolicy cachePolicy = CachePolicy.makePolicyWithExpirationMS( maxCacheSeconds * 1000 );
+        return pwmApplication.getCacheService().get( cacheKey, cachePolicy, classOfT, cacheLoader );
+    }
+
     private String figurePhotoURL(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity
@@ -533,7 +551,7 @@ class PeopleSearchDataReader
             final List<FormConfiguration> detailForm,
             final Map<String, String> searchResults
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final Set<String> searchAttributes = getSearchAttributes( pwmRequest.getConfig() );
         final Map<String, AttributeDetailBean> returnObj = new LinkedHashMap<>();
@@ -625,19 +643,34 @@ class PeopleSearchDataReader
     void checkIfUserIdentityViewable(
             final UserIdentity userIdentity
     )
-            throws PwmUnrecoverableException, PwmOperationalException
+            throws PwmUnrecoverableException
     {
-        final String filterSetting = getSearchFilter( pwmRequest.getConfig() );
-        String filterString = filterSetting.replace( PwmConstants.VALUE_REPLACEMENT_USERNAME, "*" );
-        while ( filterString.contains( "**" ) )
+        final Instant startTime = Instant.now();
+        final CacheLoader<Boolean> cacheLoader = () ->
         {
-            filterString = filterString.replace( "**", "*" );
-        }
+            final String filterSetting = getSearchFilter( pwmRequest.getConfig() );
+            String filterString = filterSetting.replace( PwmConstants.VALUE_REPLACEMENT_USERNAME, "*" );
+            while ( filterString.contains( "**" ) )
+            {
+                filterString = filterString.replace( "**", "*" );
+            }
 
-        final boolean match = LdapPermissionTester.testQueryMatch( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userIdentity, filterString );
-        if ( !match )
+            return LdapPermissionTester.testQueryMatch( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userIdentity, filterString );
+        };
+
+        final boolean result = storeDataInCache( CacheIdentifier.checkIfViewable, userIdentity.toDelimitedKey(), Boolean.class, cacheLoader );
+        try
+        {
+            if ( !result )
+            {
+                final String msg = "attempt to read data of out-of-scope userDN '" + userIdentity.toDisplayString() + "' by user " + userIdentity.toDisplayString();
+                LOGGER.warn( pwmRequest, msg );
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg );
+            }
+        }
+        finally
         {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "requested userDN is not available within configured search filter" ) );
+            LOGGER.trace( pwmRequest, "completed checkIfUserViewable for " + userIdentity.toDisplayString() + " in " + TimeDuration.compactFromCurrent( startTime ) );
         }
     }
 
@@ -696,12 +729,12 @@ class PeopleSearchDataReader
 
         if ( peopleSearchConfiguration.isOrgChartEnabled() )
         {
-            final String orgChartParentAttr = peopleSearchConfiguration.getOrgChartParentAttr();
+            final String orgChartParentAttr = peopleSearchConfiguration.getOrgChartParentAttr( userIdentity );
             if ( !attributeHeaderMap.containsKey( orgChartParentAttr ) )
             {
                 attributeHeaderMap.put( orgChartParentAttr, orgChartParentAttr );
             }
-            final String orgChartChildAttr = peopleSearchConfiguration.getOrgChartParentAttr();
+            final String orgChartChildAttr = peopleSearchConfiguration.getOrgChartParentAttr( userIdentity );
             if ( !attributeHeaderMap.containsKey( orgChartChildAttr ) )
             {
                 attributeHeaderMap.put( orgChartChildAttr, orgChartChildAttr );
@@ -743,13 +776,13 @@ class PeopleSearchDataReader
             final String username,
             final boolean includeDisplayName
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
 
         if ( username == null || username.length() < 1 )
         {
-            return new SearchResultBean();
+            return SearchResultBean.builder().searchResults( Collections.emptyList() ).build();
         }
 
         final boolean useProxy = useProxy();
@@ -811,9 +844,7 @@ class PeopleSearchDataReader
         LOGGER.trace( pwmRequest.getPwmSession(), "finished rest peoplesearch search in "
                 + searchDuration.asCompactString() + " not using cache, size=" + results.getResults().size() );
 
-        final SearchResultBean searchResultBean = new SearchResultBean();
-        searchResultBean.setSearchResults( resultOutput );
-        searchResultBean.setSizeExceeded( sizeExceeded );
+
         final String aboutMessage = LocaleHelper.getLocalizedMessage(
                 pwmRequest.getLocale(),
                 Display.Display_SearchResultsInfo.getKey(),
@@ -824,7 +855,189 @@ class PeopleSearchDataReader
                                 String.valueOf( results.getResults().size() ), searchDuration.asLongString( pwmRequest.getLocale() ),
                         }
         );
-        searchResultBean.setAboutResultMessage( aboutMessage );
-        return searchResultBean;
+
+        return SearchResultBean.builder()
+                .sizeExceeded( sizeExceeded )
+                .searchResults( resultOutput )
+                .aboutResultMessage( aboutMessage )
+                .build();
     }
+
+    private String readUserAttribute(
+            final UserIdentity userIdentity,
+            final String attribute
+    )
+            throws PwmUnrecoverableException
+    {
+        final CacheLoader<String> cacheLoader = () ->
+        {
+            try
+            {
+                return getChaiUser( userIdentity ).readStringAttribute( attribute );
+            }
+            catch ( ChaiOperationException e )
+            {
+                LOGGER.trace( pwmRequest, "error reading attribute for user '" + userIdentity.toDisplayString() + "', error: " + e.getMessage() );
+                return null;
+            }
+            catch ( ChaiUnavailableException e )
+            {
+                throw PwmUnrecoverableException.fromChaiException( e );
+            }
+        };
+
+        return storeDataInCache( CacheIdentifier.attributeRead, userIdentity.toDelimitedKey() + "|" + attribute, String.class, cacheLoader );
+    }
+
+    void writeUserOrgChartDetailToCsv(
+            final CSVPrinter csvPrinter,
+            final UserIdentity userIdentity,
+            final int depth
+    )
+    {
+        final Instant startTime = Instant.now();
+        LOGGER.trace( pwmRequest, "beginning csv export starting with user " + userIdentity.toDisplayString() + " and depth of " + depth );
+
+
+        final int threadCount = peopleSearchConfiguration.getExportCsvMaxThreads();
+        final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmRequest.getPwmApplication(), OrgChartCsvRowOutputJob.class ), true );
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                threadCount,
+                threadCount,
+                1,
+                TimeUnit.MINUTES,
+                new ArrayBlockingQueue<>( 5000 ),
+                threadFactory
+        );
+
+        final AtomicInteger rowCounter = new AtomicInteger( 0 );
+        final OrgChartExportState orgChartExportState = new OrgChartExportState(
+                executor,
+                csvPrinter,
+                rowCounter,
+                Collections.singleton( OrgChartExportState.IncludeData.displayForm )
+        );
+
+        final OrgChartCsvRowOutputJob job = new OrgChartCsvRowOutputJob( orgChartExportState, userIdentity, depth, null );
+        executor.execute( job );
+
+        final TimeDuration maxDuration = peopleSearchConfiguration.getExportCsvMaxDuration();
+        JavaHelper.pause( maxDuration.asMillis(), 1000, o -> ( executor.getQueue().size() + executor.getActiveCount() <= 0 ) );
+        executor.shutdown();
+
+        final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
+        LOGGER.trace( pwmRequest, "completed csv export of " + rowCounter.get() + " records in " + timeDuration.asCompactString() );
+    }
+
+    @Value
+    private static class OrgChartExportState
+    {
+        private final Executor executor;
+        private final CSVPrinter csvPrinter;
+        private final AtomicInteger rowCounter;
+        private final Set<IncludeData> includeData;
+
+        enum IncludeData
+        {
+            displayCard,
+            displayForm,
+        }
+    }
+
+    private class OrgChartCsvRowOutputJob implements Runnable
+    {
+        private final OrgChartExportState orgChartExportState;
+        private final UserIdentity userIdentity;
+        private final int depth;
+        private final String parentWorkforceID;
+
+        OrgChartCsvRowOutputJob(
+                final OrgChartExportState orgChartExportState,
+                final UserIdentity userIdentity,
+                final int depth,
+                final String parentWorkforceID
+        )
+        {
+            this.orgChartExportState = orgChartExportState;
+            this.userIdentity = userIdentity;
+            this.depth = depth;
+            this.parentWorkforceID = parentWorkforceID;
+        }
+
+        @Override
+        public void run()
+        {
+            try
+            {
+                doJob();
+            }
+            catch ( Exception e )
+            {
+                LOGGER.error( pwmRequest, "error exporting csv row data: " + e.getMessage() );
+            }
+        }
+
+        private void doJob()
+                throws PwmUnrecoverableException, IOException
+        {
+            final List<String> outputRowValues = new ArrayList<>( );
+            final String workforceIDattr = peopleSearchConfiguration.getOrgChartWorkforceIDAttr( userIdentity );
+
+            final String workforceID = readUserAttribute( userIdentity, workforceIDattr );
+            outputRowValues.add( workforceID == null ? "" : workforceID );
+            outputRowValues.add( parentWorkforceID == null ? "" : parentWorkforceID );
+
+            final OrgChartDataBean orgChartDataBean = makeOrgChartData( userIdentity, false );
+
+            // export display card
+            if ( orgChartExportState.getIncludeData().contains( OrgChartExportState.IncludeData.displayCard ) )
+            {
+                outputRowValues.addAll( orgChartDataBean.getSelf().displayNames );
+            }
+
+
+            // export form detail
+            if ( orgChartExportState.getIncludeData().contains( OrgChartExportState.IncludeData.displayForm ) )
+            {
+                final UserDetailBean userDetailBean = makeUserDetailRequest( userIdentity );
+                for ( final Map.Entry<String, AttributeDetailBean> entry : userDetailBean.getDetail().entrySet() )
+                {
+                    final List<String> values = entry.getValue().getValues();
+                    if ( JavaHelper.isEmpty( values ) )
+                    {
+                        outputRowValues.add( " " );
+                    }
+                    else if ( values.size() == 1 )
+                    {
+                        outputRowValues.add( values.iterator().next() );
+                    }
+                    else
+                    {
+                        final String row = StringUtil.collectionToString( values, " " );
+                        outputRowValues.add( row );
+                    }
+                }
+            }
+
+            orgChartExportState.getCsvPrinter().printRecord( outputRowValues );
+            orgChartExportState.getCsvPrinter().flush();
+
+            if ( depth > 0 && orgChartExportState.getRowCounter().get() < peopleSearchConfiguration.getExportCsvMaxItems() )
+            {
+                final List<OrgChartReferenceBean> children = orgChartDataBean.getChildren();
+                if ( !JavaHelper.isEmpty( children ) )
+                {
+                    for ( final OrgChartReferenceBean child : children )
+                    {
+                        final String childKey = child.getUserKey();
+                        final UserIdentity childIdentity = PeopleSearchServlet.readUserIdentityFromKey( pwmRequest, childKey );
+                        final OrgChartCsvRowOutputJob job = new OrgChartCsvRowOutputJob( orgChartExportState, childIdentity, depth - 1, workforceID );
+                        orgChartExportState.getExecutor().execute( job );
+                        orgChartExportState.getRowCounter().incrementAndGet();
+                    }
+                }
+            }
+        }
+    }
+
 }

+ 76 - 45
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -23,6 +23,7 @@
 package password.pwm.http.servlet.peoplesearch;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
+import org.apache.commons.csv.CSVPrinter;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
@@ -30,6 +31,8 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpContentType;
+import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.JspUrl;
 import password.pwm.http.ProcessStatus;
@@ -40,7 +43,9 @@ import password.pwm.http.servlet.ControlledPwmServlet;
 import password.pwm.ldap.PhotoDataBean;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.ws.server.RestResultBean;
 
@@ -58,6 +63,7 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
     private static final PwmLogger LOGGER = PwmLogger.forClass( PeopleSearchServlet.class );
 
     private static final String PARAM_USERKEY = "userKey";
+    private static final String PARAM_DEPTH = "depth";
 
     public enum PeopleSearchActions implements ProcessAction
     {
@@ -65,7 +71,8 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
         detail( HttpMethod.GET ),
         photo( HttpMethod.GET ),
         clientData( HttpMethod.GET ),
-        orgChartData( HttpMethod.GET ),;
+        orgChartData( HttpMethod.GET ),
+        exportOrgChart ( HttpMethod.GET ),;
 
         private final HttpMethod method;
 
@@ -118,11 +125,9 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
         final PeopleSearchConfiguration peopleSearchConfiguration = PeopleSearchConfiguration.forRequest( pwmRequest );
 
         final PeopleSearchClientConfigBean peopleSearchClientConfigBean = PeopleSearchClientConfigBean.fromConfig(
-                pwmRequest.getPwmApplication(),
+                pwmRequest,
                 peopleSearchConfiguration,
-                pwmRequest.getLocale(),
-                pwmRequest.getUserInfoIfLoggedIn(),
-                pwmRequest.getSessionLabel()
+                pwmRequest.getUserInfoIfLoggedIn()
         );
 
         final RestResultBean restResultBean = RestResultBean.withData( peopleSearchClientConfigBean );
@@ -164,11 +169,6 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
     {
         final PeopleSearchConfiguration peopleSearchConfiguration = PeopleSearchConfiguration.forRequest( pwmRequest );
 
-        if ( !peopleSearchConfiguration.isOrgChartEnabled() )
-        {
-            throw new PwmUnrecoverableException( PwmError.ERROR_SERVICE_NOT_AVAILABLE );
-        }
-
         final UserIdentity userIdentity;
         {
             final String userKey = pwmRequest.readParameterAsString( PARAM_USERKEY, PwmHttpRequestWrapper.Flag.BypassValidation );
@@ -186,6 +186,11 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
             }
         }
 
+        if ( !peopleSearchConfiguration.isOrgChartEnabled( ) )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_SERVICE_NOT_AVAILABLE );
+        }
+
         final boolean noChildren = pwmRequest.readParameterAsBoolean( "noChildren" );
 
         try
@@ -210,28 +215,17 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
     private ProcessStatus restUserDetailRequest(
             final PwmRequest pwmRequest
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
+            throws ChaiUnavailableException, PwmUnrecoverableException, IOException
     {
         final String userKey = pwmRequest.readParameterAsString( PARAM_USERKEY, PwmHttpRequestWrapper.Flag.BypassValidation );
-        if ( userKey == null || userKey.isEmpty() )
-        {
-            return ProcessStatus.Halt;
-        }
+        final UserIdentity userIdentity = readUserIdentityFromKey( pwmRequest, userKey );
 
-        try
-        {
-            final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader( pwmRequest );
-            final UserDetailBean detailData = peopleSearchDataReader.makeUserDetailRequest( userKey );
+        final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader( pwmRequest );
+        final UserDetailBean detailData = peopleSearchDataReader.makeUserDetailRequest( userIdentity );
 
-            addExpiresHeadersToResponse( pwmRequest );
-            pwmRequest.outputJsonResult( RestResultBean.withData( detailData ) );
-            pwmRequest.getPwmApplication().getStatisticsManager().incrementValue( Statistic.PEOPLESEARCH_DETAILS );
-        }
-        catch ( PwmOperationalException e )
-        {
-            LOGGER.error( pwmRequest, "error generating user detail object: " + e.getMessage() );
-            pwmRequest.respondWithError( e.getErrorInformation() );
-        }
+        addExpiresHeadersToResponse( pwmRequest );
+        pwmRequest.outputJsonResult( RestResultBean.withData( detailData ) );
+        pwmRequest.getPwmApplication().getStatisticsManager().incrementValue( Statistic.PEOPLESEARCH_DETAILS );
 
         return ProcessStatus.Halt;
     }
@@ -251,19 +245,7 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
 
 
         final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader( pwmRequest );
-        final UserIdentity userIdentity = UserIdentity.fromKey( userKey, pwmRequest.getPwmApplication() );
-        try
-        {
-            peopleSearchDataReader.checkIfUserIdentityViewable( userIdentity );
-        }
-        catch ( PwmOperationalException e )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED,
-                    "error during photo request while checking if requested userIdentity is within search scope: " + e.getMessage() );
-            LOGGER.error( pwmRequest, errorInformation );
-            pwmRequest.respondWithError( errorInformation, false );
-            return ProcessStatus.Halt;
-        }
+        final UserIdentity userIdentity = readUserIdentityFromKey( pwmRequest, userKey );
 
         LOGGER.debug( pwmRequest, "received user photo request to view user " + userIdentity.toString() );
 
@@ -287,7 +269,10 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
             final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
             resp.setContentType( photoData.getMimeType() );
 
-            outputStream.write( photoData.getContents() );
+            if ( photoData.getContents() != null )
+            {
+                outputStream.write( photoData.getContents().getBytes() );
+            }
         }
         return ProcessStatus.Halt;
     }
@@ -295,8 +280,54 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
     private void addExpiresHeadersToResponse( final PwmRequest pwmRequest )
     {
         final long maxCacheSeconds = pwmRequest.getConfig().readSettingAsLong( PwmSetting.PEOPLE_SEARCH_MAX_CACHE_SECONDS );
-        final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
-        resp.setDateHeader( "Expires", System.currentTimeMillis() + ( maxCacheSeconds * 1000L ) );
-        resp.setHeader( "Cache-Control", "private, max-age=" + maxCacheSeconds );
+        pwmRequest.getPwmResponse().getHttpServletResponse().setDateHeader( HttpHeader.Expires.getHttpName(), System.currentTimeMillis() + ( maxCacheSeconds * 1000L ) );
+        pwmRequest.getPwmResponse().setHeader( HttpHeader.CacheControl,  "private, max-age=" + maxCacheSeconds );
+    }
+
+    @ActionHandler( action = "exportOrgChart" )
+    private ProcessStatus processExportOrgChartRequest( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException
+    {
+        final String userKey = pwmRequest.readParameterAsString( PARAM_USERKEY, PwmHttpRequestWrapper.Flag.BypassValidation );
+        final int requestedDepth = pwmRequest.readParameterAsInt( PARAM_DEPTH, 1 );
+        final UserIdentity userIdentity = readUserIdentityFromKey( pwmRequest, userKey );
+        final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader( pwmRequest );
+
+        final PeopleSearchConfiguration peopleSearchConfiguration = PeopleSearchConfiguration.forRequest( pwmRequest );
+        final PeopleSearchClientConfigBean peopleSearchClientConfigBean = PeopleSearchClientConfigBean.fromConfig( pwmRequest, peopleSearchConfiguration, userIdentity );
+
+        if ( !peopleSearchClientConfigBean.isEnableExport() )
+        {
+            final String msg = "export service is not enabled";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg );
+        }
+
+        final int effectiveDepth = Math.max( peopleSearchClientConfigBean.getExportMaxDepth(), requestedDepth );
+
+        pwmRequest.getPwmResponse().getHttpServletResponse().setBufferSize( 0 );
+        pwmRequest.getPwmResponse().markAsDownload( HttpContentType.csv, "userData.csv" );
+
+        try ( CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( pwmRequest.getPwmResponse().getOutputStream() ) )
+        {
+            peopleSearchDataReader.writeUserOrgChartDetailToCsv( csvPrinter, userIdentity, effectiveDepth );
+        }
+
+        return ProcessStatus.Halt;
+    }
+
+    static UserIdentity readUserIdentityFromKey( final PwmRequest pwmRequest, final String userKey )
+            throws PwmUnrecoverableException
+    {
+        if ( StringUtil.isEmpty( userKey ) )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, PARAM_USERKEY + " parameter is missing" );
+            LOGGER.error( pwmRequest, errorInformation );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader( pwmRequest );
+        final UserIdentity userIdentity = UserIdentity.fromKey( userKey, pwmRequest.getPwmApplication() );
+        peopleSearchDataReader.checkIfUserIdentityViewable( userIdentity );
+        return userIdentity;
     }
 }

+ 6 - 42
server/src/main/java/password/pwm/http/servlet/peoplesearch/SearchResultBean.java

@@ -22,55 +22,19 @@
 
 package password.pwm.http.servlet.peoplesearch;
 
+import lombok.Builder;
+import lombok.Value;
+
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+@Value
+@Builder( toBuilder = true )
 class SearchResultBean implements Serializable
 {
-    private List<Map<String, Object>> searchResults = new ArrayList<>();
+    private List<Map<String, Object>> searchResults;
     private boolean sizeExceeded;
     private String aboutResultMessage;
     private boolean fromCache;
-
-    public List<Map<String, Object>> getSearchResults( )
-    {
-        return searchResults;
-    }
-
-    public void setSearchResults( final List<Map<String, Object>> searchResults )
-    {
-        this.searchResults = searchResults;
-    }
-
-    public boolean isSizeExceeded( )
-    {
-        return sizeExceeded;
-    }
-
-    public void setSizeExceeded( final boolean sizeExceeded )
-    {
-        this.sizeExceeded = sizeExceeded;
-    }
-
-    public String getAboutResultMessage( )
-    {
-        return aboutResultMessage;
-    }
-
-    public void setAboutResultMessage( final String aboutResultMessage )
-    {
-        this.aboutResultMessage = aboutResultMessage;
-    }
-
-    public boolean isFromCache( )
-    {
-        return fromCache;
-    }
-
-    public void setFromCache( final boolean fromCache )
-    {
-        this.fromCache = fromCache;
-    }
 }

+ 2 - 1
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -46,6 +46,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheKey;
@@ -978,7 +979,7 @@ public class LdapOperationsHelper
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_UNKNOWN, "error reading user photo ldap attribute: " + e.getMessage() ) );
         }
-        return new PhotoDataBean( mimeType, photoData );
+        return new PhotoDataBean( mimeType, new ImmutableByteArray( photoData ) );
     }
 
 }

+ 2 - 14
server/src/main/java/password/pwm/ldap/PhotoDataBean.java

@@ -23,23 +23,11 @@
 package password.pwm.ldap;
 
 import lombok.Value;
-
-import java.util.Arrays;
+import password.pwm.http.bean.ImmutableByteArray;
 
 @Value
 public class PhotoDataBean
 {
     private String mimeType;
-    private byte[] contents;
-
-    public PhotoDataBean( final String mimeType, final byte[] contents )
-    {
-        this.mimeType = mimeType;
-        this.contents = contents == null ? null : Arrays.copyOf( contents, contents.length );
-    }
-
-    public byte[] getContents( )
-    {
-        return contents == null ? null : Arrays.copyOf( contents, contents.length );
-    }
+    private ImmutableByteArray contents;
 }

+ 31 - 0
server/src/main/java/password/pwm/svc/cache/CacheLoader.java

@@ -0,0 +1,31 @@
+/*
+ * 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.svc.cache;
+
+import password.pwm.error.PwmUnrecoverableException;
+
+public interface CacheLoader<T>
+{
+    T read() throws PwmUnrecoverableException;
+}

+ 30 - 19
server/src/main/java/password/pwm/svc/cache/CacheService.java

@@ -42,6 +42,7 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.TreeMap;
 
 public class CacheService implements PwmService
@@ -133,37 +134,28 @@ public class CacheService implements PwmService
         {
             return;
         }
-        if ( cacheKey == null )
-        {
-            throw new NullPointerException( "cacheKey can not be null" );
-        }
-        if ( cachePolicy == null )
-        {
-            throw new NullPointerException( "cachePolicy can not be null" );
-        }
-        if ( payload == null )
-        {
-            throw new NullPointerException( "payload can not be null" );
-        }
+
+        Objects.requireNonNull( cacheKey );
+        Objects.requireNonNull( cachePolicy );
+        Objects.requireNonNull( payload );
+
         final Instant expirationDate = cachePolicy.getExpiration();
         memoryCacheStore.store( cacheKey, expirationDate, payload );
 
         traceDebugOutputter.conditionallyExecuteTask();
     }
 
-    public <T> T get( final CacheKey cacheKey, final Class<T> classOfT  )
+    public <T extends Serializable> T get( final CacheKey cacheKey, final Class<T> classOfT  )
     {
-        if ( cacheKey == null )
-        {
-            return null;
-        }
+        Objects.requireNonNull( cacheKey );
+        Objects.requireNonNull( classOfT );
 
         if ( status != STATUS.OPEN )
         {
             return null;
         }
 
-        Object payload = null;
+        T payload = null;
         if ( memoryCacheStore != null )
         {
             payload = memoryCacheStore.read( cacheKey, classOfT );
@@ -171,7 +163,26 @@ public class CacheService implements PwmService
 
         traceDebugOutputter.conditionallyExecuteTask();
 
-        return (T) payload;
+        return payload;
+    }
+
+    public <T extends Serializable> T get( final CacheKey cacheKey, final CachePolicy cachePolicy, final Class<T> classOfT, final CacheLoader<T> cacheLoader )
+            throws PwmUnrecoverableException
+    {
+        Objects.requireNonNull( cacheKey );
+        Objects.requireNonNull( cachePolicy );
+        Objects.requireNonNull( classOfT );
+        Objects.requireNonNull( cacheLoader );
+
+        if ( status != STATUS.OPEN )
+        {
+            return cacheLoader.read();
+        }
+
+        traceDebugOutputter.conditionallyExecuteTask();
+
+        final Instant expirationDate = cachePolicy.getExpiration();
+        return memoryCacheStore.readAndStore( cacheKey, expirationDate, classOfT, cacheLoader );
     }
 
     private void outputTraceInfo( )

+ 4 - 1
server/src/main/java/password/pwm/svc/cache/CacheStore.java

@@ -32,7 +32,10 @@ public interface CacheStore
 {
     void store( CacheKey cacheKey, Instant expirationDate, Serializable data ) throws PwmUnrecoverableException;
 
-    <T> T read( CacheKey cacheKey, Class<T> classOfT ) throws PwmUnrecoverableException;
+    <T extends Serializable> T readAndStore( CacheKey cacheKey, Instant expirationDate, Class<T> classOfT, CacheLoader<T> cacheLoader )
+            throws PwmUnrecoverableException;
+
+    <T extends Serializable> T read( CacheKey cacheKey, Class<T> classOfT ) throws PwmUnrecoverableException;
 
     CacheStoreInfo getCacheStoreInfo( );
 

+ 47 - 10
server/src/main/java/password/pwm/svc/cache/MemoryCacheStore.java

@@ -24,8 +24,7 @@ package password.pwm.svc.cache;
 
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -57,14 +56,34 @@ class MemoryCacheStore implements CacheStore
             throws PwmUnrecoverableException
     {
         cacheStoreInfo.incrementStoreCount();
-        memoryStore.put( cacheKey, new CacheValueWrapper( cacheKey, expirationDate, data ) );
+
+        final String jsonData = JsonUtil.serialize( data );
+        memoryStore.put( cacheKey, new CacheValueWrapper( cacheKey, expirationDate, jsonData ) );
     }
 
     @Override
-    public <T> T read( final CacheKey cacheKey, final Class<T> classOfT )
+    public <T extends Serializable> T readAndStore( final CacheKey cacheKey, final Instant expirationDate, final Class<T> classOfT, final CacheLoader<T> cacheLoader )
+            throws PwmUnrecoverableException
     {
         cacheStoreInfo.incrementReadCount();
-        final CacheValueWrapper valueWrapper = memoryStore.getIfPresent( cacheKey );
+        {
+            final CacheValueWrapper valueWrapper = memoryStore.getIfPresent( cacheKey );
+            final T extractedValue = extractValue( classOfT, valueWrapper, cacheKey );
+            if ( extractedValue != null )
+            {
+                return extractedValue;
+            }
+        }
+
+        final T data = cacheLoader.read();
+        final String jsonIfiedData = JsonUtil.serialize( data );
+        cacheStoreInfo.incrementMissCount();
+        memoryStore.put( cacheKey, new CacheValueWrapper( cacheKey, expirationDate, jsonIfiedData ) );
+        return data;
+    }
+
+    private <T extends Serializable> T extractValue( final Class<T> classOfT, final CacheValueWrapper valueWrapper, final CacheKey cacheKey )
+    {
         if ( valueWrapper != null )
         {
             if ( cacheKey.equals( valueWrapper.getCacheKey() ) )
@@ -72,10 +91,26 @@ class MemoryCacheStore implements CacheStore
                 if ( valueWrapper.getExpirationDate().isAfter( Instant.now() ) )
                 {
                     cacheStoreInfo.incrementHitCount();
-                    return (T) valueWrapper.getPayload();
+                    final String jsonValue  = valueWrapper.getPayload();
+                    return JsonUtil.deserialize( jsonValue, classOfT );
                 }
             }
         }
+
+        return null;
+    }
+
+    @Override
+    public <T extends Serializable> T read( final CacheKey cacheKey, final Class<T> classOfT )
+    {
+        cacheStoreInfo.incrementReadCount();
+        final CacheValueWrapper valueWrapper = memoryStore.getIfPresent( cacheKey );
+        final T extractedValue = extractValue( classOfT, valueWrapper, cacheKey );
+        if ( extractedValue != null )
+        {
+            return extractedValue;
+        }
+
         memoryStore.invalidate( cacheKey );
         cacheStoreInfo.incrementMissCount();
         return null;
@@ -121,13 +156,15 @@ class MemoryCacheStore implements CacheStore
         return Collections.unmodifiableList( items );
     }
 
-    @Getter
-    @AllArgsConstructor
-    static class CacheValueWrapper implements Serializable
+    @Value
+    private static class CacheValueWrapper implements Serializable
     {
         private final CacheKey cacheKey;
         private final Instant expirationDate;
-        private final Serializable payload;
+
+        // serialize to json even though stored in memory, this prevents object-reuse because we don't know
+        // if the object is immutable.  Thus an effective clone is made for each store/read.
+        private final String payload;
     }
 
     Map<String, Integer> storedClassHistogram( final String prefix )

+ 5 - 0
server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java

@@ -60,6 +60,11 @@ public class UserAgentUtils
         return cachedParser;
     }
 
+    public static void initializeCache() throws PwmUnrecoverableException
+    {
+        getUserAgentParser();
+    }
+
     public static void checkIfPreIE11( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     {
         final String userAgentString = pwmRequest.readHeaderValueAsString( HttpHeader.UserAgent );

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

@@ -346,6 +346,7 @@ public class JsonUtil
         gsonBuilder.registerTypeAdapter( X509Certificate.class, new X509CertificateAdapter() );
         gsonBuilder.registerTypeAdapter( byte[].class, new ByteArrayToBase64TypeAdapter() );
         gsonBuilder.registerTypeAdapter( PasswordData.class, new PasswordDataTypeAdapter() );
+        gsonBuilder.registerTypeAdapter( PwmLdapVendorTypeAdaptor.class, new PwmLdapVendorTypeAdaptor() );
         return gsonBuilder;
     }
 

+ 6 - 2
server/src/main/resources/password/pwm/AppProperty.properties

@@ -248,11 +248,15 @@ password.strength.threshold.strong=75
 password.strength.threshold.good=45
 password.strength.threshold.weak=20
 password.strength.threshold.veryWeak=0
+peoplesearch.export.csv.maxDepth=1
+peoplesearch.export.csv.maxItems=1000
+peoplesearch.export.csv.maxSeconds=600
+peoplesearch.export.csv.threads=10
+peoplesearch.orgChart.enableChildCount=true
+peoplesearch.orgChart.maxParents=50
 peoplesearch.values.verifyUserDN=true
 peoplesearch.values.maxCount=100
 peoplesearch.view.detail.links=
-peoplesearch.orgChart.enableChildCount=true
-peoplesearch.orgChart.maxParents=50
 pwNotify.batch.count=100
 pwNotify.batch.delayTimeMultiplier=1.0
 pwNotify.maxLdapSearchSize=1000000

+ 15 - 0
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -3130,6 +3130,11 @@
             <value>true</value>
         </default>
     </setting>
+    <setting hidden="false" key="peopleSearch.enableExport" level="1">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <setting hidden="false" key="peopleSearch.queryMatch" level="1" required="true">
         <ldapPermission actor="self_other" access="read"/>
         <default syntaxVersion="2">
@@ -3236,6 +3241,10 @@
         <default/>
     </setting>
     <setting hidden="false" key="peopleSearch.maxCacheSeconds" level="2">
+        <properties>
+            <property key="Minimum">60</property>
+            <property key="Maximum">86400</property>
+        </properties>
         <default>
             <value>600</value>
         </default>
@@ -3268,6 +3277,12 @@
             <value>assistant</value>
         </default>
     </setting>
+    <setting hidden="false" key="peopleSearch.orgChart.workforceIdAttribute" level="1">
+        <ldapPermission actor="self_other" access="read"/>
+        <default>
+            <value>workforceID</value>
+        </default>
+    </setting>
     <setting hidden="false" key="ldap.edirectory.storeNmasResponses" level="1" required="true">
         <default>
             <value>false</value>

+ 5 - 1
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -578,6 +578,7 @@ Setting_Description_peopleSearch.detail.form=Specify attributes to show in the d
 Setting_Description_peopleSearch.displayName.cardLabels=Specify the display labels for the user panel in the People Search detail and on the organizational chart views.  You can use LDAP attribute value such as <code>@LDAP\:givenName@</code> macros.
 Setting_Description_peopleSearch.displayName.user=Specify the display name for userDN type records.  Use macros to control the presentation such as the LDAP attribute macro <code>@LDAP\:givenName@</code>.
 Setting_Description_peopleSearch.enable=Enable this option to enable the People Search module.
+Setting_Description_peopleSearch.enableExport=Enable this option to allow download of organizational chart data.
 Setting_Description_peopleSearch.enableOrgChart=Enable this option to show an organizational chart of users.
 Setting_Description_peopleSearch.enablePublic=Enable this option to allow access to the People Search module for unauthenticated users.
 Setting_Description_peopleSearch.idleTimeout=Specify the number of seconds after which an authenticated session becomes unauthenticated.   If the value is set to 0, then @PwmAppName@ uses then the system-wide idle timeout value.  If a user is using the People Search module without authenticating, then the system does not apply a timeout.
@@ -585,6 +586,7 @@ Setting_Description_peopleSearch.maxCacheSeconds=Specify the number of seconds t
 Setting_Description_peopleSearch.orgChart.assistantAttribute=Specify the attribute that contains the LDAP DN of the assistant for a user.  If this setting is blank, @PwmAppName@ will not show the assistant on the organizational chart view.
 Setting_Description_peopleSearch.orgChart.childAttribute=Specify the attribute that contains the LDAP DN of the direct reports for a user.  If this setting is blank, @PwmAppName@ does not show the organizational chart view.
 Setting_Description_peopleSearch.orgChart.parentAttribute=Specify the attribute that contains the LDAP DN of the manager.  If this setting is blank, @PwmAppName@ does not show the organizational chart view.
+Setting_Description_peopleSearch.orgChart.workforceIdAttribute=Specify the attribute that contains the workforce ID of the user.   If this setting is blank, @PwmAppName@  data exports will not contain the workforce ID.
 Setting_Description_peopleSearch.photo.ldapAttribute=Specify the LDAP Attribute to use for a photo. Leave this option blank, if you do not want to display a photo.
 Setting_Description_peopleSearch.photo.queryFilter=Specify an LDAP permission filter to control photo visibility when displaying an organizational chart or detail record view. If a user does not match this permission, @PwmAppName@ does not display the user's photo.
 Setting_Description_peopleSearch.photo.urlOverride=Specify a URL to override the photo. If the LDAP directory does not store the users' photos, this setting can show photos from an external system.  If you specify this setting, @PwmAppName@ does not load the the photo from the LDAP directory.<br/><br/>Example\:<code>http\://photos.example.com/employee/@LDAP\:workforceID@.jpg</code>
@@ -1080,13 +1082,15 @@ Setting_Label_peopleSearch.detail.form=Search Detail Attributes
 Setting_Label_peopleSearch.displayName.cardLabels=Person Detail Display Labels
 Setting_Label_peopleSearch.displayName.user=UserDN Name Display
 Setting_Label_peopleSearch.enable=Enable People Search
+Setting_Label_peopleSearch.enableExport=Enable Export
 Setting_Label_peopleSearch.enableOrgChart=Enable Organizational Chart
 Setting_Label_peopleSearch.enablePublic=Enable People Search Public (Non-Authenticated) Access
 Setting_Label_peopleSearch.idleTimeout=Idle Timeout Seconds
-Setting_Label_peopleSearch.maxCacheSeconds=Search Maximum Cache Seconds
+Setting_Label_peopleSearch.maxCacheSeconds=Maximum Cache Seconds
 Setting_Label_peopleSearch.orgChart.assistantAttribute=Organizational Assistant Attribute
 Setting_Label_peopleSearch.orgChart.childAttribute=Organizational Chart Child Attribute
 Setting_Label_peopleSearch.orgChart.parentAttribute=Organizational Chart Parent Attribute
+Setting_Label_peopleSearch.orgChart.workforceIdAttribute=Organizational Chart Workforce ID Attribute
 Setting_Label_peopleSearch.photo.ldapAttribute=LDAP Photo Attribute
 Setting_Label_peopleSearch.photo.queryFilter=Photo Display Permission
 Setting_Label_peopleSearch.photo.urlOverride=Photo URL Override

+ 0 - 2
webapp/src/main/webapp/META-INF/context.xml

@@ -20,8 +20,6 @@
   -->
 
 <Context tldValidation="false" unloadDelay="30000" useHttpOnly="true" mapperContextRootRedirectEnabled="true">
-  <!--
   <Manager pathname=""/>
-  -->
   <!--pathname param prevents session persistence across restarts -->
 </Context>