001/*
002 * (C) Copyright 2006-2014 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 *     Anahide Tchertchian
018 *
019 */
020
021package org.nuxeo.ecm.directory;
022
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.common.collections.ScopeType;
035import org.nuxeo.common.collections.ScopedMap;
036import org.nuxeo.ecm.core.api.DataModel;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.DocumentModelList;
039import org.nuxeo.ecm.core.api.NuxeoPrincipal;
040import org.nuxeo.ecm.core.api.PropertyException;
041import org.nuxeo.ecm.core.api.impl.DataModelImpl;
042import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
043import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
044import org.nuxeo.ecm.core.api.local.ClientLoginModule;
045import org.nuxeo.ecm.core.api.security.SecurityConstants;
046import org.nuxeo.ecm.directory.api.DirectoryDeleteConstraint;
047import org.nuxeo.ecm.directory.api.DirectoryService;
048import org.nuxeo.runtime.api.Framework;
049import org.nuxeo.runtime.api.login.LoginComponent;
050
051/**
052 * Base session class with helper methods common to all kinds of directory sessions.
053 *
054 * @author Anahide Tchertchian
055 * @since 5.2M4
056 */
057public abstract class BaseSession implements Session {
058
059    protected static final String POWER_USERS_GROUP = "powerusers";
060
061    protected static final String READONLY_ENTRY_FLAG = "READONLY_ENTRY";
062
063    protected static final String MULTI_TENANT_ID_FORMAT = "tenant_%s_%s";
064
065    private final static Log log = LogFactory.getLog(BaseSession.class);
066
067    protected final Directory directory;
068
069    protected PermissionDescriptor[] permissions = null;
070
071    protected BaseSession(Directory directory) {
072        this.directory = directory;
073    }
074
075    /** To be implemented with a more specific return type. */
076    public abstract Directory getDirectory();
077
078    @Override
079    public String getIdField() {
080        return directory.getIdField();
081    }
082
083    @Override
084    public String getPasswordField() {
085        return directory.getPasswordField();
086    }
087
088    @Override
089    public boolean isAuthenticating() {
090        return directory.getPasswordField() != null;
091    }
092
093    @Override
094    public boolean isReadOnly() {
095        return directory.isReadOnly();
096    }
097
098    /**
099     * Checks the current user rights for the given permission against the read-only flag and the permission descriptor.
100     * <p>
101     * Throws {@link DirectorySecurityException} if the user does not have adequate privileges.
102     *
103     * @throws DirectorySecurityException if access is denied
104     * @since 8.3
105     */
106    public void checkPermission(String permission) {
107        if (hasPermission(permission)) {
108            return;
109        }
110        if (permission.equals(SecurityConstants.WRITE) && isReadOnly()) {
111            throw new DirectorySecurityException("Directory is read-only");
112        } else {
113            NuxeoPrincipal user = ClientLoginModule.getCurrentPrincipal();
114            throw new DirectorySecurityException("User " + user + " does not have " + permission + " permission");
115        }
116    }
117
118    /**
119     * Checks that there are no constraints for deleting the given entry id.
120     *
121     * @since 8.4
122     */
123    public void checkDeleteConstraints(String entryId) {
124        List<DirectoryDeleteConstraint> deleteConstraints = directory.getDirectoryDeleteConstraints();
125        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
126        if (deleteConstraints != null && !deleteConstraints.isEmpty()) {
127            for (DirectoryDeleteConstraint deleteConstraint : deleteConstraints) {
128                if (!deleteConstraint.canDelete(directoryService, entryId)) {
129                    throw new DirectoryDeleteConstraintException("This entry is referenced in another vocabulary.");
130                }
131            }
132        }
133    }
134
135    /**
136     * Checks the current user rights for the given permission against the read-only flag and the permission descriptor.
137     * <p>
138     * Returns {@code false} if the user does not have adequate privileges.
139     *
140     * @return {@code false} if access is denied
141     * @since 8.3
142     */
143    public boolean hasPermission(String permission) {
144        if (permission.equals(SecurityConstants.WRITE) && isReadOnly()) {
145            if (log.isTraceEnabled()) {
146                log.trace("Directory is read-only");
147            }
148            return false;
149        }
150        NuxeoPrincipal user = ClientLoginModule.getCurrentPrincipal();
151        if (user == null) {
152            return true;
153        }
154        String username = user.getName();
155        if (username.equals(LoginComponent.SYSTEM_USERNAME)) {
156            return true;
157        }
158
159        if (permissions == null || permissions.length == 0) {
160            if (user.isAdministrator()) {
161                return true;
162            }
163            if (user.isMemberOf(POWER_USERS_GROUP)) {
164                return true;
165            }
166            // Return true for read access to anyone when nothing defined
167            if (permission.equals(SecurityConstants.READ)) {
168                return true;
169            }
170            // Deny in all other cases
171            if (log.isTraceEnabled()) {
172                log.trace("User " + user + " does not have " + permission + " permission");
173            }
174            return false;
175        }
176
177        List<String> groups = new ArrayList<>(user.getAllGroups());
178        groups.add(SecurityConstants.EVERYONE);
179        boolean allowed = hasPermission(permission, username, groups);
180        if (!allowed) {
181            // if the permission Read is not explicitly granted, check Write which includes it
182            if (permission.equals(SecurityConstants.READ)) {
183                allowed = hasPermission(SecurityConstants.WRITE, username, groups);
184            }
185        }
186        if (!allowed && log.isTraceEnabled()) {
187            log.trace("User " + user + " does not have " + permission + " permission");
188        }
189        return allowed;
190    }
191
192    protected boolean hasPermission(String permission, String username, List<String> groups) {
193        for (PermissionDescriptor desc : permissions) {
194            if (!desc.name.equals(permission)) {
195                continue;
196            }
197            if (desc.groups != null) {
198                for (String group : desc.groups) {
199                    if (groups.contains(group)) {
200                        return true;
201                    }
202                }
203            }
204            if (desc.users != null) {
205                for (String user : desc.users) {
206                    if (user.equals(username)) {
207                        return true;
208                    }
209                }
210            }
211        }
212        return false;
213    }
214
215    /**
216     * Returns a bare document model suitable for directory implementations.
217     * <p>
218     * Can be used for creation screen.
219     *
220     * @since 5.2M4
221     */
222    public static DocumentModel createEntryModel(String sessionId, String schema, String id, Map<String, Object> values)
223            throws PropertyException {
224        DocumentModelImpl entry = new DocumentModelImpl(sessionId, schema, id, null, null, null, null,
225                new String[] { schema }, new HashSet<String>(), null, null);
226        DataModel dataModel;
227        if (values == null) {
228            values = Collections.emptyMap();
229        }
230        dataModel = new DataModelImpl(schema, values);
231        entry.addDataModel(dataModel);
232        return entry;
233    }
234
235    /**
236     * Returns a bare document model suitable for directory implementations.
237     * <p>
238     * Allow setting the readonly entry flag to {@code Boolean.TRUE}. See {@code Session#isReadOnlyEntry(DocumentModel)}
239     *
240     * @since 5.3.1
241     */
242    public static DocumentModel createEntryModel(String sessionId, String schema, String id,
243            Map<String, Object> values, boolean readOnly) throws PropertyException {
244        DocumentModel entry = createEntryModel(sessionId, schema, id, values);
245        if (readOnly) {
246            setReadOnlyEntry(entry);
247        }
248        return entry;
249    }
250
251    protected static Map<String, Serializable> mkSerializableMap(Map<String, Object> map) {
252        Map<String, Serializable> serializableMap = null;
253        if (map != null) {
254            serializableMap = new HashMap<String, Serializable>();
255            for (String key : map.keySet()) {
256                serializableMap.put(key, (Serializable) map.get(key));
257            }
258        }
259        return serializableMap;
260    }
261
262    protected static Map<String, Object> mkObjectMap(Map<String, Serializable> map) {
263        Map<String, Object> objectMap = null;
264        if (map != null) {
265            objectMap = new HashMap<String, Object>();
266            for (String key : map.keySet()) {
267                objectMap.put(key, map.get(key));
268            }
269        }
270        return objectMap;
271    }
272
273    /**
274     * Test whether entry comes from a read-only back-end directory.
275     *
276     * @since 5.3.1
277     */
278    public static boolean isReadOnlyEntry(DocumentModel entry) {
279        ScopedMap contextData = entry.getContextData();
280        return contextData.getScopedValue(ScopeType.REQUEST, READONLY_ENTRY_FLAG) == Boolean.TRUE;
281    }
282
283    /**
284     * Set the read-only flag of a directory entry. To be used by EntryAdaptor implementations for instance.
285     *
286     * @since 5.3.2
287     */
288    public static void setReadOnlyEntry(DocumentModel entry) {
289        ScopedMap contextData = entry.getContextData();
290        contextData.putScopedValue(ScopeType.REQUEST, READONLY_ENTRY_FLAG, Boolean.TRUE);
291    }
292
293    /**
294     * Unset the read-only flag of a directory entry. To be used by EntryAdaptor implementations for instance.
295     *
296     * @since 5.3.2
297     */
298    public static void setReadWriteEntry(DocumentModel entry) {
299        ScopedMap contextData = entry.getContextData();
300        contextData.putScopedValue(ScopeType.REQUEST, READONLY_ENTRY_FLAG, Boolean.FALSE);
301    }
302
303    /**
304     * Compute a multi tenant directory id based on the given {@code tenantId}.
305     *
306     * @return the computed directory id
307     * @since 5.6
308     */
309    public static String computeMultiTenantDirectoryId(String tenantId, String id) {
310        return String.format(MULTI_TENANT_ID_FORMAT, tenantId, id);
311    }
312
313    @Override
314    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
315            boolean fetchReferences, int limit, int offset) throws DirectoryException {
316        log.info("Call an unoverrided query with offset and limit.");
317        DocumentModelList entries = query(filter, fulltext, orderBy, fetchReferences);
318        int toIndex = offset + limit;
319        if (toIndex > entries.size()) {
320            toIndex = entries.size();
321        }
322
323        return new DocumentModelListImpl(entries.subList(offset, toIndex));
324    }
325
326}