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