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}