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