/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 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.util.macro; import com.novell.ldapchai.exception.ChaiException; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.LoginInfoBean; import password.pwm.bean.UserIdentity; import password.pwm.bean.UserInfoBean; import password.pwm.config.PwmSetting; import password.pwm.error.PwmUnrecoverableException; import password.pwm.ldap.UserDataReader; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.StringUtil; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import password.pwm.util.secure.PwmRandom; import java.net.MalformedURLException; import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.regex.Pattern; public abstract class StandardMacros { private static final PwmLogger LOGGER = PwmLogger.forClass(StandardMacros.class); public static final Map<Class<? extends MacroImplementation>,MacroImplementation.Scope> STANDARD_MACROS; static { final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> defaultMacros = new LinkedHashMap<>(); // system macros defaultMacros.put(CurrentTimeMacro.class, MacroImplementation.Scope.System); defaultMacros.put(InstanceIDMacro.class, MacroImplementation.Scope.System); defaultMacros.put(DefaultEmailFromAddressMacro.class, MacroImplementation.Scope.System); defaultMacros.put(SiteURLMacro.class, MacroImplementation.Scope.System); defaultMacros.put(SiteHostMacro.class, MacroImplementation.Scope.System); defaultMacros.put(RandomCharMacro.class, MacroImplementation.Scope.System); defaultMacros.put(RandomNumberMacro.class, MacroImplementation.Scope.System); defaultMacros.put(UUIDMacro.class, MacroImplementation.Scope.System); // user or system macros defaultMacros.put(UserIDMacro.class, MacroImplementation.Scope.System); // user macros defaultMacros.put(LdapMacro.class, MacroImplementation.Scope.User); defaultMacros.put(UserPwExpirationTimeMacro.class, MacroImplementation.Scope.User); defaultMacros.put(UserPwExpirationTimeDefaultMacro.class, MacroImplementation.Scope.User); defaultMacros.put(UserDaysUntilPwExpireMacro.class, MacroImplementation.Scope.User); defaultMacros.put(UserEmailMacro.class, MacroImplementation.Scope.User); defaultMacros.put(UserPasswordMacro.class, MacroImplementation.Scope.User); defaultMacros.put(UserLdapProfileMacro.class, MacroImplementation.Scope.User); defaultMacros.put(OtpSetupTimeMacro.class, MacroImplementation.Scope.User); defaultMacros.put(ResponseSetupTimeMacro.class, MacroImplementation.Scope.User); // wrapper macros: must be at the end to allow Macro in Macro during parsing defaultMacros.put(EncodingMacro.class, MacroImplementation.Scope.System); STANDARD_MACROS = Collections.unmodifiableMap(defaultMacros); } public static class LdapMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@LDAP" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) throws MacroParseException { final UserDataReader userDataReader = macroRequestInfo.getUserDataReader(); if (userDataReader == null) { return ""; } final List<String> parameters = splitMacroParameters(matchValue,"LDAP"); final String ldapAttr; if (parameters.size() > 0 && !parameters.get(0).isEmpty()) { ldapAttr = parameters.get(0); } else { throw new MacroParseException("required attribute name parameter is missing"); } final int length; if (parameters.size() > 1 && !parameters.get(1).isEmpty()) { try { length = Integer.parseInt(parameters.get(1)); } catch (NumberFormatException e) { throw new MacroParseException("error parsing length parameter: " + e.getMessage()); } final int maxLengthPermitted = Integer.parseInt(macroRequestInfo.getPwmApplication().getConfig().readAppProperty(AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH)); if (length > maxLengthPermitted) { throw new MacroParseException("maximum permitted length of LDAP attribute (" + maxLengthPermitted + ") exceeded"); } else if (length <= 0) { throw new MacroParseException("length parameter must be greater than zero"); } } else { length = 0; } final String paddingChar; if (parameters.size() > 2 && !parameters.get(2).isEmpty()) { paddingChar = parameters.get(2); } else { paddingChar = ""; } if (parameters.size() > 3) { throw new MacroParseException("too many parameters"); } final String ldapValue; if ("dn".equalsIgnoreCase(ldapAttr)) { ldapValue = userDataReader.getUserDN(); } else { try { ldapValue = userDataReader.readStringAttribute(ldapAttr); } catch (ChaiException e) { LOGGER.trace("could not replace value for '" + matchValue + "', ldap error: " + e.getMessage()); return ""; } if (ldapValue == null || ldapValue.length() < 1) { LOGGER.trace("could not replace value for '" + matchValue + "', user does not have value for '" + ldapAttr + "'"); return ""; } } String returnValue = ldapValue == null ? "" : ldapValue; if (length > 0 && length < returnValue.length()) { returnValue = returnValue.substring(0, length); } if (length > 0 && paddingChar.length() > 0) { while (returnValue.length() < length) { returnValue += paddingChar; } } return returnValue; } } public static class InstanceIDMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@InstanceID@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication(); if (pwmApplication == null) { LOGGER.error("could not replace value for '" + matchValue + "', pwmApplication is null"); return ""; } return pwmApplication.getInstanceID(); } } public static class CurrentTimeMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@CurrentTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) throws MacroParseException { final List<String> parameters = splitMacroParameters(matchValue,"CurrentTime"); final DateFormat dateFormat; if (parameters.size() > 0 && !parameters.get(0).isEmpty()) { try { dateFormat = new SimpleDateFormat(parameters.get(0)); } catch (IllegalArgumentException e) { throw new MacroParseException(e.getMessage()); } } else { dateFormat = new SimpleDateFormat(PwmConstants.DEFAULT_DATETIME_FORMAT_STR); } final TimeZone tz; if (parameters.size() > 1 && !parameters.get(1).isEmpty()) { final String desiredTz = parameters.get(1); final List<String> avalibleIDs = Arrays.asList(TimeZone.getAvailableIDs()); if (!avalibleIDs.contains(desiredTz)) { throw new MacroParseException("unknown timezone"); } tz = TimeZone.getTimeZone(desiredTz); } else { tz = PwmConstants.DEFAULT_TIMEZONE; } if (parameters.size() > 2) { throw new MacroParseException("too many parameters"); } dateFormat.setTimeZone(tz); return dateFormat.format(new Date()); } } public static class UserPwExpirationTimeMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:PwExpireTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) throws MacroParseException { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean == null) { return ""; } final Instant pwdExpirationTime = userInfoBean.getPasswordExpirationTime(); if (pwdExpirationTime == null) { return ""; } final String datePattern = matchValue.substring(19, matchValue.length() - 1); if (datePattern.length() > 0) { try { final DateFormat dateFormat = new SimpleDateFormat(datePattern); return dateFormat.format(pwdExpirationTime); } catch (IllegalArgumentException e) { throw new MacroParseException(e.getMessage()); } } return JavaHelper.toIsoDate(pwdExpirationTime); } } public static class UserPwExpirationTimeDefaultMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:PwExpireTime@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean == null) { return ""; } final Instant pwdExpirationTime = userInfoBean.getPasswordExpirationTime(); if (pwdExpirationTime == null) { return ""; } return JavaHelper.toIsoDate(pwdExpirationTime); } } public static class UserDaysUntilPwExpireMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:DaysUntilPwExpire@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean == null) { LOGGER.error("could not replace value for '" + matchValue + "', userInfoBean is null"); return ""; } final Instant pwdExpirationTime = userInfoBean.getPasswordExpirationTime(); final TimeDuration timeUntilExpiration = TimeDuration.fromCurrent(pwdExpirationTime); final long daysUntilExpiration = timeUntilExpiration.getDays(); return String.valueOf(daysUntilExpiration); } } public static class UserIDMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:ID@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean == null || userInfoBean.getUsername() == null) { return ""; } return userInfoBean.getUsername(); } } public static class UserLdapProfileMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:LdapProfile@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean != null) { final UserIdentity userIdentity = userInfoBean.getUserIdentity(); if (userIdentity != null) { return userIdentity.getLdapProfileID(); } } return ""; } } public static class UserEmailMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:Email@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean == null || userInfoBean.getUserEmailAddress() == null) { return ""; } return userInfoBean.getUserEmailAddress(); } } public static class UserPasswordMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@User:Password@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { final LoginInfoBean loginInfoBean = macroRequestInfo.getLoginInfoBean(); try { if (loginInfoBean == null || loginInfoBean.getUserCurrentPassword() == null) { return ""; } return loginInfoBean.getUserCurrentPassword().getStringValue(); } catch (PwmUnrecoverableException e) { LOGGER.error("error decrypting in memory password during macro replacement: " + e.getMessage()); return ""; } } public MacroDefinitionFlag[] flags() { return new MacroDefinitionFlag[] { MacroDefinitionFlag.SensitiveValue }; } } public static class SiteURLMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@SiteURL@|@Site:URL@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { return macroRequestInfo.getPwmApplication().getConfig().readSettingAsString(PwmSetting.PWM_SITE_URL); } } public static class DefaultEmailFromAddressMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@DefaultEmailFromAddress@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { return macroRequestInfo.getPwmApplication().getConfig().readSettingAsString(PwmSetting.EMAIL_DEFAULT_FROM_ADDRESS); } } public static class SiteHostMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@SiteHost@|@Site:Host@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { try { final String siteUrl = macroRequestInfo.getPwmApplication().getConfig().readSettingAsString(PwmSetting.PWM_SITE_URL); final URL url = new URL(siteUrl); return url.getHost(); } catch (MalformedURLException e) { LOGGER.error("unable to parse configured/detected site URL: " + e.getMessage()); } return ""; } } public static class RandomCharMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@RandomChar(:[^@]*)?@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) throws MacroParseException { if (matchValue == null || matchValue.length() < 1) { return ""; } final List<String> parameters = splitMacroParameters(matchValue,"RandomChar"); int length = 1; if (parameters.size() > 0 && !parameters.get(0).isEmpty()) { final int maxLengthPermitted = Integer.parseInt(macroRequestInfo.getPwmApplication().getConfig().readAppProperty(AppProperty.MACRO_RANDOM_CHAR_MAX_LENGTH)); try { length = Integer.parseInt(parameters.get(0)); if (length > maxLengthPermitted) { throw new MacroParseException("maximum permitted length of RandomChar (" + maxLengthPermitted + ") exceeded"); } else if (length <= 0) { throw new MacroParseException("length of RandomChar (" + maxLengthPermitted + ") must be greater than zero"); } } catch (NumberFormatException e) { throw new MacroParseException("error parsing length parameter of RandomChar: " + e.getMessage()); } } if (parameters.size() > 1 && !parameters.get(1).isEmpty()) { final String chars = parameters.get(1); return PwmRandom.getInstance().alphaNumericString(chars,length); } else { return PwmRandom.getInstance().alphaNumericString(length); } } } public static class RandomNumberMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@RandomNumber(:[^@]*)?@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) throws MacroParseException { if (matchValue == null || matchValue.length() < 1) { return ""; } final List<String> parameters = splitMacroParameters(matchValue,"RandomNumber"); if (parameters.size() != 2) { throw new MacroParseException("incorrect number of parameter of RandomNumber: " + parameters.size() + ", should be 2"); } final int min; final int max; try { min = Integer.parseInt(parameters.get(0)); } catch (NumberFormatException e) { throw new MacroParseException("error parsing minimum value parameter of RandomNumber: " + e.getMessage()); } try { max = Integer.parseInt(parameters.get(1)); } catch (NumberFormatException e) { throw new MacroParseException("error parsing maximum value parameter of RandomNumber: " + e.getMessage()); } if (min > max) { throw new MacroParseException("minimum value is less than maximum value parameter of RandomNumber"); } final int range = max - min; return String.valueOf(PwmRandom.getInstance().nextInt(range) + min); } } public static class UUIDMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@UUID@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) { return PwmRandom.getInstance().randomUUID().toString(); } } public static class EncodingMacro extends AbstractMacro { private static final Pattern PATTERN = Pattern.compile("@Encode:[^:]+:\\[\\[.*\\]\\]@"); // @Encode:ENCODE_TYPE:value@ private enum EncodeType { urlPath, urlParameter, base64, ; private String encode(final String input) throws MacroParseException { switch (this) { case urlPath: return StringUtil.urlEncode(input); case urlParameter: return StringUtil.urlEncode(input); case base64: return StringUtil.base64Encode(input.getBytes(PwmConstants.DEFAULT_CHARSET)); default: throw new MacroParseException("unimplemented encodeType '" + this.toString() + "' for Encode macro"); } } private static EncodeType forString(final String input) { for (final EncodeType encodeType : EncodeType.values()) { if (encodeType.toString().equalsIgnoreCase(input)) { return encodeType; } } return null; } } public Pattern getRegExPattern() { return PATTERN; } public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo ) throws MacroParseException { if (matchValue == null || matchValue.length() < 1) { return ""; } final String[] colonParts = matchValue.split(":"); if (colonParts.length < 3) { throw new MacroParseException("not enough arguments for Encode macro"); } final String encodeMethodStr = colonParts[1]; final EncodeType encodeType = EncodeType.forString(encodeMethodStr); if (encodeType == null) { throw new MacroParseException("unknown encodeType '" + encodeMethodStr + "' for Encode macro"); } String value = matchValue; // can't use colonParts[2] as it may be split if value contains a colon. value = value.replaceAll("^@Encode:[^:]+:\\[\\[",""); value = value.replaceAll("\\]\\]@$",""); return encodeType.encode(value); } } public static class OtpSetupTimeMacro extends InternalMacros.InternalAbstractMacro { private static final Pattern PATTERN = Pattern.compile("@OtpSetupTime@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue(final String matchValue, final MacroRequestInfo macroRequestInfo) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean != null && userInfoBean.getOtpUserRecord() != null && userInfoBean.getOtpUserRecord().getTimestamp() != null) { return JavaHelper.toIsoDate(userInfoBean.getOtpUserRecord().getTimestamp()); } return ""; } } public static class ResponseSetupTimeMacro extends InternalMacros.InternalAbstractMacro { private static final Pattern PATTERN = Pattern.compile("@ResponseSetupTime@"); public Pattern getRegExPattern() { return PATTERN; } public String replaceValue(final String matchValue, final MacroRequestInfo macroRequestInfo) { final UserInfoBean userInfoBean = macroRequestInfo.getUserInfoBean(); if (userInfoBean != null && userInfoBean.getResponseInfoBean() != null && userInfoBean.getResponseInfoBean().getTimestamp() != null) { return JavaHelper.toIsoDate(userInfoBean.getResponseInfoBean().getTimestamp()); } return ""; } } }