/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos 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.
Cyclos 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 Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.services.access;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.AdminSystemPermission;
import nl.strohalm.cyclos.access.BasicPermission;
import nl.strohalm.cyclos.access.BrokerPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.dao.access.LoginHistoryDAO;
import nl.strohalm.cyclos.dao.access.PasswordHistoryLogDAO;
import nl.strohalm.cyclos.dao.access.PermissionDeniedTraceDAO;
import nl.strohalm.cyclos.dao.access.SessionDAO;
import nl.strohalm.cyclos.dao.access.UserDAO;
import nl.strohalm.cyclos.dao.access.WrongCredentialAttemptsDAO;
import nl.strohalm.cyclos.dao.access.WrongUsernameAttemptsDAO;
import nl.strohalm.cyclos.dao.accounts.cards.CardDAO;
import nl.strohalm.cyclos.dao.members.ElementDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.access.AdminUser;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.access.Channel.Credentials;
import nl.strohalm.cyclos.entities.access.LoginHistoryLog;
import nl.strohalm.cyclos.entities.access.MemberUser;
import nl.strohalm.cyclos.entities.access.OperatorUser;
import nl.strohalm.cyclos.entities.access.PasswordHistoryLog;
import nl.strohalm.cyclos.entities.access.PasswordHistoryLog.PasswordType;
import nl.strohalm.cyclos.entities.access.Session;
import nl.strohalm.cyclos.entities.access.SessionQuery;
import nl.strohalm.cyclos.entities.access.User;
import nl.strohalm.cyclos.entities.access.User.TransactionPasswordStatus;
import nl.strohalm.cyclos.entities.accounts.cards.Card;
import nl.strohalm.cyclos.entities.accounts.cards.CardType;
import nl.strohalm.cyclos.entities.alerts.MemberAlert;
import nl.strohalm.cyclos.entities.alerts.SystemAlert;
import nl.strohalm.cyclos.entities.customization.fields.CustomField.Type;
import nl.strohalm.cyclos.entities.customization.fields.CustomFieldValue;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.AdminGroup;
import nl.strohalm.cyclos.entities.groups.BasicGroupSettings;
import nl.strohalm.cyclos.entities.groups.BasicGroupSettings.PasswordPolicy;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroupSettings;
import nl.strohalm.cyclos.entities.groups.OperatorGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.Operator;
import nl.strohalm.cyclos.entities.services.ServiceClient;
import nl.strohalm.cyclos.entities.settings.AccessSettings;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.exceptions.MailSendingException;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.access.exceptions.AlreadyConnectedException;
import nl.strohalm.cyclos.services.access.exceptions.BlockedCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.CredentialsAlreadyUsedException;
import nl.strohalm.cyclos.services.access.exceptions.InactiveMemberException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidCardException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidUserForChannelException;
import nl.strohalm.cyclos.services.access.exceptions.NotConnectedException;
import nl.strohalm.cyclos.services.access.exceptions.SessionAlreadyInUseException;
import nl.strohalm.cyclos.services.access.exceptions.SystemOfflineException;
import nl.strohalm.cyclos.services.access.exceptions.UserNotFoundException;
import nl.strohalm.cyclos.services.accounts.cards.CardServiceLocal;
import nl.strohalm.cyclos.services.alerts.AlertServiceLocal;
import nl.strohalm.cyclos.services.application.ApplicationServiceLocal;
import nl.strohalm.cyclos.services.elements.ElementServiceLocal;
import nl.strohalm.cyclos.services.elements.ResetTransactionPasswordDTO;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.EntityHelper;
import nl.strohalm.cyclos.utils.HashHandler;
import nl.strohalm.cyclos.utils.MailHandler;
import nl.strohalm.cyclos.utils.PropertyHelper;
import nl.strohalm.cyclos.utils.RangeConstraint;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.StringHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.logging.LoggingHandler;
import nl.strohalm.cyclos.utils.logging.TraceLogDTO;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.IteratorList;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.transaction.TransactionEndListener;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.LengthValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredError;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
/**
* Implementation class for access services Service.
* @author rafael
* @author luis
*/
public class AccessServiceImpl implements AccessServiceLocal, InitializingService {
private final class LoginPasswordValidation implements PropertyValidation {
private final Element element;
private static final long serialVersionUID = -4369049571487478881L;
private LoginPasswordValidation(final Element element) {
this.element = element;
}
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String loginPassword = (String) value;
final AccessSettings accessSettings = settingsService.getAccessSettings();
final boolean numeric = accessSettings.isNumericPassword();
return resolveValidationError(true, numeric, element, object, property, loginPassword);
}
}
private final class PinValidation implements PropertyValidation {
private final Member member;
private static final long serialVersionUID = -4369049571487478881L;
private PinValidation(final Member member) {
this.member = member;
}
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
final String loginPassword = (String) value;
// pin is always numeric
return resolveValidationError(false, true, member, object, property, loginPassword);
}
}
private static final String ALLOW_LOGIN_FOR_GROUPS_KEY = "cyclos.allowLoginForGroups";
private static final String DISALLOW_LOGIN_FOR_GROUPS_KEY = "cyclos.disallowLoginForGroups";
private static final Relationship FETCH = RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP);
private static final Log LOG = LogFactory.getLog(AccessServiceImpl.class);
private AlertServiceLocal alertService;
private FetchServiceLocal fetchService;
private ElementServiceLocal elementService;
private PermissionServiceLocal permissionService;
private SettingsServiceLocal settingsService;
private ElementDAO elementDao;
private UserDAO userDao;
private CardDAO cardDao;
private SessionDAO sessionDao;
private WrongCredentialAttemptsDAO wrongCredentialAttemptsDao;
private WrongUsernameAttemptsDAO wrongUsernameAttemptsDao;
private PermissionDeniedTraceDAO permissionDeniedTraceDao;
private LoginHistoryDAO loginHistoryDao;
private PasswordHistoryLogDAO passwordHistoryLogDao;
private MailHandler mailHandler;
private LoggingHandler loggingHandler;
private HashHandler hashHandler;
private ChannelServiceLocal channelService;
private ApplicationServiceLocal applicationService;
private CardServiceLocal cardService;
private MemberNotificationHandler memberNotificationHandler;
private Collection<Long> allowLoginForGroups;
private Collection<Long> disallowLoginForGroups;
private TransactionHelper transactionHelper;
@Override
public void addLoginPasswordValidation(final Element element, final Property property) {
property.add(new LoginPasswordValidation(element));
}
@Override
public void addPinValidation(final Member member, final Property property) {
property.add(new PinValidation(member));
}
@Override
public boolean canChangeChannelsAccess(final Member member) {
return permissionService.permission(member)
.admin(AdminMemberPermission.ACCESS_CHANGE_CHANNELS_ACCESS)
.broker(BrokerPermission.MEMBER_ACCESS_CHANGE_CHANNELS_ACCESS)
.member(MemberPermission.ACCESS_CHANGE_CHANNELS_ACCESS)
.hasPermission();
}
@Override
public Member changeChannelsAccess(Member member, final Collection<Channel> channels, final boolean verifySmsChannel) {
member = fetchService.fetch(member, Member.Relationships.CHANNELS);
final Channel smsChannel = channelService.getSmsChannel();
// When SMS channel is enabled, it is not set directly by this method. So, we need to ensure it remains related to the member after saving
if (verifySmsChannel && smsChannel != null && member.getChannels().contains(smsChannel)) {
channels.add(smsChannel);
}
member.setChannels(channels);
return elementDao.update(member);
}
@Override
public void changeCredentials(final MemberUser user, final String newCredentials) throws CredentialsAlreadyUsedException {
ServiceClient client = fetchService.fetch(LoggedUser.serviceClient(), RelationshipHelper.nested(ServiceClient.Relationships.CHANNEL, Channel.Relationships.PRINCIPALS));
switch (client.getChannel().getCredentials()) {
case LOGIN_PASSWORD:
changePassword(user, null, newCredentials, false);
break;
case PIN:
changePin(user, newCredentials);
break;
}
}
@Override
public User changePassword(final ChangeLoginPasswordDTO params) throws InvalidCredentialsException, BlockedCredentialsException, CredentialsAlreadyUsedException {
validateChangePassword(params);
User loggedUser = LoggedUser.user();
boolean myPassword = loggedUser.equals(params.getUser());
if (myPassword) {
// We'll only check the old password if it's not expired
final boolean isExpired = hasPasswordExpired();
if (!isExpired) {
// Check the current password
final Element loggedElement = LoggedUser.element();
String member = null;
if (LoggedUser.isOperator()) {
final Operator operator = LoggedUser.element();
member = operator.getMember().getUsername();
}
checkPassword(member, loggedElement.getUsername(), params.getOldPassword(), LoggedUser.remoteAddress());
}
}
final User user = fetchService.fetch(params.getUser(), RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
return changePassword(user, params.getNewPassword(), params.isForceChange());
}
@Override
public MemberUser changePin(final ChangePinDTO params) throws InvalidCredentialsException, BlockedCredentialsException, CredentialsAlreadyUsedException {
validateChangePin(params);
User loggedUser = LoggedUser.user();
boolean myPin = loggedUser.equals(params.getUser());
if (myPin) {
// Check whether to enforce the login or transaction password
final Member loggedMember = (Member) fetchService.fetch(loggedUser.getElement(), Element.Relationships.GROUP);
final boolean usesTransactionPassword = loggedMember.getMemberGroup().getBasicSettings().getTransactionPassword().isUsed();
// If the password (or transaction password) is incorrect an exception is thrown
if (usesTransactionPassword) {
checkTransactionPassword(loggedMember.getUser(), params.getCredentials(), LoggedUser.remoteAddress());
} else {
checkPassword(loggedMember.getUser(), params.getCredentials(), LoggedUser.remoteAddress());
}
}
final MemberUser user = fetchService.fetch(params.getUser(), RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
return changePin(user, params.getNewPin());
}
@Override
public MemberUser checkCredentials(Channel channel, MemberUser user, final String credentials, final String remoteAddress, final Member relatedMember) {
if (StringUtils.isEmpty(credentials)) {
throw new InvalidCredentialsException(channel.getCredentials(), user);
}
channel = fetchService.fetch(channel, Channel.Relationships.PRINCIPALS);
user = fetchService.fetch(user);
switch (channel.getCredentials()) {
case DEFAULT:
case LOGIN_PASSWORD:
checkPassword(user, credentials, remoteAddress);
break;
case PIN:
checkPin(user, credentials, channel.getInternalName(), relatedMember);
break;
case TRANSACTION_PASSWORD:
checkTransactionPassword(user, credentials, remoteAddress);
break;
case CARD_SECURITY_CODE:
final Card card = cardService.getActiveCard(user.getMember());
checkCardSecurityCode(card.getCardNumber(), credentials, channel.getInternalName());
break;
}
return user;
}
@Override
public User checkPassword(final String member, final String username, final String plainPassword, final String remoteAddress) {
final User user = loadUser(member, username);
return checkPassword(user, plainPassword, remoteAddress);
}
@Override
public Session checkSession(final String sessionId) throws NotConnectedException {
try {
Session session = sessionDao.load(sessionId, false);
User user = session.getUser();
final Long id = session.getId();
final Calendar newExpiration = getSessionTimeout(user, session.isPosWeb()).add(Calendar.getInstance());
// After the main transaction ends, we need to update the session
CurrentTransactionData.addTransactionEndListener(new TransactionEndListener() {
@Override
protected void onTransactionEnd(final boolean commit) {
updateSessionExpiration(id, newExpiration);
}
});
return session;
} catch (EntityNotFoundException e) {
throw new NotConnectedException();
}
}
@Override
public User checkTransactionPassword(final String transactionPassword) throws InvalidCredentialsException, BlockedCredentialsException {
final User user = LoggedUser.user();
return checkTransactionPassword(user, transactionPassword, LoggedUser.remoteAddress());
}
@Override
public User disconnect(Session session) throws NotConnectedException {
try {
session = fetchService.fetch(session, RelationshipHelper.nested(Session.Relationships.USER, User.Relationships.ELEMENT));
} catch (EntityNotFoundException e) {
throw new NotConnectedException();
}
sessionDao.delete(session.getId());
User user = session.getUser();
if (!isLoggedIn(user)) {
// If there are no other sessions for that user, set the lastLogin
user.setLastLogin(Calendar.getInstance());
}
return user;
}
@Override
public User disconnect(final User user) {
int sessions = sessionDao.delete(user);
if (sessions > 0) {
// Store the last login
user.setLastLogin(Calendar.getInstance());
}
return fetchService.fetch(user, User.Relationships.ELEMENT);
}
@Override
public void disconnectAllButLogged() {
final User loggedUser = LoggedUser.user();
IteratorList<User> iterator = sessionDao.listLoggedUsers();
try {
CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
for (User user : iterator) {
if (!loggedUser.equals(user)) {
disconnect(user);
cacheCleaner.clearCache();
}
}
} finally {
DataIteratorHelper.close(iterator);
}
}
@Override
public String generatePassword(Group group) {
if (group instanceof OperatorGroup) {
group = fetchService.fetch(group, RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
}
final Integer min = group.getBasicSettings().getPasswordLength().getMin();
final boolean onlyNumbers = settingsService.getAccessSettings().isNumericPassword();
return RandomStringUtils.random(min == null ? 4 : min, !onlyNumbers, true).toLowerCase();
}
@Override
public String generateTransactionPassword() {
User user = LoggedUser.user();
// Load operatorĀ“s member and group of operatorĀ“s member
if (user instanceof OperatorUser) {
Element element = user.getElement();
element = fetchService.fetch(element, RelationshipHelper.nested(Operator.Relationships.MEMBER, Element.Relationships.GROUP));
}
// If the transaction password is not used for the logged user, return null
if (!requestTransactionPassword(user)) {
throw new UnexpectedEntityException();
}
// If there is already a password, return null
final String current = user.getTransactionPassword();
if (current != null) {
return null;
}
// Get the chars
final AccessSettings accessSettings = settingsService.getAccessSettings();
final String chars = accessSettings.getTransactionPasswordChars();
// Get the password length
Group group = user.getElement().getGroup();
if (group instanceof OperatorGroup) {
group = fetchService.fetch(group, RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
}
final BasicGroupSettings basicSettings = group.getBasicSettings();
final int length = basicSettings.getTransactionPasswordLength();
// Generate a new one, and store it
final StringBuilder buffer = new StringBuilder(length);
final Random rnd = new Random();
for (int i = 0; i < length; i++) {
buffer.append(chars.charAt(rnd.nextInt(chars.length())));
}
final String transactionPassword = buffer.toString();
user.setTransactionPassword(hashHandler.hash(user.getSalt(), transactionPassword));
user.setTransactionPasswordStatus(TransactionPasswordStatus.ACTIVE);
user = userDao.update(user);
return transactionPassword;
}
@SuppressWarnings("unchecked")
@Override
public Collection<Channel> getChannelsEnabledForMember(Member member) {
member = fetchService.fetch(member, Element.Relationships.GROUP);
return CollectionUtils.retainAll(member.getChannels(), member.getMemberGroup().getChannels());
}
@Override
public User getLoggedUser(final String sessionId) throws NotConnectedException {
try {
Session session = sessionDao.load(sessionId, false);
return session.getUser();
} catch (EntityNotFoundException e) {
throw new NotConnectedException();
}
}
@Override
public boolean hasPasswordExpired() {
Group group = LoggedUser.group();
if (group instanceof OperatorGroup) {
group = fetchService.fetch(group, RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
}
final TimePeriod exp = group.getBasicSettings().getPasswordExpiresAfter();
final Calendar passwordDate = LoggedUser.user().getPasswordDate();
if (passwordDate == null) {
return true;
}
if (exp != null && exp.getNumber() > 0 && passwordDate != null) {
final Calendar expiresAt = exp.remove(Calendar.getInstance());
return expiresAt.after(passwordDate);
}
return false;
}
@Override
public void initializeService() {
purgeTraces(Calendar.getInstance());
purgeExpiredSessions();
}
@Override
public boolean isCardSecurityCodeBlocked(Card card) {
card = fetchService.reload(card);
return isBlocked(card.getCardSecurityCodeBlockedUntil());
}
@Override
public boolean isChannelAllowedToBeEnabledForMember(final Channel channel, Member member) {
member = fetchService.fetch(member, Element.Relationships.GROUP, Member.Relationships.CHANNELS);
switch (channel.getCredentials()) {
case TRANSACTION_PASSWORD:
// Check if the member's group uses transaction password.
if (!member.getMemberGroup().getBasicSettings().getTransactionPassword().isUsed()) {
LOG.warn("The member's group doesn't use transaction password, member: " + member);
return false;
}
break;
case CARD_SECURITY_CODE:
// Check if the member's group has a Card Type
if (member.getMemberGroup().getCardType() == null) {
LOG.warn("The member's group doesn't have a card type, member: " + member);
return false;
}
break;
}
// If the group doesn't have access to the channel, the member can't access it too
if (!isChannelEnabledForGroup(channel, member.getMemberGroup())) {
return false;
}
return true;
}
@Override
public boolean isChannelEnabledForMember(final Channel channel, Member member) {
// Check the member access customization too
member = fetchService.fetch(member, Member.Relationships.CHANNELS);
return isChannelAllowedToBeEnabledForMember(channel, member) && (channel.getInternalName().equals(Channel.WEB) || member.getChannels().contains(channel));
}
@Override
public boolean isChannelEnabledForMember(final String channelInternalName, final Member member) {
final Channel channel = channelService.loadByInternalName(channelInternalName);
return isChannelEnabledForMember(channel, member);
}
@Override
public boolean isLoggedIn(final User user) {
return sessionDao.isLoggedIn(user);
}
@Override
public boolean isLoginBlocked(User user) {
user = fetchService.reload(user);
return isBlocked(user.getPasswordBlockedUntil());
}
@Override
public boolean isObviousCredential(Element element, final String credential) {
// Ensure not to fetch the element for new records
if (element.isPersistent()) {
element = fetchService.fetch(element, Element.Relationships.USER, Member.Relationships.CUSTOM_VALUES);
}
// If the credential is equals to the username, it is obvious
if (credential.equalsIgnoreCase(element.getUsername())) {
return true;
}
// If the credential is equals to any word of the full name, it is obvious
final String[] nameParts = StringUtils.split(element.getName(), " .,/-\\");
for (final String part : nameParts) {
if (credential.equalsIgnoreCase(part)) {
return true;
}
}
// If the credential is equals to the user part of the e-mail, it is obvious
final String email = element.getEmail();
if (StringUtils.isNotEmpty(email) && email.contains("@")) {
final String mailUser = StringUtils.split(email, "@", 1)[0];
if (credential.equalsIgnoreCase(mailUser)) {
return true;
}
}
// If the credential matches custom field values, it is obvious
final Collection<CustomFieldValue> customValues = PropertyHelper.get(element, "customValues");
final LocalSettings localSettings = settingsService.getLocalSettings();
for (final CustomFieldValue fieldValue : customValues) {
final Type type = fieldValue.getField().getType();
final String stringValue = fieldValue.getStringValue();
if (StringUtils.isEmpty(stringValue)) {
continue;
}
switch (type) {
case DATE:
// The credential cannot be the same as a date
final String unmasked = StringHelper.removeMask(localSettings.getDatePattern().getPattern(), stringValue);
if (credential.equals(unmasked)) {
return true;
}
break;
case INTEGER:
// The credential cannot be equal to an integer
if (credential.equals(stringValue)) {
return true;
}
break;
case STRING:
// The credential cannot be contained in a string
if (stringValue.contains(credential)) {
return true;
}
break;
}
}
// When all chars have the same distance, it's obvious (things like 1234, 7654, abcdef and so on...)
if (credential.length() > 1) {
final Set<Integer> diffs = new HashSet<Integer>();
for (int i = 1, len = credential.length(); i < len; i++) {
final char current = credential.charAt(i);
final char previous = credential.charAt(i - 1);
diffs.add(current - previous);
if (diffs.size() > 1) {
// More than 1 difference means it's not obvious
break;
}
}
if (diffs.size() == 1) {
return true;
}
}
return false;
}
@Override
public boolean isPinBlocked(MemberUser user) {
user = fetchService.reload(user);
return isBlocked(user.getPinBlockedUntil());
}
@Override
public User login(User user, final String plainCredentials, final String channelName, final boolean isPosWeb, final String remoteAddress, final String sessionId) throws UserNotFoundException, InvalidCredentialsException, BlockedCredentialsException, SessionAlreadyInUseException, AlreadyConnectedException {
if (user == null) {
throw new UnexpectedEntityException();
}
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// When the system is offline, only allow login users with manage online state permission
if (!applicationService.isOnline()) {
if (!permissionService.hasPermission(user.getElement().getGroup(), AdminSystemPermission.TASKS_ONLINE_STATE)) {
throw new SystemOfflineException();
}
}
// Validate the given session id
try {
Session session = sessionDao.load(sessionId, false);
if (session.getUser().equals(user) && remoteAddress.equals(session.getRemoteAddress()) && isPosWeb == session.isPosWeb()) {
// If the same logged user and remote address are under the same session id, return ok
return session.getUser();
}
// The session is in use by another user / remote address
throw new SessionAlreadyInUseException(user.getUsername());
} catch (EntityNotFoundException e) {
// Ok, no session with the given id
}
final Channel channel = channelService.loadByInternalName(channelName);
final boolean isMainWebChannel = Channel.WEB.equals(channel.getInternalName());
if (user instanceof MemberUser) {
// For members, check the channel's credentials
checkCredentials(channel, (MemberUser) user, plainCredentials, remoteAddress, null);
// Also, ensure the channel is enabled for the member (if not the main web channel)
if (!isMainWebChannel && !isChannelEnabledForMember(channelName, ((MemberUser) user).getMember())) {
throw new InvalidUserForChannelException(user.getUsername());
}
} else {
// For admins or operators, always check the login
if (channel != null && !isMainWebChannel) {
// Also, they can only login in the main web channel
throw new PermissionDeniedException();
}
checkPassword(user, plainCredentials, remoteAddress);
}
// Check if already connected
AccessSettings accessSettings = settingsService.getAccessSettings();
if (!accessSettings.isAllowMultipleLogins() && isLoggedIn(user)) {
throw new AlreadyConnectedException();
}
// Initialize the permission denied counter
permissionDeniedTraceDao.clear(user);
Calendar now = Calendar.getInstance();
// Store the session
Session session = new Session();
session.setCreationDate(now);
session.setExpirationDate(getSessionTimeout(user, isPosWeb).add(now));
session.setUser(user);
session.setIdentifier(sessionId);
session.setRemoteAddress(remoteAddress);
session.setPosWeb(isPosWeb);
sessionDao.insert(session);
// Save an entry in the login history
final LoginHistoryLog loginHistoryLog = new LoginHistoryLog();
loginHistoryLog.setUser(user);
loginHistoryLog.setDate(now);
loginHistoryLog.setRemoteAddress(remoteAddress);
loginHistoryDao.insert(loginHistoryLog);
// Generate log
TraceLogDTO logParams = new TraceLogDTO();
logParams.setUser(user);
logParams.setSessionId(sessionId);
logParams.setRemoteAddress(remoteAddress);
loggingHandler.traceLogin(logParams);
// Initialize the login data
LoggedUser.init(user);
return user;
}
@Override
public User logout(final String sessionId) {
try {
Session session = sessionDao.load(sessionId, true);
sessionDao.delete(session.getId());
User user = session.getUser();
user.setLastLogin(Calendar.getInstance());
// Trace to the log file
TraceLogDTO logParams = new TraceLogDTO();
logParams.setUser(user);
logParams.setSessionId(sessionId);
logParams.setRemoteAddress(session.getRemoteAddress());
loggingHandler.traceLogout(logParams);
// Clean up the LoggedUser
if (LoggedUser.hasUser() && user.equals(LoggedUser.user())) {
LoggedUser.cleanup();
}
return user;
} catch (EntityNotFoundException e) {
return null;
}
}
@Override
public boolean notifyPermissionDeniedException() {
if (!LoggedUser.hasUser()) {
return false;
}
return transactionHelper.runInNewTransaction(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(final TransactionStatus status) {
final User user = fetchService.fetch(LoggedUser.user(), RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// Already blocked
if (isBlocked(user.getPasswordBlockedUntil())) {
return true;
}
// Check if maxTries has been reached
final BasicGroupSettings basicSettings = user.getElement().getGroup().getBasicSettings();
final int maxTries = basicSettings.getMaxPasswordWrongTries();
if (maxTries > 0) {
permissionDeniedTraceDao.record(user);
int tries = permissionDeniedTraceDao.count(wrongAttemptsLimit(), user);
if (tries == maxTries) {
// Send an alert
if (user instanceof AdminUser) {
alertService.create(SystemAlert.Alerts.ADMIN_LOGIN_BLOCKED_BY_PERMISSION_DENIEDS, user.getUsername(), tries, LoggedUser.remoteAddress());
} else if (user instanceof MemberUser) {
alertService.create(((MemberUser) user).getMember(), MemberAlert.Alerts.LOGIN_BLOCKED_BY_PERMISSION_DENIEDS, tries, LoggedUser.remoteAddress());
}
// Block the user and clear any previous traces
final Calendar mayLoginAt = basicSettings.getDeactivationAfterMaxPasswordTries().add(Calendar.getInstance());
user.setPasswordBlockedUntil(mayLoginAt);
permissionDeniedTraceDao.clear(user);
return true;
}
}
return false;
}
});
}
@Override
public void purgeExpiredSessions() {
sessionDao.purgeExpired();
}
@Override
public void purgeTraces(final Calendar time) {
Calendar limit = (Calendar) time.clone();
limit.add(Calendar.DAY_OF_MONTH, -1);
wrongCredentialAttemptsDao.clear(limit);
wrongUsernameAttemptsDao.clear(limit);
permissionDeniedTraceDao.clear(limit);
}
@Override
public User reenableLogin(User user) {
user = fetchService.fetch(user);
user.setPasswordBlockedUntil(null);
wrongCredentialAttemptsDao.clear(user, Credentials.LOGIN_PASSWORD);
return user;
}
@Override
public MemberUser resetPassword(MemberUser user) throws MailSendingException {
// Check if password will be sent by mail
user = fetchService.fetch(user, FETCH);
final MemberGroup group = user.getMember().getMemberGroup();
final boolean sendPasswordByEmail = group.getMemberSettings().isSendPasswordByEmail();
String newPassword = null;
if (sendPasswordByEmail) {
// If send by mail, generate a new password
newPassword = generatePassword(group);
}
// Update the user
user.setPassword(hashHandler.hash(user.getSalt(), newPassword));
user.setPasswordDate(null);
userDao.update(user);
if (sendPasswordByEmail) {
// Send the password by mail
mailHandler.sendResetPassword(user.getMember(), newPassword);
}
return user;
}
@Override
public User resetTransactionPassword(final ResetTransactionPasswordDTO dto) {
User user = dto.getUser();
user.setTransactionPassword(null);
user.setTransactionPasswordStatus(dto.isAllowGeneration() ? TransactionPasswordStatus.PENDING : TransactionPasswordStatus.BLOCKED);
return userDao.update(user);
}
@Override
public List<Session> searchSessions(final SessionQuery query) {
return sessionDao.search(query);
}
public void setAlertServiceLocal(final AlertServiceLocal alertService) {
this.alertService = alertService;
}
public void setApplicationServiceLocal(final ApplicationServiceLocal applicationService) {
this.applicationService = applicationService;
}
public void setCardDao(final CardDAO cardDao) {
this.cardDao = cardDao;
}
public void setCardServiceLocal(final CardServiceLocal cardService) {
this.cardService = cardService;
}
public void setChannelServiceLocal(final ChannelServiceLocal channelService) {
this.channelService = channelService;
}
public void setCyclosProperties(final Properties cyclosProperties) {
try {
allowLoginForGroups = EntityHelper.parseIds(cyclosProperties.getProperty(ALLOW_LOGIN_FOR_GROUPS_KEY));
} catch (Exception e) {
throw new IllegalStateException("Invalid value for " + ALLOW_LOGIN_FOR_GROUPS_KEY + " in cyclos.properties", e);
}
try {
disallowLoginForGroups = EntityHelper.parseIds(cyclosProperties.getProperty(DISALLOW_LOGIN_FOR_GROUPS_KEY));
} catch (Exception e) {
throw new IllegalStateException("Invalid value for " + DISALLOW_LOGIN_FOR_GROUPS_KEY + " in cyclos.properties", e);
}
}
public void setElementDao(final ElementDAO elementDao) {
this.elementDao = elementDao;
}
public void setElementServiceLocal(final ElementServiceLocal elementService) {
this.elementService = elementService;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setHashHandler(final HashHandler hashHandler) {
this.hashHandler = hashHandler;
}
public void setLoggingHandler(final LoggingHandler loggingHandler) {
this.loggingHandler = loggingHandler;
}
public void setLoginHistoryDao(final LoginHistoryDAO loginHistoryDao) {
this.loginHistoryDao = loginHistoryDao;
}
public void setMailHandler(final MailHandler mailHandler) {
this.mailHandler = mailHandler;
}
public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
this.memberNotificationHandler = memberNotificationHandler;
}
public void setPasswordHistoryLogDao(final PasswordHistoryLogDAO passwordHistoryLogDao) {
this.passwordHistoryLogDao = passwordHistoryLogDao;
}
public void setPermissionDeniedTraceDao(final PermissionDeniedTraceDAO permissionDeniedTraceDao) {
this.permissionDeniedTraceDao = permissionDeniedTraceDao;
}
public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
this.permissionService = permissionService;
}
public void setSessionDao(final SessionDAO sessionDao) {
this.sessionDao = sessionDao;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setTransactionHelper(final TransactionHelper transactionHelper) {
this.transactionHelper = transactionHelper;
}
public void setUserDao(final UserDAO userDao) {
this.userDao = userDao;
}
public void setWrongCredentialAttemptsDao(final WrongCredentialAttemptsDAO wrongCredentialAttemptsDao) {
this.wrongCredentialAttemptsDao = wrongCredentialAttemptsDao;
}
public void setWrongUsernameAttemptsDao(final WrongUsernameAttemptsDAO wrongUsernameAttemptsDao) {
this.wrongUsernameAttemptsDao = wrongUsernameAttemptsDao;
}
@Override
public Card unblockCardSecurityCode(final BigInteger cardNumber) {
Card card = cardDao.loadByNumber(cardNumber);
card.setCardSecurityCodeBlockedUntil(null);
wrongCredentialAttemptsDao.clear(card);
return card;
}
@Override
public MemberUser unblockPin(MemberUser user) {
user = fetchService.fetch(user);
user.setPinBlockedUntil(null);
wrongCredentialAttemptsDao.clear(user, Credentials.PIN);
return user;
}
@Override
public void validateChangePassword(final ChangeLoginPasswordDTO params) throws ValidationException {
final Validator validator = new Validator("changePassword");
validator.property("user").required();
if (LoggedUser.hasUser() && LoggedUser.user().equals(params.getUser())) {
// The old password is required if it is not expired
if (!hasPasswordExpired()) {
validator.property("oldPassword").required();
}
}
final User user = fetchService.fetch(params.getUser(), RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
if (user != null) {
validator.property("newPassword").required().add(new LoginPasswordValidation(user.getElement()));
validator.property("newPasswordConfirmation").required();
}
validator.general(new GeneralValidation() {
private static final long serialVersionUID = -4110708889147050967L;
@Override
public ValidationError validate(final Object object) {
final ChangeLoginPasswordDTO params = (ChangeLoginPasswordDTO) object;
final String newPassword = params.getNewPassword();
final String newPasswordConfirmation = params.getNewPasswordConfirmation();
if (StringUtils.isNotEmpty(newPassword) && StringUtils.isNotEmpty(newPasswordConfirmation) && !newPassword.equals(newPasswordConfirmation)) {
return new ValidationError("errors.passwords");
}
return null;
}
});
validator.validate(params);
}
@Override
public void validateChangePin(final ChangePinDTO params) {
if (params.getUser() == null) {
throw new ValidationException("user", "changePin.user", new RequiredError());
}
boolean myPin = LoggedUser.user().equals(params.getUser());
final Validator validator = new Validator("changePin");
if (myPin) {
final MemberGroup group = LoggedUser.group();
final boolean isTP = group.getBasicSettings().getTransactionPassword().isUsed();
final String key = isTP ? "channel.credentials.TRANSACTION_PASSWORD" : "channel.credentials.LOGIN_PASSWORD";
validator.property("credentials").key(key).required();
} else {
validator.property("user").required();
}
final MemberUser user = fetchService.fetch(params.getUser(), User.Relationships.ELEMENT);
if (user != null) {
validator.property("newPin").required().add(new PinValidation(user.getMember()));
validator.property("newPinConfirmation").required();
}
validator.general(new GeneralValidation() {
private static final long serialVersionUID = -4110708889147050967L;
@Override
public ValidationError validate(final Object object) {
final ChangePinDTO params = (ChangePinDTO) object;
final String newPin = params.getNewPin();
final String newPinConfirmation = params.getNewPinConfirmation();
if (StringUtils.isNotEmpty(newPin) && StringUtils.isNotEmpty(newPinConfirmation) && !newPin.equals(newPinConfirmation)) {
return new ValidationError("changePin.error.pinsAreNotEqual");
}
return null;
}
});
validator.validate(params);
}
public ValidationError validateLoginPassword(final Element element, final String loginPassword) {
return new LoginPasswordValidation(element).validate(element.getUser(), "password", loginPassword);
}
@Override
public User verifyLogin(final String member, final String username, final String remoteAddress) throws UserNotFoundException, InactiveMemberException, PermissionDeniedException {
try {
final User user = loadUser(member, username);
// Check if this user is allowed to login on this Cyclos instance
Long groupId;
if (user instanceof OperatorUser) {
// For operators, check for the member's group id
groupId = ((OperatorUser) user).getOperator().getMember().getGroup().getId();
} else {
groupId = user.getElement().getGroup().getId();
}
// Check for the group white-list
if (!allowLoginForGroups.isEmpty() && !allowLoginForGroups.contains(groupId)) {
// Not allowed to login - respond just like an invalid user
throw new UserNotFoundException(username);
}
// Check for the group black-list
if (!disallowLoginForGroups.isEmpty() && disallowLoginForGroups.contains(groupId)) {
// Not allowed to login - respond just like an invalid user
throw new UserNotFoundException(username);
}
// Check if there's an active password
if (StringUtils.isEmpty(user.getPassword())) {
throw new InactiveMemberException(username);
}
// Check if the member has permission to login
if (!permissionService.hasPermission(user.getElement().getGroup(), BasicPermission.BASIC_LOGIN)) {
throw new PermissionDeniedException();
}
// Username exists: remove the records for the given remote address
wrongUsernameAttemptsDao.clear(remoteAddress);
return user;
} catch (final EntityNotFoundException e) {
// Record the incorrect attempt
wrongUsernameAttemptsDao.record(remoteAddress);
// Check if an alert will be sent
final int maxTries = settingsService.getAlertSettings().getAmountIncorrectLogin();
if (maxTries > 0) {
int wrongTries = wrongUsernameAttemptsDao.count(wrongAttemptsLimit(), remoteAddress);
if (wrongTries == maxTries) {
alertService.create(SystemAlert.Alerts.MAX_INCORRECT_LOGIN_ATTEMPTS, wrongTries, remoteAddress);
wrongUsernameAttemptsDao.clear(remoteAddress);
}
}
throw new UserNotFoundException(username);
}
}
@Override
public Calendar wrongAttemptsLimit() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -1);
return calendar;
}
private <U extends User> U changePassword(U user, final String oldPassword, final String plainNewPassword, final boolean forceChange) {
user = fetchService.reload(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// Before changing, ensure that the new password is valid
final ValidationError validationResult = new LoginPasswordValidation(user.getElement()).validate(user, "password", plainNewPassword);
if (validationResult != null) {
throw new ValidationException("password", "channel.credentials.LOGIN_PASSWORD", validationResult);
}
final String currentPassword = user.getPassword();
final String hashedOldPassword = hashHandler.hash(user.getSalt(), oldPassword);
if (StringUtils.isNotEmpty(oldPassword) && !hashedOldPassword.equalsIgnoreCase(currentPassword)) {
throw new InvalidCredentialsException(Credentials.LOGIN_PASSWORD, user);
}
final String newPassword = hashHandler.hash(user.getSalt(), plainNewPassword);
final PasswordPolicy passwordPolicy = user.getElement().getGroup().getBasicSettings().getPasswordPolicy();
if (passwordPolicy != PasswordPolicy.NONE) {
// Check if it was already in use when there's a password policy
if (StringUtils.trimToEmpty(currentPassword).equalsIgnoreCase(newPassword) || passwordHistoryLogDao.wasAlreadyUsed(user, PasswordType.LOGIN, newPassword)) {
throw new CredentialsAlreadyUsedException(Credentials.LOGIN_PASSWORD, user);
}
}
// Ensure the login password is not equals to the pin or transaction password
if (newPassword.equalsIgnoreCase(user.getTransactionPassword()) || (user instanceof MemberUser && newPassword.equalsIgnoreCase(((MemberUser) user).getPin()))) {
throw new ValidationException("changePassword.error.sameAsTransactionPasswordOrPin");
}
user.setPassword(newPassword);
user.setPasswordDate(forceChange ? null : Calendar.getInstance());
// Ensure that the returning user will have the ELEMENT and GROUP fetched
user = fetchService.fetch(userDao.update(user), FETCH);
if (user instanceof OperatorUser) {
// When an operator, also ensure that the member will have the ELEMENT and GROUP fetched
final Operator operator = ((OperatorUser) user).getOperator();
final Member member = fetchService.fetch(operator.getMember(), Element.Relationships.USER, Element.Relationships.GROUP);
operator.setMember(member);
}
// Log the password history
if (StringUtils.isNotEmpty(currentPassword)) {
final PasswordHistoryLog log = new PasswordHistoryLog();
log.setDate(Calendar.getInstance());
log.setUser(user);
log.setType(PasswordType.LOGIN);
log.setPassword(currentPassword);
passwordHistoryLogDao.insert(log);
}
return user;
}
private User changePassword(User user, final String plainPassword, final boolean forceChange) {
// Before changing, ensure that the new password is valid
final ValidationError validationResult = new LoginPasswordValidation(user.getElement()).validate(user, "password", plainPassword);
if (validationResult != null) {
throw new ValidationException("password", "channel.credentials.LOGIN_PASSWORD", validationResult);
}
final String password = hashHandler.hash(user.getSalt(), plainPassword);
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
final String currentPassword = user.getPassword();
final PasswordPolicy passwordPolicy = user.getElement().getGroup().getBasicSettings().getPasswordPolicy();
if (passwordPolicy != null && passwordPolicy != PasswordPolicy.NONE) {
// Check if it was already in use when there's a password policy
if (StringUtils.trimToEmpty(currentPassword).equalsIgnoreCase(password) || passwordHistoryLogDao.wasAlreadyUsed(user, PasswordType.LOGIN, password)) {
throw new CredentialsAlreadyUsedException(Credentials.LOGIN_PASSWORD, user);
}
}
// Ensure the login password is not equals to the pin or transaction password
final String pin = user instanceof MemberUser ? ((MemberUser) user).getPin() : null;
if (password.equalsIgnoreCase(pin) || password.equalsIgnoreCase(user.getTransactionPassword())) {
throw new ValidationException("changePassword.error.sameAsTransactionPasswordOrPin");
}
// Set the new password
user.setPassword(password);
if (forceChange) {
user.setPasswordDate(null);
} else {
user.setPasswordDate(Calendar.getInstance());
}
// Log the password history
if (StringUtils.isNotEmpty(currentPassword)) {
final PasswordHistoryLog log = new PasswordHistoryLog();
log.setDate(Calendar.getInstance());
log.setUser(user);
log.setType(PasswordType.LOGIN);
log.setPassword(currentPassword);
passwordHistoryLogDao.insert(log);
}
user = userDao.update(user);
return user;
}
private MemberUser changePin(MemberUser user, final String plainPin) {
// Before changing, ensure that the new pin is valid
final ValidationError validationResult = new PinValidation(user.getMember()).validate(user, "pin", plainPin);
if (validationResult != null) {
throw new ValidationException("pin", "channel.credentials.PIN", validationResult);
}
final String pin = hashHandler.hash(user.getSalt(), plainPin);
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
final String currentPin = user.getPin();
final PasswordPolicy passwordPolicy = user.getElement().getGroup().getBasicSettings().getPasswordPolicy();
if (passwordPolicy != null && passwordPolicy != PasswordPolicy.NONE) {
// Check if it was already in use when there's a password policy
if (StringUtils.trimToEmpty(currentPin).equalsIgnoreCase(pin) || passwordHistoryLogDao.wasAlreadyUsed(user, PasswordType.PIN, pin)) {
throw new CredentialsAlreadyUsedException(Credentials.PIN, user);
}
}
// Ensure the login password is not equals to the pin or transaction password
if (pin.equalsIgnoreCase(user.getPassword()) || pin.equalsIgnoreCase(user.getTransactionPassword())) {
throw new ValidationException("changePin.error.sameAsLoginOrTransactionPassword");
}
// Set the new pin
user.setPin(pin);
// Log the pin history
if (StringUtils.isNotEmpty(currentPin)) {
final PasswordHistoryLog log = new PasswordHistoryLog();
log.setDate(Calendar.getInstance());
log.setUser(user);
log.setType(PasswordType.PIN);
log.setPassword(currentPin);
passwordHistoryLogDao.insert(log);
}
return user;
}
private Card checkCardSecurityCode(final BigInteger cardNumber, String securityCode, final String channel) {
Card card;
try {
card = cardService.loadByNumber(cardNumber, RelationshipHelper.nested(Card.Relationships.OWNER, Element.Relationships.USER), Card.Relationships.CARD_TYPE);
if (card.getStatus() != Card.Status.ACTIVE) {
// Ensure the card is active
throw new Exception();
}
} catch (final Exception e) {
throw new InvalidCardException();
}
User user = card.getOwner().getUser();
// Check if not already blocked
if (isBlocked(card.getCardSecurityCodeBlockedUntil())) {
throw new BlockedCredentialsException(Credentials.CARD_SECURITY_CODE, user);
}
// If the securiry code is manual, it is stored hashed, so we must hash the input as well
if (!card.getCardType().isShowCardSecurityCode()) {
securityCode = hashHandler.hash(user.getSalt(), securityCode);
}
final String storedCode = card.getCardSecurityCode();
if (StringUtils.isEmpty(storedCode) || !securityCode.equals(storedCode)) {
final CardType cardType = card.getCardType();
final int maxTries = cardType.getMaxSecurityCodeTries();
wrongCredentialAttemptsDao.record(card);
int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), card);
if (wrongAttempts == maxTries) {
// Notify blocking
memberNotificationHandler.blockedCredentialsNotification(user, Credentials.CARD_SECURITY_CODE);
// Mark the card as blocked, and remove previous attempts
final TimePeriod blockTime = cardType.getSecurityCodeBlockTime();
card.setCardSecurityCodeBlockedUntil(blockTime.add(Calendar.getInstance()));
wrongCredentialAttemptsDao.clear(card);
throw new BlockedCredentialsException(Credentials.CARD_SECURITY_CODE, user);
}
throw new InvalidCredentialsException(Credentials.CARD_SECURITY_CODE, user);
}
// Everything ok - remove any previous wrong attempts
wrongCredentialAttemptsDao.clear(card);
return card;
}
private User checkPassword(User user, final String plainPassword, final String remoteAddress) {
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// Check if not already blocked
if (isBlocked(user.getPasswordBlockedUntil())) {
throw new BlockedCredentialsException(Credentials.LOGIN_PASSWORD, user);
}
// Check the password
final String password = hashHandler.hash(user.getSalt(), plainPassword);
final String userPassword = user.getPassword();
if (userPassword == null || !userPassword.equalsIgnoreCase(StringUtils.trimToNull(password))) {
final BasicGroupSettings basicSettings = user.getElement().getGroup().getBasicSettings();
// Check if maxTries has been reached
final int maxTries = basicSettings.getMaxPasswordWrongTries();
wrongCredentialAttemptsDao.record(user, Credentials.LOGIN_PASSWORD);
int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), user, Credentials.LOGIN_PASSWORD);
if (wrongAttempts == maxTries) {
TimePeriod blockTime = basicSettings.getDeactivationAfterMaxPasswordTries();
// Create the alert
if (user instanceof AdminUser) {
alertService.create(SystemAlert.Alerts.ADMIN_LOGIN_BLOCKED_BY_TRIES, user.getUsername(), wrongAttempts, remoteAddress);
} else if (user instanceof MemberUser) {
alertService.create(((MemberUser) user).getMember(), MemberAlert.Alerts.LOGIN_BLOCKED_BY_TRIES, wrongAttempts, remoteAddress);
}
// Notify the user
memberNotificationHandler.blockedCredentialsNotification(user, Credentials.LOGIN_PASSWORD);
// Set the block time and clear previous attempts
user.setPasswordBlockedUntil(blockTime.add(Calendar.getInstance()));
wrongCredentialAttemptsDao.clear(user, Credentials.LOGIN_PASSWORD);
throw new BlockedCredentialsException(Credentials.LOGIN_PASSWORD, user);
}
throw new InvalidCredentialsException(Credentials.LOGIN_PASSWORD, user);
}
// Everything ok - remove any previous wrong attempts
wrongCredentialAttemptsDao.clear(user, Credentials.LOGIN_PASSWORD);
return user;
}
private MemberUser checkPin(MemberUser user, final String plainPin, final String channel, final Member relatedMember) {
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// Check if not already blocked
if (isBlocked(user.getPinBlockedUntil())) {
throw new BlockedCredentialsException(Credentials.PIN, user);
}
final Member member = user.getMember();
final String userPin = user.getPin();
final String pin = hashHandler.hash(user.getSalt(), plainPin);
// Check the pin
if (userPin == null || !userPin.equalsIgnoreCase(StringUtils.trimToNull(pin))) {
final MemberGroupSettings memberSettings = member.getMemberGroup().getMemberSettings();
final int maxTries = memberSettings.getMaxPinWrongTries();
wrongCredentialAttemptsDao.record(user, Credentials.PIN);
int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), user, Credentials.PIN);
if (wrongAttempts == maxTries) {
final TimePeriod blockTime = memberSettings.getPinBlockTimeAfterMaxTries();
final String relatedUsername = relatedMember == null ? "" : relatedMember.getUsername();
// Create an alert and notify the member
alertService.create(member, MemberAlert.Alerts.PIN_BLOCKED_BY_TRIES, maxTries, channel, relatedUsername);
memberNotificationHandler.blockedCredentialsNotification(user, Credentials.PIN);
// Mark the pin as blocked, and clear previous wrong attempts
user.setPinBlockedUntil(blockTime.add(Calendar.getInstance()));
wrongCredentialAttemptsDao.clear(user, Credentials.PIN);
throw new BlockedCredentialsException(Credentials.PIN, user);
}
throw new InvalidCredentialsException(Credentials.PIN, user);
}
// Everything ok - remove any previous wrong attempts
wrongCredentialAttemptsDao.clear(user, Credentials.PIN);
return user;
}
private User checkTransactionPassword(User user, final String plainTransactionPassword, final String remoteAddress) {
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// Check if not already blocked
if (user.getTransactionPasswordStatus().equals(MemberUser.TransactionPasswordStatus.BLOCKED)) {
throw new BlockedCredentialsException(Credentials.TRANSACTION_PASSWORD, user);
}
final String transactionPassword = hashHandler.hash(user.getSalt(), plainTransactionPassword == null ? null : plainTransactionPassword.toUpperCase());
String userTransactionPassword = user.getTransactionPassword();
if (userTransactionPassword == null || !userTransactionPassword.equalsIgnoreCase(transactionPassword)) {
final Group group = user.getElement().getGroup();
final BasicGroupSettings settings = group.getBasicSettings();
final int maxTries = settings.getMaxTransactionPasswordWrongTries();
wrongCredentialAttemptsDao.record(user, Credentials.TRANSACTION_PASSWORD);
int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), user, Credentials.TRANSACTION_PASSWORD);
if (wrongAttempts == maxTries) {
// Send an alert
if (user instanceof AdminUser) {
alertService.create(SystemAlert.Alerts.ADMIN_TRANSACTION_PASSWORD_BLOCKED_BY_TRIES, user.getUsername(), wrongAttempts, remoteAddress);
} else {
Member m;
if (user instanceof MemberUser) {
m = ((MemberUser) user).getMember();
} else {
m = ((OperatorUser) user).getOperator().getMember();
}
alertService.create(m, MemberAlert.Alerts.TRANSACTION_PASSWORD_BLOCKED_BY_TRIES, wrongAttempts, remoteAddress);
memberNotificationHandler.blockedCredentialsNotification(user, Credentials.TRANSACTION_PASSWORD);
}
// Block the transaction password and clear previous attempts
final ResetTransactionPasswordDTO dto = new ResetTransactionPasswordDTO();
dto.setAllowGeneration(false);
dto.setUser(user);
resetTransactionPassword(dto);
wrongCredentialAttemptsDao.clear(user, Credentials.TRANSACTION_PASSWORD);
throw new BlockedCredentialsException(Credentials.TRANSACTION_PASSWORD, user);
}
throw new InvalidCredentialsException(Credentials.TRANSACTION_PASSWORD, user);
}
// Everything ok - remove any previous wrong attempts
wrongCredentialAttemptsDao.clear(user, Credentials.TRANSACTION_PASSWORD);
return user;
}
/**
* Returns the session timeout for the given user
*/
private TimePeriod getSessionTimeout(final User user, final boolean isPosWeb) {
AccessSettings accessSettings = settingsService.getAccessSettings();
TimePeriod sessionTimeout;
if (isPosWeb) {
sessionTimeout = accessSettings.getPoswebTimeout();
} else if (user instanceof AdminUser) {
sessionTimeout = accessSettings.getAdminTimeout();
} else {
sessionTimeout = accessSettings.getMemberTimeout();
}
return sessionTimeout;
}
private boolean isBlocked(final Calendar date) {
return date != null && date.after(Calendar.getInstance());
}
private boolean isChannelEnabledForGroup(final Channel channel, MemberGroup memberGroup) {
memberGroup = fetchService.fetch(memberGroup, MemberGroup.Relationships.CHANNELS);
return memberGroup.getChannels().contains(channel);
}
private User loadUser(final String member, final String username) {
if (StringUtils.isEmpty(member)) {
// Normal user login
final User user = elementService.loadUser(username, FETCH);
if (user instanceof OperatorUser) {
// Cannot login as operator without giving a member username too
throw new EntityNotFoundException(User.class);
}
return user;
} else {
// Member's operator login - First load the member
final User loadedMember = elementService.loadUser(member, FETCH);
MemberUser memberUser;
try {
memberUser = (MemberUser) loadedMember;
} catch (final ClassCastException e) {
throw new EntityNotFoundException(MemberUser.class);
}
final Member m = memberUser.getMember();
// Then the operator
final OperatorUser user = elementService.loadOperatorUser(m, username, FETCH);
// Assign the already fetched member to the operator
user.getOperator().setMember(m);
return user;
}
}
private boolean requestTransactionPassword(User user) {
user = fetchService.fetch(user, FETCH);
Group group = user.getElement().getGroup();
if (group instanceof OperatorGroup) {
group = fetchService.fetch(group, RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
}
final BasicGroupSettings settings = group.getBasicSettings();
return settings.getTransactionPassword().isUsed();
}
private ValidationError resolveValidationError(final boolean loginPassword, final boolean numeric, final Element element, final Object object, final Object property, final String credential) {
if (StringUtils.isEmpty(credential)) {
return null;
}
final Group group = fetchService.fetch(element.getGroup());
final BasicGroupSettings settings = group.getBasicSettings();
RangeConstraint length;
if (loginPassword) {
length = settings.getPasswordLength();
} else {
final MemberGroup memberGroup = (MemberGroup) group;
length = memberGroup.getMemberSettings().getPinLength();
}
// Validate the password length
final ValidationError lengthResult = new LengthValidation(length).validate(object, property, credential);
if (lengthResult != null) {
return lengthResult;
}
final String keyPrefix = loginPassword ? "changePassword.error." : "changePin.error.";
// Check for characters
if (numeric && !(group instanceof AdminGroup)) {
// Must be numeric
if (!StringUtils.isNumeric(credential)) {
return new ValidationError(keyPrefix + "mustBeNumeric");
}
}
if (loginPassword && !numeric) {
// When the virtual keyboard is enabled, make sure that the login password has no special characters
final AccessSettings accessSettings = settingsService.getAccessSettings();
if (accessSettings.isVirtualKeyboard() && StringHelper.hasSpecial(credential)) {
return new ValidationError("changePassword.error.mustContainOnlyLettersOrNumbers");
}
}
// Validate the password policy
final PasswordPolicy policy = settings.getPasswordPolicy();
if (policy == null || policy == PasswordPolicy.NONE) {
// Nothing to enforce
return null;
}
// Check for characters
if (loginPassword && !numeric) {
// Keys are hard coded with changePassword.error because pin is always numeric
switch (policy) {
case AVOID_OBVIOUS_LETTERS_NUMBERS:
// Must include letters and numbers
if (!StringHelper.hasDigits(credential) || !StringHelper.hasLetters(credential)) {
return new ValidationError("changePassword.error.mustIncludeLettersNumbers");
}
break;
case AVOID_OBVIOUS_LETTERS_NUMBERS_SPECIAL:
// Must include letters, numbers and special characters
if (!StringHelper.hasDigits(credential) || !StringHelper.hasLetters(credential) || !StringHelper.hasSpecial(credential)) {
return new ValidationError("changePassword.error.mustIncludeLettersNumbersSpecial");
}
break;
}
}
// Check for obvious password
if (isObviousCredential(element, credential)) {
return new ValidationError(keyPrefix + "obvious");
}
return null;
}
private void updateSessionExpiration(final Long id, final Calendar newExpiration) {
transactionHelper.runAsync(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
sessionDao.updateExpiration(id, newExpiration);
}
});
}
}