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