OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

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);
        }
    }
}