/*
* $Id$
*
* Copyright 2006-2014 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.logic;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import ome.annotations.NotNull;
import ome.annotations.PermitAll;
import ome.annotations.RolesAllowed;
import ome.api.IAdmin;
import ome.api.RawFileStore;
import ome.api.ServiceInterface;
import ome.api.local.LocalAdmin;
import ome.conditions.ApiUsageException;
import ome.conditions.AuthenticationException;
import ome.conditions.InternalException;
import ome.conditions.SecurityViolation;
import ome.conditions.ValidationException;
import ome.model.IGlobal;
import ome.model.IObject;
import ome.model.annotations.ExperimenterAnnotationLink;
import ome.model.annotations.FileAnnotation;
import ome.model.core.Image;
import ome.model.core.OriginalFile;
import ome.model.core.Pixels;
import ome.model.enums.ChecksumAlgorithm;
import ome.model.internal.Permissions;
import ome.model.meta.Experimenter;
import ome.model.meta.ExperimenterGroup;
import ome.model.meta.GroupExperimenterMap;
import ome.parameters.Parameters;
import ome.security.ACLVoter;
import ome.security.AdminAction;
import ome.security.ChmodStrategy;
import ome.security.SecureAction;
import ome.security.SecuritySystem;
import ome.security.auth.PasswordChangeException;
import ome.security.auth.PasswordProvider;
import ome.security.auth.PasswordUtil;
import ome.security.auth.RoleProvider;
import ome.security.basic.BasicSecuritySystem;
import ome.services.query.Definitions;
import ome.services.query.Query;
import ome.services.query.QueryParameterDef;
import ome.services.sessions.events.UserGroupUpdateEvent;
import ome.system.EventContext;
import ome.system.OmeroContext;
import ome.system.Roles;
import ome.system.SimpleEventContext;
import ome.tools.hibernate.QueryBuilder;
import ome.tools.hibernate.SecureMerge;
import ome.tools.hibernate.SessionFactory;
import ome.util.SqlAction;
import ome.util.Utils;
import ome.util.checksum.ChecksumProviderFactory;
import ome.util.checksum.ChecksumType;
/**
* Provides methods for administering user accounts, passwords, as well as
* methods which require special privileges.
*
* Developer note: As can be expected, to perform these privileged the Admin
* service has access to several resources that should not be generally used
* while developing services. Misuse could circumvent security or auditing.
*
* @author Josh Moore, josh.moore at gmx.de
* @see SecuritySystem
* @see Permissions
* @since 3.0-M3
*/
@Transactional(readOnly = true)
public class AdminImpl extends AbstractLevel2Service implements LocalAdmin,
ApplicationContextAware {
protected final SqlAction sql;
protected final SessionFactory osf;
protected final MailSender mailSender;
protected final SimpleMailMessage templateMessage;
protected final ACLVoter aclVoter;
protected final PasswordProvider passwordProvider;
protected final RoleProvider roleProvider;
protected final PasswordUtil passwordUtil;
protected final LdapImpl ldapUtil;
protected final ChmodStrategy chmod;
protected final ChecksumProviderFactory cpf;
protected OmeroContext context;
public void setApplicationContext(ApplicationContext ctx)
throws BeansException {
this.context = (OmeroContext) ctx;
}
public AdminImpl(SqlAction sql, SessionFactory osf,
MailSender mailSender, SimpleMailMessage templateMessage,
ACLVoter aclVoter, PasswordProvider passwordProvider,
RoleProvider roleProvider, LdapImpl ldapUtil, PasswordUtil passwordUtil,
ChmodStrategy chmod, ChecksumProviderFactory cpf) {
this.sql = sql;
this.osf = osf;
this.mailSender = mailSender;
this.templateMessage = templateMessage;
this.aclVoter = aclVoter;
this.passwordProvider = passwordProvider;
this.roleProvider = roleProvider;
this.ldapUtil = ldapUtil;
this.passwordUtil = passwordUtil;
this.chmod = chmod;
this.cpf = cpf;
}
public Class<? extends ServiceInterface> getServiceInterface() {
return IAdmin.class;
}
// ~ LOCAL PUBLIC METHODS
// =========================================================================
@RolesAllowed("user")
public Experimenter userProxy(final Long id) {
if (id == null) {
throw new ApiUsageException("Id argument cannot be null.");
}
Experimenter e = iQuery.get(Experimenter.class, id);
return e;
}
@RolesAllowed("user")
public Experimenter userProxy(final String omeName) {
if (omeName == null) {
throw new ApiUsageException("omeName argument cannot be null.");
}
Experimenter e = iQuery.findByString(Experimenter.class, "omeName",
roleProvider.isIgnoreCaseLookup() ? omeName.toLowerCase()
: omeName);
if (e == null) {
throw new ApiUsageException("No such experimenter: " + omeName);
}
return e;
}
@RolesAllowed("user")
public ExperimenterGroup groupProxy(Long id) {
if (id == null) {
throw new ApiUsageException("Id argument cannot be null.");
}
ExperimenterGroup g = iQuery.get(ExperimenterGroup.class, id);
return g;
}
@RolesAllowed("user")
public ExperimenterGroup groupProxy(final String groupName) {
if (groupName == null) {
throw new ApiUsageException("groupName argument cannot be null.");
}
ExperimenterGroup g = iQuery.findByString(ExperimenterGroup.class,
"name", groupName);
if (g == null) {
throw new ApiUsageException("No such group: " + groupName);
}
return g;
}
@RolesAllowed("user")
public List<Long> getLeaderOfGroupIds(final Experimenter e) {
Assert.notNull(e);
Assert.notNull(e.getId());
final QueryBuilder qb = new QueryBuilder();
qb.select("g.id").from("ExperimenterGroup", "g");
qb.join("g.groupExperimenterMap", "m", false, false);
qb.where();
qb.and("m.owner = true");
qb.and("m.parent.id = g.id");
qb.and("m.child.id = :id");
qb.param("id", e.getId());
List<Long> groupIds = iQuery.execute(new HibernateCallback<List<Long>>() {
public List<Long> doInHibernate(Session session)
throws HibernateException, SQLException {
org.hibernate.Query q = qb.query(session);
return (List<Long>) q.list();
}
});
return groupIds;
}
@SuppressWarnings("unchecked")
@RolesAllowed("user")
public List<Long> getMemberOfGroupIds(final Experimenter e) {
return (List<Long>) getGroupField(e, "id");
}
@SuppressWarnings("unchecked")
@RolesAllowed("user")
public List<String> getUserRoles(final Experimenter e) {
return (List<String>) getGroupField(e, "name");
}
@SuppressWarnings("rawtypes")
private List getGroupField(final Experimenter e, final String name) {
Assert.notNull(e);
Assert.notNull(e.getId());
List<String> groupNames = iQuery.execute(new HibernateCallback() {
public Object doInHibernate(Session session)
throws HibernateException, SQLException {
org.hibernate.Query q = session
.createQuery("select m.parent." + name + " " +
"from Experimenter e " +
"join e.groupExperimenterMap m " +
"where e.id = :id order by index(m)");
q.setParameter("id", e.getId());
return q.list();
}
});
return groupNames;
}
// ~ User accessible interface methods
// =========================================================================
@RolesAllowed("user")
public boolean canAnnotate(final IObject obj) {
if (obj == null) {
throw new ApiUsageException("Argument cannot be null");
}
final Class<? extends IObject> c = Utils.trueClass(obj.getClass());
IObject trusted = iQuery.get(c, obj.getId());
return aclVoter.allowAnnotate(trusted, trusted.getDetails());
}
@RolesAllowed("user")
public boolean canUpdate(final IObject obj) {
if (obj == null) {
throw new ApiUsageException("Argument cannot be null");
}
final Class<? extends IObject> c = Utils.trueClass(obj.getClass());
IObject trusted = iQuery.get(c, obj.getId());
return aclVoter.allowUpdate(trusted, trusted.getDetails());
}
@RolesAllowed("user")
public Experimenter getExperimenter(final long id) {
Experimenter e = iQuery.execute(new UserQ(new Parameters().addId(id)));
if (e == null) {
throw new ApiUsageException("No such experimenter: " + id);
}
return e;
}
@RolesAllowed("user")
public Experimenter lookupExperimenter(final String omeName) {
Experimenter e = iQuery.execute(new UserQ(new Parameters().addString(
"name",
roleProvider.isIgnoreCaseLookup() ? omeName.toLowerCase()
: omeName)));
if (e == null) {
throw new ApiUsageException("No such experimenter: " + omeName);
}
return e;
}
@RolesAllowed("user")
public List<Experimenter> lookupExperimenters() {
return iQuery.findAllByQuery("select distinct e from Experimenter e "
+ "left outer join fetch e.groupExperimenterMap m "
+ "left outer join fetch m.parent g", null);
}
@RolesAllowed("user")
public List<Map<String, Object>> lookupLdapAuthExperimenters() {
return ldapUtil.lookupLdapAuthExperimenters();
}
@RolesAllowed("user")
public String lookupLdapAuthExperimenter(long id) {
return ldapUtil.lookupLdapAuthExperimenter(id);
}
@RolesAllowed("user")
public ExperimenterGroup getGroup(long id) {
ExperimenterGroup g = iQuery.execute(new GroupQ(new Parameters()
.addId(id)));
if (g == null) {
throw new ApiUsageException("No such group: " + id);
}
return g;
}
@RolesAllowed("user")
public ExperimenterGroup lookupGroup(final String groupName) {
ExperimenterGroup g = iQuery.execute(new GroupQ(new Parameters()
.addString("name", groupName)));
if (g == null) {
throw new ApiUsageException("No such group: " + groupName);
}
return g;
}
@RolesAllowed("user")
public List<ExperimenterGroup> lookupGroups() {
return iQuery.findAllByQuery("select distinct g from ExperimenterGroup g "
+ "left outer join fetch g.groupExperimenterMap m "
+ "left outer join fetch m.child u "
+ "left outer join fetch u.groupExperimenterMap m2 "
+ "left outer join fetch m2.parent", null);
}
@RolesAllowed("user")
public Experimenter[] containedExperimenters(long groupId) {
List<Experimenter> experimenters = iQuery.findAllByQuery(
"select distinct e from Experimenter as e "
+ "join fetch e.groupExperimenterMap as map "
+ "join fetch map.parent g "
+ "where e.id in "
+ " (select m.child from GroupExperimenterMap m "
+ " where m.parent.id = :id )", new Parameters()
.addId(groupId));
return experimenters.toArray(new Experimenter[experimenters.size()]);
}
@RolesAllowed("user")
public ExperimenterGroup[] containedGroups(long experimenterId) {
List<ExperimenterGroup> groups = iQuery
.findAllByQuery(
"select distinct g from ExperimenterGroup as g "
+ "join fetch g.groupExperimenterMap as map "
+ "join fetch map.parent e "
+ "left outer join fetch map.child u "
+ "left outer join fetch u.groupExperimenterMap m2 "
+ "where g.id in "
+ " (select m.parent from GroupExperimenterMap m "
+ " where m.child.id = :id )",
new Parameters().addId(experimenterId));
return groups.toArray(new ExperimenterGroup[groups.size()]);
}
// ~ System-only interface methods
// =========================================================================
@RolesAllowed("system")
@Transactional(readOnly = false)
public void synchronizeLoginCache() {
final Logger log = getBeanHelper().getLogger();
final List<Map<String, Object>> dnIds = ldapUtil.lookupLdapAuthExperimenters();
if (dnIds.size() > 0) {
log.info("Synchronizing " + dnIds.size() + " ldap user(s)");
}
for (Map<String, Object> dnId: dnIds) {
String dn = (String) dnId.get("dn");
Long id = (Long) dnId.get("experimenter_id");
try {
Experimenter e = userProxy(id);
ldapUtil.synchronizeLdapUser(e.getOmeName());
} catch (ApiUsageException aue) {
// User likely doesn't exist
log.debug("User not found: " + dn);
} catch (Exception e) {
log.error("synchronizeLdapUser:" + dnId, e);
}
}
context.publishEvent(new UserGroupUpdateEvent(this));
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void updateSelf(@NotNull
Experimenter e) {
EventContext ec = getSecuritySystem().getEventContext();
final Experimenter self = getExperimenter(ec.getCurrentUserId());
self.setFirstName(e.getFirstName());
self.setMiddleName(e.getMiddleName());
self.setLastName(e.getLastName());
self.setEmail(e.getEmail());
self.setInstitution(e.getInstitution());
getSecuritySystem().runAsAdmin(new AdminAction() {
public void runAsAdmin() {
iUpdate.flush();
}
});
getBeanHelper().getLogger().info(
"Updated own user info: " + self.getOmeName());
}
protected static final String NSEXPERIMENTERPHOTO = "openmicroscopy.org/omero/experimenter/photo";
public List<OriginalFile> getMyUserPhotos() {
Parameters parameters = new Parameters();
parameters.addId(getEventContext().getCurrentUserId());
parameters.addString("ns", NSEXPERIMENTERPHOTO);
List<OriginalFile> photos = iQuery.findAllByQuery(
"select f from Experimenter e join e.annotationLinks l " +
"join l.child a join a.file f where e.id = :id and a.ns = :ns",
parameters);
return photos;
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public long uploadMyUserPhoto(String filename, String mimetype, byte[] data) {
Long uid = getEventContext().getCurrentUserId();
List<OriginalFile> photos = getMyUserPhotos();
OriginalFile file = null;
if (photos.size() > 0) {
file = photos.get(0);
}
if (file == null) {
file = new OriginalFile();
file.setName(filename);
file.setPath(filename); // FIXME this should be something like /users/<name>/photo
file.setSize((long) data.length);
file.setHasher(new ChecksumAlgorithm("SHA1-160"));
file.setHash(cpf.getProvider(ChecksumType.SHA1).putBytes(data)
.checksumAsString());
file.setMimetype(mimetype);
FileAnnotation fa = new FileAnnotation();
fa.setNs(NSEXPERIMENTERPHOTO);
fa.setFile(file);
ExperimenterAnnotationLink link = new ExperimenterAnnotationLink();
link.link(new Experimenter(uid, false), fa);
link = iUpdate.saveAndReturnObject(link);
fa = (FileAnnotation) link.getChild();
file = fa.getFile();
internalMoveToCommonSpace(file);
internalMoveToCommonSpace(fa);
internalMoveToCommonSpace(link);
} else {
file.setName(filename);
file.setPath(filename);
file.setMimetype(mimetype);
file = iUpdate.saveAndReturnObject(file);
}
RawFileStore rfs = (RawFileStore) context.getBean("internal-ome.api.RawFileStore");
try {
rfs.setFileId(file.getId());
rfs.write(data, 0, data.length);
file = rfs.save();
} finally {
rfs.close();
}
return file.getId();
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void updateExperimenter(@NotNull final
Experimenter experimenter) {
try {
adminOrPiOfUser(experimenter);
String name = experimenter.getOmeName();
copyAndSaveExperimenter(experimenter);
getBeanHelper().getLogger().info("Updated user info for " + name);
} catch (SecurityViolation sv) {
final Long currentID = getEventContext().getCurrentUserId();
final Long experimenterID = experimenter.getId();
// If we're not an admin, allow for the possibility
// of delegating to updateSelf.
if (currentID.equals(experimenterID)) {
updateSelf(experimenter);
} else {
// But throw if that's not the case.
throw sv;
}
}
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void updateExperimenterWithPassword(@NotNull final
Experimenter experimenter, final String password) {
adminOrPiOfUser(experimenter);
copyAndSaveExperimenter(experimenter);
final Experimenter orig = userProxy(experimenter.getId());
String name = orig.getOmeName();
changeUserPassword(name, password);
getBeanHelper().getLogger().info(
"Updated user info and password for " + name);
}
/**
* @param experimenter
*/
private void copyAndSaveExperimenter(final Experimenter experimenter) {
final Experimenter orig = userProxy(experimenter.getId());
final String origOmeName = orig.getOmeName();
final String newOmeName = experimenter.getOmeName();
if (!origOmeName.equals(newOmeName)) {
final Roles roles = getSecurityRoles();
final Set<String> fixedExperimenterNames =
ImmutableSet.of(roles.getRootName(), roles.getGuestName());
if (fixedExperimenterNames.contains(origOmeName)) {
throw new ValidationException("cannot change name of special experimenter '" + origOmeName + "'");
} else if (fixedExperimenterNames.contains(newOmeName)) {
throw new ValidationException("cannot change name to special experimenter '" + newOmeName + "'");
}
}
orig.setOmeName(newOmeName);
orig.setEmail(experimenter.getEmail());
orig.setFirstName(experimenter.getFirstName());
orig.setMiddleName(experimenter.getMiddleName());
orig.setLastName(experimenter.getLastName());
orig.setInstitution(experimenter.getInstitution());
reallySafeSave(orig);
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void updateGroup(@NotNull final
ExperimenterGroup group) {
adminOrPiOfGroup(group);
Permissions p = group.getDetails().getPermissions();
if (p != null) {
// Setting permissions is not allowed via IUpdate
// so use the logic in changePermissions and then
// reset permissions to the current value.
changePermissions(group, p); // ticket:1776 WORKAROUND
}
final ExperimenterGroup orig = getGroup(group.getId());
final String origName = orig.getName();
final String newName = group.getName();
if (!origName.equals(newName)) {
final Roles roles = getSecurityRoles();
final Set<String> fixedGroupNames =
ImmutableSet.of(roles.getGuestGroupName(), roles.getSystemGroupName(), roles.getUserGroupName());
if (fixedGroupNames.contains(origName)) {
throw new ValidationException("cannot change name of special group '" + origName + "'");
} else if (fixedGroupNames.contains(newName)) {
throw new ValidationException("cannot change name to special group '" + newName + "'");
}
if (group.getId().equals(getEventContext().getCurrentGroupId())) {
throw new ValidationException("cannot rename the current group context '" + origName + "'");
}
}
orig.setName(newName);
orig.setDescription(group.getDescription());
reallySafeSave(orig);
getBeanHelper().getLogger().info("Updated group info for " + group);
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public long createUser(final Experimenter newUser, String defaultGroup) {
// logged via createExperimenter
final ExperimenterGroup proxy = groupProxy(defaultGroup);
// logged & secured via createExperimenter
return createExperimenter(newUser, proxy, groupProxy(sec
.getSecurityRoles().getUserGroupName()));
}
@RolesAllowed("system")
@Transactional(readOnly = false)
public long createSystemUser(Experimenter newSystemUser) {
// logged & secured via createExperimenter
return createExperimenter(newSystemUser,
groupProxy(sec.getSecurityRoles().getSystemGroupName()),
groupProxy(sec.getSecurityRoles().getUserGroupName()));
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public long createExperimenter(final Experimenter experimenter,
ExperimenterGroup defaultGroup, ExperimenterGroup... otherGroups) {
adminOrPiOfNonUserGroups(defaultGroup, otherGroups);
long uid = roleProvider.createExperimenter(experimenter, defaultGroup, otherGroups);
// If this method passes, then the Experimenter is valid.
changeUserPassword(experimenter.getOmeName(), " ");
getBeanHelper().getLogger().info(
"Created user with blank password: "
+ experimenter.getOmeName());
return uid;
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public long createExperimenterWithPassword(final Experimenter experimenter,
final String password, final ExperimenterGroup defaultGroup,
final ExperimenterGroup... otherGroups) {
adminOrPiOfNonUserGroups(defaultGroup, otherGroups);
long uid = roleProvider.createExperimenter(experimenter,
defaultGroup, otherGroups);
// If this method passes, then the Experimenter is valid.
changeUserPassword(experimenter.getOmeName(), password);
getBeanHelper().getLogger().info(
"Created user with password: " + experimenter.getOmeName());
return uid;
}
@RolesAllowed("system")
@Transactional(readOnly = false)
public long createGroup(ExperimenterGroup group) {
long gid = roleProvider.createGroup(group);
getBeanHelper().getLogger().info("Created group: " + group.getName());
return gid;
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void addGroups(final Experimenter user,
final ExperimenterGroup... groups) {
if (groups == null || groups.length == 0) {
throw new ValidationException("Nothing to do.");
}
assertManaged(user);
for (ExperimenterGroup group : groups) {
assertManaged(group);
}
adminOrPiOfGroups(null, groups);
roleProvider.addGroups(user, groups);
getBeanHelper().getLogger().info(
String.format("Added user %s to groups %s", userProxy(
user.getId()).getOmeName(), Arrays.asList(groups)));
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void removeGroups(final Experimenter user, final ExperimenterGroup... groups) {
if (user == null) {
return;
}
if (groups == null) {
return;
}
adminOrPiOfGroups(null, groups);
final Roles roles = getSecurityRoles();
final boolean removeSystemOrUser =
Iterators.any(Iterators.forArray(groups),Predicates.or(roles.IS_SYSTEM_GROUP, roles.IS_USER_GROUP));
if (removeSystemOrUser && roles.isRootUser(user)) {
throw new ValidationException("experimenter '" + roles.getRootName() + "' may not be removed from the '" +
roles.getSystemGroupName() + "' or '" + roles.getUserGroupName() + "' group");
}
final EventContext eventContext = getEventContext();
final boolean userOperatingOnThemself = eventContext.getCurrentUserId().equals(user.getId());
if (removeSystemOrUser && userOperatingOnThemself) {
throw new ValidationException("experimenters may not remove themselves from the '" +
roles.getSystemGroupName() + "' or '" + roles.getUserGroupName() + "' group");
}
/* The properly loaded user object is needed for collecting the group-experimenter map. */
final Experimenter loadedUser = userProxy(user.getId());
final Set<Long> resultingGroupIds = new HashSet<Long>();
for (final GroupExperimenterMap map : loadedUser.<GroupExperimenterMap>collectGroupExperimenterMap(null)) {
resultingGroupIds.add(map.parent().getId());
}
for (final ExperimenterGroup group : groups) {
resultingGroupIds.remove(group.getId());
}
if (resultingGroupIds.isEmpty()) {
throw new ValidationException("experimenter must remain a member of some group");
} else if (resultingGroupIds.equals(Collections.singleton(roles.getUserGroupId()))) {
throw new ValidationException("experimenter cannot be a member of only the '" +
roles.getUserGroupName() + "' group, a different default group is also required");
}
roleProvider.removeGroups(user, groups);
getBeanHelper().getLogger().info(
String.format("Removed user %s from groups %s", user, groups));
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void setDefaultGroup(Experimenter user, ExperimenterGroup group) {
if (user == null) {
return;
}
if (group == null) {
return;
}
if (group.getId() == null) {
throw new ApiUsageException("Group argument to setDefaultGroup "
+ "must be managed (i.e. have an id)");
}
EventContext ec = getSecuritySystem().getEventContext();
if (!ec.isCurrentUserAdmin()
&& !ec.getCurrentUserId().equals(user.getId())) {
throw new SecurityViolation("User " + user.getId()
+ " can only set own default group.");
}
Roles roles = getSecuritySystem().getSecurityRoles();
if (Long.valueOf(roles.getUserGroupId()).equals(group.getId())) {
throw new ApiUsageException("Cannot set default group to: "
+ roles.getUserGroupName());
}
roleProvider.setDefaultGroup(user, group);
getBeanHelper().getLogger().info(
String.format("Changing default group for %s to %s", user, group));
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void setGroupOwner(final ExperimenterGroup group, final Experimenter owner) {
adminOrPiOfGroup(group);
toggleGroupOwner(group, owner, Boolean.TRUE);
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void unsetGroupOwner(final ExperimenterGroup group, final Experimenter owner) {
adminOrPiOfGroup(group);
toggleGroupOwner(group, owner, Boolean.FALSE);
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void addGroupOwners(final ExperimenterGroup group, final Experimenter... owner) {
adminOrPiOfGroup(group);
for (Experimenter o : owner) {
toggleGroupOwner(group, o, Boolean.TRUE);
}
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void removeGroupOwners(final ExperimenterGroup group, final Experimenter... owner) {
adminOrPiOfGroup(group);
for (Experimenter o : owner) {
toggleGroupOwner(group, o, Boolean.FALSE);
}
}
private void toggleGroupOwner(ExperimenterGroup group, Experimenter owner,
Boolean value) {
if (owner == null) {
return;
}
if (group == null) {
return;
}
if (group.getId() == null) {
throw new ApiUsageException("Group argument to setGroupOwner "
+ "must be managed (i.e. have an id)");
}
// TODO add an @Managed annotation
if (owner.getId() == null) {
throw new ApiUsageException("Owner argument to setGroupOwner "
+ "must be managed (i.e. have an id)");
}
GroupExperimenterMap m = findLink(group, owner);
if (m == null) {
addGroups(owner, group);
m = findLink(group, owner);
}
m.setOwner(value);
getSecuritySystem().runAsAdmin(new AdminAction() {
public void runAsAdmin() {
iUpdate.flush();
}
});
getBeanHelper().getLogger().info(
String.format("%s user %s as owner of group %s",
value ? "Setting" : "Unsetting",
owner.getId(), group.getId()));
}
private GroupExperimenterMap findLink(ExperimenterGroup group,
Experimenter owner) {
GroupExperimenterMap m = iQuery.findByQuery(
"select m from GroupExperimenterMap m " +
"where m.parent.id = :pid " +
"and m.child.id = :cid",
new Parameters()
.addLong("pid", group.getId())
.addLong("cid", owner.getId()));
return m;
}
@RolesAllowed("user")
public ExperimenterGroup getDefaultGroup(@NotNull
long experimenterId) {
ExperimenterGroup g = iQuery.findByQuery(
"select g from ExperimenterGroup g, Experimenter e "
+ "join e.groupExperimenterMap m "
+ "where e.id = :id and m.parent = g.id "
+ "and g.name != :userGroup and index(m) = 0",
new Parameters().addId(experimenterId).addString("userGroup",
sec.getSecurityRoles().getUserGroupName()));
if (g == null) {
throw new ValidationException("The user " + experimenterId
+ " has no default group set.");
}
return g;
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void deleteExperimenter(Experimenter user) {
adminOrPiOfUser(user);
final Experimenter e = userProxy(user.getId());
int count = sql.removePassword(e.getId());
if (count == 0) {
getBeanHelper().getLogger().info(
"No password found for user " + e.getOmeName()
+ ". Cannot delete.");
}
getSecuritySystem().runAsAdmin(new AdminAction() {
public void runAsAdmin() {
iUpdate.deleteObject(e);
}
});
getBeanHelper().getLogger().info("Deleted user: " + e.getOmeName());
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void deleteGroup(ExperimenterGroup group) {
adminOrPiOfGroup(group);
final ExperimenterGroup g = groupProxy(group.getId());
getSecuritySystem().runAsAdmin(new AdminAction() {
public void runAsAdmin() {
iUpdate.deleteObject(g);
}
});
getBeanHelper().getLogger().info("Deleted group: " + g.getName());
}
// ~ chown / chgrp / chmod
// =========================================================================
@RolesAllowed("user")
@Transactional(readOnly = false)
public void changeOwner(IObject iObject, String omeName) {
// should take an Owner
IObject copy = iQuery.get(iObject.getClass(), iObject.getId());
Experimenter owner = userProxy(omeName);
copy.getDetails().setOwner(owner);
iUpdate.saveObject(copy);
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public void changeGroup(IObject iObject, String groupName) {
// should take a group
final IObject copy = iQuery.get(iObject.getClass(), iObject.getId());
final ExperimenterGroup group = groupProxy(groupName);
// Check object
final EventContext ec = getSecuritySystem().getEventContext();
if (!ec.getCurrentUserId().equals(copy.getDetails().getOwner().getId())
&& !ec.isCurrentUserAdmin()) {
throw new SecurityViolation("Cannot change group for:" + iObject);
}
// Check target group
if (getSecurityRoles().getUserGroupId() == group.getId().longValue()) {
throw new SecurityViolation("Use moveToCommonSpace for moving to user group");
} else if (!ec.getMemberOfGroupsList().contains(group.getId())) {
throw new SecurityViolation("Can't change to group; " +
"not a member of " + group.getId());
}
// make change.
copy.getDetails().setGroup(group);
secureFlush(copy);
if (copy instanceof Image) {
Image img = (Image) copy;
Iterator<Pixels> it = img.iteratePixels();
while (it.hasNext()) {
Pixels pix = it.next();
pix.getDetails().setGroup(group);
secureFlush(pix);
}
}
// Detect group mismatch
// What would need to be changed?
@SuppressWarnings("unchecked")
Map<String, Long> locks = getLockingIds(
(Class<IObject>) copy.getClass(), copy.getId(), group.getId());
Long total = locks.get("*");
if (total != null && total > 0) {
throw new SecurityViolation("Locks: " + locks);
}
}
private void secureFlush(final IObject copy) {
getSecuritySystem().doAction(new SecureAction(){
public <T extends IObject> T updateObject(T... objs) {
iUpdate.flush();
return null;
}}, copy);
}
/**
* the implementation of this method is somewhat tricky in that
* {@link Permissions} changes must be allowed even when other updates are
* not. Therefore, we must manually check if the object belongs to this user
* or is admin (before the call to
* {@link SecuritySystem#runAsAdmin(AdminAction)}
*
* This logic is duplicated in
* {@link BasicSecuritySystem#checkManagedDetails(IObject, ome.model.internal.Details)}.
*
* As of OMERO 4.2 (ticket:1434), this method has special handling for an
* instance of {@link ExperimenterGroup} and <em>limited</em> capabilities
* for changing any other object type (ticket:1776).
*
* For groups, the permission changes will be propagated to all the
* contained objects. For other objects, changes may not override group
* settings.
*
* @see IAdmin#changePermissions(IObject, Permissions)
* @see <a
* href="http://trac.openmicroscopy.org.uk/ome/ticket/293">ticket:293</a>
* @see <a
* href="http://trac.openmicroscopy.org.uk/ome/ticket/1434">ticket:1434</a>
*/
@RolesAllowed("user")
@Transactional(readOnly = false)
public void changePermissions(final IObject iObject, final Permissions perms) {
final Session s = osf.getSession();
final String p = perms.toString();
final Object[] checks = chmod.getChecks(iObject, p);
chmod.chmod(iObject, p);
for (Object check : checks) {
chmod.check(iObject, check);
}
}
@RolesAllowed("user")
@SuppressWarnings("unchecked")
@Transactional(readOnly = false)
public void moveToCommonSpace(IObject... iObjects) {
// ticket:1794
for (IObject object : iObjects) {
if (object != null) {
Long id = object.getId();
Class<IObject> c = (Class<IObject>) Utils.trueClass(object.getClass());
IObject o = iQuery.get(c, id);
ExperimenterGroup g = o.getDetails().getGroup();
if (!g.getId().equals(getSecurityRoles().getUserGroupId())) {
adminOrPiOfGroup(g);
internalMoveToCommonSpace(o);
}
}
}
}
/**
* Helpers which unconditionally moves the object to the common space. This
* can be used by other methods like {@link #uploadMyUserPhoto(String, String, byte[])}
*
* @param obj a model object, linked to the current session; never {@code null}
*/
public void internalMoveToCommonSpace(IObject obj) {
/* Can this next line be removed? - ajp */
final Session session = osf.getSession();
obj.getDetails().setGroup(
groupProxy(getSecurityRoles().getUserGroupId()));
secureFlush(obj);
getBeanHelper().getLogger().info("Moved object to common space: " + obj);
}
@SuppressWarnings("unchecked")
public Map<String, Long> getLockingIds(IObject object) {
return getLockingIds((Class<IObject>) object.getClass(), object.getId(), null);
}
public Map<String, Long> getLockingIds(final Class<IObject> type,
final long id, final Long groupId) {
String groupClause = "";
if (groupId != null) {
groupClause = "and details.group.id <> " + groupId;
}
// since it's a managed entity it's class.getName() might
// contain
// some byte-code generation string
final Class<? extends IObject> klass = Utils.trueClass(type);
// the values that could possibly link to this instance.
final String[][] checks = metadata.getLockChecks(klass);
return this.metadata.countLocks(osf.getSession(), id, checks, groupClause);
}
// ~ Passwords
// =========================================================================
@PermitAll
@Transactional(readOnly = false)
public void reportForgottenPassword(final String name, final String email)
throws AuthenticationException {
if (name == null) {
throw new IllegalArgumentException("Unexpected null username.");
}
if (email == null) {
throw new IllegalArgumentException("Unexpected null e-mail.");
}
sec.runAsAdmin(new AdminAction() {
public void runAsAdmin() {
Experimenter e = iQuery.findByString(Experimenter.class,
"omeName", name);
if (e == null) {
throw new AuthenticationException("Unknown user.");
} else if (e.getEmail() == null) {
throw new AuthenticationException(
"User has no email address.");
} else if (!e.getEmail().equals(email)) {
throw new AuthenticationException(
"Email address does not match.");
} else if (passwordUtil.getDnById(e.getId())) {
throw new AuthenticationException(
"User is authenticated by LDAP server you cannot reset this password.");
} else {
String passwd = passwordUtil.generateRandomPasswd();
sendEmail(e, passwd);
// changeUserPassword checks adminOrPiOfUser
// Skipping that. See #7327
_changePassword(e.getOmeName(), passwd);
}
}
});
}
private boolean sendEmail(Experimenter e, String newPassword) {
// Create a thread safe "copy" of the template message and customize it
SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage);
msg.setSubject("OMERO - Reset password");
msg.setTo(e.getEmail());
msg.setText("Dear " + e.getFirstName() + " " + e.getLastName() + " ("
+ e.getOmeName() + ")" + " your new password is: "
+ newPassword);
try {
this.mailSender.send(msg);
} catch (Exception ex) {
throw new RuntimeException(
"Exception: "
+ ex.getMessage()
+ ". "
+ "Password was not changed because email could not be sent "
+ "to user:"
+ e.getOmeName()
+ ". Please turn on the debug "
+ "mode in omero.properties by the: omero.mail.debug=true");
}
return true;
}
// ~ Password access
// =========================================================================
@PermitAll
@Transactional(readOnly = false)
public void changeExpiredCredentials(String name, String oldCred,
String newCred) throws AuthenticationException {
throw new UnsupportedOperationException();
}
@RolesAllowed({"user", "HasPassword"})
@Transactional(readOnly = false)
public void changePassword(String newPassword) {
String user = getSecuritySystem().getEventContext().getCurrentUserName();
_changePassword(user, newPassword);
}
@RolesAllowed({"user"})
@Transactional(readOnly = false)
public void changePasswordWithOldPassword(String oldPassword, String newPassword) {
String user = getSecuritySystem().getEventContext().getCurrentUserName();
if (!checkPassword(user, oldPassword, false)) {
throw new SecurityViolation("Old password is invalid");
}
_changePassword(user, newPassword);
}
@RolesAllowed({"user", "HasPassword"})
@Transactional(readOnly = false)
public void changeUserPassword(final String user, final String newPassword) {
adminOrPiOfUser(userProxy(user));
_changePassword(user, newPassword);
}
private void _changePassword(String user, String newPassword) {
try {
passwordProvider.changePassword(user, newPassword);
getBeanHelper().getLogger().info("Changed password for user: " + user);
} catch (PasswordChangeException e) {
throw new SecurityViolation("PasswordChangeException: "
+ e.getMessage());
}
}
/**
* If ldap plugin turned, creates Ldap accounts and authentication by LDAP
* available.
*/
public boolean checkPassword(String name, String password, boolean readOnly) {
Boolean result = passwordProvider.checkPassword(name, password, readOnly);
if (result == null) {
getBeanHelper().getLogger().warn("Password provider returned null: "
+ passwordProvider);
return false;
} else {
return result.booleanValue();
}
}
// ~ Security context
// =========================================================================
@PermitAll
public Roles getSecurityRoles() {
return getSecuritySystem().getSecurityRoles();
}
@PermitAll
public EventContext getEventContext() {
return new SimpleEventContext(getSecuritySystem().getEventContext(true));
}
public EventContext getEventContextQuiet() {
return new SimpleEventContext(getSecuritySystem().getEventContext(false));
}
// ~ Helpers
// =========================================================================
protected void assertManaged(IObject o) {
if (o == null) {
throw new ApiUsageException("Argument may not be null.");
} else if (o.getId() == null) {
throw new ApiUsageException(o.getClass().getName() + " has no id.");
}
}
// ~ Queries for pulling full experimenter/experimenter group graphs
// =========================================================================
static abstract class BaseQ<T> extends Query<T> {
static Definitions defs = new Definitions(new QueryParameterDef("name",
String.class, true), new QueryParameterDef("id", Long.class,
true));
public BaseQ(Parameters params) {
super(defs, new Parameters().unique().addAll(params));
}
}
static class UserQ extends BaseQ<Experimenter> {
public UserQ(Parameters params) {
super(params);
}
@Override
protected void buildQuery(Session session) throws HibernateException,
SQLException {
Criteria c = session.createCriteria(Experimenter.class);
Criteria m = c.createCriteria("groupExperimenterMap",
Query.LEFT_JOIN);
Criteria g = m.createCriteria("parent", Query.LEFT_JOIN);
/* Should these calls be using g not c? - ajp */
if (value("name") != null) {
c.add(Restrictions.eq("omeName", value("name")));
}
else if (value("id") != null) {
c.add(Restrictions.eq("id", value("id")));
}
else {
throw new InternalException(
"Name and id are both null for user query.");
}
setCriteria(c);
}
}
static class GroupQ extends BaseQ<ExperimenterGroup> {
public GroupQ(Parameters params) {
super(params);
}
@Override
protected void buildQuery(Session session) throws HibernateException,
SQLException {
QueryBuilder qb = new QueryBuilder();
qb.select("g");
qb.from("ExperimenterGroup", "g");
qb.join("g.groupExperimenterMap","m",true, true);
qb.join("m.child","user", true,true);
qb.where();
Object name = value("name");
Object id = value("id");
if (name != null) {
qb.and("g.name = :name");
qb.param("name", name);
}
else if (id != null) {
qb.and("g.id = :id");
qb.param("id",id);
}
else {
throw new InternalException(
"Name and id are both null for group query.");
}
setQuery(qb.query(session));
}
}
// ~ group permissions
// =========================================================================
private Set<String> classes() {
return getExtendedMetadata().getClasses();
}
private String table(String className) {
try {
Class<?> c = Class.forName(className);
Table t = null;
if (IGlobal.class.isAssignableFrom(c)) {
return null;
} else if (c.getAnnotation(Table.class) == null) {
return null;
} else if (c.getAnnotation(PrimaryKeyJoinColumn.class) != null) {
return null;
} else {
t = c.getAnnotation(Table.class);
}
return t.name();
} catch (Exception e) {
throw new InternalException("Miss configuration. Should never happen.");
}
}
// ticket:1781 - group-owner admin privileges
// =========================================================================
/**
* Saves an object as admin.
*
* Due to the disabling of the MergeEventListener, it is necessary to
* jump through several hoops to get non-admin saving of system types
* to work properly.
*/
private void reallySafeSave(final IObject obj) {
final Session session = osf.getSession();
sec.doAction(new SecureMerge(session), obj);
sec.runAsAdmin(new AdminAction(){
public void runAsAdmin() {
session.flush();
}});
}
private boolean isAdmin() {
return getEventContext().isCurrentUserAdmin();
}
private boolean isPiOf(Experimenter user) {
if (user == null) {
return true;
}
List<Long> userIn = getMemberOfGroupIds(user);
List<Long> piOf = getEventContext().getLeaderOfGroupsList();
for (Long id : piOf) {
if (userIn.contains(id)) {
return true;
}
}
return false;
}
private boolean isPiOf(ExperimenterGroup group) {
if (group == null) {
return true;
}
EventContext ec = getEventContext();
List<Long> piOf = ec.getLeaderOfGroupsList();
return piOf.contains(group.getId());
}
private void throwNonAdminOrPi() {
String msg = "Current user is neither admin nor group-leader for " +
"the given user(s)/group(s)";
throw new SecurityViolation(msg);
}
private void adminOrPiOfUser(Experimenter user) {
if (!isAdmin() && ! isPiOf(user)) {
throwNonAdminOrPi();
}
}
private void adminOrPiOfGroup(ExperimenterGroup group) {
if (!isAdmin() && ! isPiOf(group)) {
throwNonAdminOrPi();
}
}
private void adminOrPiOfGroups(ExperimenterGroup group, ExperimenterGroup ... groups) {
if (!isAdmin()) {
if (!isPiOf(group)) {
throwNonAdminOrPi();
} else {
for (ExperimenterGroup g : groups) {
if (!isPiOf(g)) {
throwNonAdminOrPi();
}
}
}
}
}
/**
* Filters out the "user" group since it is unlikely that anyone will be an
* owner of it.
*
* @param defaultGroup
* @param otherGroups
*/
private void adminOrPiOfNonUserGroups(ExperimenterGroup defaultGroup,
ExperimenterGroup... otherGroups) {
Set<ExperimenterGroup> nonUserGroupGroups = new HashSet<ExperimenterGroup>();
for (ExperimenterGroup eg : otherGroups) {
if (!eg.getId().equals(getSecurityRoles().getUserGroupId())) {
nonUserGroupGroups.add(eg);
}
}
adminOrPiOfGroups(defaultGroup,
nonUserGroupGroups.toArray(new ExperimenterGroup[0]));
}
}