/*
* $Id$
*
* Copyright 2006 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.security.basic;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.EntityMode;
import org.hibernate.Interceptor;
import org.hibernate.Transaction;
import org.hibernate.collection.PersistentList;
import org.hibernate.engine.CollectionEntry;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.type.ComponentType;
import org.hibernate.type.Type;
import org.springframework.util.Assert;
import ome.conditions.ApiUsageException;
import ome.conditions.GroupSecurityViolation;
import ome.conditions.InternalException;
import ome.conditions.OptimisticLockException;
import ome.conditions.PermissionMismatchGroupSecurityViolation;
import ome.conditions.ReadOnlyGroupSecurityViolation;
import ome.conditions.SecurityViolation;
import ome.conditions.ValidationException;
import ome.model.IAnnotationLink;
import ome.model.IMutable;
import ome.model.IObject;
import ome.model.core.Image;
import ome.model.core.Pixels;
import ome.model.display.RenderingDef;
import ome.model.display.Thumbnail;
import ome.model.internal.Details;
import ome.model.internal.NamedValue;
import ome.model.internal.Permissions;
import ome.model.internal.Permissions.Right;
import ome.model.internal.Permissions.Role;
import ome.model.meta.Experimenter;
import ome.model.meta.ExperimenterGroup;
import ome.model.meta.ExternalInfo;
import ome.model.roi.Roi;
import ome.security.SecuritySystem;
import ome.security.SystemTypes;
import ome.services.sessions.stats.SessionStats;
import ome.system.EventContext;
import ome.system.Roles;
import ome.tools.hibernate.ExtendedMetadata;
import ome.tools.hibernate.HibernateUtils;
/**
* implements {@link org.hibernate.Interceptor} for controlling various aspects
* of the Hibernate runtime. Where no special requirements exist, methods
* delegate to {@link EmptyInterceptor}
*
* Current responsibilities include the proper (re-)setting of {@link Details}
*
* @author Josh Moore, josh.moore at gmx.de
* @version $Revision$, $Date$
* @see EmptyInterceptor
* @see Interceptor
* @since 3.0-M3
*/
public class OmeroInterceptor implements Interceptor {
static volatile String last = null;
static volatile int count = 1;
private static Logger log = LoggerFactory.getLogger(OmeroInterceptor.class);
private final Interceptor EMPTY = EmptyInterceptor.INSTANCE;
private final SystemTypes sysTypes;
private final CurrentDetails currentUser;
private final TokenHolder tokenHolder;
private final ExtendedMetadata em;
private final SessionStats stats;
private final Roles roles;
public OmeroInterceptor(Roles roles, SystemTypes sysTypes, ExtendedMetadata em,
CurrentDetails cd, TokenHolder tokenHolder, SessionStats stats) {
Assert.notNull(tokenHolder);
Assert.notNull(sysTypes);
// Assert.notNull(em); Permitting null for testing
// Assert.notNull(cd); Permitting null for testing
Assert.notNull(stats);
Assert.notNull(roles);
this.tokenHolder = tokenHolder;
this.currentUser = cd;
this.sysTypes = sysTypes;
this.stats = stats;
this.roles = roles;
this.em = em;
}
/**
* default logic, but we may want to use them eventually for
* dependency-injection.
*/
public Object instantiate(String entityName, EntityMode entityMode,
Serializable id) throws CallbackException {
debug("Intercepted instantiate.");
return EMPTY.instantiate(entityName, entityMode, id);
}
/** default logic. */
public boolean onLoad(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) throws CallbackException {
debug("Intercepted load.");
this.stats.loadedObjects(1);
return EMPTY.onLoad(entity, id, state, propertyNames, types);
}
/** default logic */
public int[] findDirty(Object entity, Serializable id,
Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) {
debug("Intercepted dirty check.");
return EMPTY.findDirty(entity, id, currentState, previousState,
propertyNames, types);
}
/**
* callsback to {@link BasicSecuritySystem#newTransientDetails(IObject)} for
* properly setting {@link IObject#getDetails() Details}
*/
public boolean onSave(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
debug("Intercepted save.");
this.stats.updatedObjects(1);
if (entity instanceof IObject) {
IObject iobj = (IObject) entity;
int idx = HibernateUtils.detailsIndex(propertyNames);
Details d = evaluateLinkages(iobj);
// Get a new details based on the current context
d = newTransientDetails(iobj, d);
state[idx] = d;
}
return true; // transferDetails ALWAYS edits the new entity.
}
/**
* calls back to
* {@link BasicSecuritySystem#checkManagedDetails(IObject, Details)} for
* properly setting {@link IObject#getDetails() Details}.
*/
public boolean onFlushDirty(Object entity, Serializable id,
Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) {
debug("Intercepted update.");
this.stats.updatedObjects(1);
boolean altered = false;
if (entity instanceof IObject) {
IObject iobj = (IObject) entity;
int idx = HibernateUtils.detailsIndex(propertyNames);
Details newDetails = evaluateLinkages(iobj);
altered |= resetDetails(iobj, currentState, previousState, idx,
newDetails);
}
return altered;
}
/** default logic */
public void onDelete(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) throws CallbackException {
debug("Intercepted delete.");
EMPTY.onDelete(entity, id, state, propertyNames, types);
}
// ~ Collections (of interest)
// =========================================================================
public void onCollectionRecreate(Object collection, Serializable key)
throws CallbackException {
debug("Intercepted collection recreate.");
}
public void onCollectionRemove(Object collection, Serializable key)
throws CallbackException {
debug("Intercepted collection remove.");
}
public void onCollectionUpdate(Object collection, Serializable key)
throws CallbackException {
debug("Intercepted collection update.");
if (collection instanceof PersistentList) {
PersistentList list = (PersistentList) collection;
PersistenceContext context = list.getSession().getPersistenceContext();
CollectionEntry entry = context.getCollectionEntry(list);
if (!(entry.getCurrentPersister().getElementType()
instanceof ComponentType)) {
// We assume that any modification of any
// CollectionOfElements like NamedValue-lists
// should be subject to the security of the
// parent. If this *isn't* such a collection,
// then exit.
return;
}
List snapshot = (List) entry.getSnapshot();
Object owner = list.getOwner();
if (list.size() == 0 && snapshot.size() == 0) {
// Nothing here, so we don't care
return;
}
boolean equals = true;
if (list.size() == snapshot.size()) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i) == null) {
if (snapshot.get(i) == null) {
continue;
}
} else { // first element is not null
Object lhs = list.get(i);
if (lhs instanceof NamedValue) {
if (((NamedValue) lhs).equals(snapshot.get(i))) {
continue;
}
}
}
// If we reach this point, there's a non-match and the
// bumping of the version number should proceed.
equals = false;
break;
}
// The two lists were found to be equal, do not bump the
// version number;
if (equals) {
return;
}
}
// https://hibernate.atlassian.net/browse/HHH-4897 workaround:
// ----------------------------------------------------------
// Assuming we get here, we bump the version number for the
// object which will hopefully cause the regular security
// checks to fail.
try {
IObject iobj = (IObject) owner;
Method getter = iobj.getClass().getMethod("getVersion");
Integer oldVersion = (Integer) getter.invoke(iobj);
Integer newVersion = oldVersion == null ? 1 : oldVersion + 1;
Method setter = iobj.getClass().getMethod("setVersion", Integer.class);
setter.invoke(iobj, newVersion);
log.info("Updating version for collections from {} to {}",
oldVersion, newVersion);
} catch (Exception e) {
InternalException ie = new InternalException("Failed to set version");
ie.initCause(e);
throw ie;
}
}
}
// ~ Flush (currently unclear semantics)
// =========================================================================
public void preFlush(Iterator entities) throws CallbackException {
debug("Intercepted preFlush.");
EMPTY.preFlush(entities);
}
public void postFlush(Iterator entities) throws CallbackException {
debug("Intercepted postFlush.");
EMPTY.postFlush(entities);
}
// ~ Serialization
// =========================================================================
private static final long serialVersionUID = 7616611615023614920L;
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFoundException {
s.defaultReadObject();
}
// ~ Unused interface methods
// =========================================================================
public void afterTransactionBegin(Transaction tx) {
}
public void afterTransactionCompletion(Transaction tx) {
}
public void beforeTransactionCompletion(Transaction tx) {
}
public Object getEntity(String entityName, Serializable id)
throws CallbackException {
return EMPTY.getEntity(entityName, id);
}
public String getEntityName(Object object) throws CallbackException {
return EMPTY.getEntityName(object);
}
public Boolean isTransient(Object entity) {
return EMPTY.isTransient(entity);
}
public String onPrepareStatement(String sql) {
// start
if (!log.isDebugEnabled()) {
return sql;
}
// from
StringBuilder sb = new StringBuilder();
String[] first = sql.split("\\sfrom\\s");
sb.append(first[0]);
for (int i = 1; i < first.length; i++) {
sb.append("\n from ");
sb.append(first[i]);
}
// where
String[] second = sb.toString().split("\\swhere\\s");
sb = new StringBuilder();
sb.append(second[0]);
for (int j = 1; j < second.length; j++) {
sb.append("\n where ");
sb.append(second[j]);
}
return sb.toString();
}
// ~ Helpers
// =========================================================================
/**
* asks {@link BasicSecuritySystem} to create a new managed {@link Details}
* based on the previous state of this entity. If the previous state is null
* (see ticket:3929) then throw an exception.
*
* @param entity
* IObject to be updated
* @param currentState
* the possibly changed field data for this entity
* @param previousState
* the field data as seen in the db
* @param idx
* the index of Details in the state arrays.
*/
protected boolean resetDetails(IObject entity, Object[] currentState,
Object[] previousState, int idx, Details newDetails) {
if (previousState == null) {
log.warn(String.format("Null previousState for %s(loaded=%s). Details=%s",
entity, entity.isLoaded(), currentState[idx]));
throw new InternalException("Previous state is null. Possibly caused by evict. See ticket:3929");
}
final Details previous = (Details) previousState[idx];
final Details result = checkManagedDetails(entity, previous, newDetails);
if (previous != result) {
currentState[idx] = result;
return true;
}
return false;
}
protected void log(String msg) {
if (msg.equals(last)) {
count++;
}
else if (log.isDebugEnabled()) {
String times = " ( " + count + " times )";
log.debug(msg + times);
last = msg;
count = 1;
}
}
private void debug(String msg) {
if (log.isDebugEnabled()) {
log(msg);
}
}
// Methods moved from BasicSecuritySystem
// =========================================================================
/**
* Checks the details of the objects which the given object links to in
* order to guarantee that linkages are valid. In the case of a non-specific
* UID or GID, then the Details object returned by this method can be used
* as the basis for unknown user/group.
*
* This method is called during
* {@link OmeroInterceptor#onSave(Object, java.io.Serializable, Object[], String[], org.hibernate.type.Type[])
* save} and
* {@link OmeroInterceptor#onFlushDirty(Object, java.io.Serializable, Object[], Object[], String[], org.hibernate.type.Type[])
* update} since this is the only time that new entity references can be
* created.
*
* @param changedObject
* new or updated entity which may reference other entities which
* then require locking. Nulls are tolerated but do nothing.
*/
public Details evaluateLinkages(IObject changedObject) {
if (changedObject == null) {
return null;
}
final Class<?> changedClass = changedObject.getClass();
final Details rv = changedObject.getDetails().newInstance();
// Valid to link to any system type or object in system group
// See #1784 and #8571
if (sysTypes.isSystemType(changedObject.getClass()) ||
sysTypes.isInSystemGroup(changedObject.getDetails())) {
return rv;
}
final Long currentGroupId = currentUser.getGroup().getId();
final boolean currentGroupNegative = currentGroupId < 0;
final IObject[] candidates = em.getLockCandidates(changedObject);
for (IObject linkedObject : candidates) {
// If the linked object is a system type or object in the system
// group, or further in the shared user group, then we permit
// the linkage.
if (sysTypes.isSystemType(linkedObject.getClass()) ||
sysTypes.isInSystemGroup(linkedObject.getDetails()) ||
sysTypes.isInUserGroup(linkedObject.getDetails())) {
continue;
}
final Class<?> linkedClass = linkedObject.getClass();
final Details linkedDetails = linkedObject.getDetails();
if (linkedDetails == null) {
// ticket:2575. Previously, the details of the candidates
// were never null. the addition of the reagent linkages
// *somehow* led to NPEs here. for the moment, we're assuming
// if null, then the object can't be mis-linked. (i.e. it's
// probably new)
continue;
}
// If this is -1 situation, then we pass back out the
// group for the linked object. In the case of new transient
// objects, this will be set as the new group.
if (currentGroupNegative) {
if (rv.getGroup() == null) {
// If this is the first linked object then we assume
// that this object should use this value.
rv.setGroup(linkedDetails.getGroup());
} else {
throwIfGroupsDontMatch(rv.getGroup(), changedObject,
linkedDetails.getGroup(), linkedObject);
}
} else {
throwIfGroupsDontMatch(currentUser.getGroup(),
changedObject, linkedDetails.getGroup(),
linkedObject);
}
// Rather than as in <=4.1 in which objects were scheduled
// for locking which prevented later actions, now we check
// whether or not we're graph critical and if so, and if
// the objects do not belong to the current user, then we abort.
final Experimenter linkedOwner = linkedObject.getDetails().getOwner();
final ExperimenterGroup linkedGroup = linkedObject.getDetails().getGroup();
if (linkedOwner == null || linkedGroup == null) {
continue; // Only for system types which should be filtered
}
final Long linkedUid = linkedOwner.getId();
final Long linkedGid = linkedGroup.getId();
if (linkedUid == null || linkedGid == null) {
continue; // Highly unlikely.
}
final EventContext ec = currentUser.getCurrentEventContext();
final boolean isOwner = ec.getCurrentUserId().equals(linkedUid);
final boolean isOwnerOrSupervisor = currentUser.isOwnerOrSupervisor(linkedObject);
final boolean isSupervisor = (!isOwner) && isOwnerOrSupervisor;
final boolean isMember = ec.getMemberOfGroupsList().contains(linkedGid);
final Permissions p = currentUser.getCurrentEventContext()
.getCurrentGroupPermissions();
if (!isOwner && currentUser.isGraphCritical(rv)) {
// ticket:1769
String gname = currentUser.getGroup().getName();
String oname = currentUser.getOwner().getOmeName();
Long changedUid = null;
if (changedObject.getDetails().getOwner() != null) {
changedUid = changedObject.getDetails().getOwner().getId();
}
// ticket:8979 - allow admin users to specify the owner
// for such a graph critical situation since if the new
// object also belongs to the user of the linked obj, then
// visibility will be guaranteed.
if (changedUid == null || !changedUid.equals(linkedUid)) {
throw new ReadOnlyGroupSecurityViolation(String.format(
"Cannot link to %s\n" +
"Current user (%s) is an admin or the owner of\n" +
"the private group (%s=%s). It is not allowed to\n" +
"link to users' data.", linkedObject, oname, gname, p));
}
}
final Right neededRight = neededRight(changedClass, linkedClass);
final Role neededRole = neededRole(isOwner, isMember);
if (!isSupervisor) {
throwIfNotGranted(p, neededRole, neededRight, linkedObject);
}
}
return rv;
}
private Role neededRole(boolean isOwner, boolean isMember) {
if (isOwner) {
return Role.USER;
} else if (isMember) {
return Role.GROUP;
} else {
return Role.WORLD;
}
}
/**
* The default right need for a linkage is {@link Right#WRITE}.
* If however, this is only an annotation or only a viewing,
* then less permission is needed.
* @param changedClass the changed class
* @param linkedClass the linked class
* @return the right that is needed
*/
protected Right neededRight(final Class<?> changedClass,
final Class<?> linkedClass) {
Right neededRight = Right.WRITE;
if (RenderingDef.class.isAssignableFrom(linkedClass) ||
RenderingDef.class.isAssignableFrom(changedClass) ||
(Pixels.class.isAssignableFrom(linkedClass) &&
Thumbnail.class.isAssignableFrom(changedClass))) {
neededRight = Right.READ;
} else if (IAnnotationLink.class.isAssignableFrom(changedClass) ||
(Roi.class.isAssignableFrom(changedClass) &&
Image.class.isAssignableFrom(linkedClass))) {
neededRight = Right.ANNOTATE;
}
return neededRight;
}
// TODO is this natural? perhaps permissions don't belong in details
// details are the only thing that users can change the rest is
// read only...
/**
* @see SecuritySystem#newTransientDetails(IObject)
*/
public Details newTransientDetails(IObject obj) {
if (obj == null) {
throw new ApiUsageException("Argument cannot be null.");
}
final Details newDetails = obj.getDetails().newInstance();
return newTransientDetails(obj, newDetails);
}
/**
* Like {@link #newTransientDetails(IObject)} but allows passing in a
* newDetails object with possibly preset values.
* @see #evaluateLinkages(IObject)
*/
protected Details newTransientDetails(final IObject obj,
final Details newDetails) {
if (tokenHolder.hasPrivilegedToken(obj)) {
return obj.getDetails(); // EARLY EXIT
}
final Details source = obj.getDetails();
final BasicEventContext bec = currentUser.current();
// Allow values to be passed in.
newDetails.copyWhereUnset(null, currentUser.createDetails());
// OWNER
// users *aren't* allowed to set the owner of an item.
if (source.getOwner() != null
&& !newDetails.getOwner().getId().equals(
source.getOwner().getId())) {
// but this is root
if (bec.isCurrentUserAdmin()) {
newDetails.setOwner(source.getOwner());
} else {
throw new SecurityViolation(String.format(
"You are not authorized to set the Experimenter"
+ " for %s to %s", obj, source.getOwner()));
}
}
// GROUP
// users are only allowed to set to the current group
// if, however, the current group is -1 (all groups)
// and the user is a member of that group or an admin,
// then permit the setting with the assumption that the
// later link check will catch any inappropriate linking.
if (source.getGroup() != null && source.getGroup().getId() != null) {
final long sourceGroupId = source.getGroup().getId();
final boolean isAdmin = bec.isCurrentUserAdmin();
// ticket:1434
if (bec.getCurrentGroupId().equals(sourceGroupId)) {
newDetails.setGroup(source.getGroup());
}
// ticket:1794
else if (bec.isCurrentUserAdmin() &&
Long.valueOf(roles.getUserGroupId())
.equals(source.getGroup().getId())) {
newDetails.setGroup(source.getGroup());
}
// ticket:3529
else if ((bec.getCurrentGroupId() < 0) &&
(isAdmin || bec.getMemberOfGroupsList()
.contains(sourceGroupId))) {
newDetails.setGroup(source.getGroup());
}
// oops. boom!
else {
throw new SecurityViolation(String.format(
"You are not authorized to set the ExperimenterGroup"
+ " for %s to %s", obj, source.getGroup()));
}
}
// PERMISSIONS: ticket:1434 and #1731 and #1779 (systypes)
// before 4.2, users were allowed to manually set the permissions
// on an object, and even set a umask to be applied. for the initial
// 4.2 version, however, we are disallowing manually setting
// permissions so that all objects will match group permissions.
// Doing this after the setting of newDetails.group in case the
// user is logged into user or system.
if (source.getPermissions() != null) {
Permissions groupPerms = currentUser.getCurrentEventContext()
.getCurrentGroupPermissions();
boolean isInSysGrp = sysTypes.isInSystemGroup(newDetails);
boolean isInUsrGrp = sysTypes.isInUserGroup(newDetails);
if (groupPerms.identical(source.getPermissions())) {
// ok. weird that they're set. probably an instance
// of a managed object being passed in as with
// ticket:2055
} else if (!sysTypes.isSystemType(obj.getClass())) {
if (isInSysGrp) {
// allow admin to do what they want. is this right?
} else if (isInUsrGrp) {
// similarly, allow whatever in user group for the moment.
} else {
throw new PermissionMismatchGroupSecurityViolation(
"Manually setting permissions currently disallowed");
}
}
// Above didn't throw, so set permissions.
newDetails.setPermissions(source.getPermissions());
}
// EXTERNALINFO
// users _are_ allowed to set the external info on a new object.
// subsequent operations, however, will not be able to edit this
// value.
newDetails.setExternalInfo(source.getExternalInfo());
// CREATION/UPDATEVENT : currently ignore what users do
return newDetails;
}
/**
* @see SecuritySystem#checkManagedDetails(IObject, Details)
*/
public Details checkManagedDetails(final IObject iobj,
final Details previousDetails) {
if (iobj == null) {
throw new ApiUsageException("Argument cannot be null.");
}
return checkManagedDetails(iobj, previousDetails,
iobj.getDetails().newInstance());
}
/**
* Like {@link #checkManagedDetails(IObject, Details, Details)} but allows
* passing in a specific {@link Details} instance.
* @see SecuritySystem#checkManagedDetails(IObject, Details)
* @see #evaluateLinkages(IObject)
*/
protected Details checkManagedDetails(final IObject iobj,
final Details previousDetails, /* not final */Details newDetails) {
if (iobj == null) {
throw new ApiUsageException("Argument cannot be null.");
}
if (iobj.getId() == null) {
throw new ValidationException(
"Id required on all detached instances.");
}
// Note: privileged check moved into the if statement below.
// done first as validation.
if (iobj instanceof IMutable) {
Integer version = ((IMutable) iobj).getVersion();
if (version == null || version.intValue() < 0) {
;
// throw new ValidationException(
// "Version must properly be set on managed objects :\n"+
// obj.toString()
// );
// TODO
}
}
// check if the newDetails variable has been reset or if the instance
// has been changed.
boolean altered = false;
final Details currentDetails = iobj.getDetails();
newDetails.copyWhereUnset(previousDetails, currentUser.createDetails());
// This happens if all fields of details are null (which can't happen)
// And is so uninteresting for all of our checks. The object can't be
// locked and nothing can be edited. Just return null.
if (previousDetails == null) {
newDetails = null;
altered = true;
if (log.isDebugEnabled()) {
log.debug("Setting details on " + iobj
+ " to null like original");
}
}
// Also uninteresting. If the users say nothing, then the originals.
// Probably common since users don't worry about this information.
else if (currentDetails == null) {
newDetails = previousDetails.copy();
altered = true;
if (log.isDebugEnabled()) {
log.debug("Setting details on " + iobj
+ " to copy of original details.");
}
// Now we have to make sure certain things do not happen. The
// following
// take into account whether or not the entity is privileged (has a
// token),
// is locked in the database, and who the current user and group
// are.
} else {
boolean privileged = false;
if (tokenHolder.hasPrivilegedToken(iobj)) {
privileged = true;
}
// Acquiring the context here to prevent multiple
// accesses to the threadlocal
final BasicEventContext bec = currentUser.current();
// ticket:1784 - NOTE: here we are NOT including a check
// for sysTypes.isInSystemGroup(), since that implies that
// the object doesn't have owner/group
final boolean sysType = sysTypes.isSystemType(iobj.getClass());
// As of 5.2, we are no longer being restrictive about external
// info. It is now a user-concern and can be changed like other
// fields.
// implies that owner doesn't matter
if (!sysType) {
altered |= managedOwner(privileged, iobj,
previousDetails, currentDetails, newDetails, bec);
}
// implies that group doesn't matter
if (!sysType) {
altered |= managedGroup(privileged, iobj,
previousDetails, currentDetails, newDetails, bec);
}
// the event check needs to be last, because we need to test
// whether or not it is necessary to change the updateEvent
// (i.e. last modification)
// implies that event doesn't matter
if (!sysType) {
altered |= managedEvent(privileged, iobj,
previousDetails, currentDetails, newDetails);
}
}
// ticket:8277 all permissions are ignored. We simply don't trust
// any coming in from outside. In the case of chgrp, the value
// will be modified in the DB directly. They've been marked as
// immutable in the model.
return altered ? newDetails : previousDetails;
}
/**
* responsible for guaranteeing that external info is not modified by any
* users, including root. This does not apply to the "client concern" fields
* which can be modified after the fact.
*
* @param privileged if the user is privileged
* @param obj the model object
* @param previousDetails
* details representing the known DB state
* @param currentDetails
* details representing the user request (UNTRUSTED)
* @param newDetails
* details from the current context. Holder for the merged
* {@link Permissions}
* @return true if the {@link Permissions} of newDetails are changed.
*/
@Deprecated
protected boolean managedExternalInfo(boolean privileged,
IObject obj, Details previousDetails, Details currentDetails,
Details newDetails) {
boolean altered = false;
ExternalInfo previous = previousDetails == null ? null
: previousDetails.getExternalInfo();
ExternalInfo current = currentDetails == null ? null : currentDetails
.getExternalInfo();
if (previous == null) {
if (current != null) {
newDetails.setExternalInfo(current);
altered = true;
}
}
// The ExternalInfo was previously set. We do not allow it to be
// changed,
// similar to not allowing the Event for an entity to be changed.
else {
if (!HibernateUtils.idEqual(previous, current)) {
throw new SecurityViolation(String.format(
"Cannot update ExternalInfo for %s from %s to %s", obj,
previous, current));
}
}
return altered;
}
protected boolean managedOwner(boolean privileged,
IObject obj, Details previousDetails, Details currentDetails,
Details newDetails, final BasicEventContext bec) {
if (!HibernateUtils.idEqual(previousDetails.getOwner(), currentDetails
.getOwner())) {
// !idEquals implies that they aren't both null; if current_owner is
// null, then it was *probably* not intended, so just fix it and
// move on. this goes for root and admins as well.
if (currentDetails.getOwner() == null) {
newDetails.setOwner(previousDetails.getOwner());
return true;
}
// if the current user is an admin or if the entity has been
// marked privileged, then use the current owner.
else if (bec.isCurrentUserAdmin() || privileged) {
// ok
}
// everyone else can't change them at all.
else {
throw new SecurityViolation(String.format(
"You are not authorized to change "
+ "the owner for %s from %s to %s", obj,
previousDetails.getOwner(), currentDetails.getOwner()));
}
}
else {
// values are the same. ensure they are the same for
// newDetails as well
newDetails.setOwner(previousDetails.getOwner());
}
return false;
}
protected boolean managedGroup(boolean privileged,
IObject obj, Details previousDetails, Details currentDetails,
Details newDetails, final BasicEventContext bec) {
if (null != previousDetails.getGroup()) {
long objGroupId = previousDetails.getGroup().getId();
long sessGroupId = currentUser.getGroup().getId();
long userGroupId = roles.getUserGroupId();
if (sessGroupId != objGroupId && objGroupId != userGroupId) { // ticket:1794 & ticket:2058
throw new SecurityViolation(String.format(
"Currently logged into group %s. Cannot alter object in group %s",
sessGroupId, objGroupId));
}
}
// previous and current have different ids. either change it and return
// true if permitted, or throw an exception.
if (!HibernateUtils.idEqual(previousDetails.getGroup(), currentDetails
.getGroup())) {
// !idEquals implies that they aren't both null; if current_group is
// null, then it was *probably* not intended, so just fix it and
// move on. this goes for root and admins as well.
if (currentDetails.getGroup() == null) {
newDetails.setGroup(previousDetails.getGroup());
return true;
}
// if user is a member of the group or the current user is an admin
// or if the entity has been marked as privileged, then use the
// current group.
// TODO refactor
else if ((!currentDetails.getGroup().getId().equals(
roles.getUserGroupId()) &&
bec.getMemberOfGroupsList().contains(
currentDetails.getGroup().getId())) // ticket:1794
|| bec.isCurrentUserAdmin() || privileged) {
newDetails.setGroup(currentDetails.getGroup());
return true;
}
// everyone else can't change them at all.
else {
throw new SecurityViolation(String.format(
"You are not authorized to change "
+ "the group for %s from %s to %s", obj,
previousDetails.getGroup(), currentDetails.getGroup()));
}
}
// previous and current are the same, but we need to set
// that value on newDetails.
else {
// This doesn't need to return true, because it'll only
// be used if something else was changed.
newDetails.setGroup(previousDetails.getGroup());
}
return false;
}
protected boolean managedEvent(boolean privileged,
IObject obj, Details previousDetails, Details currentDetails,
Details newDetails) {
// TODO no longer need to keep track of alteration boolean. like
// transient with update event, managedDetails will now ALWAYS return
// an updated details.
// -------------------
boolean altered = false;
// creation event~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (!HibernateUtils.idEqual(previousDetails.getCreationEvent(),
currentDetails.getCreationEvent())) {
// !idEquals implies that they aren't both null; if current_event is
// null, then it was *probably* not intended, so just fix it and
// move on. this goes for root and admins as well.
if (currentDetails.getCreationEvent() == null) {
newDetails.setCreationEvent(previousDetails.getCreationEvent());
altered = true;
}
// otherwise throw an exception, because as seen in ticket:346,
// it can lead to confusion otherwise. See:
// http://trac.openmicroscopy.org.uk/ome/ticket/346
else {
// no one change them.
throw new SecurityViolation(String.format(
"You are not authorized to change "
+ "the creation event for %s from %s to %s",
obj, previousDetails.getCreationEvent(), currentDetails
.getCreationEvent()));
}
}
// they are equal meaning no change was intended but in case other
// changes took place, we have to make sure newDetails has the correct
// value
else {
newDetails.setCreationEvent(previousDetails.getCreationEvent());
}
// update event ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (!HibernateUtils.idEqual(previousDetails.getUpdateEvent(),
currentDetails.getUpdateEvent())) {
// !idEquals implies that they aren't both null; if current_event is
// null, then it was *probably* not intended, so just fix it and
// move on. this goes for root and admins as well.
if (currentDetails.getUpdateEvent() == null) {
newDetails.setUpdateEvent(previousDetails.getUpdateEvent());
altered = true;
}
// otherwise throw an exception, because as seen in ticket:346,
// it can lead to confusion otherwise. See:
// http://trac.openmicroscopy.org.uk/ome/ticket/346
else {
// no one change them, but this is less likely intentional
// and more likely an optimistic lock issue. ticket:2162
throw new OptimisticLockException(String.format(
"You are not authorized to change "
+ "the update event for %s from %s to %s\n"
+ "You may need to reload the object before continuing.", obj,
previousDetails.getUpdateEvent(), currentDetails
.getUpdateEvent()));
}
}
// they are equal meaning no change was intended but in case other
// changes took place, we have to make sure newDetails has the correct
// value
else {
newDetails.setUpdateEvent(previousDetails.getUpdateEvent());
}
// QUATSCH
// update event : newDetails keeps its update event, which is by
// necessity different then what was in currentDetails and there-
// fore we now return true;
return altered;
}
// ~ Details checks. Used by to examine transient and managed Details.
// =========================================================================
// Also copied from BasicSecuritySystem
/**
* everyone is allowed to set the umask if desired. if the user does not set
* a permissions, then the DEFAULT value as defined in the Permissions class
* is used. if there's a umask for this session then that will be AND'd
* against the given permissions.
*/
boolean copyNonNullPermissions(Details target, Permissions p) {
if (p != null) {
target.setPermissions(p);
return true;
}
return false;
}
void throwIfGroupsDontMatch(
ExperimenterGroup changedObjectGroup, IObject changedObject,
ExperimenterGroup linkedGroup, IObject linkedObject) {
if (linkedGroup != null &&
!HibernateUtils.idEqual(linkedGroup, changedObjectGroup)) {
throw new GroupSecurityViolation(String.format(
"MIXED GROUP: " +
"%s(group=%s) and %s(group=%s) cannot be linked.",
changedObject, changedObjectGroup,
linkedObject, linkedGroup));
}
}
void throwIfNotGranted(Permissions p, Role role, Right right,
IObject linkedObject) {
if (!p.isGranted(role, right)) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Group is %s. ", p));
sb.append("Cannot link to object: ");
sb.append(linkedObject);
throw new SecurityViolation(sb.toString());
}
}
}