Dépôt officiel du code source de l'ERP OpenConcerto
Rev 156 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.sql.users.rights;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.SQLData;
import org.openconcerto.sql.model.SQLDataListener;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTableModifiedListener;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.request.SQLCache;
import org.openconcerto.sql.request.SQLCacheWatcher;
import org.openconcerto.sql.users.UserManager;
import org.openconcerto.sql.users.UserSingleton;
import org.openconcerto.sql.users.UserSingletonManager;
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.CompareUtils.Equalizer;
import org.openconcerto.utils.ExceptionHandler;
import org.openconcerto.utils.IFutureTask;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.ThreadFactory;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Tuple3;
import org.openconcerto.utils.cache.CacheItem.RemovalType;
import org.openconcerto.utils.cache.CacheResult;
import org.openconcerto.utils.cache.CacheWatcher;
import org.openconcerto.utils.cache.CacheWatcherFactory;
import org.openconcerto.utils.cache.ICache;
import org.openconcerto.utils.cache.ICache.ItemEvent;
import org.openconcerto.utils.cache.ICacheSupport;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.IFactory;
import org.openconcerto.utils.cc.ITransformer;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import net.jcip.annotations.GuardedBy;
public class UserRightsManager implements UserSingleton {
public static final String USER_RIGHT_TABLE = UserRightSQLElement.TABLE_NAME;
private static final UserSingletonManager<UserRightsManager> sMngr = new UserSingletonManager<UserRightsManager>(USER_RIGHT_TABLE) {
@Override
protected UserRightsManager createInstance(SQLTable t) {
return new UserRightsManager(t);
}
};
public static final String SUPERUSER_FIELD = "SUPERUSER";
private static final int ADMIN_ID = SQLRow.NONEXISTANT_ID;
/**
* Only administrators can see user rights.
*/
public static final String ADMIN_FIELD = "ADMIN";
private static final ListMap<String, Tuple2<String, Boolean>> SUPERUSER_RIGHTS = ListMap.singleton(null, Tuple2.create((String) null, true));
private static final ListMap<String, Tuple2<String, Boolean>> NO_RIGHTS = ListMap.singleton(null, Tuple2.create((String) null, false));
public static final List<MacroRight> DEFAULT_MACRO_RIGHTS = Collections.synchronizedList(new ArrayList<MacroRight>());
static {
// "addRight() ambiguous"
assert ADMIN_ID < SQLRow.MIN_VALID_ID;
DEFAULT_MACRO_RIGHTS.add(new LockAdminUserRight());
DEFAULT_MACRO_RIGHTS.add(new TableAllRights(true));
DEFAULT_MACRO_RIGHTS.add(new TableAllRights(false));
}
public static UserSingletonManager<UserRightsManager> getSingletonManager() {
return sMngr;
}
/**
* Set the instance using the table in the passed root.
*
* @param root the root where the rights should be.
* @return the new instance, <code>null</code> if <code>root</code> does not contain the
* {@value #USER_RIGHT_TABLE} table.
*/
public static UserRightsManager setInstanceFromRoot(final DBRoot root) {
return getSingletonManager().setInstanceFromRoot(root);
}
/**
* Set the instance.
*
* @param t the table, <code>null</code> to remove.
* @return the new instance, <code>null</code> if t was.
*/
public static UserRightsManager setInstance(final SQLTable t) {
return getSingletonManager().setInstance(t);
}
public static UserRightsManager getInstance() {
return getSingletonManager().getInstance();
}
public static final UserRights getCurrentUserRights() {
final UserManager mngr = UserManager.getInstance();
final UserRightsManager rightsMngr = getInstance();
return getCurrentUserRights(rightsMngr, mngr);
}
public static final UserRights getCurrentUserRights(final UserRightsManager rightsMngr, final UserManager mngr) {
// if right table doesn't exist, give access to everything
if (rightsMngr == null)
return UserRights.ALLOW_ALL;
// else if there are rights (and thus users) but no user is defined, use the default rights
else
return rightsMngr.getUserRights(mngr.getCurrentUser() == null ? null : mngr.getCurrentUser().getId());
}
private static final class JavaRights {
static final String TUPLES_CHANGED = "tuples";
static final String TUPLE_CHANGED = "tuple";
// rights by user ID, immutable
@GuardedBy("this")
private ListMapItf<Integer, RightTuple> tuples;
private final PropertyChangeSupport propSupp = new PropertyChangeSupport(this);
public JavaRights() {
this.tuples = ListMap.empty();
}
final void changeRight(final boolean add, final Integer userID, final RightTuple right) {
if (add && right == null)
throw new NullPointerException("Null right entry");
final ListMapItf<Integer, RightTuple> oldVal, newVal;
synchronized (this) {
oldVal = this.tuples;
final ListMap<Integer, RightTuple> newMap = new ListMap<Integer, RightTuple>(this.tuples);
if (add)
newMap.add(userID, right);
else if (right == null)
newMap.remove(userID);
else
newMap.removeAllInstancesOfItem(userID, right);
newVal = ListMap.unmodifiableMap(newMap);
this.tuples = newVal;
}
this.propSupp.firePropertyChange(TUPLES_CHANGED, oldVal, newVal);
this.propSupp.firePropertyChange(TUPLE_CHANGED, null, userID);
}
final synchronized ListMapItf<Integer, RightTuple> getTuples() {
return this.tuples;
}
}
// cheat a little by pretending to be SQL, so that java rights can be listened to by the cache.
private static class JavaRightsUser implements SQLData {
private final JavaRights rights;
private final int id;
public JavaRightsUser(final JavaRights rights, final int id) {
super();
this.rights = rights;
this.id = id;
}
@Override
public SQLTableModifiedListener createTableListener(final SQLDataListener l) {
return null;
}
@Override
public SQLTable getTable() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.id;
result = prime * result + this.rights.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final JavaRightsUser other = (JavaRightsUser) obj;
return this.id == other.id && this.rights.equals(other.rights);
}
}
private static class JavaRightsWatcher extends CacheWatcher<SQLData> {
private final int id;
private final PropertyChangeListener l;
public JavaRightsWatcher(JavaRightsUser u) {
super(u);
this.id = u.id;
this.l = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (((Number) evt.getNewValue()).intValue() == JavaRightsWatcher.this.id) {
dataChanged(evt);
}
}
};
}
@Override
protected void startWatching() {
((JavaRightsUser) this.getData()).rights.propSupp.addPropertyChangeListener(JavaRights.TUPLE_CHANGED, this.l);
}
@Override
protected void stopWatching() {
((JavaRightsUser) this.getData()).rights.propSupp.removePropertyChangeListener(JavaRights.TUPLE_CHANGED, this.l);
}
}
// Gérer un droit avec une classe
@GuardedBy("macroRights")
private final Map<String, MacroRight> macroRights;
// {user -> {code -> [<object, bool>]}}
@GuardedBy("rights")
private final Map<Integer, ListMapItf<String, Tuple2<String, Boolean>>> rights;
private final SQLCache<Integer, ListMapItf<String, Tuple2<String, Boolean>>> cache;
private final SQLTable table;
private final Link toUserLink;
private final JavaRights javaRights;
@GuardedBy("this")
private final Map<Integer, UserRights> userRights;
private final ExecutorService exec;
private UserRightsManager(final SQLTable t) {
if (t == null)
throw new NullPointerException("Missing table");
this.macroRights = Collections.synchronizedMap(new HashMap<String, MacroRight>());
this.rights = new HashMap<Integer, ListMapItf<String, Tuple2<String, Boolean>>>();
this.cache = new SQLCache<Integer, ListMapItf<String, Tuple2<String, Boolean>>>(15 * 60, -1, "Cache of rights") {
@Override
protected ICacheSupport<SQLData> createSupp(String name) {
final ICacheSupport<SQLData> res = new ICacheSupport<SQLData>(name);
res.setWatcherFactory(new CacheWatcherFactory<SQLData>() {
@Override
public CacheWatcher<SQLData> createWatcher(SQLData o) {
if (o instanceof JavaRightsUser) {
return new JavaRightsWatcher((JavaRightsUser) o);
} else {
return new SQLCacheWatcher(o);
}
}
});
return res;
}
};
this.javaRights = new JavaRights();
this.table = t;
this.toUserLink = t.getField("ID_USER_COMMON").getFieldGroup().getForeignLink();
defaultRegister();
this.userRights = new HashMap<Integer, UserRights>();
this.exec = Executors.newSingleThreadExecutor(new ThreadFactory(this.getClass().getSimpleName() + " executor for " + t.getSQLName(), true).setPriority(Thread.MIN_PRIORITY));
this.cache.addItemListener(new IClosure<ItemEvent<Integer, ListMapItf<String, Tuple2<String, Boolean>>, SQLData>>() {
@Override
public void executeChecked(ItemEvent<Integer, ListMapItf<String, Tuple2<String, Boolean>>, SQLData> evt) {
cacheChanged(evt);
}
});
}
private void cacheChanged(final ItemEvent<Integer, ListMapItf<String, Tuple2<String, Boolean>>, ?> evt) {
if (evt.getPropertyName().equals(ICache.ITEM_ADDED)) {
synchronized (this.rights) {
this.rights.put(evt.getNewValue().getKey(), evt.getNewValue().getValue());
}
} else if (evt.getPropertyName().equals(ICache.ITEM_REMOVED)) {
final Integer userID = evt.getOldValue().getKey();
final RemovalType removalType = evt.getOldValue().getRemovalType();
if (removalType == RemovalType.DATA_CHANGE || removalType == RemovalType.TIMEOUT) {
invokeLater(new Callable<Object>() {
@Override
public Object call() throws Exception {
// we know the value is in the map, but we want to refresh it so
// don't use the map and only update the cache. Which would then
// trigger ICache.ITEM_ADDED.
getRightsForUser(userID.intValue(), false);
return null;
}
});
} else {
assert removalType != RemovalType.SIZE_LIMIT;
synchronized (this.rights) {
this.rights.remove(userID);
}
}
}
}
/**
* enregistre les instances gérants les droits
*/
private void defaultRegister() {
synchronized (DEFAULT_MACRO_RIGHTS) {
for (final MacroRight macroRight : DEFAULT_MACRO_RIGHTS) {
register(macroRight);
}
}
}
/**
* Ajoute une instance pour la gestion d'un droit
*
* @param userRight the instance which will now be used for <code>userRight.getCode()</code>.
*/
public void register(final MacroRight userRight) {
if (userRight == null)
throw new IllegalArgumentException("Missing right");
this.macroRights.put(userRight.getCode(), userRight);
}
/**
* Add an unconditional right for the passed user. I.e. it is loaded before the SQL ones and
* even default unconditional rights are before user SQL rights.
*
* @param userID the user id, <code>null</code> meaning for everyone.
* @param right the right the user should always have.
*/
public void addRight(Integer userID, RightTuple right) {
this.javaRights.changeRight(true, getKey(userID), right);
}
/**
* Add an unconditional right for administrators. This will be after user rights and before
* default rights.
*
* @param right the right to add.
*/
public void addRightForAdmins(RightTuple right) {
this.javaRights.changeRight(true, ADMIN_ID, right);
}
private final int getKey(final Integer userID) {
if (userID != null && userID < SQLRow.MIN_VALID_ID)
throw new IllegalArgumentException("invalid ID : " + userID);
return userID == null ? getDefaultUserId() : userID;
}
/**
* Remove a right.
*
* @param userID the user id, <code>null</code> meaning default user.
* @param right the right to remove, <code>null</code> meaning remove all.
* @see #addRight(Integer, RightTuple)
*/
public void removeRight(final Integer userID, final RightTuple right) {
this.javaRights.changeRight(false, getKey(userID), right);
}
public void removeRightForAdmins(final RightTuple right) {
this.javaRights.changeRight(false, ADMIN_ID, right);
}
public synchronized final boolean isValid() {
return !this.cache.getSupp().isDying();
}
@Override
public synchronized final void destroy() {
if (this.isValid()) {
this.cache.getSupp().die();
this.exec.shutdown();
}
assert !this.isValid();
}
@Override
public final SQLTable getTable() {
return this.table;
}
public final DBRoot getRoot() {
return this.getTable().getDBRoot();
}
private final SQLTable getUserTable() {
return this.toUserLink.getTarget();
}
public synchronized final <T> Future<T> invokeLater(final Callable<T> c) {
if (!this.isValid())
return null;
return this.exec.submit(c);
}
/**
* Block until all out of date rights are refreshed. Useful since <code>haveRight()</code> will
* return the last known rights while a refresh is ongoing.
*
* @return <code>true</code> if this method blocked, <code>false</code> if this was already
* {@link #isValid() invalid}.
* @throws InterruptedException if the current thread was interrupted while waiting.
*/
public final boolean waitForCurrentRefresh() throws InterruptedException {
try {
final Future<Object> f = this.invokeLater(IFutureTask.getNoOpCallable());
if (f == null)
return false;
f.get();
return true;
} catch (ExecutionException e) {
throw new IllegalStateException("No-op failed", e);
}
}
public final synchronized UserRights getUserRights(Integer userID) {
if (userID == null)
userID = this.getDefaultUserId();
UserRights res = this.userRights.get(userID);
if (res == null) {
res = new UserRights(this, userID);
this.userRights.put(userID, res);
}
return res;
}
public final boolean haveRight(final int userID, final String code) {
return this.haveRight(userID, code, null);
}
public final boolean haveRight(final int userID, final String code, final String object) {
return this.haveRight(userID, code, object, CompareUtils.OBJECT_EQ);
}
/**
* Whether <code>userID</code> should be allowed the <code>code</code> (e.g. DELETE) right on
* <code>object</code> (e.g. TENSION).<br>
* The rights are ordered and the first one that matches is returned. Furthermore after
* searching for the passed <code>userID</code> the default user is searched. <br>
* To match, the code of the right must be equal to <code>code</code> and either the object of
* the right is <code>null</code> or <code>objectMatcher</code> returns <code>true</code> when
* passed both objects. There's also a special case if <code>object</code> is <code>null</code>
* : in that case all found objects must be allowed until a right with a <code>null</code>
* object for the right to be granted. With these rules setting the object of the right to
* <code>null</code> means giving the right to any object. And searching for the object
* <code>null</code> means asking if the right is allowed for all the objects. <br>
* For example if you have these rights (* meaning <code>null</code>) :
* <ol>
* <li>del T yes</li>
* <li>ins T no</li>
* <li>del T no</li>
* <li>ins * yes</li>
* <li>del * yes</li>
* </ol>
* then you can delete from T but not insert ; you can however do both on any other object. If
* you pass <code>null</code> for <code>object</code>, it will return <code>true</code> for del,
* but <code>false</code> for ins.
*
* @param userID the user.
* @param code the requested right.
* @param requestedObject the requested object, can be <code>null</code>.
* @param objectMatcher how to match objects, first parameter passed is the right object, the
* second is <code>requestedObject</code>.
* @return <code>true</code> if the right is allowed.
*/
public final boolean haveRight(final int userID, final String code, final String requestedObject, final Equalizer<? super String> objectMatcher) {
final Set<String> unicity = new HashSet<String>();
final Boolean userRight = haveRightP(userID, code, requestedObject, objectMatcher, unicity);
if (userRight != null)
return userRight;
final int defaultUser = getDefaultUserId();
if (defaultUser != userID) {
final Boolean defaultRight = haveRightP(defaultUser, code, requestedObject, objectMatcher, unicity);
if (defaultRight != null)
return defaultRight;
}
return false;
}
private final Boolean haveRightP(final int userID, final String code, final String object, final Equalizer<? super String> objectMatcher, Set<String> unicity) {
final ListMapItf<String, Tuple2<String, Boolean>> rightsForUser = getRightsForUser(userID);
// super-user
if (rightsForUser == SUPERUSER_RIGHTS)
return true;
if (rightsForUser == NO_RIGHTS)
return false;
if (rightsForUser.containsKey(code)) {
for (final Tuple2<String, Boolean> t : rightsForUser.getNonNull(code)) {
// as explained in expand() we need unicity for null object, we have it for each
// user, but we also need it between userID and undefinedID
if (unicity.add(t.get0())) {
// if the object of the right matches the requested object :
// null for the right matches any requested object
// null for the requested object means searching for all objects so we can't let
// objectMatcher match and thus ignore subsequent objects
if (t.get0() == null || (object != null && safeEquals(objectMatcher, t, object)))
return t.get1();
// but null for the requested object means that all right objects must be true
else if (object == null && !t.get1())
return false;
}
}
}
return null;
}
private boolean safeEquals(final Equalizer<? super String> objectMatcher, final Tuple2<String, Boolean> t, final String requestedObject) {
final String rightObject = t.get0();
try {
return objectMatcher.equals(rightObject, requestedObject);
} catch (Exception e) {
// if the right could be allowed we don't match (so the row is ignored)
// if the right could be disallowed we match
final boolean res = !t.get1();
final String desc = !res ? "Row ignored." : "Right denied.";
Log.get().warning("Couldn't compare " + rightObject + " and " + requestedObject + ". " + desc);
e.printStackTrace();
return res;
}
}
final Set<Integer> getNonBlockingUsers() {
synchronized (this.rights) {
return Collections.unmodifiableSet(new HashSet<Integer>(this.rights.keySet()));
}
}
private ListMapItf<String, Tuple2<String, Boolean>> getRightsForUser(final int userID) {
// MAYBE add an option to call waitForCurrentRefresh() here, to be sure rights are up to
// date
return this.getRightsForUser(userID, true);
}
private ListMapItf<String, Tuple2<String, Boolean>> getRightsForUser(final int userID, final boolean checkMap) {
if (checkMap) {
synchronized (this.rights) {
if (this.rights.containsKey(userID)) {
return this.rights.get(userID);
}
}
}
final Set<SQLData> data = new HashSet<SQLData>();
// for changes in admin/superuser for user
data.add(new SQLRow(getUserTable(), userID));
// for change in rights for user
data.add(getTable());
// for change in java rights (data is a Set : no need to check IDs)
data.add(new JavaRightsUser(this.javaRights, userID));
data.add(new JavaRightsUser(this.javaRights, ADMIN_ID));
data.add(new JavaRightsUser(this.javaRights, getDefaultUserId()));
final CacheResult<ListMapItf<String, Tuple2<String, Boolean>>> cached = this.cache.check(userID, data);
if (cached.getState() == CacheResult.State.INTERRUPTED)
throw new RTInterruptedException("interrupted while waiting for the cache");
else if (cached.getState() == CacheResult.State.VALID)
return cached.getRes();
final ListMapItf<String, Tuple2<String, Boolean>> res;
try {
res = loadRightsForUser(userID);
this.cache.put(cached, res);
} catch (RuntimeException exn) {
this.cache.removeRunning(cached);
throw exn;
}
assert res != null;
return res;
}
/**
* Charge les droits définit dans la table USER_RIGHT.
*
* @param userID which user.
* @return the immutable user's rights by CODE.
*/
private final ListMapItf<String, Tuple2<String, Boolean>> loadRightsForUser(final int userID) {
// snapshot of java rights
final ListMapItf<Integer, RightTuple> javaRights = this.javaRights.getTuples();
try {
// this method is called to fill our cache, so don't use the data source cache
// (SQLRowValuesListFetcher below never uses the cache)
final SQLRow userRow = new SQLRow(getUserTable(), userID).fetchValues(false);
if (userRow != null && userRow.getBoolean(SUPERUSER_FIELD))
return SUPERUSER_RIGHTS;
final ListMap<String, Tuple2<String, Boolean>> res = new ListMap<String, Tuple2<String, Boolean>>();
final Set<Tuple2<String, String>> unicity = new HashSet<Tuple2<String, String>>();
// only superuser can modify RIGHTs
expand(res, unicity, TableAllRights.createRight(TableAllRights.CODE_MODIF, this.getTable().getForeignTable("ID_RIGHT"), false));
// only admin can modify or see USER_RIGHTs
final boolean isAdmin = userRow != null && userRow.getBoolean(ADMIN_FIELD);
expand(res, unicity, TableAllRights.createRight(TableAllRights.CODE, this.getTable(), isAdmin));
// java rights have priority over SQL rights
for (final RightTuple t : javaRights.getNonNull(userID)) {
expand(res, unicity, t);
}
// perhaps allow SQL to also specify admin rights
if (isAdmin) {
for (final RightTuple t : javaRights.getNonNull(ADMIN_ID))
expand(res, unicity, t);
}
final int defaultUser = getDefaultUserId();
if (defaultUser != userID) {
// even default java rights are before user SQL rights
for (final RightTuple t : javaRights.getNonNull(defaultUser)) {
expand(res, unicity, t);
}
}
final SQLRowValues vals = new SQLRowValues(getTable()).setAllToNull();
vals.putRowValues("ID_RIGHT").setAllToNull();
final SQLRowValuesListFetcher sel = new SQLRowValuesListFetcher(vals);
sel.setOrdered(true);
sel.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
@Override
public SQLSelect transformChecked(final SQLSelect sel) {
sel.setWhere(new Where(UserRightsManager.this.toUserLink.getLabel(), "=", userID));
return sel;
}
});
final List<SQLRowValues> list = sel.fetch();
for (final SQLRowValues row : list) {
final SQLRowAccessor right = row.getForeign("ID_RIGHT");
if (row.isUndefined()) {
Log.get().warning(row.asRow() + " has undef right");
} else {
final String rightCode = right.getString("CODE");
// do *not* load null code has it means SUPERUSER
if (rightCode == null)
Log.get().warning(right + " has null CODE");
else {
final String object = row.getString("OBJECT");
final Boolean haveRight = row.getBoolean("HAVE_RIGHT");
expand(res, unicity, rightCode, object, haveRight);
}
}
}
return ListMap.unmodifiableMap(res);
} catch (Exception e) {
ExceptionHandler.handle("Erreur lors du chargement des droits utilisateurs pour l'utilisateur (Id:" + userID + ")", e);
return NO_RIGHTS;
}
}
private final void expand(final ListMap<String, Tuple2<String, Boolean>> res, final Set<Tuple2<String, String>> unicity, final RightTuple t) {
this.expand(res, unicity, t.get0(), t.get1(), t.get2());
}
private final void expand(final ListMap<String, Tuple2<String, Boolean>> res, final Set<Tuple2<String, String>> unicity, final String rightCode, final String object, final Boolean haveRight) {
if (haveRight == null)
throw new IllegalStateException("HAVE_RIGHT cannot be null");
// OK since macroRights doesn't contain null
final MacroRight macroRight = this.macroRights.get(rightCode);
if (macroRight != null) {
for (final RightTuple t : macroRight.expand(this, rightCode, object, haveRight)) {
expand(res, unicity, t);
}
} else if (unicity.add(Tuple2.create(rightCode, object))) {
// we need to have unique rights, otherwise simple queries will still work since they
// will stop at the first match. But for queries with null object we need to traverse
// all rights.
res.add(rightCode, Tuple2.create(object, haveRight));
}
}
/**
* Return the list of objects the passed user is allowed for the passed code.
*
* @param userID the user.
* @param code the requested right.
* @param allObjects depending on the rights it might be necessary to know the full list of
* possible values.
* @return the allowed objects or <code>null</code> if they're all allowed.
*/
public final Set<String> getObjects(final int userID, final String code, final IFactory<Set<String>> allObjects) {
// test for everything to avoid calling allObjects
// (also takes care of superuser)
if (this.haveRight(userID, code))
return null;
// the above line handles "* true", MAYBE we should search for e.g. "A false, * false"
// and then return {}.
// try to add all objects which we are allowed to
// but stop at the first null since it means we have to do a subtraction.
// (e.g. A f, * t)
final Set<String> unicity = new HashSet<String>();
final Set<String> userRight = getObjectsP(userID, code, unicity);
if (userRight != null) {
final Set<String> defaultRight = getObjectsP(getDefaultUserId(), code, unicity);
if (defaultRight != null) {
userRight.addAll(defaultRight);
return userRight;
}
}
// there was at least one null
final Set<String> res = new HashSet<String>();
for (final String object : allObjects.createChecked()) {
if (this.haveRight(userID, code, object))
res.add(object);
}
return res;
}
final int getDefaultUserId() {
return getUserTable().getUndefinedID();
}
public void preloadRightsForUserId(int userID) {
getRightsForUser(getDefaultUserId());
getRightsForUser(userID);
}
/**
* Invalidate all cached rights (the next call to {@link #haveRight(int, String)} will wait for
* the new rights from the DB).
*/
public void clearRights() {
// this will also clear this.rights, see cacheChanged()
this.cache.clear();
}
private final Set<String> getObjectsP(final int userID, final String code, Set<String> unicity) {
final ListMapItf<String, Tuple2<String, Boolean>> rightsForUser = getRightsForUser(userID);
// don't let it proceed, otherwise it will then load objects for undef
if (rightsForUser == NO_RIGHTS)
return null;
final Set<String> res = new HashSet<String>();
if (rightsForUser.containsKey(code)) {
for (final Tuple2<String, Boolean> t : rightsForUser.getNonNull(code)) {
// as usual don't let following rights overwrite preceding ones (ie if userID has
// "A false" and undef has "A true", then the second one should be ignored)
if (unicity.add(t.get0())) {
if (t.get0() == null)
return null;
else if (t.get1())
res.add(t.get0());
}
}
}
return res;
}
public final Set<String> getObjects(final int userID, final String code, final Set<String> objectsToTest, final Equalizer<? super String> objectMatcher) {
// test for everything to avoid looping through potentially numerous allObjects
// (also takes care of superuser)
if (this.haveRight(userID, code, null, objectMatcher))
return objectsToTest;
// if userID hasn't the right to any object we can't try to list objects since in this case
// (with an Equalizer) the objects in the DB are more like patterns.
final Set<String> res = new HashSet<String>();
for (final String object : objectsToTest) {
if (this.haveRight(userID, code, object, objectMatcher))
res.add(object);
}
return res;
}
static public final class RightTuple extends Tuple3<String, String, Boolean> {
public RightTuple(final String code, final boolean haveRight) {
this(code, null, haveRight);
}
public RightTuple(final String code, final String object, final boolean haveRight) {
super(code, object, haveRight);
}
}
}