001/*
002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 */
013package org.nuxeo.ecm.core.api.security.impl;
014
015import java.io.IOException;
016import java.io.ObjectInputStream;
017import java.util.ArrayList;
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Set;
022
023import org.apache.commons.lang.StringUtils;
024import org.nuxeo.ecm.core.api.security.ACE;
025import org.nuxeo.ecm.core.api.security.ACL;
026import org.nuxeo.ecm.core.api.security.ACP;
027import org.nuxeo.ecm.core.api.security.Access;
028import org.nuxeo.ecm.core.api.security.SecurityConstants;
029import org.nuxeo.ecm.core.api.security.UserEntry;
030
031/**
032 * The ACP implementation uses a cache used when calling getAccess().
033 */
034public class ACPImpl implements ACP {
035
036    private static final long serialVersionUID = 1L;
037
038    private final List<ACL> acls;
039
040    private transient Map<String, Access> cache;
041
042    public ACPImpl() {
043        acls = new ArrayList<ACL>();
044        cache = new HashMap<String, Access>();
045    }
046
047    /**
048     * This method must append the ACL and not insert it since it is used to append the inherited ACL which is the less
049     * significant ACL.
050     */
051    @Override
052    public void addACL(ACL acl) {
053        assert acl != null;
054        ACL oldACL = getACL(acl.getName());
055        if (!acl.equals(oldACL)) {
056            // replace existing ACL instance different from acl having the same
057            // name, if any
058            if (oldACL != null) {
059                oldACL.clear();
060                oldACL.addAll(acl);
061            } else {
062                String name = acl.getName();
063                switch (name) {
064                case ACL.INHERITED_ACL:
065                    // add the inherited ACL always at the end
066                    acls.add(acl);
067                    break;
068                case ACL.LOCAL_ACL:
069                    // add the local ACL before the inherited if any
070                    ACL inherited = getACL(ACL.INHERITED_ACL);
071                    if (inherited != null) {
072                        int i = acls.indexOf(inherited);
073                        acls.add(i, acl);
074                    } else {
075                        acls.add(acl);
076                    }
077                    break;
078                default:
079                    ACL local = getACL(ACL.LOCAL_ACL);
080                    if (local != null) {
081                        int i = acls.indexOf(local);
082                        acls.add(i, acl);
083                    } else {
084                        inherited = getACL(ACL.INHERITED_ACL);
085                        if (inherited != null) {
086                            int i = acls.indexOf(inherited);
087                            acls.add(i, acl);
088                        } else {
089                            acls.add(acl);
090                        }
091                    }
092                }
093            }
094        }
095        // if oldACL and ACL are the same instance, we just need to clear
096        // the cache
097        cache.clear();
098    }
099
100    @Override
101    public void addACL(int pos, ACL acl) {
102        ACL oldACL = getACL(acl.getName());
103        if (oldACL != null) {
104            acls.remove(oldACL);
105        }
106        acls.add(pos, acl);
107        cache.clear();
108    }
109
110    @Override
111    public void addACL(String afterMe, ACL acl) {
112        if (afterMe == null) {
113            addACL(0, acl);
114        } else {
115            int i;
116            int len = acls.size();
117            for (i = 0; i < len; i++) {
118                if (acls.get(i).getName().equals(afterMe)) {
119                    break;
120                }
121            }
122            addACL(i + 1, acl);
123        }
124    }
125
126    @Override
127    public ACL getACL(String name) {
128        if (name == null) {
129            name = ACL.LOCAL_ACL;
130        }
131        int len = acls.size();
132        for (int i = 0; i < len; i++) {
133            ACL acl = acls.get(i);
134            if (acl.getName().equals(name)) {
135                return acl;
136            }
137        }
138        return null;
139    }
140
141    @Override
142    public ACL[] getACLs() {
143        return acls.toArray(new ACL[acls.size()]);
144    }
145
146    @Override
147    public ACL getMergedACLs(String name) {
148        ACL mergedAcl = new ACLImpl(name, true);
149        for (ACL acl : acls) {
150            mergedAcl.addAll(acl);
151        }
152        return mergedAcl;
153    }
154
155    public static ACL newACL(String name) {
156        return new ACLImpl(name);
157    }
158
159    @Override
160    public ACL removeACL(String name) {
161        for (int i = 0, len = acls.size(); i < len; i++) {
162            ACL acl = acls.get(i);
163            if (acl.getName().equals(name)) {
164                cache.clear();
165                return acls.remove(i);
166            }
167        }
168        return null;
169    }
170
171    @Override
172    public Access getAccess(String principal, String permission) {
173        // check first the cache
174        String key = principal + ':' + permission;
175        Access access = cache.get(key);
176        if (access == null) {
177            access = Access.UNKNOWN;
178            FOUND_ACE: for (ACL acl : acls) {
179                for (ACE ace : acl) {
180                    if (permissionsMatch(ace, permission) && principalsMatch(ace, principal)) {
181                        access = ace.isGranted() ? Access.GRANT : Access.DENY;
182                        break FOUND_ACE;
183                    }
184                }
185            }
186            cache.put(key, access);
187        }
188        return access;
189    }
190
191    @Override
192    public Access getAccess(String[] principals, String[] permissions) {
193        for (ACL acl : acls) {
194            for (ACE ace : acl) {
195                // only check for effective ACEs
196                if (ace.isEffective()) {
197                    // fully check ACE in turn against username/permissions
198                    // and usergroups/permgroups
199                    Access access = getAccess(ace, principals, permissions);
200                    if (access != Access.UNKNOWN) {
201                        return access;
202                    }
203                }
204            }
205        }
206        return Access.UNKNOWN;
207    }
208
209    public static Access getAccess(ACE ace, String[] principals, String[] permissions) {
210        String acePerm = ace.getPermission();
211        String aceUser = ace.getUsername();
212
213        for (String principal : principals) {
214            if (principalsMatch(aceUser, principal)) {
215                // check permission match only if principal is matching
216                for (String permission : permissions) {
217                    if (permissionsMatch(acePerm, permission)) {
218                        return ace.isGranted() ? Access.GRANT : Access.DENY;
219                    } // end permissionMatch
220                } // end perm for
221            } // end principalMatch
222        } // end princ for
223        return Access.UNKNOWN;
224    }
225
226    private static boolean permissionsMatch(ACE ace, String permission) {
227        String acePerm = ace.getPermission();
228
229        // RESTRICTED_READ needs special handling, is not implied by EVERYTHING.
230        if (!SecurityConstants.RESTRICTED_READ.equals(permission)) {
231            if (SecurityConstants.EVERYTHING.equals(acePerm)) {
232                return true;
233            }
234        }
235        return StringUtils.equals(acePerm, permission);
236    }
237
238    private static boolean permissionsMatch(String acePerm, String permission) {
239        // RESTRICTED_READ needs special handling, is not implied by EVERYTHING.
240        if (SecurityConstants.EVERYTHING.equals(acePerm)) {
241            if (!SecurityConstants.RESTRICTED_READ.equals(permission)) {
242                return true;
243            }
244        }
245        return StringUtils.equals(acePerm, permission);
246    }
247
248    private static boolean principalsMatch(ACE ace, String principal) {
249        String acePrincipal = ace.getUsername();
250        if (SecurityConstants.EVERYONE.equals(acePrincipal)) {
251            return true;
252        }
253        return StringUtils.equals(acePrincipal, principal);
254    }
255
256    private static boolean principalsMatch(String acePrincipal, String principal) {
257        if (SecurityConstants.EVERYONE.equals(acePrincipal)) {
258            return true;
259        }
260        return StringUtils.equals(acePrincipal, principal);
261    }
262
263    public void addAccessRule(String aclName, ACE ace) {
264        ACL acl = getACL(aclName);
265        if (acl == null) {
266            acl = new ACLImpl(aclName);
267            addACL(acl);
268        }
269        acl.add(ace);
270    }
271
272    @Override
273    public ACL getOrCreateACL(String name) {
274        ACL acl = getACL(name);
275        if (acl == null) {
276            acl = new ACLImpl(name);
277            addACL(acl);
278        }
279        return acl;
280    }
281
282    @Override
283    public ACL getOrCreateACL() {
284        return getOrCreateACL(ACL.LOCAL_ACL);
285    }
286
287    // Rules.
288
289    @Override
290    public void setRules(String aclName, UserEntry[] userEntries) {
291        setRules(aclName, userEntries, true);
292    }
293
294    @Override
295    public void setRules(String aclName, UserEntry[] userEntries, boolean overwrite) {
296
297        ACL acl = getACL(aclName);
298        if (acl == null) { // create the loca ACL
299            acl = new ACLImpl(aclName);
300            addACL(acl);
301        } else if (overwrite) {
302            // :XXX: Should not overwrite entries not given as parameters here.
303            acl.clear();
304        }
305        for (UserEntry entry : userEntries) {
306            String username = entry.getUserName();
307            for (String permission : entry.getGrantedPermissions()) {
308                acl.add(new ACE(username, permission, true));
309            }
310            for (String permission : entry.getDeniedPermissions()) {
311                acl.add(new ACE(username, permission, false));
312            }
313        }
314        cache.clear();
315    }
316
317    @Override
318    public void setRules(UserEntry[] userEntries) {
319        setRules(ACL.LOCAL_ACL, userEntries);
320    }
321
322    @Override
323    public void setRules(UserEntry[] userEntries, boolean overwrite) {
324        setRules(ACL.LOCAL_ACL, userEntries, overwrite);
325    }
326
327    // Serialization.
328
329    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
330        // always perform the default de-serialization first
331        in.defaultReadObject();
332        // initialize cache to avoid NPE
333        cache = new HashMap<String, Access>();
334    }
335
336    /*
337     * NXP-1822 Rux: method for validating in one shot the users allowed to perform an oration. It gets the list of
338     * individual permissions which supposedly all grant.
339     */
340    @Override
341    public String[] listUsernamesForAnyPermission(Set<String> perms) {
342        List<String> usernames = new ArrayList<String>();
343        ACL merged = getMergedACLs("merged");
344        for (ACE ace : merged.getACEs()) {
345            if (perms.contains(ace.getPermission()) && ace.isGranted()) {
346                String username = ace.getUsername();
347                if (!usernames.contains(username)) {
348                    usernames.add(username);
349                }
350            }
351        }
352        return usernames.toArray(new String[usernames.size()]);
353    }
354
355    @Override
356    public ACPImpl clone() {
357        ACPImpl copy = new ACPImpl();
358        for (ACL acl : acls) {
359            copy.acls.add((ACL) acl.clone());
360        }
361        return copy;
362    }
363
364    @Override
365    public boolean blockInheritance(String aclName, String username) {
366        if (aclName == null) {
367            throw new NullPointerException("'aclName' cannot be null");
368        }
369        if (username == null) {
370            throw new NullPointerException("'username' cannot be null");
371        }
372
373        ACL acl = getOrCreateACL(aclName);
374        boolean aclChanged = acl.blockInheritance(username);
375        if (aclChanged) {
376            addACL(acl);
377        }
378        return aclChanged;
379    }
380
381    @Override
382    public boolean unblockInheritance(String aclName) {
383        if (aclName == null) {
384            throw new NullPointerException("'aclName' cannot be null");
385        }
386
387        ACL acl = getOrCreateACL(aclName);
388        boolean aclChanged = acl.unblockInheritance();
389        if (aclChanged) {
390            addACL(acl);
391        }
392        return aclChanged;
393    }
394
395    @Override
396    public boolean addACE(String aclName, ACE ace) {
397        if (aclName == null) {
398            throw new NullPointerException("'aclName' cannot be null");
399        }
400
401        ACL acl = getOrCreateACL(aclName);
402        boolean aclChanged = acl.add(ace);
403        if (aclChanged) {
404            addACL(acl);
405        }
406        return aclChanged;
407    }
408
409    @Override
410    public boolean replaceACE(String aclName, ACE oldACE, ACE newACE) {
411        if (aclName == null) {
412            throw new NullPointerException("'aclName' cannot be null");
413        }
414
415        ACL acl = getOrCreateACL(aclName);
416        boolean aclChanged = acl.replace(oldACE, newACE);
417        if (aclChanged) {
418            addACL(acl);
419        }
420        return aclChanged;
421    }
422
423    @Override
424    public boolean removeACE(String aclName, ACE ace) {
425        if (aclName == null) {
426            throw new NullPointerException("'aclName' cannot be null");
427        }
428
429        ACL acl = getOrCreateACL(aclName);
430        boolean aclChanged = acl.remove(ace);
431        if (aclChanged) {
432            addACL(acl);
433        }
434        return aclChanged;
435    }
436
437    @Override
438    public boolean removeACEsByUsername(String aclName, String username) {
439        if (aclName == null) {
440            throw new NullPointerException("'aclName' cannot be null");
441        }
442
443        ACL acl = getOrCreateACL(aclName);
444        boolean aclChanged = acl.removeByUsername(username);
445        if (aclChanged) {
446            addACL(acl);
447        }
448        return aclChanged;
449    }
450
451    @Override
452    public boolean removeACEsByUsername(String username) {
453        boolean changed = false;
454        for (ACL acl : acls) {
455            boolean aclChanged = acl.removeByUsername(username);
456            if (aclChanged) {
457                addACL(acl);
458                changed = true;
459            }
460        }
461        return changed;
462    }
463}