001/*
002 * (C) Copyright 2006-2018 Nuxeo (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.lang3.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<>();
051        cache = new HashMap<>();
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        String localName = name == null ? ACL.LOCAL_ACL : name;
136        return acls.stream().filter(acl -> acl.getName().equals(localName)).findFirst().orElse(null);
137    }
138
139    @Override
140    public ACL[] getACLs() {
141        return acls.toArray(new ACL[acls.size()]);
142    }
143
144    @Override
145    public ACL getMergedACLs(String name) {
146        ACL mergedAcl = new ACLImpl(name, true);
147        for (ACL acl : acls) {
148            mergedAcl.addAll(acl);
149        }
150        return mergedAcl;
151    }
152
153    public static ACL newACL(String name) {
154        return new ACLImpl(name);
155    }
156
157    @Override
158    public ACL removeACL(String name) {
159        for (int i = 0, len = acls.size(); i < len; i++) {
160            ACL acl = acls.get(i);
161            if (acl.getName().equals(name)) {
162                cache.clear();
163                return acls.remove(i);
164            }
165        }
166        return null;
167    }
168
169    @Override
170    public Access getAccess(String principal, String permission) {
171        // check first the cache
172        String key = principal + ':' + permission;
173        Access access = cache.get(key);
174        if (access == null) {
175            access = Access.UNKNOWN;
176            FOUND_ACE: for (ACL acl : acls) {
177                for (ACE ace : acl) {
178                    if (permissionsMatch(ace, permission) && principalsMatch(ace, principal)) {
179                        access = ace.isGranted() ? Access.GRANT : Access.DENY;
180                        break FOUND_ACE;
181                    }
182                }
183            }
184            cache.put(key, access);
185        }
186        return access;
187    }
188
189    @Override
190    public Access getAccess(String[] principals, String[] permissions) {
191        for (ACL acl : acls) {
192            for (ACE ace : acl) {
193                // only check for effective ACEs
194                if (ace.isEffective()) {
195                    // fully check ACE in turn against username/permissions
196                    // and usergroups/permgroups
197                    Access access = getAccess(ace, principals, permissions);
198                    if (access != Access.UNKNOWN) {
199                        return access;
200                    }
201                }
202            }
203        }
204        return Access.UNKNOWN;
205    }
206
207    public static Access getAccess(ACE ace, String[] principals, String[] permissions) {
208        String acePerm = ace.getPermission();
209        String aceUser = ace.getUsername();
210
211        for (String principal : principals) {
212            if (principalsMatch(aceUser, principal)) {
213                // check permission match only if principal is matching
214                for (String permission : permissions) {
215                    if (permissionsMatch(acePerm, permission)) {
216                        return ace.isGranted() ? Access.GRANT : Access.DENY;
217                    } // end permissionMatch
218                } // end perm for
219            } // end principalMatch
220        } // end princ for
221        return Access.UNKNOWN;
222    }
223
224    private static boolean permissionsMatch(ACE ace, String permission) {
225        String acePerm = ace.getPermission();
226
227        // RESTRICTED_READ needs special handling, is not implied by EVERYTHING.
228        if (!SecurityConstants.RESTRICTED_READ.equals(permission)) {
229            if (SecurityConstants.EVERYTHING.equals(acePerm)) {
230                return true;
231            }
232        }
233        return StringUtils.equals(acePerm, permission);
234    }
235
236    private static boolean permissionsMatch(String acePerm, String permission) {
237        // RESTRICTED_READ needs special handling, is not implied by EVERYTHING.
238        if (SecurityConstants.EVERYTHING.equals(acePerm)) {
239            if (!SecurityConstants.RESTRICTED_READ.equals(permission)) {
240                return true;
241            }
242        }
243        return StringUtils.equals(acePerm, permission);
244    }
245
246    private static boolean principalsMatch(ACE ace, String principal) {
247        String acePrincipal = ace.getUsername();
248        if (SecurityConstants.EVERYONE.equals(acePrincipal)) {
249            return true;
250        }
251        return StringUtils.equals(acePrincipal, principal);
252    }
253
254    private static boolean principalsMatch(String acePrincipal, String principal) {
255        if (SecurityConstants.EVERYONE.equals(acePrincipal)) {
256            return true;
257        }
258        return StringUtils.equals(acePrincipal, principal);
259    }
260
261    public void addAccessRule(String aclName, ACE ace) {
262        ACL acl = getACL(aclName);
263        if (acl == null) {
264            acl = new ACLImpl(aclName);
265            addACL(acl);
266        }
267        acl.add(ace);
268    }
269
270    @Override
271    public ACL getOrCreateACL(String name) {
272        ACL acl = getACL(name);
273        if (acl == null) {
274            acl = new ACLImpl(name);
275            addACL(acl);
276        }
277        return acl;
278    }
279
280    @Override
281    public ACL getOrCreateACL() {
282        return getOrCreateACL(ACL.LOCAL_ACL);
283    }
284
285    // Rules.
286
287    @Override
288    public void setRules(String aclName, UserEntry[] userEntries) {
289        setRules(aclName, userEntries, true);
290    }
291
292    @Override
293    public void setRules(String aclName, UserEntry[] userEntries, boolean overwrite) {
294
295        ACL acl = getACL(aclName);
296        if (acl == null) { // create the loca ACL
297            acl = new ACLImpl(aclName);
298            addACL(acl);
299        } else if (overwrite) {
300            // :XXX: Should not overwrite entries not given as parameters here.
301            acl.clear();
302        }
303        for (UserEntry entry : userEntries) {
304            String username = entry.getUserName();
305            for (String permission : entry.getGrantedPermissions()) {
306                acl.add(new ACE(username, permission, true));
307            }
308            for (String permission : entry.getDeniedPermissions()) {
309                acl.add(new ACE(username, permission, false));
310            }
311        }
312        cache.clear();
313    }
314
315    @Override
316    public void setRules(UserEntry[] userEntries) {
317        setRules(ACL.LOCAL_ACL, userEntries);
318    }
319
320    @Override
321    public void setRules(UserEntry[] userEntries, boolean overwrite) {
322        setRules(ACL.LOCAL_ACL, userEntries, overwrite);
323    }
324
325    // Serialization.
326
327    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
328        // always perform the default de-serialization first
329        in.defaultReadObject();
330        // initialize cache to avoid NPE
331        cache = new HashMap<>();
332    }
333
334    /*
335     * NXP-1822 Rux: method for validating in one shot the users allowed to perform an oration. It gets the list of
336     * individual permissions which supposedly all grant.
337     */
338    @Override
339    public String[] listUsernamesForAnyPermission(Set<String> perms) {
340        List<String> usernames = new ArrayList<>();
341        ACL merged = getMergedACLs("merged");
342        for (ACE ace : merged.getACEs()) {
343            if (perms.contains(ace.getPermission()) && ace.isGranted()) {
344                String username = ace.getUsername();
345                if (!usernames.contains(username)) {
346                    usernames.add(username);
347                }
348            }
349        }
350        return usernames.toArray(new String[usernames.size()]);
351    }
352
353    @Override
354    public ACPImpl clone() {
355        ACPImpl copy = new ACPImpl();
356        for (ACL acl : acls) {
357            copy.acls.add((ACL) acl.clone());
358        }
359        return copy;
360    }
361
362    @Override
363    public boolean blockInheritance(String aclName, String username) {
364        if (aclName == null) {
365            throw new NullPointerException("'aclName' cannot be null");
366        }
367        if (username == null) {
368            throw new NullPointerException("'username' cannot be null");
369        }
370
371        ACL acl = getOrCreateACL(aclName);
372        boolean aclChanged = acl.blockInheritance(username);
373        if (aclChanged) {
374            addACL(acl);
375        }
376        return aclChanged;
377    }
378
379    @Override
380    public boolean unblockInheritance(String aclName) {
381        if (aclName == null) {
382            throw new NullPointerException("'aclName' cannot be null");
383        }
384
385        ACL acl = getOrCreateACL(aclName);
386        boolean aclChanged = acl.unblockInheritance();
387        if (aclChanged) {
388            addACL(acl);
389        }
390        return aclChanged;
391    }
392
393    @Override
394    public boolean addACE(String aclName, ACE ace) {
395        if (aclName == null) {
396            throw new NullPointerException("'aclName' cannot be null");
397        }
398
399        ACL acl = getOrCreateACL(aclName);
400        boolean aclChanged = acl.add(ace);
401        if (aclChanged) {
402            addACL(acl);
403        }
404        return aclChanged;
405    }
406
407    @Override
408    public boolean replaceACE(String aclName, ACE oldACE, ACE newACE) {
409        if (aclName == null) {
410            throw new NullPointerException("'aclName' cannot be null");
411        }
412
413        ACL acl = getOrCreateACL(aclName);
414        boolean aclChanged = acl.replace(oldACE, newACE);
415        if (aclChanged) {
416            addACL(acl);
417        }
418        return aclChanged;
419    }
420
421    @Override
422    public boolean removeACE(String aclName, ACE ace) {
423        if (aclName == null) {
424            throw new NullPointerException("'aclName' cannot be null");
425        }
426
427        ACL acl = getOrCreateACL(aclName);
428        boolean aclChanged = acl.remove(ace);
429        if (aclChanged) {
430            addACL(acl);
431        }
432        return aclChanged;
433    }
434
435    @Override
436    public boolean removeACEsByUsername(String aclName, String username) {
437        if (aclName == null) {
438            throw new NullPointerException("'aclName' cannot be null");
439        }
440
441        ACL acl = getOrCreateACL(aclName);
442        boolean aclChanged = acl.removeByUsername(username);
443        if (aclChanged) {
444            addACL(acl);
445        }
446        return aclChanged;
447    }
448
449    @Override
450    public boolean removeACEsByUsername(String username) {
451        boolean changed = false;
452        for (ACL acl : acls) {
453            boolean aclChanged = acl.removeByUsername(username);
454            if (aclChanged) {
455                addACL(acl);
456                changed = true;
457            }
458        }
459        return changed;
460    }
461}