/*
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.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import nl.strohalm.cyclos.access.AdminAdminPermission;
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.entities.access.Channel;
import nl.strohalm.cyclos.entities.access.MemberUser;
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.accounts.cards.Card;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.Group.Nature;
import nl.strohalm.cyclos.entities.groups.GroupQuery;
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.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.BaseServiceSecurity;
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.NotConnectedException;
import nl.strohalm.cyclos.services.access.exceptions.SessionAlreadyInUseException;
import nl.strohalm.cyclos.services.access.exceptions.UserNotFoundException;
import nl.strohalm.cyclos.services.elements.ResetTransactionPasswordDTO;
import nl.strohalm.cyclos.services.groups.GroupServiceLocal;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.access.PermissionHelper;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import org.apache.commons.collections.CollectionUtils;
/**
* Security layer for {@link AccessService}
*
* @author luis
*/
public class AccessServiceSecurity extends BaseServiceSecurity implements AccessService {
private AccessServiceLocal accessService;
private GroupServiceLocal groupService;
@Override
public boolean canChangeChannelsAccess(final Member member) {
// Nothing to check
return accessService.canChangeChannelsAccess(member);
}
@Override
public Member changeChannelsAccess(Member member, final Collection<Channel> channels, final boolean verifySmsChannel) {
member = fetchService.fetch(member, Element.Relationships.GROUP);
PermissionHelper.checkSelection(member.getMemberGroup().getChannels(), channels);
if (!canChangeChannelsAccess(member)) {
throw new PermissionDeniedException();
}
return accessService.changeChannelsAccess(member, channels, verifySmsChannel);
}
@Override
public User changePassword(final ChangeLoginPasswordDTO params) throws InvalidCredentialsException, BlockedCredentialsException, CredentialsAlreadyUsedException {
checkChangePassword(params);
return accessService.changePassword(params);
}
@Override
public MemberUser changePin(final ChangePinDTO params) throws InvalidCredentialsException, BlockedCredentialsException, CredentialsAlreadyUsedException {
checkChangePin(params);
return accessService.changePin(params);
}
@Override
public MemberUser checkCredentials(final Channel channel, final MemberUser user, final String credentials, final String remoteAddress, final Member relatedMember) throws InvalidCredentialsException, BlockedCredentialsException, InvalidCardException {
// We cannot enforce permissions here, as this method is used under different contexts, like web services, webshop receive payments, etc...
return accessService.checkCredentials(channel, user, credentials, remoteAddress, relatedMember);
}
@Override
public Session checkSession(final String sessionId) throws NotConnectedException {
// This is invoked even before LoggedUser is initialized. So, there's nothing to enforce...
return accessService.checkSession(sessionId);
}
@Override
public User checkTransactionPassword(final String transactionPassword) throws InvalidCredentialsException, BlockedCredentialsException {
// Nothing to check, as this method only affects the logged user
return accessService.checkTransactionPassword(transactionPassword);
}
@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();
}
Element element = session.getUser().getElement();
checkDisconnect(element);
accessService.disconnect(session);
return session.getUser();
}
@Override
public User disconnect(User user) throws NotConnectedException {
user = fetchService.fetch(user, User.Relationships.ELEMENT);
checkDisconnect(user.getElement());
return accessService.disconnect(user);
}
@Override
public String generateTransactionPassword() throws UnexpectedEntityException {
// Nothing to check, as this method only affects the logged user
return accessService.generateTransactionPassword();
}
@Override
public Collection<Channel> getChannelsEnabledForMember(final Member member) {
permissionService.checkManages(member);
return accessService.getChannelsEnabledForMember(member);
}
@Override
public User getLoggedUser(final String sessionId) throws NotConnectedException {
User user = accessService.getLoggedUser(sessionId);
permissionService.checkManages(user.getElement());
return user;
}
@Override
public boolean hasPasswordExpired() {
// Nothing to check, as this method only affects the logged user
return accessService.hasPasswordExpired();
}
@Override
public boolean isCardSecurityCodeBlocked(Card card) {
card = fetchService.fetch(card, Card.Relationships.OWNER);
permissionService.permission(card.getOwner())
.admin(AdminMemberPermission.CARDS_VIEW)
.broker(BrokerPermission.CARDS_VIEW)
.member(MemberPermission.CARDS_VIEW)
.check();
return accessService.isCardSecurityCodeBlocked(card);
}
@Override
public boolean isChannelEnabledForMember(final Channel channel, final Member member) {
// We cannot check management here, as this is invoked, for example, on web services restricted to a member -
// we just need to know whether a channel is enabled
permissionService.checkRelatesTo(member);
return accessService.isChannelEnabledForMember(channel, member);
}
@Override
public boolean isChannelEnabledForMember(final String channelInternalName, final Member member) {
// We cannot check management here, as this is invoked, for example, on web services restricted to a member -
// we just need to know whether a channel is enabled
permissionService.checkRelatesTo(member);
return accessService.isChannelEnabledForMember(channelInternalName, member);
}
@Override
public boolean isLoggedIn(User user) throws NotConnectedException {
user = fetchService.fetch(user, User.Relationships.ELEMENT);
// We cannot just throw PermissionDeniedException here, as this method is used over related (not managed) users.
// In that case, just assume the user is not logged in
if (!permissionService.manages(user.getElement())) {
return false;
}
return accessService.isLoggedIn(user);
}
@Override
public boolean isLoginBlocked(User user) {
user = fetchService.fetch(user, User.Relationships.ELEMENT);
// We cannot just throw PermissionDeniedException here, as this method is used over related (not managed) users.
// In that case, just assume the user login is not blocked
if (!permissionService.manages(user.getElement())) {
return false;
}
return accessService.isLoginBlocked(user);
}
@Override
public boolean isPinBlocked(MemberUser user) {
user = fetchService.fetch(user, User.Relationships.ELEMENT);
// We cannot just throw PermissionDeniedException here, as this method is used over related (not managed) users.
// In that case, just assume the user pin is not blocked
if (!permissionService.manages(user.getElement())) {
return false;
}
return accessService.isPinBlocked(user);
}
@Override
public User login(User user, final String password, final String channel, final boolean isPosWeb, final String remoteAddress, final String sessionId) throws UserNotFoundException, InvalidCredentialsException, BlockedCredentialsException, SessionAlreadyInUseException {
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
permissionService.permission(user.getElement().getGroup())
.basic(BasicPermission.BASIC_LOGIN)
.check();
return accessService.login(user, password, channel, isPosWeb, remoteAddress, sessionId);
}
@Override
public User logout(final String sessionId) {
// This method is invoked by a session listener, so, probably there's nothing on LoggedUser => nothing to check
return accessService.logout(sessionId);
}
@Override
public boolean notifyPermissionDeniedException() {
// Nothing to check, as this method only affects the logged user
return accessService.notifyPermissionDeniedException();
}
@Override
public User reenableLogin(User user) {
user = fetchService.fetch(user, RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
// Check that the user doesn't belong to a removed group.
if (user.getElement().getGroup().isRemoved()) {
throw new PermissionDeniedException();
}
permissionService.permission(user.getElement())
.admin(AdminAdminPermission.ACCESS_ENABLE_LOGIN, AdminMemberPermission.ACCESS_ENABLE_LOGIN)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
return accessService.reenableLogin(user);
}
@Override
public MemberUser resetPassword(MemberUser user) {
user = fetchService.fetch(user, User.Relationships.ELEMENT);
permissionService.permission(user.getElement())
.admin(AdminMemberPermission.ACCESS_RESET_PASSWORD)
.broker(BrokerPermission.MEMBER_ACCESS_RESET_PASSWORD)
.check();
return accessService.resetPassword(user);
}
@Override
public User resetTransactionPassword(final ResetTransactionPasswordDTO dto) {
User user = fetchService.fetch(dto.getUser(), User.Relationships.ELEMENT);
dto.setUser(user);
permissionService.permission(user.getElement())
.admin(AdminAdminPermission.ACCESS_TRANSACTION_PASSWORD, AdminMemberPermission.ACCESS_TRANSACTION_PASSWORD)
.broker(BrokerPermission.MEMBER_ACCESS_TRANSACTION_PASSWORD)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
return accessService.resetTransactionPassword(dto);
}
@Override
public List<Session> searchSessions(final SessionQuery query) {
if (LoggedUser.isAdministrator()) {
Collection<Nature> natures = query.getNatures();
if (CollectionUtils.isEmpty(natures)) {
// As usual, empty means all. We want to ensure one-by-one, so we add them here
natures = EnumSet.allOf(Nature.class);
}
if (!permissionService.hasPermission(AdminSystemPermission.STATUS_VIEW_CONNECTED_ADMINS)) {
natures.remove(Nature.ADMIN);
}
if (!permissionService.hasPermission(AdminSystemPermission.STATUS_VIEW_CONNECTED_MEMBERS)) {
natures.remove(Nature.MEMBER);
}
if (!permissionService.hasPermission(AdminSystemPermission.STATUS_VIEW_CONNECTED_BROKERS)) {
natures.remove(Nature.BROKER);
}
if (!permissionService.hasPermission(AdminSystemPermission.STATUS_VIEW_CONNECTED_OPERATORS)) {
natures.remove(Nature.OPERATOR);
}
if (natures.isEmpty()) {
// Nothing left to see
throw new PermissionDeniedException();
}
// Apply the allowed groups
Collection<Group> allowedGroups = new HashSet<Group>();
allowedGroups.addAll(permissionService.getVisibleMemberGroups());
if (natures.contains(Nature.ADMIN)) {
// Add all admin groups, as they are not present on the permissionService.getVisibleMemberGroups()
GroupQuery admins = new GroupQuery();
admins.setNatures(Group.Nature.ADMIN);
allowedGroups.addAll(groupService.search(admins));
}
if (natures.contains(Nature.OPERATOR)) {
// Add all operator groups, as they are not present on the permissionService.getVisibleMemberGroups()
GroupQuery operators = new GroupQuery();
operators.setIgnoreManagedBy(true);
operators.setNatures(Group.Nature.OPERATOR);
allowedGroups.addAll(groupService.search(operators));
}
query.setGroups(PermissionHelper.checkSelection(allowedGroups, query.getGroups()));
} else {
// Members can only view connected operators
permissionService.permission(query.getMember()).member(MemberPermission.OPERATORS_MANAGE).check();
}
return accessService.searchSessions(query);
}
public void setAccessServiceLocal(final AccessServiceLocal accessService) {
this.accessService = accessService;
}
public void setGroupServiceLocal(final GroupServiceLocal groupService) {
this.groupService = groupService;
}
@Override
public MemberUser unblockPin(MemberUser user) {
user = fetchService.fetch(user, User.Relationships.ELEMENT);
permissionService.permission(user.getElement())
.admin(AdminMemberPermission.ACCESS_UNBLOCK_PIN)
.broker(BrokerPermission.MEMBER_ACCESS_CHANGE_PIN)
.member(MemberPermission.ACCESS_UNBLOCK_PIN)
.check();
return accessService.unblockPin(user);
}
@Override
public void validateChangePassword(final ChangeLoginPasswordDTO params) throws ValidationException {
checkChangePassword(params);
accessService.validateChangePassword(params);
}
@Override
public void validateChangePin(final ChangePinDTO params) throws ValidationException {
checkChangePin(params);
accessService.validateChangePin(params);
}
@Override
public User verifyLogin(final String member, final String username, final String remoteAddress) throws UserNotFoundException, InactiveMemberException, PermissionDeniedException {
// This method is invoked before logging in, so we cannot check anything here
return accessService.verifyLogin(member, username, remoteAddress);
}
private void checkChangePassword(final ChangeLoginPasswordDTO params) {
User user = fetchService.fetch(params.getUser(), RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
params.setUser(user);
if (LoggedUser.user().equals(user)) {
// No permission to check - an user can always change his own password
return;
}
// Can't change password of a removed element.
if (user.getElement().getGroup().isRemoved()) {
throw new PermissionDeniedException();
}
permissionService.permission(user.getElement())
.admin(AdminAdminPermission.ACCESS_CHANGE_PASSWORD, AdminMemberPermission.ACCESS_CHANGE_PASSWORD)
.broker(BrokerPermission.MEMBER_ACCESS_CHANGE_PASSWORD)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
}
private void checkChangePin(final ChangePinDTO params) {
MemberUser user = fetchService.fetch(params.getUser(), User.Relationships.ELEMENT);
params.setUser(user);
permissionService.permission(user.getElement())
.admin(AdminMemberPermission.ACCESS_CHANGE_PIN)
.broker(BrokerPermission.MEMBER_ACCESS_CHANGE_PIN)
.member()
.check();
}
private void checkDisconnect(final Element element) {
if (LoggedUser.isAdministrator() && element instanceof Operator) {
// Special case: admins can disconnect logged operators (even without managing them), as long as the member can be disconnected
Operator operator = (Operator) element;
permissionService.permission(operator.getMember())
.admin(AdminMemberPermission.ACCESS_DISCONNECT_OPERATOR)
.check();
} else {
permissionService.permission(element)
.admin(AdminAdminPermission.ACCESS_DISCONNECT, AdminMemberPermission.ACCESS_DISCONNECT)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
}
}
}