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.core.schema.types.Field;
045import org.nuxeo.ecm.directory.BaseDirectoryDescriptor.SubstringMatchType;
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, EntrySource {
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    // needed for test framework to be able to do a full backup of a directory including password
072    protected boolean readAllColumns;
073
074    protected String schemaName;
075
076    protected Map<String, Field> schemaFieldMap;
077
078    protected String directoryName;
079
080    protected SubstringMatchType substringMatchType;
081
082    protected Class<? extends Reference> referenceClass;
083
084    protected String passwordHashAlgorithm;
085
086    protected boolean autoincrementId;
087
088    protected BaseSession(Directory directory, Class<? extends Reference> referenceClass) {
089        this.directory = directory;
090        schemaFieldMap = directory.getSchemaFieldMap();
091        schemaName = directory.getSchema();
092        directoryName = directory.getName();
093
094        BaseDirectoryDescriptor desc = directory.getDescriptor();
095        substringMatchType = desc.getSubstringMatchType();
096        autoincrementId = desc.isAutoincrementIdField();
097        permissions = desc.permissions;
098        passwordHashAlgorithm = desc.passwordHashAlgorithm;
099        this.referenceClass = referenceClass;
100    }
101
102    /** To be implemented with a more specific return type. */
103    public abstract Directory getDirectory();
104
105    @Override
106    public void setReadAllColumns(boolean readAllColumns) {
107        this.readAllColumns = readAllColumns;
108    }
109
110    @Override
111    public String getIdField() {
112        return directory.getIdField();
113    }
114
115    @Override
116    public String getPasswordField() {
117        return directory.getPasswordField();
118    }
119
120    @Override
121    public boolean isAuthenticating() {
122        return directory.getPasswordField() != null;
123    }
124
125    @Override
126    public boolean isReadOnly() {
127        return directory.isReadOnly();
128    }
129
130    /**
131     * Checks the current user rights for the given permission against the read-only flag and the permission descriptor.
132     * <p>
133     * Throws {@link DirectorySecurityException} if the user does not have adequate privileges.
134     *
135     * @throws DirectorySecurityException if access is denied
136     * @since 8.3
137     */
138    public void checkPermission(String permission) {
139        if (hasPermission(permission)) {
140            return;
141        }
142        if (permission.equals(SecurityConstants.WRITE) && isReadOnly()) {
143            throw new DirectorySecurityException("Directory is read-only");
144        } else {
145            NuxeoPrincipal user = ClientLoginModule.getCurrentPrincipal();
146            throw new DirectorySecurityException("User " + user + " does not have " + permission + " permission");
147        }
148    }
149
150    /**
151     * Checks that there are no constraints for deleting the given entry id.
152     *
153     * @since 8.4
154     */
155    public void checkDeleteConstraints(String entryId) {
156        List<DirectoryDeleteConstraint> deleteConstraints = directory.getDirectoryDeleteConstraints();
157        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
158        if (deleteConstraints != null && !deleteConstraints.isEmpty()) {
159            for (DirectoryDeleteConstraint deleteConstraint : deleteConstraints) {
160                if (!deleteConstraint.canDelete(directoryService, entryId)) {
161                    throw new DirectoryDeleteConstraintException("This entry is referenced in another vocabulary.");
162                }
163            }
164        }
165    }
166
167    /**
168     * Checks the current user rights for the given permission against the read-only flag and the permission descriptor.
169     * <p>
170     * Returns {@code false} if the user does not have adequate privileges.
171     *
172     * @return {@code false} if access is denied
173     * @since 8.3
174     */
175    public boolean hasPermission(String permission) {
176        if (permission.equals(SecurityConstants.WRITE) && isReadOnly()) {
177            if (log.isTraceEnabled()) {
178                log.trace("Directory is read-only");
179            }
180            return false;
181        }
182        NuxeoPrincipal user = ClientLoginModule.getCurrentPrincipal();
183        if (user == null) {
184            return true;
185        }
186        String username = user.getName();
187        if (username.equals(LoginComponent.SYSTEM_USERNAME) || user.isAdministrator()) {
188            return true;
189        }
190
191        if (permissions == null || permissions.length == 0) {
192            if (user.isAdministrator()) {
193                return true;
194            }
195            if (user.isMemberOf(POWER_USERS_GROUP)) {
196                return true;
197            }
198            // Return true for read access to anyone when nothing defined
199            if (permission.equals(SecurityConstants.READ)) {
200                return true;
201            }
202            // Deny in all other cases
203            if (log.isTraceEnabled()) {
204                log.trace("User " + user + " does not have " + permission + " permission");
205            }
206            return false;
207        }
208
209        List<String> groups = new ArrayList<>(user.getAllGroups());
210        groups.add(SecurityConstants.EVERYONE);
211        boolean allowed = hasPermission(permission, username, groups);
212        if (!allowed) {
213            // if the permission Read is not explicitly granted, check Write which includes it
214            if (permission.equals(SecurityConstants.READ)) {
215                allowed = hasPermission(SecurityConstants.WRITE, username, groups);
216            }
217        }
218        if (!allowed && log.isTraceEnabled()) {
219            log.trace("User " + user + " does not have " + permission + " permission");
220        }
221        return allowed;
222    }
223
224    protected boolean hasPermission(String permission, String username, List<String> groups) {
225        for (PermissionDescriptor desc : permissions) {
226            if (!desc.name.equals(permission)) {
227                continue;
228            }
229            if (desc.groups != null) {
230                for (String group : desc.groups) {
231                    if (groups.contains(group)) {
232                        return true;
233                    }
234                }
235            }
236            if (desc.users != null) {
237                for (String user : desc.users) {
238                    if (user.equals(username)) {
239                        return true;
240                    }
241                }
242            }
243        }
244        return false;
245    }
246
247    /**
248     * Returns a bare document model suitable for directory implementations.
249     * <p>
250     * Can be used for creation screen.
251     *
252     * @since 5.2M4
253     */
254    public static DocumentModel createEntryModel(String sessionId, String schema, String id, Map<String, Object> values)
255            throws PropertyException {
256        DocumentModelImpl entry = new DocumentModelImpl(sessionId, schema, id, null, null, null, null,
257                new String[] { schema }, new HashSet<>(), null, null);
258        DataModel dataModel;
259        if (values == null) {
260            values = Collections.emptyMap();
261        }
262        dataModel = new DataModelImpl(schema, values);
263        entry.addDataModel(dataModel);
264        return entry;
265    }
266
267    /**
268     * Returns a bare document model suitable for directory implementations.
269     * <p>
270     * Allow setting the readonly entry flag to {@code Boolean.TRUE}. See {@code Session#isReadOnlyEntry(DocumentModel)}
271     *
272     * @since 5.3.1
273     */
274    public static DocumentModel createEntryModel(String sessionId, String schema, String id, Map<String, Object> values,
275            boolean readOnly) throws PropertyException {
276        DocumentModel entry = createEntryModel(sessionId, schema, id, values);
277        if (readOnly) {
278            setReadOnlyEntry(entry);
279        }
280        return entry;
281    }
282
283    /**
284     * Test whether entry comes from a read-only back-end directory.
285     *
286     * @since 5.3.1
287     */
288    public static boolean isReadOnlyEntry(DocumentModel entry) {
289        return entry.getContextData(READONLY_ENTRY_FLAG) == Boolean.TRUE;
290    }
291
292    /**
293     * Set the read-only flag of a directory entry. To be used by EntryAdaptor implementations for instance.
294     *
295     * @since 5.3.2
296     */
297    public static void setReadOnlyEntry(DocumentModel entry) {
298        entry.putContextData(READONLY_ENTRY_FLAG, Boolean.TRUE);
299    }
300
301    /**
302     * Unset the read-only flag of a directory entry. To be used by EntryAdaptor implementations for instance.
303     *
304     * @since 5.3.2
305     */
306    public static void setReadWriteEntry(DocumentModel entry) {
307        entry.putContextData(READONLY_ENTRY_FLAG, Boolean.FALSE);
308    }
309
310    /**
311     * Compute a multi tenant directory id based on the given {@code tenantId}.
312     *
313     * @return the computed directory id
314     * @since 5.6
315     */
316    public static String computeMultiTenantDirectoryId(String tenantId, String id) {
317        return String.format(MULTI_TENANT_ID_FORMAT, tenantId, id);
318    }
319
320    @Override
321    public DocumentModel getEntry(String id) throws DirectoryException {
322        return getEntry(id, true);
323    }
324
325    @Override
326    public DocumentModel getEntry(String id, boolean fetchReferences) throws DirectoryException {
327        if (!hasPermission(SecurityConstants.READ)) {
328            return null;
329        }
330        if (readAllColumns) {
331            // bypass cache when reading all columns
332            return getEntryFromSource(id, fetchReferences);
333        }
334        return directory.getCache().getEntry(id, this, fetchReferences);
335    }
336
337    @Override
338    public DocumentModelList getEntries() throws DirectoryException {
339        if (!hasPermission(SecurityConstants.READ)) {
340            return new DocumentModelListImpl();
341        }
342        return query(Collections.emptyMap());
343    }
344
345    @Override
346    public DocumentModel getEntryFromSource(String id, boolean fetchReferences) throws DirectoryException {
347        String idFieldName = schemaFieldMap != null ? schemaFieldMap.get(getIdField()).getName().getPrefixedName()
348                : getIdField();
349        DocumentModelList result = query(Collections.singletonMap(idFieldName, id), Collections.emptySet(),
350                Collections.emptyMap(), true);
351        return result.isEmpty() ? null : result.get(0);
352    }
353
354    @Override
355    public DocumentModel createEntry(DocumentModel documentModel) {
356        return createEntry(documentModel.getProperties(schemaName));
357    }
358
359    @Override
360    public DocumentModel createEntry(Map<String, Object> fieldMap) throws DirectoryException {
361        checkPermission(SecurityConstants.WRITE);
362        DocumentModel docModel = createEntryWithoutReferences(fieldMap);
363
364        // Add references fields
365        String idFieldName = schemaFieldMap != null ? schemaFieldMap.get(getIdField()).getName().getPrefixedName()
366                : getIdField();
367        Object entry = fieldMap.get(idFieldName);
368        String sourceId = docModel.getId();
369        for (Reference reference : getDirectory().getReferences()) {
370            String referenceFieldName = schemaFieldMap.get(reference.getFieldName()).getName().getPrefixedName();
371            if (getDirectory().getReferences(reference.getFieldName()).size() > 1) {
372                if (log.isWarnEnabled()) {
373                    log.warn("Directory " + directoryName + " cannot create field " + reference.getFieldName()
374                            + " for entry " + entry + ": this field is associated with more than one reference");
375                }
376                continue;
377            }
378
379            @SuppressWarnings("unchecked")
380            List<String> targetIds = (List<String>) fieldMap.get(referenceFieldName);
381            if (reference.getClass() == referenceClass) {
382                reference.addLinks(sourceId, targetIds, this);
383            } else {
384                reference.addLinks(sourceId, targetIds);
385            }
386        }
387
388        getDirectory().invalidateCaches();
389        return docModel;
390    }
391
392    @Override
393    public void updateEntry(DocumentModel docModel) throws DirectoryException {
394        checkPermission(SecurityConstants.WRITE);
395
396        // Retrieve the references to update in the document model, and update the rest
397        List<String> referenceFieldList = updateEntryWithoutReferences(docModel);
398
399        // update reference fields
400        for (String referenceFieldName : referenceFieldList) {
401            List<Reference> references = directory.getReferences(referenceFieldName);
402            if (references.size() > 1) {
403                // not supported
404                if (log.isWarnEnabled()) {
405                    log.warn("Directory " + getDirectory().getName() + " cannot update field " + referenceFieldName
406                            + " for entry " + docModel.getId()
407                            + ": this field is associated with more than one reference");
408                }
409            } else {
410                Reference reference = references.get(0);
411                @SuppressWarnings("unchecked")
412                List<String> targetIds = (List<String>) docModel.getProperty(schemaName, referenceFieldName);
413                if (reference.getClass() == referenceClass) {
414                    reference.setTargetIdsForSource(docModel.getId(), targetIds, this);
415                } else {
416                    reference.setTargetIdsForSource(docModel.getId(), targetIds);
417                }
418            }
419        }
420        getDirectory().invalidateCaches();
421    }
422
423    @Override
424    public void deleteEntry(DocumentModel docModel) throws DirectoryException {
425        deleteEntry(docModel.getId());
426    }
427
428    @Override
429    @Deprecated
430    public void deleteEntry(String id, Map<String, String> map) throws DirectoryException {
431        deleteEntry(id);
432    }
433
434    @Override
435    public void deleteEntry(String id) throws DirectoryException {
436        checkPermission(SecurityConstants.WRITE);
437        checkDeleteConstraints(id);
438
439        for (Reference reference : getDirectory().getReferences()) {
440            if (reference.getClass() == referenceClass) {
441                reference.removeLinksForSource(id, this);
442            } else {
443                reference.removeLinksForSource(id);
444            }
445        }
446        deleteEntryWithoutReferences(id);
447        getDirectory().invalidateCaches();
448    }
449
450    @Override
451    public DocumentModelList query(Map<String, Serializable> filter) throws DirectoryException {
452        return query(filter, Collections.emptySet());
453    }
454
455    @Override
456    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext) throws DirectoryException {
457        return query(filter, fulltext, new HashMap<>());
458    }
459
460    @Override
461    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy)
462            throws DirectoryException {
463        return query(filter, fulltext, orderBy, false);
464    }
465
466    @Override
467    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
468            boolean fetchReferences) throws DirectoryException {
469        return query(filter, fulltext, orderBy, fetchReferences, 0, 0);
470    }
471
472    @Override
473    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
474            boolean fetchReferences, int limit, int offset) throws DirectoryException {
475        log.info("Call an unoverrided query with offset and limit.");
476        DocumentModelList entries = query(filter, fulltext, orderBy, fetchReferences);
477        int toIndex = offset + limit;
478        if (toIndex > entries.size()) {
479            toIndex = entries.size();
480        }
481
482        return new DocumentModelListImpl(entries.subList(offset, toIndex));
483    }
484
485    @Override
486    public List<String> getProjection(Map<String, Serializable> filter, String columnName) throws DirectoryException {
487        return getProjection(filter, Collections.emptySet(), columnName);
488    }
489
490    @Override
491    public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName)
492            throws DirectoryException {
493        DocumentModelList docList = query(filter, fulltext);
494        List<String> result = new ArrayList<>();
495        for (DocumentModel docModel : docList) {
496            Object obj = docModel.getProperty(schemaName, columnName);
497            String propValue = String.valueOf(obj);
498            result.add(propValue);
499        }
500        return result;
501    }
502
503    /** To be implemented for specific creation. */
504    protected abstract DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap);
505
506    /** To be implemented for specific update. */
507    protected abstract List<String> updateEntryWithoutReferences(DocumentModel docModel) throws DirectoryException;
508
509    /** To be implemented for specific deletion. */
510    protected abstract void deleteEntryWithoutReferences(String id) throws DirectoryException;
511
512}