/*
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.elements;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
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.Relationship;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.access.PrincipalType;
import nl.strohalm.cyclos.entities.access.User;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.BrokerGroup;
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.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.AdminQuery;
import nl.strohalm.cyclos.entities.members.Administrator;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.ElementQuery;
import nl.strohalm.cyclos.entities.members.FullTextElementQuery;
import nl.strohalm.cyclos.entities.members.FullTextMemberQuery;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.MemberQuery;
import nl.strohalm.cyclos.entities.members.OperatorQuery;
import nl.strohalm.cyclos.entities.members.PendingEmailChange;
import nl.strohalm.cyclos.entities.members.PendingMember;
import nl.strohalm.cyclos.entities.members.PendingMemberQuery;
import nl.strohalm.cyclos.exceptions.MailSendingException;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.BaseServiceSecurity;
import nl.strohalm.cyclos.services.access.ChannelServiceLocal;
import nl.strohalm.cyclos.services.elements.exceptions.MemberHasBalanceException;
import nl.strohalm.cyclos.services.elements.exceptions.MemberHasOpenInvoicesException;
import nl.strohalm.cyclos.services.elements.exceptions.RegistrationAgreementNotAcceptedException;
import nl.strohalm.cyclos.services.groups.GroupServiceLocal;
import nl.strohalm.cyclos.utils.ElementVO;
import nl.strohalm.cyclos.utils.EntityHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.access.PermissionHelper;
import nl.strohalm.cyclos.utils.validation.ValidationException;
/**
* Security implementation for {@link ElementService}
*
* @author Rinke
*/
public class ElementServiceSecurity extends BaseServiceSecurity implements ElementService {
private ElementServiceLocal elementService;
private GroupServiceLocal groupService;
private ChannelServiceLocal channelService;
@Override
public void acceptAgreement(final String remoteAddress) {
// Only members can accept an agreement. There's really not much to check
if (!LoggedUser.isMember()) {
throw new PermissionDeniedException();
}
elementService.acceptAgreement(remoteAddress);
}
@Override
public BulkMemberActionResultVO bulkChangeMemberChannels(final FullTextMemberQuery query, final Collection<Channel> enableChannels, final Collection<Channel> disableChannels) throws ValidationException {
if (!elementService.applyQueryRestrictions(query)) {
throw new PermissionDeniedException();
}
permissionService.permission().admin(AdminMemberPermission.BULK_ACTIONS_CHANGE_CHANNELS).check();
// The "web" channel can not be customized by any user
final Channel webChannel = channelService.loadByInternalName(Channel.WEB);
if (enableChannels.contains(webChannel) || disableChannels.contains(webChannel)) {
throw new PermissionDeniedException();
}
return elementService.bulkChangeMemberChannels(query, enableChannels, disableChannels);
}
@Override
public BulkMemberActionResultVO bulkChangeMemberGroup(final FullTextMemberQuery query, final MemberGroup newGroup, final String comments) throws ValidationException {
if (!elementService.applyQueryRestrictions(query)) {
throw new PermissionDeniedException();
}
permissionService.permission().admin(AdminMemberPermission.BULK_ACTIONS_CHANGE_GROUP).check();
permissionService.checkManages(newGroup);
return elementService.bulkChangeMemberGroup(query, newGroup, comments);
}
@Override
public <E extends Element> E changeGroup(final E element, final Group newGroup, final String comments) throws MemberHasBalanceException, MemberHasOpenInvoicesException, ValidationException {
checkChangeGroup(element);
// No matter what, an admin cannot change his own group. Also, the new group must be visible
if (LoggedUser.element().equals(element) || !permissionService.getAllVisibleGroups().contains(newGroup)) {
throw new PermissionDeniedException();
}
return elementService.changeGroup(element, newGroup, comments);
}
@Override
public <E extends Element> E changeProfile(final E element) {
// An user can always change his own profile. Otherwise, needs permissions
if (!LoggedUser.element().equals(element)) {
permissionService.permission(element)
.admin(AdminAdminPermission.ADMINS_CHANGE_PROFILE, AdminMemberPermission.MEMBERS_CHANGE_PROFILE)
.broker(BrokerPermission.MEMBERS_CHANGE_PROFILE)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
}
return elementService.changeProfile(element);
}
@Override
public PendingEmailChange confirmEmailChange(final String key) throws EntityNotFoundException {
// The impl already checks that the given key belongs to the logged user, or throws ENFE. Here, we just validate the logged user is a member
if (!LoggedUser.isMember()) {
throw new PermissionDeniedException();
}
return elementService.confirmEmailChange(key);
}
@Override
public List<? extends Element> fullTextSearch(final FullTextElementQuery query) {
if (!elementService.applyQueryRestrictions(query)) {
throw new PermissionDeniedException();
}
return elementService.fullTextSearch(query);
}
@Override
public ElementVO getElementVO(final long id) {
// Nothing to check.
return elementService.getElementVO(id);
}
@Override
public Calendar getFirstMemberActivationDate() {
// Used only on statistics
permissionService.permission().admin(AdminSystemPermission.REPORTS_STATISTICS).check();
return elementService.getFirstMemberActivationDate();
}
@Override
public PendingEmailChange getPendingEmailChange(final Member member) {
permissionService.checkManages(member);
return elementService.getPendingEmailChange(member);
}
@Override
public List<? extends Group> getPossibleNewGroups(final Element element) {
checkChangeGroup(element);
return elementService.getPossibleNewGroups(element);
}
@Override
public void invitePerson(final String email) {
permissionService.permission().basic(BasicPermission.BASIC_INVITE_MEMBER).check();
elementService.invitePerson(email);
}
@Override
public <T extends Element> T load(final Long id, final Relationship... fetch) throws EntityNotFoundException {
T element = elementService.<T> load(id, fetch);
permissionService.checkRelatesTo(element);
return element;
}
@Override
public Member loadByPrincipal(final PrincipalType principalType, final String principal, final Relationship... fetch) throws EntityNotFoundException {
// This method is used, for example, to load users about to login. So, we cannot enforce anything here...
return elementService.loadByPrincipal(principalType, principal, fetch);
}
@Override
public PendingMember loadPendingMember(final Long id, final Relationship... fetch) {
PendingMember pendingMember = elementService.loadPendingMember(id, fetch);
checkManagePending(pendingMember);
return pendingMember;
}
@Override
public PendingMember loadPendingMemberByKey(final String key, final Relationship... fetch) {
// This method is invoked by guests, exactly to validate the registration. Nothing to check
return elementService.loadPendingMemberByKey(key, fetch);
}
@Override
public <T extends User> T loadUser(final Long id, final Relationship... fetch) throws EntityNotFoundException {
T user = elementService.<T> loadUser(id, fetch);
permissionService.checkRelatesTo(user.getElement());
return user;
}
@Override
public <T extends User> T loadUser(final String username, final Relationship... fetch) throws EntityNotFoundException {
T user = elementService.<T> loadUser(username, fetch);
permissionService.checkRelatesTo(user.getElement());
return user;
}
@Override
public Member publicValidateRegistration(final String key) throws EntityNotFoundException, RegistrationAgreementNotAcceptedException {
// Nothing to check on public e-mail validation
return elementService.publicValidateRegistration(key);
}
@SuppressWarnings("unchecked")
@Override
public Object register(final Element element, final boolean forceChangePassword, final String remoteAddress) {
// We need the group in order to check anything
if (element.getGroup() == null) {
throw new ValidationException();
}
if (LoggedUser.hasUser()) {
// When no logged user, we don't need to check permission here - it will only validate the given initial group
permissionService.checkManages(element.getGroup());
Nature groupNature = element.getGroup().getNature();
permissionService.permission()
.admin(groupNature == Group.Nature.MEMBER || groupNature == Group.Nature.BROKER ? AdminMemberPermission.MEMBERS_REGISTER : AdminAdminPermission.ADMINS_REGISTER)
.broker(BrokerPermission.MEMBERS_REGISTER)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
}
// Check the initial group
PermissionHelper.checkContains((Collection<Group>) getPossibleInitialGroups(element), element.getGroup());
return elementService.register(element, forceChangePassword, remoteAddress);
}
@Override
public <T extends User> T reloadUser(final Long id, final Relationship... fetch) throws EntityNotFoundException {
T user = elementService.<T> reloadUser(id, fetch);
permissionService.checkRelatesTo(user.getElement());
return user;
}
@Override
public void remove(final Long id) throws UnexpectedEntityException {
Element element = elementService.load(id);
permissionService.permission(element)
.admin(AdminMemberPermission.MEMBERS_REMOVE, AdminAdminPermission.ADMINS_REMOVE)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
elementService.remove(id);
}
@Override
public int removePendingMembers(final Long... ids) {
// We can reuse the same checkManagePending() method as there is a single permission for pending members
for (Long id : ids) {
PendingMember pendingMember = elementService.loadPendingMember(id);
checkManagePending(pendingMember);
}
return elementService.removePendingMembers(ids);
}
@Override
public PendingMember resendEmail(PendingMember pendingMember) throws MailSendingException {
// We can reuse the same checkManagePending() method as there is a single permission for pending members
pendingMember = fetchService.fetch(pendingMember);
checkManagePending(pendingMember);
return elementService.resendEmail(pendingMember);
}
@Override
public PendingEmailChange resendEmailChange(final Long memberId) throws MailSendingException {
Member member = fetchService.fetch(EntityHelper.reference(Member.class, memberId));
permissionService.permission(member)
.admin(AdminMemberPermission.MEMBERS_CHANGE_EMAIL)
.broker(BrokerPermission.MEMBERS_CHANGE_EMAIL)
.member(MemberPermission.PROFILE_CHANGE_EMAIL)
.check();
return elementService.resendEmailChange(memberId);
}
@Override
public List<? extends Element> search(final ElementQuery query) {
if (!applyRestrictions(query)) {
throw new PermissionDeniedException();
}
return elementService.search(query);
}
@Override
public List<PendingMember> search(final PendingMemberQuery params) {
permissionService.permission()
.admin(AdminMemberPermission.MEMBERS_MANAGE_PENDING)
.broker(BrokerPermission.MEMBERS_MANAGE_PENDING)
.check();
if (LoggedUser.isBroker()) {
params.setBroker(LoggedUser.<Member> element());
} else { // an admin
params.setGroups(PermissionHelper.checkSelection(permissionService.getManagedMemberGroups(), params.getGroups()));
}
return elementService.search(params);
}
@Override
public List<? extends Element> searchAtDate(final MemberQuery query, final Calendar date) {
if (!applyRestrictions(query)) {
throw new PermissionDeniedException();
}
return elementService.searchAtDate(query, date);
}
public void setChannelServiceLocal(final ChannelServiceLocal channelService) {
this.channelService = channelService;
}
public void setElementServiceLocal(final ElementServiceLocal elementService) {
this.elementService = elementService;
}
public void setGroupServiceLocal(final GroupServiceLocal groupService) {
this.groupService = groupService;
}
@Override
public void setRegistrationAgreementAgreed(final PendingMember pendingMember) {
// This method is invoked before actually having a Member, so, nothing to check
elementService.setRegistrationAgreementAgreed(pendingMember);
}
@Override
public boolean shallAcceptAgreement(final Member member) {
permissionService.checkManages(member);
return elementService.shallAcceptAgreement(member);
}
@Override
public PendingMember update(final PendingMember pendingMember) {
checkManagePending(pendingMember);
return elementService.update(pendingMember);
}
@Override
public void validate(final Element element, final WhenSaving when, final boolean manualPassword) throws ValidationException {
// No permission check needed for validation
elementService.validate(element, when, manualPassword);
}
@Override
public void validate(final PendingMember pendingMember) throws ValidationException {
// No permission check needed for validation
elementService.validate(pendingMember);
}
@Override
public void validateBulkChangeChannels(final FullTextMemberQuery query, final Collection<Channel> enableChannels, final Collection<Channel> disableChannels) {
// No permission check needed for validation
elementService.validateBulkChangeChannels(query, enableChannels, disableChannels);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private boolean applyRestrictions(final ElementQuery query) {
if (query instanceof AdminQuery) {
// Only admins with permissions can search admins
permissionService.permission().admin(AdminAdminPermission.ADMINS_VIEW).check();
} else if (query instanceof OperatorQuery) {
// Only members and operators can see other operators from the member himself
Member member = ((OperatorQuery) query).getMember();
if (member == null) {
throw new ValidationException();
}
permissionService.permission(member).member().operator().check();
} else {
// For members, just enforce the visible groups
Collection<Group> visibleGroups = (Collection) permissionService.getVisibleMemberGroups(false);
if (visibleGroups.isEmpty()) {
return false;
}
Collection<Group> queryGroups = (Collection<Group>) query.getGroups();
query.setGroups(PermissionHelper.checkSelection(visibleGroups, queryGroups));
}
// Ensure that only enabled users will be returned
if (!LoggedUser.isAdministrator()) {
query.setEnabled(true);
}
return true;
}
private void checkChangeGroup(final Element element) {
permissionService.permission(element)
.admin(AdminAdminPermission.ADMINS_CHANGE_GROUP, AdminMemberPermission.MEMBERS_CHANGE_GROUP)
.member(MemberPermission.OPERATORS_MANAGE)
.check();
}
private void checkManagePending(final PendingMember pendingMember) {
permissionService.permission()
.admin(AdminMemberPermission.MEMBERS_MANAGE_PENDING)
.broker(BrokerPermission.MEMBERS_MANAGE_PENDING)
.check();
if (LoggedUser.isAdministrator() && !permissionService.manages(pendingMember.getMemberGroup())) {
throw new PermissionDeniedException();
}
if (LoggedUser.isBroker() && !LoggedUser.element().equals(pendingMember.getBroker())) {
throw new PermissionDeniedException();
}
}
private Collection<? extends Group> getPossibleInitialGroups(final Element element) {
Collection<? extends Group> groups;
if (LoggedUser.getAccessType() == null) {
// Public registration
if (element instanceof Member) {
groups = groupService.getPossibleInitialGroups(null);
} else {
groups = Collections.emptyList();
}
} else {
if (element instanceof Administrator) {
// Registering an administrator
GroupQuery query = new GroupQuery();
query.setStatus(Group.Status.NORMAL);
query.setNature(Group.Nature.ADMIN);
groups = groupService.search(query);
} else if (element instanceof Member) {
// Registering a member
if (LoggedUser.isAdministrator()) {
// By admin
groups = permissionService.getManagedMemberGroups();
} else if (LoggedUser.isBroker()) {
// By broker
BrokerGroup brokerGroup = (BrokerGroup) fetchService.fetch(LoggedUser.group(), BrokerGroup.Relationships.POSSIBLE_INITIAL_GROUPS);
groups = brokerGroup.getPossibleInitialGroups();
} else {
groups = Collections.emptyList();
}
} else {
// Registering an operator
GroupQuery query = new GroupQuery();
query.setMember(LoggedUser.member());
query.setNature(Group.Nature.OPERATOR);
query.setStatus(Group.Status.NORMAL);
groups = groupService.search(query);
}
}
return groups;
}
}