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 *     Nuxeo - initial API and implementation
018 *
019 */
020
021package org.nuxeo.ecm.core.security;
022
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Set;
030import java.util.TreeSet;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.NuxeoException;
036import org.nuxeo.ecm.core.api.security.UserVisiblePermission;
037
038/**
039 * @author Bogdan Stefanescu
040 * @author Olivier Grisel
041 */
042public class DefaultPermissionProvider implements PermissionProviderLocal {
043
044    @SuppressWarnings("unused")
045    private static final Log log = LogFactory.getLog(DefaultPermissionProvider.class);
046
047    private final List<PermissionDescriptor> registeredPermissions = new LinkedList<>();
048
049    // to be recomputed each time a new PermissionDescriptor is registered -
050    // null means invalidated
051    private Map<String, MergedPermissionDescriptor> mergedPermissions;
052
053    private Map<String, Set<String>> mergedGroups;
054
055    private final List<PermissionVisibilityDescriptor> registeredPermissionsVisibility = new LinkedList<>();
056
057    private Map<String, PermissionVisibilityDescriptor> mergedPermissionsVisibility;
058
059    public DefaultPermissionProvider() {
060        mergedPermissionsVisibility = null;
061    }
062
063    @Override
064    public synchronized List<UserVisiblePermission> getUserVisiblePermissionDescriptors(String typeName) {
065        if (mergedPermissionsVisibility == null) {
066            computeMergedPermissionsVisibility();
067        }
068        // grab the default items (type is "")
069        PermissionVisibilityDescriptor defaultVisibility = mergedPermissionsVisibility.get(typeName);
070        if (defaultVisibility == null) {
071            // fallback to default
072            defaultVisibility = mergedPermissionsVisibility.get("");
073        }
074        if (defaultVisibility == null) {
075            throw new NuxeoException("no permission visibility configuration registered");
076        }
077        return defaultVisibility.getSortedUIPermissionDescriptor();
078    }
079
080    @Override
081    public List<UserVisiblePermission> getUserVisiblePermissionDescriptors() {
082        return getUserVisiblePermissionDescriptors("");
083    }
084
085    // called synchronized
086    protected void computeMergedPermissionsVisibility() {
087        mergedPermissionsVisibility = new HashMap<>();
088        for (PermissionVisibilityDescriptor pvd : registeredPermissionsVisibility) {
089            PermissionVisibilityDescriptor mergedPvd = mergedPermissionsVisibility.get(pvd.getTypeName());
090            if (mergedPvd == null) {
091                mergedPvd = new PermissionVisibilityDescriptor(pvd);
092                if (!StringUtils.isEmpty(pvd.getTypeName())) {
093                    PermissionVisibilityDescriptor defaultPerms = new PermissionVisibilityDescriptor(
094                            mergedPermissionsVisibility.get(""));
095                    defaultPerms.merge(mergedPvd);
096                    mergedPvd.setPermissionUIItems(
097                            defaultPerms.getPermissionUIItems().toArray(new PermissionUIItemDescriptor[] {}));
098                }
099                mergedPermissionsVisibility.put(mergedPvd.getTypeName(), mergedPvd);
100            } else {
101                mergedPvd.merge(pvd);
102            }
103        }
104    }
105
106    @Override
107    public synchronized String[] getSubPermissions(String perm) {
108        List<String> permissions = getPermission(perm).getSubPermissions();
109        return permissions.toArray(new String[permissions.size()]);
110    }
111
112    @Override
113    public synchronized String[] getAliasPermissions(String perm) {
114        List<String> permissions = getPermission(perm).getSubPermissions();
115        return permissions.toArray(new String[permissions.size()]);
116    }
117
118    // called synchronized
119    protected MergedPermissionDescriptor getPermission(String perm) {
120        if (mergedPermissions == null) {
121            computeMergedPermissions();
122        }
123        MergedPermissionDescriptor mpd = mergedPermissions.get(perm);
124        if (mpd == null) {
125            throw new NuxeoException(perm + " is not a registered permission");
126        }
127        return mpd;
128    }
129
130    // OG: this is an awkward method prototype left unchanged for BBB
131    @Override
132    public synchronized String[] getPermissionGroups(String perm) {
133        if (mergedGroups == null) {
134            computeMergedGroups();
135        }
136        Set<String> groups = mergedGroups.get(perm);
137        if (groups != null && !groups.isEmpty()) {
138            // OG: why return null instead of an empty array
139            return groups.toArray(new String[groups.size()]);
140        }
141        return null;
142    }
143
144    // called synchronized
145    protected void computeMergedGroups() {
146        if (mergedPermissions == null) {
147            computeMergedPermissions();
148        }
149        mergedGroups = new HashMap<>();
150
151        // scanning sub permissions to collect direct group membership
152        for (MergedPermissionDescriptor mpd : mergedPermissions.values()) {
153            for (String subPermission : mpd.getSubPermissions()) {
154                Set<String> groups = mergedGroups.get(subPermission);
155                if (groups == null) {
156                    groups = new TreeSet<>();
157                    groups.add(mpd.getName());
158                    mergedGroups.put(subPermission, groups);
159                } else {
160                    if (!groups.contains(mpd.getName())) {
161                        groups.add(mpd.getName());
162                    }
163                }
164            }
165        }
166
167        // building the transitive closure on groups membership with a recursive
168        // method
169        Set<String> alreadyProcessed = new HashSet<>();
170        for (Entry<String, Set<String>> groupEntry : mergedGroups.entrySet()) {
171            String permissionName = groupEntry.getKey();
172            Set<String> groups = groupEntry.getValue();
173            Set<String> allGroups = computeAllGroups(permissionName, alreadyProcessed);
174            groups.addAll(allGroups);
175        }
176    }
177
178    // called synchronized
179    protected Set<String> computeAllGroups(String permissionName, Set<String> alreadyProcessed) {
180        Set<String> allGroups = mergedGroups.get(permissionName);
181        if (allGroups == null) {
182            allGroups = new TreeSet<>();
183        }
184        if (alreadyProcessed.contains(permissionName)) {
185            return allGroups;
186        } else {
187            // marking it processed early to avoid infinite loops in case of
188            // recursive inclusion
189            alreadyProcessed.add(permissionName);
190            for (String directGroupName : new TreeSet<>(allGroups)) {
191                allGroups.addAll(computeAllGroups(directGroupName, alreadyProcessed));
192            }
193            return allGroups;
194        }
195    }
196
197    // OG: this is an awkward method prototype left unchanged for BBB
198    @Override
199    public synchronized String[] getPermissions() {
200        if (mergedPermissions == null) {
201            computeMergedPermissions();
202        }
203        // TODO OG: should we add aliased permissions here as well?
204        return mergedPermissions.keySet().toArray(new String[mergedPermissions.size()]);
205    }
206
207    // called synchronized
208    protected void computeMergedPermissions() {
209        mergedPermissions = new HashMap<>();
210        for (PermissionDescriptor pd : registeredPermissions) {
211            MergedPermissionDescriptor mpd = mergedPermissions.get(pd.getName());
212            if (mpd == null) {
213                mpd = new MergedPermissionDescriptor(pd);
214                mergedPermissions.put(mpd.getName(), mpd);
215            } else {
216                mpd.mergeDescriptor(pd);
217            }
218        }
219    }
220
221    @Override
222    public synchronized void registerDescriptor(PermissionDescriptor descriptor) {
223        // check that all included permission have previously been registered
224        Set<String> alreadyRegistered = new HashSet<>();
225        for (PermissionDescriptor registeredPerm : registeredPermissions) {
226            alreadyRegistered.add(registeredPerm.getName());
227        }
228        for (String includePerm : descriptor.getIncludePermissions()) {
229            if (!alreadyRegistered.contains(includePerm)) {
230                throw new NuxeoException(
231                        String.format("Permission '%s' included by '%s' is not a registered permission", includePerm,
232                                descriptor.getName()));
233            }
234        }
235        // invalidate merged permission
236        mergedPermissions = null;
237        mergedGroups = null;
238        // append the new descriptor
239        registeredPermissions.add(descriptor);
240    }
241
242    @Override
243    public synchronized void unregisterDescriptor(PermissionDescriptor descriptor) {
244        int lastOccurence = registeredPermissions.lastIndexOf(descriptor);
245        if (lastOccurence != -1) {
246            // invalidate merged permission
247            mergedPermissions = null;
248            mergedGroups = null;
249            // remove the last occurrence of the descriptor
250            registeredPermissions.remove(lastOccurence);
251        }
252    }
253
254    @Override
255    public synchronized void registerDescriptor(PermissionVisibilityDescriptor descriptor) {
256        // invalidate cached merged descriptors
257        mergedPermissionsVisibility = null;
258        registeredPermissionsVisibility.add(descriptor);
259    }
260
261    @Override
262    public synchronized void unregisterDescriptor(PermissionVisibilityDescriptor descriptor) {
263        int lastOccurence = registeredPermissionsVisibility.lastIndexOf(descriptor);
264        if (lastOccurence != -1) {
265            // invalidate merged descriptors
266            mergedPermissionsVisibility = null;
267            // remove the last occurrence of the descriptor
268            registeredPermissionsVisibility.remove(lastOccurence);
269        }
270    }
271
272}