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