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}