Browse Source

- targetuser macro additions

Jason Rivard 4 years ago
parent
commit
000a8f7119

+ 6 - 10
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -37,6 +37,7 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
 import java.util.Map;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.TreeMap;
+import java.util.function.Function;
 
 
 public enum PwmAboutProperty
 public enum PwmAboutProperty
 {
 {
@@ -111,17 +112,12 @@ public enum PwmAboutProperty
             pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.databaseProductVersion ) ),;
             pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.databaseProductVersion ) ),;
 
 
     private final String label;
     private final String label;
-    private final ValueProvider valueProvider;
+    private final transient Function<PwmApplication, String> value;
 
 
-    PwmAboutProperty( final String label, final ValueProvider valueProvider )
+    PwmAboutProperty( final String label, final Function<PwmApplication, String> value )
     {
     {
         this.label = label;
         this.label = label;
-        this.valueProvider = valueProvider;
-    }
-
-    private interface ValueProvider
-    {
-        String value( PwmApplication pwmApplication );
+        this.value = value;
     }
     }
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmAboutProperty.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmAboutProperty.class );
@@ -134,12 +130,12 @@ public enum PwmAboutProperty
 
 
         for ( final PwmAboutProperty pwmAboutProperty : PwmAboutProperty.values() )
         for ( final PwmAboutProperty pwmAboutProperty : PwmAboutProperty.values() )
         {
         {
-            final ValueProvider valueProvider = pwmAboutProperty.valueProvider;
+            final Function<PwmApplication, String> valueProvider = pwmAboutProperty.value;
             if ( valueProvider != null )
             if ( valueProvider != null )
             {
             {
                 try
                 try
                 {
                 {
-                    final String value = valueProvider.value( pwmApplication );
+                    final String value = valueProvider.apply( pwmApplication );
                     aboutMap.put( pwmAboutProperty.name(), value == null ? "" : value );
                     aboutMap.put( pwmAboutProperty.name(), value == null ? "" : value );
                 }
                 }
                 catch ( final Throwable t )
                 catch ( final Throwable t )

+ 34 - 35
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -401,7 +401,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorString, new String[]
             final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorString, new String[]
                     {
                     {
                             errorString,
                             errorString,
-                    }
+                            }
             );
             );
             pwmRequest.outputJsonResult( RestResultBean.fromError( errorInfo, pwmRequest ) );
             pwmRequest.outputJsonResult( RestResultBean.fromError( errorInfo, pwmRequest ) );
             LOGGER.error( pwmRequest, errorInfo );
             LOGGER.error( pwmRequest, errorInfo );
@@ -526,45 +526,44 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final String searchTerm = valueMap.get( "search" );
         final String searchTerm = valueMap.get( "search" );
         final StoredConfiguration storedConfiguration = configManagerBean.getStoredConfiguration();
         final StoredConfiguration storedConfiguration = configManagerBean.getStoredConfiguration();
 
 
-        if ( searchTerm != null && !searchTerm.isEmpty() )
+        if ( StringUtil.isEmpty( searchTerm ) )
         {
         {
-            final Set<StoredConfigItemKey> searchResults = StoredConfigurationUtil.search( storedConfiguration, searchTerm, locale );
-            final ConcurrentHashMap<String, Map<String, SearchResultItem>> returnData = new ConcurrentHashMap<>();
+            pwmRequest.outputJsonResult( RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "missing search parameter" ) ) );
+            return ProcessStatus.Halt;
+        }
 
 
-            searchResults
-                    .parallelStream()
-                    .filter( key -> key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
-                    .forEach( recordID ->
-                    {
-                        final PwmSetting setting = recordID.toPwmSetting();
-                        final SearchResultItem item = new SearchResultItem(
-                                setting.getCategory().toString(),
-                                storedConfiguration.readSetting( setting, recordID.getProfileID() ).toDebugString( locale ),
-                                setting.getCategory().toMenuLocationDebug( recordID.getProfileID(), locale ),
-                                storedConfiguration.isDefaultValue( setting, recordID.getProfileID() ),
-                                recordID.getProfileID()
-                        );
-                        final String returnCategory = item.getNavigation();
-
-
-                        returnData.putIfAbsent( returnCategory, new ConcurrentHashMap<>() );
-                        returnData.get( returnCategory ).put( setting.getKey(), item );
-                    } );
-
-            final TreeMap<String, Map<String, SearchResultItem>> outputMap = new TreeMap<>();
-            for ( final String key : returnData.keySet() )
-            {
-                outputMap.put( key, new TreeMap<>( returnData.get( key ) ) );
-            }
 
 
-            restResultBean = RestResultBean.withData( outputMap );
-            LOGGER.trace( pwmRequest, () -> "finished search operation with " + returnData.size() + " results in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
-        }
-        else
+        final Set<StoredConfigItemKey> searchResults = StoredConfigurationUtil.search( storedConfiguration, searchTerm, locale );
+        final ConcurrentHashMap<String, Map<String, SearchResultItem>> returnData = new ConcurrentHashMap<>();
+
+        searchResults
+                .parallelStream()
+                .filter( key -> key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                .forEach( recordID ->
+                {
+                    final PwmSetting setting = recordID.toPwmSetting();
+                    final SearchResultItem item = new SearchResultItem(
+                            setting.getCategory().toString(),
+                            storedConfiguration.readSetting( setting, recordID.getProfileID() ).toDebugString( locale ),
+                            setting.getCategory().toMenuLocationDebug( recordID.getProfileID(), locale ),
+                            storedConfiguration.isDefaultValue( setting, recordID.getProfileID() ),
+                            recordID.getProfileID()
+                    );
+                    final String returnCategory = item.getNavigation();
+
+
+                    returnData.putIfAbsent( returnCategory, new ConcurrentHashMap<>() );
+                    returnData.get( returnCategory ).put( setting.getKey(), item );
+                } );
+
+        final TreeMap<String, Map<String, SearchResultItem>> outputMap = new TreeMap<>();
+        for ( final String key : returnData.keySet() )
         {
         {
-            restResultBean = RestResultBean.withData( new ArrayList() );
+            outputMap.put( key, new TreeMap<>( returnData.get( key ) ) );
         }
         }
 
 
+        restResultBean = RestResultBean.withData( outputMap );
+        LOGGER.trace( pwmRequest, () -> "finished search operation with " + returnData.size() + " results", () -> TimeDuration.fromCurrent( startTime ) );
         pwmRequest.outputJsonResult( restResultBean );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
@@ -732,7 +731,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "unknown format type: " + e.getMessage(), new String[]
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "unknown format type: " + e.getMessage(), new String[]
                             {
                             {
                                     "format",
                                     "format",
-                            }
+                                    }
                     ) );
                     ) );
                 }
                 }
 
 

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskCardInfoBean.java

@@ -66,7 +66,7 @@ public class HelpdeskCardInfoBean implements Serializable
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
         LOGGER.trace( pwmRequest, () -> "beginning to assemble card data report for user " + userIdentity );
         LOGGER.trace( pwmRequest, () -> "beginning to assemble card data report for user " + userIdentity );
         final Locale actorLocale = pwmRequest.getLocale();
         final Locale actorLocale = pwmRequest.getLocale();
-        final ChaiUser theUser = HelpdeskServlet.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+        final ChaiUser theUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
 
 
         if ( !theUser.exists() )
         if ( !theUser.exists() )
         {
         {

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

@@ -114,7 +114,7 @@ public class HelpdeskDetailInfoBean implements Serializable
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
         LOGGER.trace( pwmRequest, () -> "beginning to assemble detail data report for user " + userIdentity );
         LOGGER.trace( pwmRequest, () -> "beginning to assemble detail data report for user " + userIdentity );
         final Locale actorLocale = pwmRequest.getLocale();
         final Locale actorLocale = pwmRequest.getLocale();
-        final ChaiUser theUser = HelpdeskServlet.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+        final ChaiUser theUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
 
 
         if ( !theUser.exists() )
         if ( !theUser.exists() )
         {
         {

+ 24 - 47
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -213,7 +213,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
         // verify the chaiProvider is available - ie, password is supplied, proxy available etc.
         // verify the chaiProvider is available - ie, password is supplied, proxy available etc.
         // we do this now so redirects can handle properly instead of during a later rest request.
         // we do this now so redirects can handle properly instead of during a later rest request.
         final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfo().getUserIdentity();
         final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfo().getUserIdentity();
-        getChaiUser( pwmRequest, helpdeskProfile, loggedInUser ).getChaiProvider();
+        HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, loggedInUser ).getChaiProvider();
 
 
         return ProcessStatus.Continue;
         return ProcessStatus.Continue;
     }
     }
@@ -246,8 +246,8 @@ public class HelpdeskServlet extends ControlledPwmServlet
             pwmRequest.respondWithError( errorInformation, false );
             pwmRequest.respondWithError( errorInformation, false );
             return ProcessStatus.Halt;
             return ProcessStatus.Halt;
         }
         }
-        final UserIdentity userIdentity = UserIdentity.fromKey( userKey, pwmRequest.getPwmApplication() );
-        LOGGER.debug( pwmRequest, () -> "received executeAction request for user " + userIdentity.toString() );
+        final UserIdentity targetUserIdentity = UserIdentity.fromKey( userKey, pwmRequest.getPwmApplication() );
+        LOGGER.debug( pwmRequest, () -> "received executeAction request for user " + targetUserIdentity.toString() );
 
 
         final List<ActionConfiguration> actionConfigurations = helpdeskProfile.readSettingAsAction( PwmSetting.HELPDESK_ACTIONS );
         final List<ActionConfiguration> actionConfigurations = helpdeskProfile.readSettingAsAction( PwmSetting.HELPDESK_ACTIONS );
         final String requestedName = pwmRequest.readParameterAsString( "name" );
         final String requestedName = pwmRequest.readParameterAsString( "name" );
@@ -271,17 +271,14 @@ public class HelpdeskServlet extends ControlledPwmServlet
         }
         }
 
 
         // check if user should be seen by actor
         // check if user should be seen by actor
-        HelpdeskServletUtil.checkIfUserIdentityViewable( pwmRequest, helpdeskProfile, userIdentity );
+        HelpdeskServletUtil.checkIfUserIdentityViewable( pwmRequest, helpdeskProfile, targetUserIdentity );
 
 
-        final boolean useProxy = helpdeskProfile.readSettingAsBoolean( PwmSetting.HELPDESK_USE_PROXY );
         try
         try
         {
         {
             final PwmSession pwmSession = pwmRequest.getPwmSession();
             final PwmSession pwmSession = pwmRequest.getPwmSession();
 
 
-            final ChaiUser chaiUser = useProxy
-                    ? pwmRequest.getPwmApplication().getProxiedChaiUser( userIdentity )
-                    : pwmRequest.getPwmSession().getSessionManager().getActor( userIdentity );
-            final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, userIdentity );
+            final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, targetUserIdentity );
+            final MacroRequest macroRequest = HelpdeskServletUtil.getTargetUserMacroRequest( pwmRequest, helpdeskProfile, targetUserIdentity );
             final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmRequest.getPwmApplication(), chaiUser )
             final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmRequest.getPwmApplication(), chaiUser )
                     .setExpandPwmMacros( true )
                     .setExpandPwmMacros( true )
                     .setMacroMachine( macroRequest )
                     .setMacroMachine( macroRequest )
@@ -295,7 +292,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
                         AuditEvent.HELPDESK_ACTION,
                         AuditEvent.HELPDESK_ACTION,
                         pwmSession.getUserInfo().getUserIdentity(),
                         pwmSession.getUserInfo().getUserIdentity(),
                         action.getName(),
                         action.getName(),
-                        userIdentity,
+                        targetUserIdentity,
                         pwmSession.getSessionStateBean().getSrcAddress(),
                         pwmSession.getSessionStateBean().getSrcAddress(),
                         pwmSession.getSessionStateBean().getSrcHostname()
                         pwmSession.getSessionStateBean().getSrcHostname()
                 );
                 );
@@ -517,7 +514,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             {
             {
                 final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfo().getUserIdentity();
                 final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfo().getUserIdentity();
                 builder.ldapProfile( loggedInUser.getLdapProfileID() );
                 builder.ldapProfile( loggedInUser.getLdapProfileID() );
-                builder.chaiProvider( getChaiUser( pwmRequest, helpdeskProfile, loggedInUser ).getChaiProvider() );
+                builder.chaiProvider( HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, loggedInUser ).getChaiProvider() );
             }
             }
 
 
             switch ( searchMode )
             switch ( searchMode )
@@ -605,7 +602,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
 
 
         try
         try
         {
         {
-            final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+            final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
 
 
             // send notice email
             // send notice email
             HelpdeskServletUtil.sendUnlockNoticeEmail( pwmRequest, helpdeskProfile, userIdentity, chaiUser );
             HelpdeskServletUtil.sendUnlockNoticeEmail( pwmRequest, helpdeskProfile, userIdentity, chaiUser );
@@ -743,14 +740,8 @@ public class HelpdeskServlet extends ControlledPwmServlet
         final Configuration config = pwmRequest.getConfig();
         final Configuration config = pwmRequest.getConfig();
         final Map<String, String> bodyParams = pwmRequest.readBodyAsJsonStringMap();
         final Map<String, String> bodyParams = pwmRequest.readBodyAsJsonStringMap();
 
 
-        final UserIdentity userIdentity = UserIdentity.fromKey( bodyParams.get( PwmConstants.PARAM_USERKEY ), pwmRequest.getPwmApplication() );
-        final UserInfo userInfo = UserInfoFactory.newUserInfo(
-                pwmRequest.getPwmApplication(),
-                pwmRequest.getLabel(),
-                pwmRequest.getLocale(),
-                userIdentity,
-                getChaiUser( pwmRequest, helpdeskProfile, userIdentity ).getChaiProvider()
-        );
+        final UserIdentity targetUserIdentity = UserIdentity.fromKey( bodyParams.get( PwmConstants.PARAM_USERKEY ), pwmRequest.getPwmApplication() );
+        final UserInfo targetUserInfo = HelpdeskServletUtil.getTargetUserInfo( pwmRequest, helpdeskProfile, targetUserIdentity );
 
 
         final String requestedTokenID = bodyParams.get( "id" );
         final String requestedTokenID = bodyParams.get( "id" );
 
 
@@ -760,7 +751,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
                     pwmRequest.getPwmApplication(),
                     pwmRequest.getPwmApplication(),
                     pwmRequest.getLabel(),
                     pwmRequest.getLabel(),
                     pwmRequest.getLocale(),
                     pwmRequest.getLocale(),
-                    userInfo,
+                    targetUserInfo,
                     MessageSendMethod.CHOICE_SMS_EMAIL  );
                     MessageSendMethod.CHOICE_SMS_EMAIL  );
 
 
             final Optional<TokenDestinationItem> selectedTokenDest = TokenDestinationItem.tokenDestinationItemForID( items, requestedTokenID );
             final Optional<TokenDestinationItem> selectedTokenDest = TokenDestinationItem.tokenDestinationItemForID( items, requestedTokenID );
@@ -775,7 +766,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             }
             }
         }
         }
 
 
-        final HelpdeskDetailInfoBean helpdeskDetailInfoBean = HelpdeskDetailInfoBean.makeHelpdeskDetailInfo( pwmRequest, helpdeskProfile, userIdentity );
+        final HelpdeskDetailInfoBean helpdeskDetailInfoBean = HelpdeskDetailInfoBean.makeHelpdeskDetailInfo( pwmRequest, helpdeskProfile, targetUserIdentity );
         if ( helpdeskDetailInfoBean == null )
         if ( helpdeskDetailInfoBean == null )
         {
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "unable to read helpdesk detail data for specified user" );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "unable to read helpdesk detail data for specified user" );
@@ -783,12 +774,12 @@ public class HelpdeskServlet extends ControlledPwmServlet
             pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
             pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
             return ProcessStatus.Halt;
             return ProcessStatus.Halt;
         }
         }
-        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
+        final MacroRequest macroRequest = HelpdeskServletUtil.getTargetUserMacroRequest( pwmRequest, helpdeskProfile, targetUserIdentity );
         final String configuredTokenString = config.readAppProperty( AppProperty.HELPDESK_TOKEN_VALUE );
         final String configuredTokenString = config.readAppProperty( AppProperty.HELPDESK_TOKEN_VALUE );
         final String tokenKey = macroRequest.expandMacros( configuredTokenString );
         final String tokenKey = macroRequest.expandMacros( configuredTokenString );
         final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_HELPDESK_TOKEN, pwmRequest.getLocale() );
         final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_HELPDESK_TOKEN, pwmRequest.getLocale() );
 
 
-        LOGGER.debug( pwmRequest, () -> "generated token code for " + userIdentity.toDelimitedKey() );
+        LOGGER.debug( pwmRequest, () -> "generated token code for " + targetUserIdentity.toDelimitedKey() );
 
 
         final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_HELPDESK_TOKEN_TEXT, pwmRequest.getLocale() );
         final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_HELPDESK_TOKEN_TEXT, pwmRequest.getLocale() );
 
 
@@ -797,7 +788,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             TokenService.TokenSender.sendToken(
             TokenService.TokenSender.sendToken(
                     TokenService.TokenSendInfo.builder()
                     TokenService.TokenSendInfo.builder()
                             .pwmApplication( pwmRequest.getPwmApplication() )
                             .pwmApplication( pwmRequest.getPwmApplication() )
-                            .userInfo( userInfo )
+                            .userInfo( targetUserInfo )
                             .macroRequest( macroRequest )
                             .macroRequest( macroRequest )
                             .configuredEmailSetting( emailItemBean )
                             .configuredEmailSetting( emailItemBean )
                             .tokenDestinationItem( tokenDestinationItem )
                             .tokenDestinationItem( tokenDestinationItem )
@@ -831,7 +822,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
         LOGGER.debug( pwmRequest, () -> "helpdesk operator "
         LOGGER.debug( pwmRequest, () -> "helpdesk operator "
                 + pwmRequest.getUserInfoIfLoggedIn().toDisplayString()
                 + pwmRequest.getUserInfoIfLoggedIn().toDisplayString()
                 + " issued token for verification against user "
                 + " issued token for verification against user "
-                + userIdentity.toDisplayString()
+                + targetUserIdentity.toDisplayString()
                 + " sent to destination(s) "
                 + " sent to destination(s) "
                 + tokenDestinationItem.getDisplay()
                 + tokenDestinationItem.getDisplay()
                 + " (" + TimeDuration.fromCurrent( startTime ).asCompactString() + ")" );
                 + " (" + TimeDuration.fromCurrent( startTime ).asCompactString() + ")" );
@@ -940,7 +931,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
         {
         {
 
 
             final OtpService service = pwmRequest.getPwmApplication().getOtpService();
             final OtpService service = pwmRequest.getPwmApplication().getOtpService();
-            final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+            final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
             service.clearOTPUserConfiguration( pwmRequest, userIdentity, chaiUser );
             service.clearOTPUserConfiguration( pwmRequest, userIdentity, chaiUser );
             {
             {
                 // mark the event log
                 // mark the event log
@@ -970,20 +961,6 @@ public class HelpdeskServlet extends ControlledPwmServlet
     }
     }
 
 
 
 
-    static ChaiUser getChaiUser(
-            final PwmRequest pwmRequest,
-            final HelpdeskProfile helpdeskProfile,
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        final boolean useProxy = helpdeskProfile.readSettingAsBoolean( PwmSetting.HELPDESK_USE_PROXY );
-        return useProxy
-                ? pwmRequest.getPwmApplication().getProxiedChaiUser( userIdentity )
-                : pwmRequest.getPwmSession().getSessionManager().getActor( userIdentity );
-    }
-
-
     @ActionHandler( action = "checkVerification" )
     @ActionHandler( action = "checkVerification" )
     private ProcessStatus restCheckVerification( final PwmRequest pwmRequest )
     private ProcessStatus restCheckVerification( final PwmRequest pwmRequest )
             throws IOException, PwmUnrecoverableException, ServletException
             throws IOException, PwmUnrecoverableException, ServletException
@@ -1055,7 +1032,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             }
             }
 
 
             final Map<String, String> bodyMap = JsonUtil.deserializeStringMap( bodyString );
             final Map<String, String> bodyMap = JsonUtil.deserializeStringMap( bodyString );
-            final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+            final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
 
 
             int successCount = 0;
             int successCount = 0;
             for ( final FormConfiguration formConfiguration : verificationForms )
             for ( final FormConfiguration formConfiguration : verificationForms )
@@ -1167,7 +1144,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             }
             }
         }
         }
 
 
-        final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+        final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
         final String userGUID = LdapOperationsHelper.readLdapGuidValue(
         final String userGUID = LdapOperationsHelper.readLdapGuidValue(
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getLabel(),
                 pwmRequest.getLabel(),
@@ -1213,7 +1190,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
 
 
         HelpdeskServletUtil.checkIfUserIdentityViewable( pwmRequest, helpdeskProfile, userIdentity );
         HelpdeskServletUtil.checkIfUserIdentityViewable( pwmRequest, helpdeskProfile, userIdentity );
 
 
-        final ChaiUser chaiUser = getChaiUser( pwmRequest, getHelpdeskProfile( pwmRequest ), userIdentity );
+        final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, getHelpdeskProfile( pwmRequest ), userIdentity );
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getLabel(),
                 pwmRequest.getLabel(),
@@ -1261,7 +1238,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
         );
         );
 
 
         final UserIdentity userIdentity = UserIdentity.fromKey( jsonInput.getUsername(), pwmRequest.getPwmApplication() );
         final UserIdentity userIdentity = UserIdentity.fromKey( jsonInput.getUsername(), pwmRequest.getPwmApplication() );
-        final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+        final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getLabel(),
                 pwmRequest.getLabel(),
@@ -1347,7 +1324,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
 
 
         HelpdeskServletUtil.checkIfUserIdentityViewable( pwmRequest, helpdeskProfile, userIdentity );
         HelpdeskServletUtil.checkIfUserIdentityViewable( pwmRequest, helpdeskProfile, userIdentity );
 
 
-        final ChaiUser chaiUser = getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
+        final ChaiUser chaiUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity );
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getLabel(),
                 pwmRequest.getLabel(),
@@ -1408,7 +1385,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
         final PhotoDataReader.Settings settings = PhotoDataReader.Settings.builder()
         final PhotoDataReader.Settings settings = PhotoDataReader.Settings.builder()
                 .enabled( enabled )
                 .enabled( enabled )
                 .photoPermissions( null )
                 .photoPermissions( null )
-                .chaiProvider( getChaiUser( pwmRequest, helpdeskProfile, userIdentity ).getChaiProvider() )
+                .chaiProvider( HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, userIdentity ).getChaiProvider() )
                 .build();
                 .build();
 
 
         return new PhotoDataReader( pwmRequest, settings, userIdentity );
         return new PhotoDataReader( pwmRequest, settings, userIdentity );

+ 59 - 8
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServletUtil.java

@@ -46,6 +46,7 @@ import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.HelpdeskAuditRecord;
 import password.pwm.svc.event.HelpdeskAuditRecord;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
@@ -266,10 +267,11 @@ public class HelpdeskServletUtil
     )
     )
     {
     {
         final Collection<IdentityVerificationMethod> requiredMethods = helpdeskProfile.readRequiredVerificationMethods();
         final Collection<IdentityVerificationMethod> requiredMethods = helpdeskProfile.readRequiredVerificationMethods();
-        if ( requiredMethods == null || requiredMethods.isEmpty() )
+        if ( JavaHelper.isEmpty( requiredMethods ) )
         {
         {
             return true;
             return true;
         }
         }
+
         for ( final IdentityVerificationMethod method : requiredMethods )
         for ( final IdentityVerificationMethod method : requiredMethods )
         {
         {
             if ( verificationStateBean.hasRecord( userIdentity, method ) )
             if ( verificationStateBean.hasRecord( userIdentity, method ) )
@@ -277,6 +279,7 @@ public class HelpdeskServletUtil
                 return true;
                 return true;
             }
             }
         }
         }
+
         return false;
         return false;
     }
     }
 
 
@@ -287,7 +290,7 @@ public class HelpdeskServletUtil
             final ChaiUser chaiUser
             final ChaiUser chaiUser
 
 
     )
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException
+            throws PwmUnrecoverableException
     {
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final Configuration config = pwmRequest.getConfig();
         final Configuration config = pwmRequest.getConfig();
@@ -308,12 +311,7 @@ public class HelpdeskServletUtil
                 chaiUser.getChaiProvider()
                 chaiUser.getChaiProvider()
         );
         );
 
 
-        final MacroRequest macroRequest = MacroRequest.forUser(
-                pwmApplication,
-                pwmRequest.getLabel(),
-                userInfo,
-                null
-        );
+        final MacroRequest macroRequest = getTargetUserMacroRequest( pwmRequest, helpdeskProfile, userIdentity );
 
 
         pwmApplication.getEmailQueue().submitEmail(
         pwmApplication.getEmailQueue().submitEmail(
                 configuredEmailSetting,
                 configuredEmailSetting,
@@ -322,4 +320,57 @@ public class HelpdeskServletUtil
         );
         );
     }
     }
 
 
+    static ChaiUser getChaiUser(
+            final PwmRequest pwmRequest,
+            final HelpdeskProfile helpdeskProfile,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final boolean useProxy = helpdeskProfile.readSettingAsBoolean( PwmSetting.HELPDESK_USE_PROXY );
+        return useProxy
+                ? pwmRequest.getPwmApplication().getProxiedChaiUser( userIdentity )
+                : pwmRequest.getPwmSession().getSessionManager().getActor( userIdentity );
+    }
+
+    static UserInfo getTargetUserInfo(
+            final PwmRequest pwmRequest,
+            final HelpdeskProfile helpdeskProfile,
+            final UserIdentity targetUserIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        return UserInfoFactory.newUserInfo(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getLabel(),
+                pwmRequest.getLocale(),
+                targetUserIdentity,
+                getChaiUser( pwmRequest, helpdeskProfile, targetUserIdentity ).getChaiProvider()
+        );
+    }
+
+    static MacroRequest getTargetUserMacroRequest(
+            final PwmRequest pwmRequest,
+            final HelpdeskProfile helpdeskProfile,
+            final UserIdentity targetUserIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final MacroRequest macroRequest = MacroRequest.forUser(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getLabel(),
+                getTargetUserInfo( pwmRequest, helpdeskProfile, targetUserIdentity ),
+                pwmRequest.getPwmSession().getLoginInfoBean()
+        );
+
+        /*
+        if ( targetUserIdentity != null )
+        {
+            final UserInfo targetUserInfo = getTargetUserInfo( pwmRequest, helpdeskProfile, targetUserIdentity );
+            return macroRequest.toBuilder().targetUserInfo( targetUserInfo ).build();
+        }
+         */
+
+        return macroRequest;
+    }
 }
 }

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

@@ -71,7 +71,7 @@ public class HelpdeskVerificationOptionsBean implements Serializable
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final ChaiUser theUser = HelpdeskServlet.getChaiUser( pwmRequest, helpdeskProfile, targetUser );
+        final ChaiUser theUser = HelpdeskServletUtil.getChaiUser( pwmRequest, helpdeskProfile, targetUser );
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
         final UserInfo userInfo = UserInfoFactory.newUserInfo(
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getLabel(),
                 pwmRequest.getLabel(),

+ 2 - 3
server/src/main/java/password/pwm/util/macro/AbstractMacro.java

@@ -58,12 +58,11 @@ public abstract class AbstractMacro implements Macro
         return result;
         return result;
     }
     }
 
 
-    static List<String> splitMacroParameters( final String input, final String... ignoreValues )
+    static List<String> splitMacroParameters( final String input, final List<String> ignoreValueList )
     {
     {
         final String strippedInput = stripMacroDelimiters( input );
         final String strippedInput = stripMacroDelimiters( input );
         final String[] splitInput = strippedInput.split( PATTERN_PARAMETER_SPLIT );
         final String[] splitInput = strippedInput.split( PATTERN_PARAMETER_SPLIT );
         final List<String> returnObj = new ArrayList<>();
         final List<String> returnObj = new ArrayList<>();
-        final List<String> ignoreValueList = Arrays.asList( ignoreValues );
         for ( final String value : splitInput )
         for ( final String value : splitInput )
         {
         {
             if ( !ignoreValueList.contains( value ) )
             if ( !ignoreValueList.contains( value ) )
@@ -114,7 +113,7 @@ public abstract class AbstractMacro implements Macro
         }
         }
     }
     }
 
 
-    static String processTimeOutputMacro( final Instant timestamp, final String matchValue, final String... ignoreValues )
+    static String processTimeOutputMacro( final Instant timestamp, final String matchValue, final List<String> ignoreValues )
             throws MacroParseException
             throws MacroParseException
     {
     {
         if ( timestamp == null )
         if ( timestamp == null )

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

@@ -30,6 +30,7 @@ public interface Macro
         Static,
         Static,
         System,
         System,
         User,
         User,
+        TargetUser,
     }
     }
 
 
     enum Sequence
     enum Sequence

+ 5 - 0
server/src/main/java/password/pwm/util/macro/MacroMachine.java

@@ -270,6 +270,11 @@ public class MacroMachine
             scopes.add( Macro.Scope.User );
             scopes.add( Macro.Scope.User );
         }
         }
 
 
+        if ( macroRequestInfo.getTargetUserInfo() != null )
+        {
+            scopes.add( Macro.Scope.TargetUser );
+        }
+
         return Collections.unmodifiableSet( scopes );
         return Collections.unmodifiableSet( scopes );
     }
     }
 }
 }

+ 108 - 68
server/src/main/java/password/pwm/util/macro/MacroRequest.java

@@ -54,25 +54,11 @@ public class MacroRequest
     private final UserInfo userInfo;
     private final UserInfo userInfo;
     private final LoginInfoBean loginInfoBean;
     private final LoginInfoBean loginInfoBean;
     private final MacroReplacer macroReplacer;
     private final MacroReplacer macroReplacer;
-
-    public MacroRequest(
-            final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel,
-            final UserInfo userInfo,
-            final LoginInfoBean loginInfoBean,
-            final MacroReplacer macroReplacer
-    )
-    {
-        this.pwmApplication = pwmApplication;
-        this.sessionLabel = sessionLabel;
-        this.userInfo = userInfo;
-        this.loginInfoBean = loginInfoBean;
-        this.macroReplacer = macroReplacer;
-    }
+    private final UserInfo targetUserInfo;
 
 
     public static MacroRequest forStatic( )
     public static MacroRequest forStatic( )
     {
     {
-        return new MacroRequest( null, null, null, null, null );
+        return new MacroRequest( null, null, null, null, null, null );
     }
     }
 
 
     public static MacroRequest forUser(
     public static MacroRequest forUser(
@@ -110,7 +96,7 @@ public class MacroRequest
             final LoginInfoBean loginInfoBean
             final LoginInfoBean loginInfoBean
     )
     )
     {
     {
-        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, null );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, null, null );
     }
     }
 
 
     public static MacroRequest forUser(
     public static MacroRequest forUser(
@@ -121,7 +107,7 @@ public class MacroRequest
             final MacroReplacer macroReplacer
             final MacroReplacer macroReplacer
     )
     )
     {
     {
-        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, macroReplacer );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, macroReplacer, null );
     }
     }
 
 
     public static MacroRequest forUser(
     public static MacroRequest forUser(
@@ -133,7 +119,7 @@ public class MacroRequest
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, null );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, null, null );
     }
     }
 
 
     public static MacroRequest forUser(
     public static MacroRequest forUser(
@@ -146,7 +132,7 @@ public class MacroRequest
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, macroReplacer );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, macroReplacer, null );
     }
     }
 
 
     public static MacroRequest forNonUserSpecific(
     public static MacroRequest forNonUserSpecific(
@@ -154,7 +140,7 @@ public class MacroRequest
             final SessionLabel sessionLabel
             final SessionLabel sessionLabel
     )
     )
     {
     {
-        return new MacroRequest( pwmApplication, sessionLabel, null, null, null );
+        return new MacroRequest( pwmApplication, sessionLabel, null, null, null, null );
     }
     }
 
 
     public String expandMacros( final String input )
     public String expandMacros( final String input )
@@ -165,63 +151,117 @@ public class MacroRequest
     public static MacroRequest sampleMacroRequest( final PwmApplication pwmApplication )
     public static MacroRequest sampleMacroRequest( final PwmApplication pwmApplication )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final Map<String, String> attributes = new LinkedHashMap<>();
-        attributes.put( "givenName", "First" );
-        attributes.put( "sn", "Last" );
-        attributes.put( "cn", "FLast" );
-        attributes.put( "fullname", "First Last" );
-        attributes.put( "uid", "FLast" );
-        attributes.put( "mail", "FLast@example.com" );
-        attributes.put( "carLicense", "6YJ S32" );
-        attributes.put( "mobile", "800-555-1212" );
-        attributes.put( "objectClass", "inetOrgPerson" );
-        attributes.put( "personalMobile", "800-555-1313" );
-        attributes.put( "title", "Title" );
-        attributes.put( "c", "USA" );
-        attributes.put( "co", "County" );
-        attributes.put( "description", "User Description" );
-        attributes.put( "department", "Department" );
-        attributes.put( "initials", "M" );
-        attributes.put( "postalcode", "12345-6789" );
-        attributes.put( "samaccountname", "FLast" );
-        attributes.put( "userprincipalname", "FLast" );
-
-
-        final UserIdentity userIdentity = new UserIdentity( "cn=test1,ou=test,o=org", "profile1" );
-        final OTPUserRecord otpUserRecord = new OTPUserRecord();
-        otpUserRecord.setTimestamp( Instant.ofEpochSecond( 941259364 ) );
-        final ResponseInfoBean responseInfoBean = new ResponseInfoBean(
-                Collections.emptyMap(),
-                Collections.emptyMap(),
-                PwmConstants.DEFAULT_LOCALE,
-                8 + 3,
-                null,
-                DataStorageMethod.LOCALDB,
-                Answer.FormatType.PBKDF2
-        );
-        responseInfoBean.setTimestamp( Instant.ofEpochSecond( 941246275 ) );
-        final UserInfoBean userInfoBean = UserInfoBean.builder()
-                .userIdentity( userIdentity )
-                .username( "jrivard" )
-                .userEmailAddress( "zippy@example.com" )
-                .attributes( attributes )
-                .passwordExpirationTime( Instant.ofEpochSecond( 949539661 ) )
-                .responseInfoBean( responseInfoBean )
-                .otpUserRecord( otpUserRecord )
-                .cachedAttributeValues( Collections.singletonMap( "givenName", "Jason" ) )
-                .build();
+        final UserInfoBean userInfoBean;
+        {
+            final Map<String, String> userAttributes = new LinkedHashMap<>();
+            userAttributes.put( "givenName", "First" );
+            userAttributes.put( "sn", "Last" );
+            userAttributes.put( "cn", "FLast" );
+            userAttributes.put( "fullname", "First Last" );
+            userAttributes.put( "uid", "FLast" );
+            userAttributes.put( "mail", "FLast@example.com" );
+            userAttributes.put( "carLicense", "6YJ S32" );
+            userAttributes.put( "mobile", "800-555-1212" );
+            userAttributes.put( "objectClass", "inetOrgPerson" );
+            userAttributes.put( "personalMobile", "800-555-1313" );
+            userAttributes.put( "title", "Title" );
+            userAttributes.put( "c", "USA" );
+            userAttributes.put( "co", "County" );
+            userAttributes.put( "description", "User Description" );
+            userAttributes.put( "department", "Department" );
+            userAttributes.put( "initials", "M" );
+            userAttributes.put( "postalcode", "12345-6789" );
+            userAttributes.put( "samaccountname", "FLast" );
+            userAttributes.put( "userprincipalname", "FLast" );
+
+
+            final OTPUserRecord otpUserRecord = new OTPUserRecord();
+            otpUserRecord.setTimestamp( Instant.ofEpochSecond( 941259364 ) );
+            final ResponseInfoBean responseInfoBean = new ResponseInfoBean(
+                    Collections.emptyMap(),
+                    Collections.emptyMap(),
+                    PwmConstants.DEFAULT_LOCALE,
+                    8 + 3,
+                    null,
+                    DataStorageMethod.LOCALDB,
+                    Answer.FormatType.PBKDF2
+            );
+            responseInfoBean.setTimestamp( Instant.ofEpochSecond( 941246275 ) );
+
+            final UserIdentity userIdentity = new UserIdentity( "cn=FLast,ou=test,o=org", "profile1" );
+
+            userInfoBean = UserInfoBean.builder()
+                    .userIdentity( userIdentity )
+                    .username( "FLast" )
+                    .userEmailAddress( "FLast@example.com" )
+                    .attributes( userAttributes )
+                    .passwordExpirationTime( Instant.ofEpochSecond( 949539661 ) )
+                    .responseInfoBean( responseInfoBean )
+                    .otpUserRecord( otpUserRecord )
+                    .build();
+        }
+
+        final UserInfoBean targetUserInfoBean;
+        {
+            final Map<String, String> userAttributes = new LinkedHashMap<>();
+            userAttributes.put( "givenName", "Target" );
+            userAttributes.put( "sn", "User" );
+            userAttributes.put( "cn", "TUser" );
+            userAttributes.put( "fullname", "Target User" );
+            userAttributes.put( "uid", "TUser" );
+            userAttributes.put( "mail", "TUser@example.com" );
+            userAttributes.put( "carLicense", "6YJ S32" );
+            userAttributes.put( "mobile", "800-555-1212" );
+            userAttributes.put( "objectClass", "inetOrgPerson" );
+            userAttributes.put( "personalMobile", "800-555-1313" );
+            userAttributes.put( "title", "Title" );
+            userAttributes.put( "c", "USA" );
+            userAttributes.put( "co", "County" );
+            userAttributes.put( "description", "Target User Description" );
+            userAttributes.put( "department", "Department" );
+            userAttributes.put( "initials", "M" );
+            userAttributes.put( "postalcode", "12345-6789" );
+            userAttributes.put( "samaccountname", "TUser" );
+            userAttributes.put( "userprincipalname", "TUser" );
+
+
+            final OTPUserRecord otpUserRecord = new OTPUserRecord();
+            otpUserRecord.setTimestamp( Instant.ofEpochSecond( 941252344 ) );
+            final ResponseInfoBean responseInfoBean = new ResponseInfoBean(
+                    Collections.emptyMap(),
+                    Collections.emptyMap(),
+                    PwmConstants.DEFAULT_LOCALE,
+                    8 + 3,
+                    null,
+                    DataStorageMethod.LOCALDB,
+                    Answer.FormatType.PBKDF2
+            );
+            responseInfoBean.setTimestamp( Instant.ofEpochSecond( 941244474 ) );
+
+            final UserIdentity userIdentity = new UserIdentity( "cn=TUser,ou=test,o=org", "profile1" );
+
+            targetUserInfoBean = UserInfoBean.builder()
+                    .userIdentity( userIdentity )
+                    .username( "TUser" )
+                    .userEmailAddress( "TUser@example.com" )
+                    .attributes( userAttributes )
+                    .passwordExpirationTime( Instant.ofEpochSecond( 94949121 ) )
+                    .responseInfoBean( responseInfoBean )
+                    .otpUserRecord( otpUserRecord )
+                    .build();
+        }
 
 
         final LoginInfoBean loginInfoBean = new LoginInfoBean();
         final LoginInfoBean loginInfoBean = new LoginInfoBean();
         loginInfoBean.setAuthenticated( true );
         loginInfoBean.setAuthenticated( true );
-        loginInfoBean.setUserIdentity( userIdentity );
+        loginInfoBean.setUserIdentity( userInfoBean.getUserIdentity() );
         loginInfoBean.setUserCurrentPassword( PasswordData.forStringValue( "PaSSw0rd" ) );
         loginInfoBean.setUserCurrentPassword( PasswordData.forStringValue( "PaSSw0rd" ) );
 
 
         return MacroRequest.builder()
         return MacroRequest.builder()
                 .pwmApplication( pwmApplication )
                 .pwmApplication( pwmApplication )
                 .userInfo( userInfoBean )
                 .userInfo( userInfoBean )
+                .targetUserInfo( targetUserInfoBean )
                 .loginInfoBean( loginInfoBean )
                 .loginInfoBean( loginInfoBean )
                 .build();
                 .build();
 
 
     }
     }
-
 }
 }

+ 4 - 4
server/src/main/java/password/pwm/util/macro/SystemMacros.java

@@ -114,7 +114,7 @@ public class SystemMacros
         )
         )
                 throws MacroParseException
                 throws MacroParseException
         {
         {
-            final List<String> parameters = splitMacroParameters( matchValue, "CurrentTime" );
+            final List<String> parameters = splitMacroParameters( matchValue, Collections.singletonList( "CurrentTime" ) );
 
 
             if ( parameters.size() > 2 )
             if ( parameters.size() > 2 )
             {
             {
@@ -152,7 +152,7 @@ public class SystemMacros
         )
         )
                 throws MacroParseException
                 throws MacroParseException
         {
         {
-            final List<String> parameters = splitMacroParameters( matchValue, "Iso8601" );
+            final List<String> parameters = splitMacroParameters( matchValue, Collections.singletonList( "Iso8601" ) );
 
 
             if ( JavaHelper.isEmpty(  parameters ) || parameters.size() != 1 )
             if ( JavaHelper.isEmpty(  parameters ) || parameters.size() != 1 )
             {
             {
@@ -270,7 +270,7 @@ public class SystemMacros
                 return "";
                 return "";
             }
             }
 
 
-            final List<String> parameters = splitMacroParameters( matchValue, "RandomChar" );
+            final List<String> parameters = splitMacroParameters( matchValue, Collections.singletonList( "RandomChar" ) );
             int length = 1;
             int length = 1;
             if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
             if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
             {
             {
@@ -327,7 +327,7 @@ public class SystemMacros
                 return "";
                 return "";
             }
             }
 
 
-            final List<String> parameters = splitMacroParameters( matchValue, "RandomNumber" );
+            final List<String> parameters = splitMacroParameters( matchValue, Collections.singletonList( "RandomNumber" ) );
             if ( parameters.size() != 2 )
             if ( parameters.size() != 2 )
             {
             {
                 throw new MacroParseException( "incorrect number of parameter of RandomNumber: "
                 throw new MacroParseException( "incorrect number of parameter of RandomNumber: "

+ 289 - 68
server/src/main/java/password/pwm/util/macro/UserMacros.java

@@ -30,8 +30,10 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.time.Instant;
 import java.time.Instant;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -50,10 +52,16 @@ public class UserMacros
             new UserPasswordMacro(),
             new UserPasswordMacro(),
             new UserLdapProfileMacro(),
             new UserLdapProfileMacro(),
             new OtpSetupTimeMacro(),
             new OtpSetupTimeMacro(),
-            new ResponseSetupTimeMacro()
-    ).collect( Collectors.toList() ) );
+            new ResponseSetupTimeMacro(),
 
 
-    private abstract static class AbstractUserMacro extends AbstractMacro
+            new TargetUserIDMacro(),
+            new TargetUserLdapMacro(),
+            new TargetUserPwExpirationTimeMacro(),
+            new TargetUserDaysUntilPwExpireMacro(),
+            new TargetUserEmailMacro()
+            ).collect( Collectors.toList() ) );
+
+    abstract static class AbstractUserMacro extends AbstractMacro
     {
     {
         @Override
         @Override
         public Scope getScope()
         public Scope getScope()
@@ -62,31 +70,26 @@ public class UserMacros
         }
         }
     }
     }
 
 
-    public static class UserLdapMacro extends AbstractUserMacro
+    abstract static class AbstractUserLdapMacro extends AbstractUserMacro
     {
     {
-        private static final Pattern PATTERN = Pattern.compile( "@(User:LDAP|LDAP)" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
         @Override
         @Override
         public String replaceValue(
         public String replaceValue(
                 final String matchValue,
                 final String matchValue,
-                final MacroRequest request
+                final MacroRequest macroRequest
         )
         )
                 throws MacroParseException
                 throws MacroParseException
         {
         {
-            final UserInfo userInfo = request.getUserInfo();
-
-            if ( userInfo == null )
+            final UserInfo userInfo;
             {
             {
-                return "";
+                final Optional<UserInfo> optionalUserInfo = loadUserInfo( macroRequest );
+                if ( !optionalUserInfo.isPresent() )
+                {
+                    return "";
+                }
+                userInfo = optionalUserInfo.get();
             }
             }
 
 
-            final List<String> parameters = splitMacroParameters( matchValue, "User", "LDAP" );
+            final List<String> parameters = splitMacroParameters( matchValue, ignoreWords() );
 
 
             final String ldapAttr;
             final String ldapAttr;
             if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
             if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
@@ -111,8 +114,8 @@ public class UserMacros
                 }
                 }
 
 
                 final int maxLengthPermitted = Integer.parseInt(
                 final int maxLengthPermitted = Integer.parseInt(
-                        request.getPwmApplication() != null
-                                ?  request.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH )
+                        macroRequest.getPwmApplication() != null
+                                ?  macroRequest.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH )
                                 :  AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH.getDefaultValue()
                                 :  AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH.getDefaultValue()
                 );
                 );
 
 
@@ -158,13 +161,13 @@ public class UserMacros
                 }
                 }
                 catch ( final PwmUnrecoverableException e )
                 catch ( final PwmUnrecoverableException e )
                 {
                 {
-                    LOGGER.trace( request.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', ldap error: " + e.getMessage() );
+                    LOGGER.trace( macroRequest.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', ldap error: " + e.getMessage() );
                     return "";
                     return "";
                 }
                 }
 
 
                 if ( ldapValue == null || ldapValue.length() < 1 )
                 if ( ldapValue == null || ldapValue.length() < 1 )
                 {
                 {
-                    LOGGER.trace( request.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', user does not have value for '" + ldapAttr + "'" );
+                    LOGGER.trace( macroRequest.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', user does not have value for '" + ldapAttr + "'" );
                     return "";
                     return "";
                 }
                 }
             }
             }
@@ -189,12 +192,38 @@ public class UserMacros
 
 
             return returnValue.toString();
             return returnValue.toString();
         }
         }
+
+        abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );
+
+        abstract List<String> ignoreWords();
     }
     }
 
 
+    public static class UserLdapMacro extends AbstractUserLdapMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@(User:LDAP|LDAP)" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getUserInfo() );
+        }
+
+        @Override
+        List<String> ignoreWords()
+        {
+            return Arrays.asList( "User", "LDAP" );
+        }
+    }
 
 
-    public static class UserIDMacro extends AbstractUserMacro
+    public static class TargetUserLdapMacro extends AbstractUserLdapMacro
     {
     {
-        private static final Pattern PATTERN = Pattern.compile( "@User:ID@" );
+        private static final Pattern PATTERN = Pattern.compile( "@TargetUser:LDAP" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
 
 
         @Override
         @Override
         public Pattern getRegExPattern( )
         public Pattern getRegExPattern( )
@@ -202,52 +231,72 @@ public class UserMacros
             return PATTERN;
             return PATTERN;
         }
         }
 
 
+        @Override
+        public Scope getScope()
+        {
+            return Scope.TargetUser;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getTargetUserInfo() );
+        }
+
+        @Override
+        List<String> ignoreWords()
+        {
+            return Arrays.asList( "TargetUser", "LDAP" );
+        }
+    }
+
+    abstract static class AbstractUserIDMacro extends AbstractUserMacro
+    {
+
         @Override
         @Override
         public String replaceValue(
         public String replaceValue(
                 final String matchValue,
                 final String matchValue,
-                final MacroRequest request
+                final MacroRequest macroRequest
         )
         )
         {
         {
-            final UserInfo userInfo = request.getUserInfo();
+            final Optional<UserInfo> optionalUserInfo = loadUserInfo( macroRequest );
 
 
             try
             try
             {
             {
-                if ( userInfo == null || StringUtil.isEmpty( userInfo.getUsername() ) )
+                if ( optionalUserInfo.isPresent() )
                 {
                 {
-                    return "";
+                    final String username = optionalUserInfo.get().getUsername();
+                    if ( !StringUtil.isEmpty( username ) )
+                    {
+                        return username;
+                    }
                 }
                 }
 
 
-                return userInfo.getUsername();
+                return "";
             }
             }
             catch ( final PwmUnrecoverableException e )
             catch ( final PwmUnrecoverableException e )
             {
             {
-                LOGGER.error( request.getSessionLabel(), () -> "error reading username during macro replacement: " + e.getMessage() );
+                LOGGER.error( macroRequest.getSessionLabel(), () -> "error reading username during macro replacement: " + e.getMessage() );
                 return "";
                 return "";
             }
             }
         }
         }
+
+        abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );
     }
     }
 
 
-    public static class UserPwExpirationTimeMacro extends AbstractUserMacro
+    abstract static class AbstractUserPwExpirationTimeMacro extends AbstractUserMacro
     {
     {
-        private static final Pattern PATTERN = Pattern.compile( "@User:PwExpireTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
         @Override
         @Override
         public String replaceValue(
         public String replaceValue(
                 final String matchValue,
                 final String matchValue,
-                final MacroRequest request
+                final MacroRequest macroRequest
 
 
         )
         )
                 throws MacroParseException
                 throws MacroParseException
         {
         {
-            final UserInfo userInfo = request.getUserInfo();
+            final Optional<UserInfo> userInfo = loadUserInfo( macroRequest );
 
 
-            if ( userInfo == null )
+            if ( !userInfo.isPresent() )
             {
             {
                 return "";
                 return "";
             }
             }
@@ -255,19 +304,116 @@ public class UserMacros
             final Instant pwdExpirationTime;
             final Instant pwdExpirationTime;
             try
             try
             {
             {
-                pwdExpirationTime = userInfo.getPasswordExpirationTime();
+                pwdExpirationTime = userInfo.get().getPasswordExpirationTime();
             }
             }
             catch ( final PwmUnrecoverableException e )
             catch ( final PwmUnrecoverableException e )
             {
             {
-                LOGGER.error( request.getSessionLabel(), () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
+                LOGGER.error( macroRequest.getSessionLabel(), () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
                 return "";
                 return "";
             }
             }
 
 
-            return processTimeOutputMacro( pwdExpirationTime, matchValue, "User", "PwExpireTime" );
+            return processTimeOutputMacro( pwdExpirationTime, matchValue, ignoreWords() );
+        }
+
+        abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );
+
+        abstract List<String> ignoreWords();
+    }
+
+    public static class UserIDMacro extends AbstractUserIDMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:ID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getUserInfo() );
+        }
+    }
+
+    public static class TargetUserIDMacro extends AbstractUserIDMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@TargetUser:ID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public Scope getScope()
+        {
+            return Scope.TargetUser;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getTargetUserInfo() );
+        }
+    }
+
+    public static class UserPwExpirationTimeMacro extends AbstractUserPwExpirationTimeMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:PwExpireTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getUserInfo() );
+        }
+
+        @Override
+        List<String> ignoreWords()
+        {
+            return Arrays.asList( "User", "PwExpireTime" );
+        }
+    }
+
+    public static class TargetUserPwExpirationTimeMacro extends AbstractUserPwExpirationTimeMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@TargetUser:PwExpireTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public Scope getScope()
+        {
+            return Scope.TargetUser;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getTargetUserInfo() );
+        }
+
+        @Override
+        List<String> ignoreWords()
+        {
+            return Arrays.asList( "TargetUser", "PwExpireTime" );
         }
         }
     }
     }
 
 
-    public static class UserDaysUntilPwExpireMacro extends AbstractUserMacro
+    public static class UserDaysUntilPwExpireMacro extends AbstractUserDaysUntilPwExpireMacro
     {
     {
         private static final Pattern PATTERN = Pattern.compile( "@User:DaysUntilPwExpire@" );
         private static final Pattern PATTERN = Pattern.compile( "@User:DaysUntilPwExpire@" );
 
 
@@ -277,67 +423,142 @@ public class UserMacros
             return PATTERN;
             return PATTERN;
         }
         }
 
 
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getUserInfo() );
+        }
+    }
+
+    public static class TargetUserDaysUntilPwExpireMacro extends AbstractUserDaysUntilPwExpireMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@TargetUser:DaysUntilPwExpire@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public Scope getScope()
+        {
+            return Scope.TargetUser;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getTargetUserInfo() );
+        }
+    }
+
+    public abstract static class AbstractUserDaysUntilPwExpireMacro extends AbstractUserMacro
+    {
         @Override
         @Override
         public String replaceValue(
         public String replaceValue(
                 final String matchValue,
                 final String matchValue,
-                final MacroRequest request
+                final MacroRequest macroRequest
         )
         )
         {
         {
-            final UserInfo userInfo = request.getUserInfo();
+            final Optional<UserInfo> userInfo = loadUserInfo( macroRequest );
 
 
-            if ( userInfo == null )
+            if ( !userInfo.isPresent() )
             {
             {
-                LOGGER.error( request.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', userInfoBean is null" );
                 return "";
                 return "";
             }
             }
 
 
             try
             try
             {
             {
-                final Instant pwdExpirationTime = userInfo.getPasswordExpirationTime();
+                final Instant pwdExpirationTime = userInfo.get().getPasswordExpirationTime();
                 final TimeDuration timeUntilExpiration = TimeDuration.fromCurrent( pwdExpirationTime );
                 final TimeDuration timeUntilExpiration = TimeDuration.fromCurrent( pwdExpirationTime );
                 final long daysUntilExpiration = timeUntilExpiration.as( TimeDuration.Unit.DAYS );
                 final long daysUntilExpiration = timeUntilExpiration.as( TimeDuration.Unit.DAYS );
                 return String.valueOf( daysUntilExpiration );
                 return String.valueOf( daysUntilExpiration );
             }
             }
             catch ( final PwmUnrecoverableException e )
             catch ( final PwmUnrecoverableException e )
             {
             {
-                LOGGER.error( request.getSessionLabel(), () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
+                LOGGER.error( macroRequest.getSessionLabel(), () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
                 return "";
                 return "";
             }
             }
         }
         }
+
+        abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );
     }
     }
 
 
-    public static class UserEmailMacro extends AbstractUserMacro
+    abstract static class AbstractUserEmailMacro extends AbstractUserMacro
     {
     {
-        private static final Pattern PATTERN = Pattern.compile( "@User:Email@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
 
 
         @Override
         @Override
         public String replaceValue(
         public String replaceValue(
                 final String matchValue,
                 final String matchValue,
-                final MacroRequest request
+                final MacroRequest macroRequest
         )
         )
         {
         {
-            final UserInfo userInfo = request.getUserInfo();
+            final Optional<UserInfo> optionalUserInfo = loadUserInfo( macroRequest );
 
 
             try
             try
             {
             {
-                if ( userInfo == null || userInfo.getUserEmailAddress() == null )
+                if ( optionalUserInfo.isPresent() )
                 {
                 {
-                    return "";
+                    final String emailAddress = optionalUserInfo.get().getUserEmailAddress();
+                    if ( !StringUtil.isEmpty( emailAddress ) )
+                    {
+
+                        return emailAddress;
+
+                    }
                 }
                 }
 
 
-                return userInfo.getUserEmailAddress();
             }
             }
             catch ( final PwmUnrecoverableException e )
             catch ( final PwmUnrecoverableException e )
             {
             {
-                LOGGER.error( request.getSessionLabel(), () -> "error reading user email address during macro replacement: " + e.getMessage() );
+                LOGGER.error( macroRequest.getSessionLabel(), () -> "error reading user email address during macro replacement: " + e.getMessage() );
                 return "";
                 return "";
             }
             }
+
+            return "";
+        }
+
+        abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );
+    }
+
+    public static class UserEmailMacro extends AbstractUserEmailMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:Email@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getUserInfo() );
+        }
+    }
+
+    public static class TargetUserEmailMacro extends AbstractUserEmailMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@TargetUser:Email@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public Scope getScope()
+        {
+            return Scope.TargetUser;
+        }
+
+        @Override
+        Optional<UserInfo> loadUserInfo( final MacroRequest macroRequest )
+        {
+            return Optional.ofNullable( macroRequest.getTargetUserInfo() );
         }
         }
     }
     }
 
 
@@ -442,7 +663,7 @@ public class UserMacros
                 LOGGER.error( request.getSessionLabel(),  () -> "error reading otp setup time during macro replacement: " + e.getMessage() );
                 LOGGER.error( request.getSessionLabel(),  () -> "error reading otp setup time during macro replacement: " + e.getMessage() );
             }
             }
 
 
-            return processTimeOutputMacro( otpSetupTime, matchValue, "OtpSetupTime" );
+            return processTimeOutputMacro( otpSetupTime, matchValue, Collections.singletonList( "OtpSetupTime" ) );
         }
         }
     }
     }
 
 
@@ -474,7 +695,7 @@ public class UserMacros
                 LOGGER.error( request.getSessionLabel(), () -> "error reading response setup time macro replacement: " + e.getMessage() );
                 LOGGER.error( request.getSessionLabel(), () -> "error reading response setup time macro replacement: " + e.getMessage() );
             }
             }
 
 
-            return processTimeOutputMacro( responseSetupTime, matchValue, "ResponseSetupTime" );
+            return processTimeOutputMacro( responseSetupTime, matchValue, Collections.singletonList( "ResponseSetupTime" ) );
         }
         }
     }
     }
 }
 }

+ 105 - 7
server/src/test/java/password/pwm/util/macro/MacroTest.java

@@ -114,11 +114,19 @@ public class MacroTest
     @Test
     @Test
     public void testUserIDMacro() throws Exception
     public void testUserIDMacro() throws Exception
     {
     {
-        final String goal = "test jrivard test";
+        final String goal = "test FLast test";
         final String expanded = macroRequest.expandMacros( "test @User:ID@ test" );
         final String expanded = macroRequest.expandMacros( "test @User:ID@ test" );
         Assert.assertEquals( goal, expanded );
         Assert.assertEquals( goal, expanded );
     }
     }
 
 
+    @Test
+    public void testTargetUserIDMacro() throws Exception
+    {
+        final String goal = "test TUser test";
+        final String expanded = macroRequest.expandMacros( "test @TargetUser:ID@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
     @Test
     @Test
     public void testUserPwExpireTimeMacro()
     public void testUserPwExpireTimeMacro()
     {
     {
@@ -147,6 +155,34 @@ public class MacroTest
         }
         }
     }
     }
 
 
+    @Test
+    public void testTargetUserPwExpireTimeMacro()
+    {
+        {
+            final String goal = "TargetUserPwExpireTime 1973-01-03T22:45:21Z test";
+            final String expanded = macroRequest.expandMacros( "TargetUserPwExpireTime @TargetUser:PwExpireTime@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "TargetUserPwExpireTime 10:45 PM, UTC test";
+            final String expanded = macroRequest.expandMacros( "TargetUserPwExpireTime @TargetUser:PwExpireTime:K/:mm a, z@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "TargetUserPwExpireTime 4:15 AM, IST test";
+            final String expanded = macroRequest.expandMacros( "TargetUserPwExpireTime @TargetUser:PwExpireTime:K/:mm a, z:IST@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "TargetUserPwExpireTime 1973.01.03 test";
+            final String expanded = macroRequest.expandMacros( "TargetUserPwExpireTime @TargetUser:PwExpireTime:yyyy.MM.dd@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+    }
+
     @Test
     @Test
     public void testUserOtpSetupTimeMacro()
     public void testUserOtpSetupTimeMacro()
     {
     {
@@ -214,6 +250,19 @@ public class MacroTest
         Assert.assertEquals( goal, expanded );
         Assert.assertEquals( goal, expanded );
     }
     }
 
 
+
+    @Test
+    public void testTargetUserDaysUntilPwExpireMacro()
+            throws PwmUnrecoverableException
+    {
+        final Duration duration = Duration.between( macroRequest.getTargetUserInfo().getPasswordExpirationTime(), Instant.now() );
+        final long days = TimeUnit.DAYS.convert( duration.toMillis(), TimeUnit.MILLISECONDS );
+
+        final String goal = "TargetUserDaysUntilPwExpire " + days + " test";
+        final String expanded = macroRequest.expandMacros( "TargetUserDaysUntilPwExpire @TargetUser:DaysUntilPwExpire@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
     @Test
     @Test
     public void testPasswordMacro()
     public void testPasswordMacro()
     {
     {
@@ -227,16 +276,26 @@ public class MacroTest
     public void testUserEmailMacro()
     public void testUserEmailMacro()
     {
     {
 
 
-        final String goal = "UserEmail zippy@example.com test";
+        final String goal = "UserEmail FLast@example.com test";
         final String expanded = macroRequest.expandMacros( "UserEmail @User:Email@ test" );
         final String expanded = macroRequest.expandMacros( "UserEmail @User:Email@ test" );
         Assert.assertEquals( goal, expanded );
         Assert.assertEquals( goal, expanded );
     }
     }
 
 
+    @Test
+    public void testTargetUserEmailMacro()
+    {
+
+        final String goal = "TargetUserEmail TUser@example.com test";
+        final String expanded = macroRequest.expandMacros( "TargetUserEmail @TargetUser:Email@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+
     @Test
     @Test
     public void testUserDNMacro()
     public void testUserDNMacro()
     {
     {
 
 
-        final String goal = "UserDN cn=test1,ou=test,o=org test";
+        final String goal = "UserDN cn=FLast,ou=test,o=org test";
         final String expanded = macroRequest.expandMacros( "UserDN @LDAP:DN@ test" );
         final String expanded = macroRequest.expandMacros( "UserDN @LDAP:DN@ test" );
         Assert.assertEquals( goal, expanded );
         Assert.assertEquals( goal, expanded );
     }
     }
@@ -255,14 +314,14 @@ public class MacroTest
     {
     {
         // userID macro
         // userID macro
         {
         {
-            final String goal = "cn=test1,ou=test,o=org";
+            final String goal = "cn=FLast,ou=test,o=org";
             final String expanded = macroRequest.expandMacros( "@LDAP:dn@" );
             final String expanded = macroRequest.expandMacros( "@LDAP:dn@" );
             Assert.assertEquals( goal, expanded );
             Assert.assertEquals( goal, expanded );
         }
         }
 
 
 
 
         {
         {
-            final String goal = "test cn%3Dtest1%2Cou%3Dtest%2Co%3Dorg";
+            final String goal = "test cn%3DFLast%2Cou%3Dtest%2Co%3Dorg";
             final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@LDAP:dn@]]@" );
             final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@LDAP:dn@]]@" );
             Assert.assertEquals( goal, expanded );
             Assert.assertEquals( goal, expanded );
         }
         }
@@ -289,19 +348,58 @@ public class MacroTest
         }
         }
     }
     }
 
 
+    @Test
+    public void testTargetUserLdapMacro()
+    {
+        // userID macro
+        {
+            final String goal = "cn=TUser,ou=test,o=org";
+            final String expanded = macroRequest.expandMacros( "@TargetUser:LDAP:dn@" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+
+        {
+            final String goal = "test cn%3DTUser%2Cou%3Dtest%2Co%3Dorg";
+            final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@TargetUser:LDAP:dn@]]@" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        // user attribute macro
+        {
+            final String goal = "test Target test";
+            final String expanded = macroRequest.expandMacros( "test @TargetUser:LDAP:givenName@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        // user attribute with max chars macro
+        {
+            final String goal = "test Targ test";
+            final String expanded = macroRequest.expandMacros( "test @TargetUser:LDAP:givenName:4@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        // user attribute with max chars and pad macro
+        {
+            final String goal = "test Targetoooo test";
+            final String expanded = macroRequest.expandMacros( "test @TargetUser:LDAP:givenName:10:o@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+    }
+
     @Test
     @Test
     public void testUserLdapMacro()
     public void testUserLdapMacro()
     {
     {
         // userID macro
         // userID macro
         {
         {
-            final String goal = "cn=test1,ou=test,o=org";
+            final String goal = "cn=FLast,ou=test,o=org";
             final String expanded = macroRequest.expandMacros( "@User:LDAP:dn@" );
             final String expanded = macroRequest.expandMacros( "@User:LDAP:dn@" );
             Assert.assertEquals( goal, expanded );
             Assert.assertEquals( goal, expanded );
         }
         }
 
 
 
 
         {
         {
-            final String goal = "test cn%3Dtest1%2Cou%3Dtest%2Co%3Dorg";
+            final String goal = "test cn%3DFLast%2Cou%3Dtest%2Co%3Dorg";
             final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@User:LDAP:dn@]]@" );
             final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@User:LDAP:dn@]]@" );
             Assert.assertEquals( goal, expanded );
             Assert.assertEquals( goal, expanded );
         }
         }