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