/* * Copyright (C) 2006-2012 University of Dundee * All rights reserved. * * This program 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. * * This program 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 this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package ome.model.internal; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.HashSet; import java.util.Set; import javax.persistence.Column; import javax.persistence.Transient; import ome.conditions.ApiUsageException; import static ome.model.internal.Permissions.Role.*; import static ome.model.internal.Permissions.Right.*; import static ome.model.internal.Permissions.Flag.*; /** * class responsible for storing all Right/Role-based information for entities * as well as various flags for the containing {@link Details} instance. It is * strongly encouraged to <em>not</em> base any code on the implementation of * the rights, roles, and flag but rather to rely on the public methods. * <p> * In the future, further roles, rights, and flags may be added to this class. * This will change the representation in the database, but the simple * grant/revoke/isSet logic will remain the same. * </p> * * @see <a * href="http://trac.openmicroscopy.org.uk/ome/ticket/180">ticket:180</a> */ public class Permissions implements Serializable { private static final long serialVersionUID = 708953452345658023L; /** * enumeration of currently active roles. The {@link #USER} role is active * when the contents of {@link Details#getOwner()} equals the current user * as determined from the Security system (Server-side only). Similary, the * {@link #GROUP} role is active when the contents of * {@link Details#getGroup()} matches the current group. {@link #WORLD} is * used for any non-USER, non-GROUP user. * * For more advanced, ACL, methods taking * {@link ome.model.meta.Experimenter} references can be implemented. */ public enum Role { USER(8), GROUP(4), WORLD(0); private final int shift; Role(int shift) { this.shift = shift; } int shift() { return this.shift; } } /** * enumeration of granted rights. The {@link #READ} right allows for a user * with the given role to retrieve an entity. This means that all fields of * that entity can be retrieved. Care is taken by the server, that once an * entity was readable and another entity was attached to it, that further * READ access will not throw an exception. In turn, * care should be taken by users to not overly soon grant {@link #READ} * permissions lest they no longer be revokable. As of 4.4, this also permits * certain view-based linkages of objects (e.g. RenderingDef, Thumbnail). * * The {@link #ANNOTATE} right allows a user with the given role to link * annotations and other non-core data to an entity. * * The {@link #WRITE} right allows for a user with the given role to alter * the fields of an entity, including changing the contents of its * collection, assigning it to another collection, or deleting it. * This does not include changing the fields of those linked * entities, only whether or not they are members of the given collection. * * Note: if WRITE is granted, ANNOTATE will also be granted. */ public enum Right { ANNOTATE(1), WRITE(2), READ(4); private final int mask; Right(int mask) { this.mask = mask; } int mask() { return this.mask; } } /** * Currently unused. */ public enum Flag { UNUSED(1 << 19); /* * Implementation note: -------------------- Flags work with reverse * logic such that the default permissions can remain -1L (all 1s), a * flag is "set" when it's bit is set to 0. This holds for everything * over 16. */ private final int bit; Flag(int bit) { this.bit = bit; } int bit() { return this.bit; } } // ~ Constructors // ========================================================================= /** * simple contructor. Will turn on all {@link Right rights} for all * {@link Role roles} */ public Permissions() { } /** * copy constructor. Will create a new {@link Permissions} with the same * {@link Right rights} as the argument. */ public Permissions(Permissions p) { if (p == null) { throw new IllegalArgumentException( "Permissions argument cannot be null."); } if (p == DUMMY) { throw new IllegalArgumentException("No valid permissions available! DUMMY permissions are not intended for copying. " + "Make sure that you have not passed omero.group=-1 for a save without context"); } this.revokeAll(p); copyRestrictions(p.restrictions, p.extendedRestrictions); } // ~ Fields // ========================================================================= /** * represents the lower 64-bits of permissions data. */ private long perm1 = -1; // all bits turned on. // These are duplicated in Constants.ice public static final int LINKRESTRICTION = 0; public static final int EDITRESTRICTION = 1; public static final int DELETERESTRICTION = 2; public static final int ANNOTATERESTRICTION = 3; /** * Calculated restrictions which are based on both the store * representation({@link #perm1}) and the current calling context. */ private boolean[] restrictions; /** * Further calculated restrictions which can be defined individually by * any service. Individual service methods should specify in their * documentation which strings must be checked by clients. */ private String[] extendedRestrictions; // ~ Getters // ========================================================================= /** tests that a given {@link Role} has the given {@link Right}. */ public boolean isGranted(Role role, Right right) { return (perm1 & right.mask() << role.shift()) == right.mask() << role .shift(); } /** tests that a given {@link Flag} is set. */ public boolean isSet(Flag flag) { return (perm1 & flag.bit()) != flag.bit(); } /** * returns the order of the bit representing the given {@link Flag}. This * is dependent on the internal representation of {@link Permissions} and * should only be used when necessary. */ public static int bit(Flag flag) { return flag.bit(); } /** * returns the order of the bit representing the given {@link Role} and * {@link Right}. This is dependent on the internal representation of * {@link Permissions} and should only be used when necessary. */ public static int bit(Role role, Right right) { return right.mask() << role.shift(); } public static Permissions parseString(String rwrwrw) { Permissions p = new Permissions(EMPTY); String regex = "([Rr_-][AaWw_-]){3}"; if (rwrwrw == null || !rwrwrw.matches(regex)) { throw new ApiUsageException("Permissions are of the form: " + regex); } char c; c = rwrwrw.charAt(0); if (c == 'r' || c == 'R') { p.grant(USER, READ); } c = rwrwrw.charAt(1); if (c == 'a' || c == 'A') { p.grant(USER, ANNOTATE); } else if (c == 'w' || c == 'W') { p.grant(USER, ANNOTATE); p.grant(USER, WRITE); } c = rwrwrw.charAt(2); if (c == 'r' || c == 'R') { p.grant(GROUP, READ); } c = rwrwrw.charAt(3); if (c == 'a' || c == 'A') { p.grant(GROUP, ANNOTATE); } else if (c == 'w' || c == 'W') { p.grant(GROUP, ANNOTATE); p.grant(GROUP, WRITE); } c = rwrwrw.charAt(4); if (c == 'r' || c == 'R') { p.grant(WORLD, READ); } c = rwrwrw.charAt(5); if (c == 'a' || c == 'A') { p.grant(WORLD, ANNOTATE); } else if (c == 'w' || c == 'W') { p.grant(WORLD, ANNOTATE); p.grant(WORLD, WRITE); } return p; } public static boolean isDisallow(final boolean[] restrictions, final int restriction) { if (restrictions != null && restrictions.length > restriction) { return restrictions[restriction]; } return false; } @Transient public boolean isDisallowAnnotate() { return isDisallow(restrictions, ANNOTATERESTRICTION); } @Transient public boolean isDisallowDelete() { return isDisallow(restrictions, DELETERESTRICTION); } @Transient public boolean isDisallowEdit() { return isDisallow(restrictions, EDITRESTRICTION); } @Transient public boolean isDisallowLink() { return isDisallow(restrictions, LINKRESTRICTION); } public void addExtendedRestrictions(Set<String> extendedRestrictions) { if (extendedRestrictions == null || extendedRestrictions.isEmpty()) { return; } if (this.extendedRestrictions == null) { this.extendedRestrictions = extendedRestrictions.toArray( new String[extendedRestrictions.size()]); } else { // Should be a much less likely case since these will // likely not have been loaded/set yet. Set<String> copy = new HashSet<String>(); for (String er : this.extendedRestrictions) { copy.add(er); } copy.addAll(extendedRestrictions); this.extendedRestrictions = copy.toArray( new String[copy.size()]); } } /** * Produce a copy of restrictions for use elsewhere. */ public boolean[] copyRestrictions() { if (restrictions == null) { return null; } boolean[] copy = new boolean[restrictions.length]; System.arraycopy(restrictions, 0, copy, 0, restrictions.length); return copy; } /** * Produce a copy of restrictions for use elsewhere. */ public String[] copyExtendedRestrictions() { if (extendedRestrictions == null) { return null; } String[] copy = new String[extendedRestrictions.length]; System.arraycopy(extendedRestrictions, 0, copy, 0, extendedRestrictions.length); return copy; } /** * Safely copy the source array. If it is null or contains no "true" values, * then the restrictions field will remain null. */ public void copyRestrictions(final boolean[] source, String[] extendedRestrictions) { if (extendedRestrictions == null || extendedRestrictions.length == 0) { this.extendedRestrictions = null; } else { final int sz = extendedRestrictions.length; this.extendedRestrictions = new String[sz]; System.arraycopy(extendedRestrictions, 0, this.extendedRestrictions, 0, sz); } if (noTrues(source)) { this.restrictions = null; } else { if (restrictions == null || source.length != restrictions.length) { restrictions = new boolean[source.length]; } System.arraycopy(source, 0, restrictions, 0, source.length); } } /** * Copy restrictions based on the integer returned by BasicACLVoter. */ public void copyRestrictions(int allow, Set<String> extendedRestrictions) { if (extendedRestrictions == null || extendedRestrictions.isEmpty()) { this.extendedRestrictions = null; } else { this.extendedRestrictions = extendedRestrictions.toArray( new String[extendedRestrictions.size()]); } if (allow == 15) { // Would be all false. this.restrictions = null; return; } if (restrictions == null) { this.restrictions = new boolean[4]; // All false } this.restrictions[LINKRESTRICTION] |= (0 == (allow & (1 << LINKRESTRICTION))); this.restrictions[EDITRESTRICTION] |= (0 == (allow & (1 << EDITRESTRICTION))); this.restrictions[DELETERESTRICTION] |= (0 == (allow & (1 << DELETERESTRICTION))); this.restrictions[ANNOTATERESTRICTION] |= (0 == (allow & (1 << ANNOTATERESTRICTION))); } private static boolean noTrues(boolean[] source) { if (source == null) { return true; } for (int i = 0; i < source.length; i++) { if (source[i]) { return false; } } return true; } // ~ Setters (return this) // ========================================================================= /** * turns on the {@link Right rights} for the given {@link Role role}. Null * or empty rights are simply ignored. For example, <code> * somePermissions().grant(USER,READ,WRITE,USE); * </code> will guarantee * that the current user has all rights on this entity. */ public Permissions grant(Role role, Right... rights) { if (rights != null && rights.length > 0) { for (Right right : rights) { perm1 = perm1 | singleBitOn(role, right); } } return this; } /** * turns off the {@link Right rights} for the given {@link Role role}. Null * or empty rights are simply ignored. For example, <code> * new Permissions().revoke(WORLD,WRITE,USE); * </code> will return a * Permissions instance which cannot be altered or linked to by members of * WORLD. */ public Permissions revoke(Role role, Right... rights) { if (rights != null && rights.length > 0) { for (Right right : rights) { perm1 = perm1 & singleBitOut(role, right); } } return this; } /** * takes a permissions instance and ORs it with the current instance. This * means that any privileges which have been granted to the argument will * also be granted to the current instance. For example, <code> * Permissions mask = new Permissions().grant(WORLD,READ); * someEntity.getDetails().getPermissions().grantAllk(mask); * </code> will allow READ access (and possibly more) to * <code>someEntity</code> for members of WORLD. */ public Permissions grantAll(Permissions mask) { if (mask == null) { return this; } long maskPerm1 = mask.getPerm1(); this.perm1 = this.perm1 | maskPerm1; return this; } /** * takes a permissions instance and ANDs it with the current instance. This * means that any privileges which have been revoked from the argument will * also be revoked from the current instance. For example, <code> * Permissions mask = new Permissions().revoke(WORLD,READ,WRITE,USE); * someEntity.getDetails().getPermissions().applyMask(mask); * </code> will disallow all access to <code>someEntity</code> for members * of WORLD. * * This also implies that applyMask can be used to make copies of * Permissions. For example, <code> * new Permissions().applyMask( somePermissions ); * </code> will produce a copy of * <code>somePermissions</code>. * * Note: the logic here is different from Unix UMASKS. */ public Permissions revokeAll(Permissions mask) { if (mask == null) { return this; } long maskPerm1 = mask.getPerm1(); this.perm1 = this.perm1 & maskPerm1; return this; } /** turn a given {@link Flag} on. A null {@link Flag} will be ignored. */ public Permissions set(Flag flag) { if (flag == null) { return this; } this.perm1 &= -1L ^ flag.bit(); return this; } /** turn a given {@link Flag} off. A null {@link Flag} will be ignored. */ public Permissions unSet(Flag flag) { if (flag == null) { return this; } this.perm1 |= 0L ^ flag.bit(); return this; } public static void setDisallow(boolean[] restrictions, int restriction, boolean disallow) { // The array is already long enough, just set the value if (restrictions != null && restrictions.length >= restriction) { restrictions[restriction] = disallow; } else { // if !disallow, then we won't need to set anything, // since the default value will be false. if (disallow) { boolean[] copy = new boolean[restriction+1]; if (restrictions != null) { System.arraycopy(restrictions, 0, copy, 0, restrictions.length); } copy[restriction] = disallow; // i.e. true restrictions = copy; } } } public Permissions setDisallowAnnotate(boolean disallowAnnotate) { setDisallow(restrictions, ANNOTATERESTRICTION, disallowAnnotate); return this; } public Permissions setDisallowDelete(boolean disallowDelete) { setDisallow(restrictions, DELETERESTRICTION, disallowDelete); return this; } public Permissions setDisallowEdit(boolean disallowEdit) { setDisallow(restrictions, EDITRESTRICTION, disallowEdit); return this; } public Permissions setDisallowLink(boolean disallowLink) { setDisallow(restrictions, LINKRESTRICTION, disallowLink); return this; } // ~ Overrides // ========================================================================= /** * produces a String representation of the {@link Permissions} similar to * those on a Unix filesystem. Unset bits are represented by a dash, while * other bits are represented by a symbolic value in the correct bit * position. For example, a Permissions with all {@link Right rights} * granted to all but WORLD {@link Role roles} would look like: rwrw-- */ @Override public String toString() { StringBuilder sb = new StringBuilder(16); sb.append(isGranted(USER, READ) ? "r" : "-"); sb.append(annotateOrWorld(USER)); sb.append(isGranted(GROUP, READ) ? "r" : "-"); sb.append(annotateOrWorld(GROUP)); sb.append(isGranted(WORLD, READ) ? "r" : "-"); sb.append(annotateOrWorld(WORLD)); return sb.toString(); } private String annotateOrWorld(Role role) { if (isGranted(role, WRITE)) { return "w"; } else if (isGranted(role, ANNOTATE)) { return "a"; } else { return "-"; } } /** * returns true if two {@link Permissions} instances have all the same * {@link Right} / {@link Role} pairs granted. */ public boolean sameRights(Permissions p) { if (p == this) { return true; } for (Role ro : Role.values()) { for (Right rt : Right.values()) { if (isGranted(ro, rt) != p.isGranted(ro, rt)) { return false; } } } return true; } /** * two {@link Permissions} instances are <code>identical</code> if they have * the same bit representation. * * @see <a * href="http://trac.openmicroscopy.org.uk/ome/ticket/291">ticket:291</a> */ // @Override public boolean identical(Permissions p) { // if (!(obj instanceof Permissions)) return false; // // Permissions p = (Permissions) obj; if (p == this) { return true; } if (p.perm1 == this.perm1) { return true; } return false; } // /** hashCode based on the bit representation of this {@link Permissions} // * instance. // */ // @Override // public int hashCode() { // int result = 11; // result = 17 * result + (int)(perm1^(perm1>>>32)); // return result; // } // ~ Property accessors : used primarily by Hibernate // ========================================================================= @Column(name = "permissions", nullable = false, updatable = false) protected long getPerm1() { return this.perm1; } protected void setPerm1(long value) { this.perm1 = value; } // ~ Helpers // ========================================================================= /** returns a long with only a single 0 defined by role/right */ final protected static long singleBitOut(Role role, Right right) { return -1L ^ right.mask() << role.shift(); } /** returns a long with only a single 1 defined by role/right */ final protected static long singleBitOn(Role role, Right right) { return 0L | right.mask() << role.shift(); } /** * an immutable wrapper around {@link Permission} instances so that commonly * used permissions can be made available as public final static constants. */ private static class ImmutablePermissions extends Permissions implements Serializable { private static final long serialVersionUID = -4407900270934589522L; /** * Factory method to create an immutable Permissions object. */ public static Permissions immutable(Permissions p) { return new ImmutablePermissions(p); } /** * the delegate {@link Permissions} which this immutable wrapper bases * all of its logic on. Not final for reasons of serialization. */ private Permissions delegate; /** * the sole constructor for an {@link ImmutablePermissions}. Note: this * does not behave like {@link Permissions#Permissions(Permissions)} -- * the copy constructor. Rather stores the {@link Permissions} instance * for delegation * * @param p * Non-null {@link Permissions} instance. */ ImmutablePermissions(Permissions p) { if (p == null) { throw new IllegalArgumentException( "Permissions may not be null"); } this.delegate = new Permissions(p); } // ~ SETTERS // ===================================================================== /** * throws {@link UnsupportedOperationException} */ @Override public Permissions grant(Role role, Right... rights) { throw new UnsupportedOperationException(); } /** * throws {@link UnsupportedOperationException} */ @Override public Permissions revoke(Role role, Right... rights) { throw new UnsupportedOperationException(); } /** * throws {@link UnsupportedOperationException} */ @Override public Permissions grantAll(Permissions mask) { throw new UnsupportedOperationException(); } /** * throws {@link UnsupportedOperationException} */ @Override public Permissions revokeAll(Permissions mask) { throw new UnsupportedOperationException(); } /** * delegates to {@link #set(ome.model.internal.Permissions.Flag)} */ @Override public Permissions set(Flag flag) { return delegate.set(flag); } /** * delegates to {@link #unSet(ome.model.internal.Permissions.Flag)} */ @Override public Permissions unSet(Flag flag) { return delegate.unSet(flag); } // ~ GETTERS // ========================================================================= /** * delegates to {@link #delegate} */ @Override public boolean isGranted(Role role, Right right) { return delegate.isGranted(role, right); } /** * delegates to {@link #delegate} */ @Override protected long getPerm1() { return delegate.getPerm1(); } /** * delegates to {@link #delegate} */ @Override protected void setPerm1(long value) { delegate.setPerm1(value); } /** * delegates to {@link #isSet(ome.model.internal.Permissions.Flag)} */ @Override public boolean isSet(Flag flag) { return delegate.isSet(flag); } // ~ Other // ===================================================================== /** * delegates to {@link #identical(Permissions)} */ @Override public boolean identical(Permissions p) { return delegate.identical(p); } /** * delegates to {@link #sameRights(Permissions)} */ @Override public boolean sameRights(Permissions p) { return delegate.sameRights(p); } /** * delegates to {@link #toString()} */ @Override public String toString() { return delegate.toString(); } // ~ Serialization // ===================================================================== private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { Permissions p = (Permissions) s.readObject(); if (p == null) { throw new IllegalArgumentException( "Permissions may not be null"); } this.delegate = new Permissions(p); } private void writeObject(ObjectOutputStream s) throws IOException { s.writeObject(delegate); } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // NOTE: when rights or roles change, the definition of EMPTY needs to // be kept in sync. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * an immutable {@link Permissions} instance with all {@link Right rights} * turned off. */ public final static Permissions EMPTY = new ImmutablePermissions( new Permissions().revoke(USER, READ, ANNOTATE, WRITE).revoke(GROUP, READ, ANNOTATE, WRITE).revoke(WORLD, READ, ANNOTATE, WRITE)); /** * Marker object which can be set on objects to show that the Permissions * instance given contains no value. */ public final static Permissions DUMMY = new ImmutablePermissions(EMPTY); // ~ Systematic // ========================================================================= /* * All possible (sensible) permission combinations are: * * R_____ user immutable RW____ user private RWR___ group readable RWRW__ * group private RWRWR_ group writeable RWRWRW world writeable RWR_R_ user * writeable R_R_R_ world immutable R_R___ group immutable */ /** * R______ : user and only the user can only read */ public final static Permissions USER_IMMUTABLE = new ImmutablePermissions( new Permissions(EMPTY).grant(USER, READ)); /** * RW____ : user and only user can read and write */ public final static Permissions USER_PRIVATE = new ImmutablePermissions( new Permissions(EMPTY).grant(USER, READ, ANNOTATE, WRITE)); /** * RWR___ : user can read and write, group can read */ public final static Permissions GROUP_READABLE = new ImmutablePermissions( new Permissions(USER_PRIVATE).grant(GROUP, READ)); /** * RWRW__ : user and group can read and write */ public final static Permissions GROUP_PRIVATE = new ImmutablePermissions( new Permissions(GROUP_READABLE).grant(GROUP, ANNOTATE, WRITE)); /** * RWRWR_ : user and group can read and write, world can read */ public final static Permissions GROUP_WRITEABLE = new ImmutablePermissions( new Permissions(GROUP_PRIVATE).grant(WORLD, READ)); /** * RWRWRW : everyone can read and write */ public final static Permissions WORLD_WRITEABLE = new ImmutablePermissions( new Permissions(GROUP_WRITEABLE).grant(WORLD, ANNOTATE, WRITE)); /** * RWR_R_ : all can read, user can write */ public final static Permissions USER_WRITEABLE = new ImmutablePermissions( new Permissions(GROUP_READABLE).grant(WORLD, READ)); /** * R_R_R_ : all can only read */ public final static Permissions WORLD_IMMUTABLE = new ImmutablePermissions( new Permissions(USER_WRITEABLE).revoke(USER, ANNOTATE, WRITE)); /** * R_R___ : user and group can only read */ public final static Permissions GROUP_IMMUTABLE = new ImmutablePermissions( new Permissions(WORLD_IMMUTABLE).revoke(WORLD, READ)); // ~ Non-systematic (easy to remember) // ========================================================================= /** * an immutable {@link Permissions} instance with all {@link Right#WRITE} * rights turned off. Identical to {@link #WORLD_IMMUTABLE} */ public final static Permissions READ_ONLY = WORLD_IMMUTABLE; /** * an immutable {@link Permissions} instance with permissions only for the * object owner.. Identical to {@link #USER_PRIVATE}. * * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1434">ticket:1434</a> */ public final static Permissions PRIVATE = USER_PRIVATE; /** * an immutable {@link Permissions} instance with permissions for group * members to read other members' data. Identical to * {@link #GROUP_READABLE}. * * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1434">ticket:1434</a> * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1992">ticket:1992</a> */ public final static Permissions COLLAB_READONLY = GROUP_READABLE; /** * an immutable {@link Permissions} instance with read and write permissions * for group members. Identical to {@link #GROUP_PRIVATE}. * * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1434">ticket:1434</a> * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1992">ticket:1992</a> */ public final static Permissions COLLAB_READLINK = GROUP_PRIVATE; /** * an immutable {@link Permissions} instance with all {@link Right Rights} * granted. Identical to {@link #WORLD_WRITEABLE} */ public final static Permissions PUBLIC = WORLD_WRITEABLE; }