/*
* Copyright (c) 2013-2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.systemservices.impl.resource;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.Status;
import javax.servlet.http.HttpServletRequest;
import com.emc.storageos.db.client.model.PasswordHistory;
import com.emc.storageos.model.password.PasswordChangeParam;
import com.emc.storageos.security.password.Constants;
import com.emc.storageos.security.password.InvalidLoginManager;
import com.emc.storageos.security.password.NotificationManager;
import com.emc.storageos.security.password.PasswordUtils;
import com.emc.storageos.svcs.errorhandling.resources.BadRequestException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.emc.storageos.security.authentication.InternalLogoutClient;
import com.emc.storageos.security.authentication.StorageOSUser;
import com.emc.storageos.security.authorization.BasePermissionsHelper;
import com.emc.storageos.security.authorization.CheckPermission;
import com.emc.storageos.security.authorization.Role;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.systemservices.impl.util.LocalPasswordHandler;
import com.emc.storageos.services.OperationTypeEnum;
import com.emc.storageos.security.audit.AuditLogManager;
import static com.emc.storageos.model.property.PropertyConstants.ENCRYPTEDSTRING;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import com.emc.storageos.db.client.DbClient;
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.password.SSHKeyUpdateParam;
import com.emc.storageos.model.property.PropertiesMetadata;
import com.emc.storageos.model.property.PropertyMetadata;
/**
* This REST API allows an authenticated local user to:
* - making request to change the user's own password.
* - If the authenticated user is with security admin role,
* making request to set another local user's password.
*/
@Path("/password")
public class PasswordService {
// Logger reference.
private static final Logger _logger = LoggerFactory.getLogger(PasswordService.class);
private static final String EVENT_SERVICE_TYPE = "password";
private static final String CRYPT_SHA_512 = "$6$";
private static SimpleDateFormat _format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Autowired
private AuditLogManager _auditMgr;
@Autowired
private InternalLogoutClient internalLogoutClient;
@Autowired
private DbClient _dbClient;
@Autowired
protected InvalidLoginManager _invLoginManager;
@Autowired
private BasePermissionsHelper _permissionsHelper = null;
@Context
SecurityContext _sc;
@Context
HttpServletRequest _req;
private LocalPasswordHandler _passwordHandler;
private PropertiesMetadata _propertiesMetadata;
private Map<String, StorageOSUser> _localUsers;
private NotificationManager _notificationManager;
// Spring injected property.
public void setPropertiesMetadata(PropertiesMetadata propertiesMetadata) {
_propertiesMetadata = propertiesMetadata;
}
public void setPasswordHandler(LocalPasswordHandler passwordHandler) {
_passwordHandler = passwordHandler;
}
public void setSecurityContext(SecurityContext sc) {
_sc = sc;
}
public void setAuditLogManager(AuditLogManager auditMgr) {
_auditMgr = auditMgr;
}
public void setLocalUsers(Map<String, StorageOSUser> localUsers) {
_localUsers = localUsers;
}
public void setNotificationManager(NotificationManager notificationManager) {
_notificationManager = notificationManager;
}
/**
* Return the map of the properties metadata.
*
* @return
*/
private Map<String, PropertyMetadata> getMetaData() {
return _propertiesMetadata.getGlobalMetadata();
}
/**
* Change an authenticated local user's own password and logs out the user's tokens.
* This interface accepts a clear test password or a
* password already hashed by the caller.
* If both form fields are specified, bad request will be returned.
*
* @brief Change your password
* @param logout Optional. If set to false, will not logout user sessions.
* @prereq none
* @throws APIException
*/
@PUT
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response updatePassword(@Context HttpServletRequest httpRequest,
@Context HttpServletResponse servletResponse,
PasswordUpdateParam passwordUpdate,
@DefaultValue("true") @QueryParam("logout_user") boolean logout) {
checkPasswordParameter(httpRequest, passwordUpdate, true);
String username = _sc.getUserPrincipal().getName();
String clientIP = _invLoginManager.getClientIP(httpRequest);
_logger.info("update Password for user {}", username);
setUserPassword(username, passwordUpdate.getPassword(), passwordUpdate.getEncPassword(), false);
auditPassword(OperationTypeEnum.CHANGE_LOCAL_AUTHUSER_PASSWORD,
AuditLogManager.AUDITLOG_SUCCESS, null, username);
_invLoginManager.removeInvalidRecord(clientIP);
if (logout && !internalLogoutClient.logoutUser(null, _req)) {
_logger.error("Password changed but unable to logout user active sessions.");
}
return Response.ok("Password Changed for " +
_sc.getUserPrincipal().getName() + "\n").build();
}
private void checkPasswordParameter(HttpServletRequest httpRequest,
PasswordUpdateParam passwordUpdate, boolean bEnableBlock) {
checkSecurityContext();
String username = _sc.getUserPrincipal().getName();
if (!((StorageOSUser) _sc.getUserPrincipal()).isLocal()) {
throw APIException.forbidden.nonLocalUserNotAllowed();
}
String clientIP = _invLoginManager.getClientIP(httpRequest);
_logger.debug("Client IP: {}", clientIP);
if (_invLoginManager.isTheClientIPBlocked(clientIP)) {
_logger.error("The client IP is blocked for too many invalid login attempts: " + clientIP);
throw APIException.unauthorized.
exceedingErrorLoginLimit(_invLoginManager.getMaxAuthnLoginAttemtsCount(),
_invLoginManager.getTimeLeftToUnblock(clientIP));
}
if (StringUtils.isEmpty(passwordUpdate.getOldPassword())) {
if (bEnableBlock) {
_invLoginManager.markErrorLogin(clientIP);
}
throw BadRequestException.badRequests.passwordInvalidOldPassword();
}
try {
_passwordHandler.getPasswordUtils().validatePasswordParameter(username, passwordUpdate);
} catch (BadRequestException badRequestException) {
if (bEnableBlock && badRequestException.getMessage().contains(_invLoginManager.OLD_PASSWORD_INVALID_ERROR)) {
_invLoginManager.markErrorLogin(clientIP);
}
throw badRequestException;
}
}
/**
* Change a given local user's password and logs out his auth tokens.
* The authenticated caller must have SEC_ADMIN role.
*
* @brief Change the password of a given user
* @param logout Optional. If set to false, will not logout user sessions.
* @prereq none
* @throws APIException
*/
@PUT
@Path("/reset")
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN })
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response updateUserPassword(PasswordResetParam passwordReset, @DefaultValue("true") @QueryParam("logout_user") boolean logout) {
checkSecurityContext();
String principal = _sc.getUserPrincipal().getName();
String username = passwordReset.getUsername();
if (!_passwordHandler.checkUserExists(username)) {
throw APIException.badRequests.parameterIsNotValid("username");
}
_passwordHandler.getPasswordUtils().validatePasswordParameter(passwordReset);
_logger.info("reset password for user {}", username);
setUserPassword(username, passwordReset.getPassword(), passwordReset.getEncPassword(), true);
if (logout && !internalLogoutClient.
logoutUser(!principal.equalsIgnoreCase(username) ? username : null, _req)) {
_logger.error("Password reset but unable to logout user active sessions.");
}
auditPassword(OperationTypeEnum.RESET_LOCAL_USER_PASSWORD,
AuditLogManager.AUDITLOG_SUCCESS, null, principal, username);
return Response.ok("Password Reset posted by " + principal +
" for " + username + "\n").build();
}
/**
* Check to see if a proposed password satisfies ViPR's password content rules
*
* The authenticated caller must have SEC_ADMIN role.
*
* @brief Validate a proposed password for a user
* @prereq none
* @throws APIException
*/
@POST
@Path("/validate")
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN })
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response validateUserPassword(PasswordValidateParam passwordParam) {
checkSecurityContext();
_passwordHandler.getPasswordUtils().validatePasswordParameter(passwordParam);
return Response.noContent().build();
}
/**
* Check to see if a proposed password update parameter satisfies ViPR's password content rules
*
* The authenticated caller must be local user to change their own password.
*
* @brief Validate a proposed password for a user by
* @prereq none
* @throws APIException
*/
@POST
@Path("/validate-update")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response validateUserPasswordForUpdate(
@Context HttpServletRequest httpRequest,
PasswordUpdateParam passwordParam) {
checkPasswordParameter(httpRequest, passwordParam, false);
return Response.noContent().build();
}
/**
* get user's expire time,
*
* for internal use
*/
@GET
@Path("/expire")
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN })
public Response getUserPasswordExpireTime(@QueryParam("username") String username) {
if (username == null || !_localUsers.containsKey(username)) {
throw APIException.badRequests.parameterIsNotValid("username");
}
PasswordHistory ph = _passwordHandler.getPasswordUtils().getPasswordHistory(username);
Calendar expireTime = ph.getExpireDate();
if (expireTime != null) {
return Response.ok(_format.format(expireTime.getTime())).build();
} else {
return Response.ok("no expire time set for the user").build();
}
}
/**
* update user's expire time, format for expire_time "yyyy-MM-dd HH:mm:ss"
*
* for internal use
*/
@PUT
@Path("/expire")
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN })
public Response setUserPasswordExpireTime(
@QueryParam("username") String username,
@QueryParam("expire_time") String expireTime) {
if (username == null || !_localUsers.containsKey(username)) {
throw APIException.badRequests.invalidParameter("username", username);
}
if (expireTime == null) {
throw APIException.badRequests.invalidParameter("expire_time", expireTime);
}
Date date = null;
try {
date = _format.parse(expireTime);
} catch (ParseException e) {
throw APIException.badRequests.invalidParameterWithCause("expire_time", expireTime, e);
}
PasswordHistory ph = _passwordHandler.getPasswordUtils().getPasswordHistory(username);
Calendar newExpireTime = Calendar.getInstance();
newExpireTime.setTime(date);
ph.setExpireDate(newExpireTime);
_dbClient.updateAndReindexObject(ph);
// update system_root_expiry_date / system_svc_expiry_date, if needed
int daysAfterEpoch = PasswordUtils.getDaysAfterEpoch(newExpireTime);
if (username.equals("root")) {
_passwordHandler.updateProperty(Constants.ROOT_EXPIRY_DAYS, String.valueOf(daysAfterEpoch));
} else if (username.equals("svcuser")) {
_passwordHandler.updateProperty(Constants.SVCUSER_EXPIRY_DAYS, String.valueOf(daysAfterEpoch));
}
return Response.ok("set " + username + "'s password expire time to " + newExpireTime.getTime()).build();
}
/**
* run mail notifier on-demand, it will send mail to users whose password to be expired.
*
* for internal use
*
* @return
*/
@POST
@Path("/run-notifier")
@CheckPermission(roles = { Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN })
public Response runMailNotifier() {
_notificationManager.runMailNotifierNow();
return Response.ok().build();
}
/**
* internal call to change user's password without login
*/
@PUT
@Path("/internal/change-password")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response changeUserPassword(PasswordChangeParam passwordChange) {
checkPasswordParameter(passwordChange);
String username = passwordChange.getUsername();
_logger.info("change password for user {}", username);
setUserPassword(username, passwordChange.getPassword(), null, false);
auditPassword(OperationTypeEnum.CHANGE_LOCAL_AUTHUSER_PASSWORD,
AuditLogManager.AUDITLOG_SUCCESS, null, null, username);
return Response.ok("Password changed for " + username).build();
}
private void checkPasswordParameter(PasswordChangeParam passwordChange) {
String username = passwordChange.getUsername();
if (!_passwordHandler.checkUserExists(username)) {
throw APIException.badRequests.parameterIsNotValid("username");
}
if (passwordChange.getOldPassword() == null || passwordChange.getOldPassword().isEmpty()) {
throw BadRequestException.badRequests.passwordInvalidOldPassword();
}
_passwordHandler.getPasswordUtils().validatePasswordParameter(passwordChange);
}
/**
* Check to see if a proposed password update parameter satisfies ViPR's password content rules
*
* The authenticated caller must be local user to change their own password.
*
* @brief Validate a proposed password for a user by
* @prereq none
* @throws APIException
*/
@POST
@Path("/internal/validate-change")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response validateUserPasswordForChange(
PasswordChangeParam passwordParam) {
checkPasswordParameter(passwordParam);
return Response.noContent().build();
}
/**
* Called by update password methods to set user's password
*
* @param username
* @param passwd
* @param encpasswd
*/
private void setUserPassword(String username, String passwd, String encpasswd, boolean bReset) {
PropertyMetadata metaData = null;
try {
final String key = "system_" + username + "_encpassword";
Map<String, PropertyMetadata> metadataMap = getMetaData();
metaData = metadataMap.get(key);
} catch (Exception e) {
_logger.error("resetPassword", e);
throw APIException.internalServerErrors.updateObjectError("password", e);
}
if (metaData == null) {
throw APIException.badRequests.parameterIsNotValid("username");
}
if (ENCRYPTEDSTRING.equalsIgnoreCase(metaData.getType())) {
_passwordHandler.setUserEncryptedPassword(username, passwd, bReset);
} else if (passwd != null && !passwd.isEmpty()) {
_passwordHandler.setUserPassword(username, passwd, bReset);
} else {
_passwordHandler.setUserHashedPassword(username, encpasswd, bReset);
}
}
/**
* Change an authenticated local user's SSH authorizedkey2.
* This interface accepts the user's SSH authorizedkey2
*
* @brief Change SSH authorizedkey2 of local user
* @prereq none
* @throws APIException
*/
@PUT
@Path("/authorizedkey2")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response updateAuthorizedkey2(SSHKeyUpdateParam sshkey) {
checkSecurityContext();
String authorizedkey2 = sshkey.getSshKey();
if (!((StorageOSUser) _sc.getUserPrincipal()).isLocal()) {
throw APIException.forbidden.nonLocalUserNotAllowed();
} else {
if ((authorizedkey2 == null) || authorizedkey2.isEmpty()) {
throw new WebApplicationException(Response.status(Status.BAD_REQUEST).
entity("Bad form paramters\n").build());
}
String username = _sc.getUserPrincipal().getName();
_logger.info("update authorizedkey2 for user {}", username);
try {
_passwordHandler.setUserAuthorizedkey2(username, authorizedkey2);
} catch (Exception e) {
_logger.error("updateAuthorizedkey2", e);
throw APIException.internalServerErrors.updateObjectError("authorized key", e);
}
auditPassword(OperationTypeEnum.CHANGE_LOCAL_AUTHUSER_AUTHKEY,
AuditLogManager.AUDITLOG_SUCCESS, null, username);
return Response.ok("Authorized Key Changed for = " +
_sc.getUserPrincipal().getName() + "\n").build();
}
}
private void checkSecurityContext() {
if (_sc == null || _sc.getUserPrincipal() == null) {
throw APIException.forbidden.invalidSecurityContext();
}
}
/**
* Record audit log for password service
*
* @param auditType Type of AuditLog
* @param operationalStatus Status of operation
* @param description Description for the AuditLog
* @param descparams Description paramters
*/
public void auditPassword(OperationTypeEnum auditType,
String operationalStatus,
String description,
Object... descparams) {
_auditMgr.recordAuditLog(null, null,
EVENT_SERVICE_TYPE,
auditType,
System.currentTimeMillis(),
operationalStatus,
description,
descparams);
}
}