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