/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.security.password;
import com.emc.storageos.coordinator.client.model.PropertyInfoExt;
import com.emc.storageos.coordinator.client.service.CoordinatorClient;
import com.emc.storageos.coordinator.exceptions.CoordinatorException;
import com.emc.storageos.db.client.DbClient;
import com.emc.storageos.db.client.impl.EncryptionProviderImpl;
import com.emc.storageos.db.client.URIUtil;
import com.emc.storageos.db.client.model.EncryptionProvider;
import com.emc.storageos.db.client.model.PasswordHistory;
import com.emc.storageos.model.password.PasswordChangeParam;
import com.emc.storageos.model.password.PasswordResetParam;
import com.emc.storageos.model.password.PasswordUpdateParam;
import com.emc.storageos.model.password.PasswordValidateParam;
import com.emc.storageos.model.property.PropertyInfo;
import com.emc.storageos.security.authentication.AuthSvcInternalApiClientIterator;
import com.emc.storageos.security.authentication.StorageOSUser;
import com.emc.storageos.security.authentication.SysSvcEndPointLocator;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.svcs.errorhandling.resources.BadRequestException;
import com.sun.jersey.api.client.ClientResponse;
import org.apache.commons.codec.digest.Crypt;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.text.MessageFormat;
import java.util.*;
public class PasswordUtils {
private static final Logger _log = LoggerFactory.getLogger(PasswordUtils.class);
private EncryptionProvider encryptionProvider;
private DbClient dbClient;
public void setDbClient(DbClient dbClient) {
this.dbClient = dbClient;
}
private static CoordinatorClient coordinator;
public void setCoordinator(CoordinatorClient coordinator) {
this.coordinator = coordinator;
EncryptionProviderImpl encryptionProvider1 = new EncryptionProviderImpl();
encryptionProvider1.setCoordinator(coordinator);
encryptionProvider1.start();
this.encryptionProvider = encryptionProvider1;
}
public void setEncryptionProvider(EncryptionProvider encryptionProvider) {
this.encryptionProvider = encryptionProvider;
}
private static Properties defaultProperties;
public synchronized static void setDefaultProperties(Properties defaults) {
defaultProperties = defaults;
}
public synchronized Properties getDefaultProperties() {
return defaultProperties;
}
private Map<String, StorageOSUser> _localUsers;
public void setLocalUsers(Map<String, StorageOSUser> localUsers) {
_localUsers = localUsers;
}
private static final URI URI_CHANGE_PASSWORD = URI.create("/password/internal/change-password");
private static final URI URI_VALIDATE_PASSWORD = URI.create("/password/internal/validate-change");
private static final int MAX_CONFIG_RETRIES = 5;
/**
* generate URI for storing user password history in Cassandra.
*
* @param username
* @return
*/
public static URI getLocalPasswordHistoryURI(String username) {
String vdcId = URIUtil.getLocation(PasswordHistory.class);
URI pwdUri = URI.create(String.format("urn:storageos:%1$s:%2$s:%3$s", PasswordHistory.class.getSimpleName(), username, vdcId));
return pwdUri;
}
/**
* retrieve user's password history from Cassandra
*
* @param username
* @return
*/
public PasswordHistory getPasswordHistory(String username) {
PasswordHistory lph = dbClient.queryObject(PasswordHistory.class, getLocalPasswordHistoryURI(username));
return lph;
}
/**
* set all local users' password expire date to the same date, if data is null, remove expire time for all users
*
* this method was used for turn on/off password expire rule
*
* @param date
*/
public void setExpireDateToAll(Calendar date) {
for (String user : _localUsers.keySet()) {
setExpireTimeOfUser(user, date);
}
}
/**
* update user's expireTime in Cassandra
*
* @param user
* @param expireTime
*/
private void setExpireTimeOfUser(String user, Calendar expireTime) {
Password password = constructUserPassword(user);
PasswordHistory ph = password.getPasswordHistory();
ph.setExpireDate(expireTime);
dbClient.updateAndReindexObject(ph);
_log.info("set new expire time for user " + user + ": "
+ (expireTime == null ? "null" : expireTime.getTime()));
}
/*
* design goals:
* 1. every password deserves a whole set of notification procedure before it expires.
* 2. after the first mail sent, user start aware of their password is about to expire. so after
* this point, the expire time shouldn't be change any more, even the expire rule changes.
*
* when change expire rule, shorten or extend expire days:
* 1. For users, whose old password expire time is before grace_point (the point "now + GRACE_DAYS"),
* will be no change. this is because user has already got their first notification mail. it is
* important to keep time in notification mails consistent and accurate, this could also
* avoid password immediate expiration issue.
*
* 2. for old expire time is after grace day, re-calculate new expire time as:
* new_expire_time = last_change_time + expire_days
* what the real expire time is, depends on the following:
* 2.1: if new expire time before grace point, set it to grace point, to fulfill goal 1.
* 2.2: if new expire time after grace point, set it as its real expire time.
*/
public void adjustExpireTime(int newDays) {
for (String user : _localUsers.keySet()) {
Calendar newExpireDate = calculateExpireDateForUser(user, newDays);
setExpireTimeOfUser(user, newExpireDate);
}
}
public Calendar calculateExpireDateForUser(String user, int newDays) {
Calendar gracePoint = Calendar.getInstance();
gracePoint.add(Calendar.DATE, Constants.GRACE_DAYS);
Password password = constructUserPassword(user);
Calendar oldExpireTime = password.getPasswordHistory().getExpireDate();
if (oldExpireTime != null && oldExpireTime.before(gracePoint)) {
return oldExpireTime;
}
long lastChangeTime = password.getLatestChangedTime();
long longNewExpireTime = lastChangeTime + dayToMilliSeconds(newDays);
Calendar newExpireTime = Calendar.getInstance();
newExpireTime.setTimeInMillis(longNewExpireTime);
if (newExpireTime.after(gracePoint)) {
return newExpireTime;
} else {
return gracePoint;
}
}
/**
* check if two passwords match, one parameter is in clear text, the other is encoded.
*
* @param clearTextPassword
* @param encpassword
* @return
*/
public boolean match(String clearTextPassword, String encpassword) {
// A hashed value will start with the SHA-512 identifier ($6$)
if (StringUtils.startsWith(encpassword, Constants.CRYPT_SHA_512)) {
String hashedValue = Crypt.crypt(clearTextPassword, encpassword);
return encpassword.equals(hashedValue);
} else {
String encryptedValue = encryptionProvider.getEncryptedString(clearTextPassword);
return encpassword.equals(encryptedValue);
}
}
/**
* get encrypted string
*/
public String getEncryptedString(String clearText) {
return encryptionProvider.getEncryptedString(clearText);
}
/**
* get current system properties
*
* @return
*/
public Map<String, String> getConfigProperties() {
Map<String, String> mergedProps = new HashMap();
Set<Map.Entry<Object, Object>> defaults = defaultProperties.entrySet();
for (Map.Entry<Object, Object> p : defaults) {
mergedProps.put((String) p.getKey(), (String) p.getValue());
}
Map<String, String> overrides = new HashMap();
Map<String, String> siteScopeprops = new HashMap();
try {
overrides = coordinator.getTargetInfo(PropertyInfoExt.class).getProperties();
} catch (Exception e) {
_log.info("Fail to get the cluster information ", e);
}
try {
PropertyInfo targetInfo = coordinator.getTargetInfo(coordinator.getSiteId(), PropertyInfoExt.class);
if (targetInfo != null) {
siteScopeprops = targetInfo.getProperties();
}
} catch (Exception e) {
_log.info("Fail to get the site information ", e);
}
for (Map.Entry<String, String> entry : overrides.entrySet()) {
mergedProps.put(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : siteScopeprops.entrySet()) {
mergedProps.put(entry.getKey(), entry.getValue());
}
return mergedProps;
}
/**
* get a property from System Properties
*
* @param key
* @return
*/
public String getConfigProperty(String key) {
Map<String, String> configProperties = getConfigProperties();
return configProperties.get(key);
}
/**
* validate PasswordUpdateParam
*
* @param username
* @param passwordUpdate
*/
public void validatePasswordParameter(String username, PasswordUpdateParam passwordUpdate) {
validatePasswordParameter(username,
passwordUpdate.getOldPassword(),
passwordUpdate.getPassword(),
passwordUpdate.getEncPassword(),
ValidatorType.UPDATE);
}
public void validatePasswordParameter(PasswordResetParam passwordReset) {
validatePasswordParameter(passwordReset.getUsername(),
null,
passwordReset.getPassword(),
passwordReset.getEncPassword(),
ValidatorType.RESET);
}
public void validatePasswordParameter(PasswordValidateParam passwordValidate) {
validatePasswordParameter(null,
null,
passwordValidate.getPassword(),
null,
ValidatorType.VALIDATE_CONTENT);
}
public void validatePasswordParameter(PasswordChangeParam passwordChange) {
validatePasswordParameter(passwordChange.getUsername(),
passwordChange.getOldPassword(),
passwordChange.getPassword(),
null,
ValidatorType.CHANGE);
}
/**
* validate password APIs input parameters.
*
* @param username
* @param oldPassword
* @param password
* @param encpassword
* @param type
*/
private void validatePasswordParameter(String username,
String oldPassword,
String password,
String encpassword,
ValidatorType type) {
// one of the parameters must present, but not both
boolean isPresent = (password != null && !password.isEmpty()) ^
(encpassword != null && !encpassword.isEmpty());
if (!isPresent) {
throw APIException.badRequests.parameterIsNullOrEmpty("password, encpassword");
}
// if oldPassword presents, verify it
if (oldPassword != null && !oldPassword.isEmpty()) {
if (!match(oldPassword, getUserPassword(username))) {
throw BadRequestException.badRequests.passwordInvalidOldPassword();
}
}
if (password != null && !password.isEmpty()) {
PasswordValidator validator = null;
switch (type) {
case CHANGE:
validator = ValidatorFactory.buildChangeValidator(getConfigProperties(), this);
break;
case RESET:
validator = ValidatorFactory.buildResetValidator(getConfigProperties());
break;
case UPDATE:
validator = ValidatorFactory.buildUpdateValidator(getConfigProperties(), this);
break;
case VALIDATE_CONTENT:
validator = ValidatorFactory.buildContentValidator(getConfigProperties());
break;
}
Password pw = new Password(username, oldPassword, password);
if (StringUtils.isNotBlank(username)) {
pw.setPasswordHistory(getPasswordHistory(username));
}
validator.validate(pw);
}
}
/**
* a wrapper to call change-password internal API or validate-change internal API in PasswordService
*
* bDryRun: if true, call validate-change internal API
* if false, call change-password internal API
*
* @param passwordChange
* @param bDryRun
* @return
*/
public Response changePassword(PasswordChangeParam passwordChange, boolean bDryRun) {
SysSvcEndPointLocator sysSvcEndPointLocator = new SysSvcEndPointLocator();
sysSvcEndPointLocator.setCoordinator(coordinator);
int attempts = 0;
ClientResponse response = null;
while (attempts < MAX_CONFIG_RETRIES) {
_log.debug("change password attempt {}", ++attempts);
AuthSvcInternalApiClientIterator sysSvcClientItr =
new AuthSvcInternalApiClientIterator(sysSvcEndPointLocator,
coordinator);
try {
if (sysSvcClientItr.hasNext()) {
if (bDryRun) {
_log.debug("change password dry run");
response = sysSvcClientItr.post(URI_VALIDATE_PASSWORD, passwordChange);
} else {
response = sysSvcClientItr.put(URI_CHANGE_PASSWORD, passwordChange);
}
_log.debug("change password response with status: " + response.getStatus());
break;
}
} catch (Exception exception) {
// log the exception and retry the request
_log.warn(exception.getMessage());
if (attempts == MAX_CONFIG_RETRIES - 1) {
throw exception;
}
}
}
Response.ResponseBuilder b = Response.status(response.getStatus());
if (!(response.getStatus() == ClientResponse.Status.NO_CONTENT.getStatusCode())) {
b.entity(response.getEntity(String.class));
}
return b.build();
}
/**
* get user's encpassword from system properties
*
* @param username
* @return
*/
public String getUserPassword(String username) {
PropertyInfo props = null;
try {
props = coordinator.getPropertyInfo();
} catch (CoordinatorException e) {
_log.error("Access local user properties failed", e);
return null;
}
if (props == null) {
_log.error("Access local user properties failed");
return null;
}
String encpassword = props.getProperty(
"system_" + username + "_encpassword");
if (StringUtils.isBlank(encpassword)) {
_log.error("No password set for user {} ", username);
return null;
}
return encpassword;
}
/**
* construct a Password object which only contains its password history information
*
* @param username
* @return
*/
public Password constructUserPassword(String username) {
Password password = new Password(username, null, null);
PasswordHistory ph = getPasswordHistory(username);
password.setPasswordHistory(ph);
return password;
}
public static long dayToMilliSeconds(long days) {
return days * 24 * 60 * 60 * 1000;
}
private enum ValidatorType {
UPDATE,
RESET,
CHANGE,
VALIDATE_CONTENT
}
/**
* update user's password expire date.
*
* if it is not reset by securityAdmin, also add the password in user's password history
*
* @param username
* @param hashedPassword
*/
public void updatePasswordHistory(String username, String hashedPassword, Calendar expireTime, boolean bReset) {
PasswordHistory lph = getPasswordHistory(username);
boolean isNew = false;
if (lph == null) {
isNew = true;
lph = new PasswordHistory();
lph.setId(getLocalPasswordHistoryURI(username));
}
Calendar now = Calendar.getInstance();
if (!bReset) {
lph.getUserPasswordHash().put(hashedPassword, now.getTimeInMillis());
}
lph.setExpireDate(expireTime);
if (isNew) {
dbClient.createObject(lph);
} else {
dbClient.updateAndReindexObject(lph);
}
}
/**
* get the days after Epoch: Jan 01, 1970
*
* @param date
* @return
*/
public static int getDaysAfterEpoch(Calendar date) {
if (date == null) {
return 0;
}
Long diff = (date.getTimeInMillis() / (24 * 60 * 60 * 1000));
return diff.intValue();
}
/**
* prompt string list for password rules which turned on.
*
* this is used to provide help infomation for UI changePassword.html.
*/
public List<String> getPasswordChangePromptRules() {
List<String> promptRules = new ArrayList<String>();
Map<String, String> properties = getConfigProperties();
for (int i = 0; i < Constants.PASSWORD_CHANGE_PROMPT.length; i++) {
String key = Constants.PASSWORD_CHANGE_PROMPT[i][0];
String value = properties.get(key);
if (NumberUtils.toInt(value) != 0) {
promptRules.add(MessageFormat.format(Constants.PASSWORD_CHANGE_PROMPT[i][1], value));
}
}
return promptRules;
}
/**
* check if a user is a local user.
*
* @param username
* @return
*/
public boolean isLocalUser(String username) {
return _localUsers.keySet().contains(username);
}
}