OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.users.rights;
15
 
16
import org.openconcerto.sql.Log;
17
import org.openconcerto.sql.model.DBRoot;
144 ilm 18
import org.openconcerto.sql.model.SQLData;
19
import org.openconcerto.sql.model.SQLDataListener;
17 ilm 20
import org.openconcerto.sql.model.SQLRow;
21
import org.openconcerto.sql.model.SQLRowAccessor;
22
import org.openconcerto.sql.model.SQLRowValues;
23
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
24
import org.openconcerto.sql.model.SQLSelect;
25
import org.openconcerto.sql.model.SQLTable;
26
import org.openconcerto.sql.model.SQLTableModifiedListener;
27
import org.openconcerto.sql.model.Where;
144 ilm 28
import org.openconcerto.sql.model.graph.Link;
29
import org.openconcerto.sql.request.SQLCache;
30
import org.openconcerto.sql.request.SQLCacheWatcher;
17 ilm 31
import org.openconcerto.sql.users.UserManager;
144 ilm 32
import org.openconcerto.sql.users.UserSingleton;
33
import org.openconcerto.sql.users.UserSingletonManager;
34
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
41 ilm 35
import org.openconcerto.utils.CompareUtils;
36
import org.openconcerto.utils.CompareUtils.Equalizer;
19 ilm 37
import org.openconcerto.utils.ExceptionHandler;
144 ilm 38
import org.openconcerto.utils.IFutureTask;
80 ilm 39
import org.openconcerto.utils.ListMap;
144 ilm 40
import org.openconcerto.utils.RTInterruptedException;
41
import org.openconcerto.utils.ThreadFactory;
17 ilm 42
import org.openconcerto.utils.Tuple2;
43
import org.openconcerto.utils.Tuple3;
144 ilm 44
import org.openconcerto.utils.cache.CacheItem.RemovalType;
45
import org.openconcerto.utils.cache.CacheResult;
46
import org.openconcerto.utils.cache.CacheWatcher;
47
import org.openconcerto.utils.cache.CacheWatcherFactory;
48
import org.openconcerto.utils.cache.ICache;
49
import org.openconcerto.utils.cache.ICache.ItemEvent;
50
import org.openconcerto.utils.cache.ICacheSupport;
51
import org.openconcerto.utils.cc.IClosure;
17 ilm 52
import org.openconcerto.utils.cc.IFactory;
53
import org.openconcerto.utils.cc.ITransformer;
54
 
144 ilm 55
import java.beans.PropertyChangeEvent;
56
import java.beans.PropertyChangeListener;
57
import java.beans.PropertyChangeSupport;
17 ilm 58
import java.util.ArrayList;
73 ilm 59
import java.util.Collections;
17 ilm 60
import java.util.HashMap;
61
import java.util.HashSet;
62
import java.util.List;
63
import java.util.Map;
64
import java.util.Set;
144 ilm 65
import java.util.concurrent.Callable;
66
import java.util.concurrent.ExecutionException;
67
import java.util.concurrent.ExecutorService;
68
import java.util.concurrent.Executors;
69
import java.util.concurrent.Future;
17 ilm 70
 
73 ilm 71
import net.jcip.annotations.GuardedBy;
72
 
144 ilm 73
public class UserRightsManager implements UserSingleton {
17 ilm 74
 
73 ilm 75
    public static final String USER_RIGHT_TABLE = UserRightSQLElement.TABLE_NAME;
144 ilm 76
    private static final UserSingletonManager<UserRightsManager> sMngr = new UserSingletonManager<UserRightsManager>(USER_RIGHT_TABLE) {
77
        @Override
78
        protected UserRightsManager createInstance(SQLTable t) {
79
            return new UserRightsManager(t);
80
        }
81
    };
82
 
73 ilm 83
    public static final String SUPERUSER_FIELD = "SUPERUSER";
80 ilm 84
    private static final int ADMIN_ID = SQLRow.NONEXISTANT_ID;
73 ilm 85
    /**
86
     * Only administrators can see user rights.
87
     */
88
    public static final String ADMIN_FIELD = "ADMIN";
83 ilm 89
    private static final ListMap<String, Tuple2<String, Boolean>> SUPERUSER_RIGHTS = ListMap.singleton(null, Tuple2.create((String) null, true));
90
    private static final ListMap<String, Tuple2<String, Boolean>> NO_RIGHTS = ListMap.singleton(null, Tuple2.create((String) null, false));
73 ilm 91
    public static final List<MacroRight> DEFAULT_MACRO_RIGHTS = Collections.synchronizedList(new ArrayList<MacroRight>());
92
    static {
80 ilm 93
        // "addRight() ambiguous"
94
        assert ADMIN_ID < SQLRow.MIN_VALID_ID;
73 ilm 95
        DEFAULT_MACRO_RIGHTS.add(new LockAdminUserRight());
96
        DEFAULT_MACRO_RIGHTS.add(new TableAllRights(true));
97
        DEFAULT_MACRO_RIGHTS.add(new TableAllRights(false));
98
    }
17 ilm 99
 
144 ilm 100
    public static UserSingletonManager<UserRightsManager> getSingletonManager() {
101
        return sMngr;
73 ilm 102
    }
103
 
104
    /**
105
     * Set the instance using the table in the passed root.
106
     *
107
     * @param root the root where the rights should be.
108
     * @return the new instance, <code>null</code> if <code>root</code> does not contain the
109
     *         {@value #USER_RIGHT_TABLE} table.
110
     */
111
    public static UserRightsManager setInstanceFromRoot(final DBRoot root) {
144 ilm 112
        return getSingletonManager().setInstanceFromRoot(root);
73 ilm 113
    }
114
 
115
    /**
116
     * Set the instance.
117
     *
118
     * @param t the table, <code>null</code> to remove.
119
     * @return the new instance, <code>null</code> if t was.
120
     */
144 ilm 121
    public static UserRightsManager setInstance(final SQLTable t) {
122
        return getSingletonManager().setInstance(t);
73 ilm 123
    }
124
 
144 ilm 125
    public static UserRightsManager getInstance() {
126
        return getSingletonManager().getInstance();
17 ilm 127
    }
128
 
129
    public static final UserRights getCurrentUserRights() {
130
        final UserManager mngr = UserManager.getInstance();
182 ilm 131
        final UserRightsManager rightsMngr = getInstance();
132
        return getCurrentUserRights(rightsMngr, mngr);
133
    }
134
 
135
    public static final UserRights getCurrentUserRights(final UserRightsManager rightsMngr, final UserManager mngr) {
17 ilm 136
        // if right table doesn't exist, give access to everything
144 ilm 137
        if (rightsMngr == null)
73 ilm 138
            return UserRights.ALLOW_ALL;
17 ilm 139
        // else if there are rights (and thus users) but no user is defined, use the default rights
140
        else
144 ilm 141
            return rightsMngr.getUserRights(mngr.getCurrentUser() == null ? null : mngr.getCurrentUser().getId());
17 ilm 142
    }
143
 
144 ilm 144
    private static final class JavaRights {
145
 
146
        static final String TUPLES_CHANGED = "tuples";
147
        static final String TUPLE_CHANGED = "tuple";
148
 
149
        // rights by user ID, immutable
150
        @GuardedBy("this")
151
        private ListMapItf<Integer, RightTuple> tuples;
152
        private final PropertyChangeSupport propSupp = new PropertyChangeSupport(this);
153
 
154
        public JavaRights() {
155
            this.tuples = ListMap.empty();
156
        }
157
 
158
        final void changeRight(final boolean add, final Integer userID, final RightTuple right) {
159
            if (add && right == null)
160
                throw new NullPointerException("Null right entry");
161
            final ListMapItf<Integer, RightTuple> oldVal, newVal;
162
            synchronized (this) {
163
                oldVal = this.tuples;
164
                final ListMap<Integer, RightTuple> newMap = new ListMap<Integer, RightTuple>(this.tuples);
165
                if (add)
166
                    newMap.add(userID, right);
167
                else if (right == null)
168
                    newMap.remove(userID);
169
                else
170
                    newMap.removeAllInstancesOfItem(userID, right);
171
                newVal = ListMap.unmodifiableMap(newMap);
172
                this.tuples = newVal;
173
            }
174
            this.propSupp.firePropertyChange(TUPLES_CHANGED, oldVal, newVal);
175
            this.propSupp.firePropertyChange(TUPLE_CHANGED, null, userID);
176
        }
177
 
178
        final synchronized ListMapItf<Integer, RightTuple> getTuples() {
179
            return this.tuples;
180
        }
181
    }
182
 
183
    // cheat a little by pretending to be SQL, so that java rights can be listened to by the cache.
184
    private static class JavaRightsUser implements SQLData {
185
 
186
        private final JavaRights rights;
187
        private final int id;
188
 
189
        public JavaRightsUser(final JavaRights rights, final int id) {
190
            super();
191
            this.rights = rights;
192
            this.id = id;
193
        }
194
 
195
        @Override
196
        public SQLTableModifiedListener createTableListener(final SQLDataListener l) {
197
            return null;
198
        }
199
 
200
        @Override
201
        public SQLTable getTable() {
202
            return null;
203
        }
204
 
205
        @Override
206
        public int hashCode() {
207
            final int prime = 31;
208
            int result = 1;
209
            result = prime * result + this.id;
210
            result = prime * result + this.rights.hashCode();
211
            return result;
212
        }
213
 
214
        @Override
215
        public boolean equals(Object obj) {
216
            if (this == obj)
217
                return true;
218
            if (obj == null)
219
                return false;
220
            if (getClass() != obj.getClass())
221
                return false;
222
            final JavaRightsUser other = (JavaRightsUser) obj;
223
            return this.id == other.id && this.rights.equals(other.rights);
224
        }
225
    }
226
 
227
    private static class JavaRightsWatcher extends CacheWatcher<SQLData> {
228
 
229
        private final int id;
230
        private final PropertyChangeListener l;
231
 
232
        public JavaRightsWatcher(JavaRightsUser u) {
233
            super(u);
234
            this.id = u.id;
235
            this.l = new PropertyChangeListener() {
236
                @Override
237
                public void propertyChange(PropertyChangeEvent evt) {
238
                    if (((Number) evt.getNewValue()).intValue() == JavaRightsWatcher.this.id) {
239
                        dataChanged(evt);
240
                    }
241
                }
242
            };
243
        }
244
 
245
        @Override
246
        protected void startWatching() {
247
            ((JavaRightsUser) this.getData()).rights.propSupp.addPropertyChangeListener(JavaRights.TUPLE_CHANGED, this.l);
248
        }
249
 
250
        @Override
251
        protected void stopWatching() {
252
            ((JavaRightsUser) this.getData()).rights.propSupp.removePropertyChangeListener(JavaRights.TUPLE_CHANGED, this.l);
253
        }
254
    }
255
 
17 ilm 256
    // Gérer un droit avec une classe
144 ilm 257
    @GuardedBy("macroRights")
17 ilm 258
    private final Map<String, MacroRight> macroRights;
259
    // {user -> {code -> [<object, bool>]}}
144 ilm 260
    @GuardedBy("rights")
261
    private final Map<Integer, ListMapItf<String, Tuple2<String, Boolean>>> rights;
262
    private final SQLCache<Integer, ListMapItf<String, Tuple2<String, Boolean>>> cache;
73 ilm 263
    private final SQLTable table;
144 ilm 264
    private final Link toUserLink;
265
    private final JavaRights javaRights;
266
 
73 ilm 267
    @GuardedBy("this")
144 ilm 268
    private final Map<Integer, UserRights> userRights;
17 ilm 269
 
144 ilm 270
    private final ExecutorService exec;
271
 
73 ilm 272
    private UserRightsManager(final SQLTable t) {
273
        if (t == null)
274
            throw new NullPointerException("Missing table");
144 ilm 275
        this.macroRights = Collections.synchronizedMap(new HashMap<String, MacroRight>());
276
        this.rights = new HashMap<Integer, ListMapItf<String, Tuple2<String, Boolean>>>();
277
        this.cache = new SQLCache<Integer, ListMapItf<String, Tuple2<String, Boolean>>>(15 * 60, -1, "Cache of rights") {
73 ilm 278
            @Override
144 ilm 279
            protected ICacheSupport<SQLData> createSupp(String name) {
280
                final ICacheSupport<SQLData> res = new ICacheSupport<SQLData>(name);
281
                res.setWatcherFactory(new CacheWatcherFactory<SQLData>() {
282
                    @Override
283
                    public CacheWatcher<SQLData> createWatcher(SQLData o) {
284
                        if (o instanceof JavaRightsUser) {
285
                            return new JavaRightsWatcher((JavaRightsUser) o);
286
                        } else {
287
                            return new SQLCacheWatcher(o);
288
                        }
289
                    }
290
                });
291
                return res;
73 ilm 292
            }
293
        };
144 ilm 294
        this.javaRights = new JavaRights();
295
        this.table = t;
156 ilm 296
        this.toUserLink = t.getField("ID_USER_COMMON").getFieldGroup().getForeignLink();
17 ilm 297
        defaultRegister();
144 ilm 298
        this.userRights = new HashMap<Integer, UserRights>();
299
        this.exec = Executors.newSingleThreadExecutor(new ThreadFactory(this.getClass().getSimpleName() + " executor for " + t.getSQLName(), true).setPriority(Thread.MIN_PRIORITY));
300
 
301
        this.cache.addItemListener(new IClosure<ItemEvent<Integer, ListMapItf<String, Tuple2<String, Boolean>>, SQLData>>() {
302
            @Override
303
            public void executeChecked(ItemEvent<Integer, ListMapItf<String, Tuple2<String, Boolean>>, SQLData> evt) {
304
                cacheChanged(evt);
305
            }
306
        });
17 ilm 307
    }
308
 
144 ilm 309
    private void cacheChanged(final ItemEvent<Integer, ListMapItf<String, Tuple2<String, Boolean>>, ?> evt) {
310
        if (evt.getPropertyName().equals(ICache.ITEM_ADDED)) {
311
            synchronized (this.rights) {
312
                this.rights.put(evt.getNewValue().getKey(), evt.getNewValue().getValue());
313
            }
314
        } else if (evt.getPropertyName().equals(ICache.ITEM_REMOVED)) {
315
            final Integer userID = evt.getOldValue().getKey();
316
            final RemovalType removalType = evt.getOldValue().getRemovalType();
317
            if (removalType == RemovalType.DATA_CHANGE || removalType == RemovalType.TIMEOUT) {
318
                invokeLater(new Callable<Object>() {
319
                    @Override
320
                    public Object call() throws Exception {
321
                        // we know the value is in the map, but we want to refresh it so
322
                        // don't use the map and only update the cache. Which would then
323
                        // trigger ICache.ITEM_ADDED.
324
                        getRightsForUser(userID.intValue(), false);
325
                        return null;
326
                    }
327
                });
328
            } else {
329
                assert removalType != RemovalType.SIZE_LIMIT;
330
                synchronized (this.rights) {
331
                    this.rights.remove(userID);
332
                }
333
            }
334
        }
335
    }
336
 
17 ilm 337
    /**
338
     * enregistre les instances gérants les droits
339
     */
340
    private void defaultRegister() {
73 ilm 341
        synchronized (DEFAULT_MACRO_RIGHTS) {
342
            for (final MacroRight macroRight : DEFAULT_MACRO_RIGHTS) {
343
                register(macroRight);
344
            }
345
        }
17 ilm 346
    }
347
 
348
    /**
349
     * Ajoute une instance pour la gestion d'un droit
350
     *
351
     * @param userRight the instance which will now be used for <code>userRight.getCode()</code>.
352
     */
353
    public void register(final MacroRight userRight) {
144 ilm 354
        if (userRight == null)
355
            throw new IllegalArgumentException("Missing right");
17 ilm 356
        this.macroRights.put(userRight.getCode(), userRight);
357
    }
358
 
359
    /**
80 ilm 360
     * Add an unconditional right for the passed user. I.e. it is loaded before the SQL ones and
361
     * even default unconditional rights are before user SQL rights.
17 ilm 362
     *
363
     * @param userID the user id, <code>null</code> meaning for everyone.
364
     * @param right the right the user should always have.
365
     */
366
    public void addRight(Integer userID, RightTuple right) {
144 ilm 367
        this.javaRights.changeRight(true, getKey(userID), right);
17 ilm 368
    }
369
 
80 ilm 370
    /**
371
     * Add an unconditional right for administrators. This will be after user rights and before
372
     * default rights.
373
     *
374
     * @param right the right to add.
375
     */
376
    public void addRightForAdmins(RightTuple right) {
144 ilm 377
        this.javaRights.changeRight(true, ADMIN_ID, right);
80 ilm 378
    }
379
 
380
    private final int getKey(final Integer userID) {
381
        if (userID != null && userID < SQLRow.MIN_VALID_ID)
382
            throw new IllegalArgumentException("invalid ID : " + userID);
383
        return userID == null ? getDefaultUserId() : userID;
384
    }
385
 
386
    /**
387
     * Remove a right.
388
     *
389
     * @param userID the user id, <code>null</code> meaning default user.
390
     * @param right the right to remove, <code>null</code> meaning remove all.
391
     * @see #addRight(Integer, RightTuple)
392
     */
393
    public void removeRight(final Integer userID, final RightTuple right) {
144 ilm 394
        this.javaRights.changeRight(false, getKey(userID), right);
80 ilm 395
    }
396
 
397
    public void removeRightForAdmins(final RightTuple right) {
144 ilm 398
        this.javaRights.changeRight(false, ADMIN_ID, right);
80 ilm 399
    }
400
 
73 ilm 401
    public synchronized final boolean isValid() {
144 ilm 402
        return !this.cache.getSupp().isDying();
17 ilm 403
    }
404
 
144 ilm 405
    @Override
73 ilm 406
    public synchronized final void destroy() {
407
        if (this.isValid()) {
144 ilm 408
            this.cache.getSupp().die();
409
            this.exec.shutdown();
73 ilm 410
        }
411
        assert !this.isValid();
412
    }
413
 
144 ilm 414
    @Override
17 ilm 415
    public final SQLTable getTable() {
416
        return this.table;
417
    }
418
 
419
    public final DBRoot getRoot() {
420
        return this.getTable().getDBRoot();
421
    }
422
 
144 ilm 423
    private final SQLTable getUserTable() {
424
        return this.toUserLink.getTarget();
425
    }
426
 
427
    public synchronized final <T> Future<T> invokeLater(final Callable<T> c) {
428
        if (!this.isValid())
429
            return null;
430
        return this.exec.submit(c);
431
    }
432
 
433
    /**
434
     * Block until all out of date rights are refreshed. Useful since <code>haveRight()</code> will
435
     * return the last known rights while a refresh is ongoing.
436
     *
437
     * @return <code>true</code> if this method blocked, <code>false</code> if this was already
438
     *         {@link #isValid() invalid}.
439
     * @throws InterruptedException if the current thread was interrupted while waiting.
440
     */
441
    public final boolean waitForCurrentRefresh() throws InterruptedException {
442
        try {
443
            final Future<Object> f = this.invokeLater(IFutureTask.getNoOpCallable());
444
            if (f == null)
445
                return false;
446
            f.get();
447
            return true;
448
        } catch (ExecutionException e) {
449
            throw new IllegalStateException("No-op failed", e);
450
        }
451
    }
452
 
453
    public final synchronized UserRights getUserRights(Integer userID) {
454
        if (userID == null)
455
            userID = this.getDefaultUserId();
456
        UserRights res = this.userRights.get(userID);
457
        if (res == null) {
458
            res = new UserRights(this, userID);
459
            this.userRights.put(userID, res);
460
        }
461
        return res;
462
    }
463
 
17 ilm 464
    public final boolean haveRight(final int userID, final String code) {
465
        return this.haveRight(userID, code, null);
466
    }
467
 
41 ilm 468
    public final boolean haveRight(final int userID, final String code, final String object) {
469
        return this.haveRight(userID, code, object, CompareUtils.OBJECT_EQ);
470
    }
471
 
17 ilm 472
    /**
41 ilm 473
     * Whether <code>userID</code> should be allowed the <code>code</code> (e.g. DELETE) right on
474
     * <code>object</code> (e.g. TENSION).<br>
17 ilm 475
     * The rights are ordered and the first one that matches is returned. Furthermore after
476
     * searching for the passed <code>userID</code> the default user is searched. <br>
477
     * To match, the code of the right must be equal to <code>code</code> and either the object of
41 ilm 478
     * the right is <code>null</code> or <code>objectMatcher</code> returns <code>true</code> when
479
     * passed both objects. There's also a special case if <code>object</code> is <code>null</code>
480
     * : in that case all found objects must be allowed until a right with a <code>null</code>
80 ilm 481
     * object for the right to be granted. With these rules setting the object of the right to
482
     * <code>null</code> means giving the right to any object. And searching for the object
483
     * <code>null</code> means asking if the right is allowed for all the objects. <br>
17 ilm 484
     * For example if you have these rights (* meaning <code>null</code>) :
485
     * <ol>
486
     * <li>del T yes</li>
487
     * <li>ins T no</li>
488
     * <li>del T no</li>
489
     * <li>ins * yes</li>
490
     * <li>del * yes</li>
491
     * </ol>
80 ilm 492
     * then you can delete from T but not insert ; you can however do both on any other object. If
17 ilm 493
     * you pass <code>null</code> for <code>object</code>, it will return <code>true</code> for del,
494
     * but <code>false</code> for ins.
495
     *
496
     * @param userID the user.
497
     * @param code the requested right.
41 ilm 498
     * @param requestedObject the requested object, can be <code>null</code>.
499
     * @param objectMatcher how to match objects, first parameter passed is the right object, the
500
     *        second is <code>requestedObject</code>.
17 ilm 501
     * @return <code>true</code> if the right is allowed.
502
     */
41 ilm 503
    public final boolean haveRight(final int userID, final String code, final String requestedObject, final Equalizer<? super String> objectMatcher) {
17 ilm 504
        final Set<String> unicity = new HashSet<String>();
41 ilm 505
        final Boolean userRight = haveRightP(userID, code, requestedObject, objectMatcher, unicity);
17 ilm 506
        if (userRight != null)
507
            return userRight;
80 ilm 508
        final int defaultUser = getDefaultUserId();
509
        if (defaultUser != userID) {
510
            final Boolean defaultRight = haveRightP(defaultUser, code, requestedObject, objectMatcher, unicity);
511
            if (defaultRight != null)
512
                return defaultRight;
513
        }
17 ilm 514
 
515
        return false;
516
    }
517
 
41 ilm 518
    private final Boolean haveRightP(final int userID, final String code, final String object, final Equalizer<? super String> objectMatcher, Set<String> unicity) {
144 ilm 519
        final ListMapItf<String, Tuple2<String, Boolean>> rightsForUser = getRightsForUser(userID);
17 ilm 520
        // super-user
521
        if (rightsForUser == SUPERUSER_RIGHTS)
522
            return true;
41 ilm 523
        if (rightsForUser == NO_RIGHTS)
524
            return false;
17 ilm 525
 
526
        if (rightsForUser.containsKey(code)) {
527
            for (final Tuple2<String, Boolean> t : rightsForUser.getNonNull(code)) {
528
                // as explained in expand() we need unicity for null object, we have it for each
529
                // user, but we also need it between userID and undefinedID
530
                if (unicity.add(t.get0())) {
531
                    // if the object of the right matches the requested object :
532
                    // null for the right matches any requested object
41 ilm 533
                    // null for the requested object means searching for all objects so we can't let
534
                    // objectMatcher match and thus ignore subsequent objects
535
                    if (t.get0() == null || (object != null && safeEquals(objectMatcher, t, object)))
17 ilm 536
                        return t.get1();
537
                    // but null for the requested object means that all right objects must be true
538
                    else if (object == null && !t.get1())
539
                        return false;
540
                }
541
            }
542
        }
543
        return null;
544
    }
545
 
41 ilm 546
    private boolean safeEquals(final Equalizer<? super String> objectMatcher, final Tuple2<String, Boolean> t, final String requestedObject) {
547
        final String rightObject = t.get0();
548
        try {
549
            return objectMatcher.equals(rightObject, requestedObject);
550
        } catch (Exception e) {
551
            // if the right could be allowed we don't match (so the row is ignored)
552
            // if the right could be disallowed we match
553
            final boolean res = !t.get1();
554
            final String desc = !res ? "Row ignored." : "Right denied.";
555
            Log.get().warning("Couldn't compare " + rightObject + " and " + requestedObject + ". " + desc);
556
            e.printStackTrace();
557
            return res;
558
        }
559
    }
560
 
144 ilm 561
    final Set<Integer> getNonBlockingUsers() {
17 ilm 562
        synchronized (this.rights) {
144 ilm 563
            return Collections.unmodifiableSet(new HashSet<Integer>(this.rights.keySet()));
17 ilm 564
        }
565
    }
566
 
144 ilm 567
    private ListMapItf<String, Tuple2<String, Boolean>> getRightsForUser(final int userID) {
568
        // MAYBE add an option to call waitForCurrentRefresh() here, to be sure rights are up to
569
        // date
570
        return this.getRightsForUser(userID, true);
571
    }
572
 
573
    private ListMapItf<String, Tuple2<String, Boolean>> getRightsForUser(final int userID, final boolean checkMap) {
574
        if (checkMap) {
575
            synchronized (this.rights) {
576
                if (this.rights.containsKey(userID)) {
577
                    return this.rights.get(userID);
578
                }
17 ilm 579
            }
580
        }
144 ilm 581
        final Set<SQLData> data = new HashSet<SQLData>();
582
        // for changes in admin/superuser for user
583
        data.add(new SQLRow(getUserTable(), userID));
584
        // for change in rights for user
585
        data.add(getTable());
586
        // for change in java rights (data is a Set : no need to check IDs)
587
        data.add(new JavaRightsUser(this.javaRights, userID));
588
        data.add(new JavaRightsUser(this.javaRights, ADMIN_ID));
589
        data.add(new JavaRightsUser(this.javaRights, getDefaultUserId()));
590
        final CacheResult<ListMapItf<String, Tuple2<String, Boolean>>> cached = this.cache.check(userID, data);
591
        if (cached.getState() == CacheResult.State.INTERRUPTED)
592
            throw new RTInterruptedException("interrupted while waiting for the cache");
593
        else if (cached.getState() == CacheResult.State.VALID)
594
            return cached.getRes();
595
 
596
        final ListMapItf<String, Tuple2<String, Boolean>> res;
597
        try {
598
            res = loadRightsForUser(userID);
599
            this.cache.put(cached, res);
600
        } catch (RuntimeException exn) {
601
            this.cache.removeRunning(cached);
602
            throw exn;
603
        }
604
        assert res != null;
605
        return res;
17 ilm 606
    }
607
 
608
    /**
609
     * Charge les droits définit dans la table USER_RIGHT.
610
     *
611
     * @param userID which user.
144 ilm 612
     * @return the immutable user's rights by CODE.
17 ilm 613
     */
144 ilm 614
    private final ListMapItf<String, Tuple2<String, Boolean>> loadRightsForUser(final int userID) {
615
        // snapshot of java rights
616
        final ListMapItf<Integer, RightTuple> javaRights = this.javaRights.getTuples();
19 ilm 617
        try {
144 ilm 618
            // this method is called to fill our cache, so don't use the data source cache
619
            // (SQLRowValuesListFetcher below never uses the cache)
620
            final SQLRow userRow = new SQLRow(getUserTable(), userID).fetchValues(false);
73 ilm 621
            if (userRow != null && userRow.getBoolean(SUPERUSER_FIELD))
19 ilm 622
                return SUPERUSER_RIGHTS;
17 ilm 623
 
83 ilm 624
            final ListMap<String, Tuple2<String, Boolean>> res = new ListMap<String, Tuple2<String, Boolean>>();
19 ilm 625
            final Set<Tuple2<String, String>> unicity = new HashSet<Tuple2<String, String>>();
626
            // only superuser can modify RIGHTs
627
            expand(res, unicity, TableAllRights.createRight(TableAllRights.CODE_MODIF, this.getTable().getForeignTable("ID_RIGHT"), false));
628
            // only admin can modify or see USER_RIGHTs
80 ilm 629
            final boolean isAdmin = userRow != null && userRow.getBoolean(ADMIN_FIELD);
630
            expand(res, unicity, TableAllRights.createRight(TableAllRights.CODE, this.getTable(), isAdmin));
17 ilm 631
 
19 ilm 632
            // java rights have priority over SQL rights
144 ilm 633
            for (final RightTuple t : javaRights.getNonNull(userID)) {
19 ilm 634
                expand(res, unicity, t);
635
            }
80 ilm 636
            // perhaps allow SQL to also specify admin rights
637
            if (isAdmin) {
144 ilm 638
                for (final RightTuple t : javaRights.getNonNull(ADMIN_ID))
80 ilm 639
                    expand(res, unicity, t);
19 ilm 640
            }
80 ilm 641
            final int defaultUser = getDefaultUserId();
642
            if (defaultUser != userID) {
643
                // even default java rights are before user SQL rights
144 ilm 644
                for (final RightTuple t : javaRights.getNonNull(defaultUser)) {
80 ilm 645
                    expand(res, unicity, t);
646
                }
647
            }
17 ilm 648
 
19 ilm 649
            final SQLRowValues vals = new SQLRowValues(getTable()).setAllToNull();
650
            vals.putRowValues("ID_RIGHT").setAllToNull();
17 ilm 651
 
19 ilm 652
            final SQLRowValuesListFetcher sel = new SQLRowValuesListFetcher(vals);
653
            sel.setOrdered(true);
654
            sel.setSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
655
                @Override
656
                public SQLSelect transformChecked(final SQLSelect sel) {
144 ilm 657
                    sel.setWhere(new Where(UserRightsManager.this.toUserLink.getLabel(), "=", userID));
19 ilm 658
                    return sel;
659
                }
660
            });
17 ilm 661
 
19 ilm 662
            final List<SQLRowValues> list = sel.fetch();
663
            for (final SQLRowValues row : list) {
664
                final SQLRowAccessor right = row.getForeign("ID_RIGHT");
665
                if (row.isUndefined()) {
666
                    Log.get().warning(row.asRow() + " has undef right");
667
                } else {
668
                    final String rightCode = right.getString("CODE");
669
                    // do *not* load null code has it means SUPERUSER
670
                    if (rightCode == null)
671
                        Log.get().warning(right + " has null CODE");
672
                    else {
673
                        final String object = row.getString("OBJECT");
674
                        final Boolean haveRight = row.getBoolean("HAVE_RIGHT");
675
                        expand(res, unicity, rightCode, object, haveRight);
676
                    }
17 ilm 677
                }
678
            }
19 ilm 679
 
144 ilm 680
            return ListMap.unmodifiableMap(res);
19 ilm 681
        } catch (Exception e) {
682
            ExceptionHandler.handle("Erreur lors du chargement des droits utilisateurs pour l'utilisateur (Id:" + userID + ")", e);
41 ilm 683
            return NO_RIGHTS;
17 ilm 684
        }
685
    }
686
 
83 ilm 687
    private final void expand(final ListMap<String, Tuple2<String, Boolean>> res, final Set<Tuple2<String, String>> unicity, final RightTuple t) {
17 ilm 688
        this.expand(res, unicity, t.get0(), t.get1(), t.get2());
689
    }
690
 
83 ilm 691
    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) {
17 ilm 692
        if (haveRight == null)
693
            throw new IllegalStateException("HAVE_RIGHT cannot be null");
694
 
144 ilm 695
        // OK since macroRights doesn't contain null
696
        final MacroRight macroRight = this.macroRights.get(rightCode);
697
        if (macroRight != null) {
698
            for (final RightTuple t : macroRight.expand(this, rightCode, object, haveRight)) {
17 ilm 699
                expand(res, unicity, t);
700
            }
701
        } else if (unicity.add(Tuple2.create(rightCode, object))) {
702
            // we need to have unique rights, otherwise simple queries will still work since they
703
            // will stop at the first match. But for queries with null object we need to traverse
704
            // all rights.
83 ilm 705
            res.add(rightCode, Tuple2.create(object, haveRight));
17 ilm 706
        }
707
    }
708
 
709
    /**
710
     * Return the list of objects the passed user is allowed for the passed code.
711
     *
712
     * @param userID the user.
713
     * @param code the requested right.
714
     * @param allObjects depending on the rights it might be necessary to know the full list of
715
     *        possible values.
716
     * @return the allowed objects or <code>null</code> if they're all allowed.
717
     */
718
    public final Set<String> getObjects(final int userID, final String code, final IFactory<Set<String>> allObjects) {
719
        // test for everything to avoid calling allObjects
720
        // (also takes care of superuser)
721
        if (this.haveRight(userID, code))
722
            return null;
723
 
41 ilm 724
        // the above line handles "* true", MAYBE we should search for e.g. "A false, * false"
17 ilm 725
        // and then return {}.
726
 
727
        // try to add all objects which we are allowed to
41 ilm 728
        // but stop at the first null since it means we have to do a subtraction.
729
        // (e.g. A f, * t)
17 ilm 730
        final Set<String> unicity = new HashSet<String>();
731
        final Set<String> userRight = getObjectsP(userID, code, unicity);
732
        if (userRight != null) {
65 ilm 733
            final Set<String> defaultRight = getObjectsP(getDefaultUserId(), code, unicity);
17 ilm 734
            if (defaultRight != null) {
735
                userRight.addAll(defaultRight);
736
                return userRight;
737
            }
738
        }
739
        // there was at least one null
740
        final Set<String> res = new HashSet<String>();
741
        for (final String object : allObjects.createChecked()) {
742
            if (this.haveRight(userID, code, object))
743
                res.add(object);
744
        }
745
        return res;
746
    }
747
 
144 ilm 748
    final int getDefaultUserId() {
749
        return getUserTable().getUndefinedID();
65 ilm 750
    }
751
 
752
    public void preloadRightsForUserId(int userID) {
753
        getRightsForUser(getDefaultUserId());
754
        getRightsForUser(userID);
755
    }
756
 
144 ilm 757
    /**
758
     * Invalidate all cached rights (the next call to {@link #haveRight(int, String)} will wait for
759
     * the new rights from the DB).
760
     */
761
    public void clearRights() {
762
        // this will also clear this.rights, see cacheChanged()
763
        this.cache.clear();
764
    }
765
 
17 ilm 766
    private final Set<String> getObjectsP(final int userID, final String code, Set<String> unicity) {
144 ilm 767
        final ListMapItf<String, Tuple2<String, Boolean>> rightsForUser = getRightsForUser(userID);
41 ilm 768
        // don't let it proceed, otherwise it will then load objects for undef
769
        if (rightsForUser == NO_RIGHTS)
770
            return null;
17 ilm 771
        final Set<String> res = new HashSet<String>();
772
        if (rightsForUser.containsKey(code)) {
773
            for (final Tuple2<String, Boolean> t : rightsForUser.getNonNull(code)) {
774
                // as usual don't let following rights overwrite preceding ones (ie if userID has
775
                // "A false" and undef has "A true", then the second one should be ignored)
776
                if (unicity.add(t.get0())) {
777
                    if (t.get0() == null)
778
                        return null;
779
                    else if (t.get1())
780
                        res.add(t.get0());
781
                }
782
            }
783
        }
784
        return res;
785
    }
786
 
41 ilm 787
    public final Set<String> getObjects(final int userID, final String code, final Set<String> objectsToTest, final Equalizer<? super String> objectMatcher) {
788
        // test for everything to avoid looping through potentially numerous allObjects
789
        // (also takes care of superuser)
790
        if (this.haveRight(userID, code, null, objectMatcher))
791
            return objectsToTest;
792
 
793
        // if userID hasn't the right to any object we can't try to list objects since in this case
794
        // (with an Equalizer) the objects in the DB are more like patterns.
795
        final Set<String> res = new HashSet<String>();
796
        for (final String object : objectsToTest) {
797
            if (this.haveRight(userID, code, object, objectMatcher))
798
                res.add(object);
799
        }
800
        return res;
801
    }
802
 
17 ilm 803
    static public final class RightTuple extends Tuple3<String, String, Boolean> {
804
        public RightTuple(final String code, final boolean haveRight) {
805
            this(code, null, haveRight);
806
        }
807
 
808
        public RightTuple(final String code, final String object, final boolean haveRight) {
809
            super(code, object, haveRight);
810
        }
811
    }
812
}