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