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