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