001/* 002 * (C) Copyright 2006-2017 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 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage.sql.coremodel; 020 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Calendar; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Map; 033import java.util.Map.Entry; 034import java.util.Set; 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import javax.resource.ResourceException; 039 040import org.apache.commons.logging.Log; 041import org.apache.commons.logging.LogFactory; 042import org.nuxeo.ecm.core.api.CoreSession; 043import org.nuxeo.ecm.core.api.DocumentNotFoundException; 044import org.nuxeo.ecm.core.api.IterableQueryResult; 045import org.nuxeo.ecm.core.api.PartialList; 046import org.nuxeo.ecm.core.api.ScrollResult; 047import org.nuxeo.ecm.core.api.VersionModel; 048import org.nuxeo.ecm.core.api.security.ACE; 049import org.nuxeo.ecm.core.api.security.ACL; 050import org.nuxeo.ecm.core.api.security.ACP; 051import org.nuxeo.ecm.core.api.security.Access; 052import org.nuxeo.ecm.core.api.security.SecurityConstants; 053import org.nuxeo.ecm.core.api.security.impl.ACLImpl; 054import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 055import org.nuxeo.ecm.core.model.Document; 056import org.nuxeo.ecm.core.model.LockManager; 057import org.nuxeo.ecm.core.model.Repository; 058import org.nuxeo.ecm.core.model.Session; 059import org.nuxeo.ecm.core.query.QueryFilter; 060import org.nuxeo.ecm.core.query.sql.NXQL; 061import org.nuxeo.ecm.core.schema.DocumentType; 062import org.nuxeo.ecm.core.schema.SchemaManager; 063import org.nuxeo.ecm.core.storage.sql.ACLRow; 064import org.nuxeo.ecm.core.storage.sql.Model; 065import org.nuxeo.ecm.core.storage.sql.Node; 066import org.nuxeo.runtime.api.Framework; 067 068/** 069 * This class is the bridge between the Nuxeo SPI Session and the actual low-level implementation of the SQL storage 070 * session. 071 * 072 * @author Florent Guillaume 073 */ 074public class SQLSession implements Session { 075 076 protected final Log log = LogFactory.getLog(SQLSession.class); 077 078 /** 079 * Framework property to control whether negative ACLs (deny) are allowed. 080 * 081 * @since 6.0 082 */ 083 public static final String ALLOW_NEGATIVE_ACL_PROPERTY = "nuxeo.security.allowNegativeACL"; 084 085 /** 086 * Framework property to disabled free-name collision detection for copy. This is useful when constraints have been 087 * added to the database to detect collisions at the database level and raise a ConcurrentUpdateException, thus 088 * letting the high-level application deal with collisions. 089 * 090 * @since 7.3 091 */ 092 public static final String COPY_FINDFREENAME_DISABLED_PROP = "nuxeo.vcs.copy.findFreeName.disabled"; 093 094 private final Repository repository; 095 096 private final org.nuxeo.ecm.core.storage.sql.Session session; 097 098 private Document root; 099 100 private final boolean negativeAclAllowed; 101 102 private final boolean copyFindFreeNameDisabled; 103 104 public SQLSession(org.nuxeo.ecm.core.storage.sql.Session session, Repository repository) { 105 this.session = session; 106 this.repository = repository; 107 Node rootNode = session.getRootNode(); 108 root = newDocument(rootNode); 109 negativeAclAllowed = Framework.isBooleanPropertyTrue(ALLOW_NEGATIVE_ACL_PROPERTY); 110 copyFindFreeNameDisabled = Framework.isBooleanPropertyTrue(COPY_FINDFREENAME_DISABLED_PROP); 111 } 112 113 /* 114 * ----- org.nuxeo.ecm.core.model.Session ----- 115 */ 116 117 @Override 118 public Document getRootDocument() { 119 return root; 120 } 121 122 @Override 123 public Document getNullDocument() { 124 return new SQLDocumentLive(null, null, this, true); 125 } 126 127 @Override 128 public void close() { 129 root = null; 130 try { 131 session.close(); 132 } catch (ResourceException e) { 133 throw new RuntimeException(e); 134 } 135 } 136 137 @Override 138 public void save() { 139 session.save(); 140 } 141 142 @Override 143 public boolean isLive() { 144 // session can become non-live behind our back 145 // through ConnectionAwareXAResource that closes 146 // all handles (sessions) at tx end() time 147 return session != null && session.isLive(); 148 } 149 150 @Override 151 public String getRepositoryName() { 152 return repository.getName(); 153 } 154 155 protected String idToString(Serializable id) { 156 return session.getModel().idToString(id); 157 } 158 159 protected Serializable idFromString(String id) { 160 return session.getModel().idFromString(id); 161 } 162 163 @Override 164 public ScrollResult<String> scroll(String query, int batchSize, int keepAliveSeconds) { 165 return session.scroll(query, batchSize, keepAliveSeconds); 166 } 167 168 @Override 169 public ScrollResult<String> scroll(String scrollId) { 170 return session.scroll(scrollId); 171 } 172 173 @Override 174 public Document getDocumentByUUID(String uuid) throws DocumentNotFoundException { 175 /* 176 * Document ids coming from higher level have been turned into strings (by {@link SQLDocument#getUUID}) but the 177 * backend may actually expect them to be Longs (for database-generated integer ids). 178 */ 179 Document doc = getDocumentById(idFromString(uuid)); 180 if (doc == null) { 181 // required by callers such as AbstractSession.exists 182 throw new DocumentNotFoundException(uuid); 183 } 184 return doc; 185 } 186 187 @Override 188 public Document resolvePath(String path) throws DocumentNotFoundException { 189 if (path.endsWith("/") && path.length() > 1) { 190 path = path.substring(0, path.length() - 1); 191 } 192 Node node = session.getNodeByPath(path, session.getRootNode()); 193 Document doc = newDocument(node); 194 if (doc == null) { 195 throw new DocumentNotFoundException(path); 196 } 197 return doc; 198 } 199 200 protected void orderBefore(Node node, Node src, Node dest) { 201 session.orderBefore(node, src, dest); 202 } 203 204 @Override 205 public Document move(Document source, Document parent, String name) { 206 assert source instanceof SQLDocument; 207 assert parent instanceof SQLDocument; 208 if (name == null) { 209 name = source.getName(); 210 } 211 Node result = session.move(((SQLDocument) source).getNode(), ((SQLDocument) parent).getNode(), name); 212 return newDocument(result); 213 } 214 215 private static final Pattern dotDigitsPattern = Pattern.compile("(.*)\\.[0-9]+$"); 216 217 protected String findFreeName(Node parentNode, String name) { 218 if (session.hasChildNode(parentNode, name, false)) { 219 Matcher m = dotDigitsPattern.matcher(name); 220 if (m.matches()) { 221 // remove trailing dot and digits 222 name = m.group(1); 223 } 224 // add dot + unique digits 225 name += "." + System.nanoTime(); 226 } 227 return name; 228 } 229 230 @Override 231 public Document copy(Document source, Document parent, String name) { 232 assert source instanceof SQLDocument; 233 assert parent instanceof SQLDocument; 234 if (name == null) { 235 name = source.getName(); 236 } 237 Node parentNode = ((SQLDocument) parent).getNode(); 238 if (!copyFindFreeNameDisabled) { 239 name = findFreeName(parentNode, name); 240 } 241 Node copy = session.copy(((SQLDocument) source).getNode(), parentNode, name); 242 return newDocument(copy); 243 } 244 245 @Override 246 public Document getVersion(String versionableId, VersionModel versionModel) { 247 Serializable vid = idFromString(versionableId); 248 Node versionNode = session.getVersionByLabel(vid, versionModel.getLabel()); 249 if (versionNode == null) { 250 return null; 251 } 252 versionModel.setDescription(versionNode.getSimpleProperty(Model.VERSION_DESCRIPTION_PROP).getString()); 253 versionModel.setCreated((Calendar) versionNode.getSimpleProperty(Model.VERSION_CREATED_PROP).getValue()); 254 return newDocument(versionNode); 255 } 256 257 @Override 258 public Document createProxy(Document doc, Document folder) { 259 Node folderNode = ((SQLDocument) folder).getNode(); 260 Node targetNode = ((SQLDocument) doc).getNode(); 261 Serializable targetId = targetNode.getId(); 262 Serializable versionableId; 263 if (doc.isVersion()) { 264 versionableId = targetNode.getSimpleProperty(Model.VERSION_VERSIONABLE_PROP).getValue(); 265 } else if (doc.isProxy()) { 266 // copy the proxy 267 targetId = targetNode.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 268 versionableId = targetNode.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue(); 269 } else { 270 // working copy (live document) 271 versionableId = targetId; 272 } 273 String name = findFreeName(folderNode, doc.getName()); 274 Node proxy = session.addProxy(targetId, versionableId, folderNode, name, null); 275 return newDocument(proxy); 276 } 277 278 @Override 279 public List<Document> getProxies(Document document, Document parent) { 280 List<Node> proxyNodes = session.getProxies(((SQLDocument) document).getNode(), 281 parent == null ? null : ((SQLDocument) parent).getNode()); 282 List<Document> proxies = new ArrayList<>(proxyNodes.size()); 283 for (Node proxyNode : proxyNodes) { 284 proxies.add(newDocument(proxyNode)); 285 } 286 return proxies; 287 } 288 289 @Override 290 public void setProxyTarget(Document proxy, Document target) { 291 Node proxyNode = ((SQLDocument) proxy).getNode(); 292 Serializable targetId = idFromString(target.getUUID()); 293 session.setProxyTarget(proxyNode, targetId); 294 } 295 296 // returned document is r/w even if a version or a proxy, so that normal 297 // props can be set 298 @Override 299 public Document importDocument(String uuid, Document parent, String name, String typeName, 300 Map<String, Serializable> properties) { 301 boolean isProxy = typeName.equals(Model.PROXY_TYPE); 302 Map<String, Serializable> props = new HashMap<>(); 303 Long pos = null; // TODO pos 304 if (!isProxy) { 305 // version & live document 306 props.put(Model.MISC_LIFECYCLE_POLICY_PROP, properties.get(CoreSession.IMPORT_LIFECYCLE_POLICY)); 307 props.put(Model.MISC_LIFECYCLE_STATE_PROP, properties.get(CoreSession.IMPORT_LIFECYCLE_STATE)); 308 309 Serializable importLockOwnerProp = properties.get(CoreSession.IMPORT_LOCK_OWNER); 310 if (importLockOwnerProp != null) { 311 props.put(Model.LOCK_OWNER_PROP, importLockOwnerProp); 312 } 313 Serializable importLockCreatedProp = properties.get(CoreSession.IMPORT_LOCK_CREATED); 314 if (importLockCreatedProp != null) { 315 props.put(Model.LOCK_CREATED_PROP, importLockCreatedProp); 316 } 317 props.put(Model.MAIN_IS_RETENTION_ACTIVE_PROP, properties.get(CoreSession.IMPORT_IS_RETENTION_ACTIVE)); 318 319 props.put(Model.MAIN_MAJOR_VERSION_PROP, properties.get(CoreSession.IMPORT_VERSION_MAJOR)); 320 props.put(Model.MAIN_MINOR_VERSION_PROP, properties.get(CoreSession.IMPORT_VERSION_MINOR)); 321 props.put(Model.MAIN_IS_VERSION_PROP, properties.get(CoreSession.IMPORT_IS_VERSION)); 322 } 323 Node parentNode; 324 if (parent == null) { 325 // version 326 parentNode = null; 327 props.put(Model.VERSION_VERSIONABLE_PROP, 328 idFromString((String) properties.get(CoreSession.IMPORT_VERSION_VERSIONABLE_ID))); 329 props.put(Model.VERSION_CREATED_PROP, properties.get(CoreSession.IMPORT_VERSION_CREATED)); 330 props.put(Model.VERSION_LABEL_PROP, properties.get(CoreSession.IMPORT_VERSION_LABEL)); 331 props.put(Model.VERSION_DESCRIPTION_PROP, properties.get(CoreSession.IMPORT_VERSION_DESCRIPTION)); 332 props.put(Model.VERSION_IS_LATEST_PROP, properties.get(CoreSession.IMPORT_VERSION_IS_LATEST)); 333 props.put(Model.VERSION_IS_LATEST_MAJOR_PROP, properties.get(CoreSession.IMPORT_VERSION_IS_LATEST_MAJOR)); 334 } else { 335 parentNode = ((SQLDocument) parent).getNode(); 336 if (isProxy) { 337 // proxy 338 props.put(Model.PROXY_TARGET_PROP, 339 idFromString((String) properties.get(CoreSession.IMPORT_PROXY_TARGET_ID))); 340 props.put(Model.PROXY_VERSIONABLE_PROP, 341 idFromString((String) properties.get(CoreSession.IMPORT_PROXY_VERSIONABLE_ID))); 342 } else { 343 // live document 344 props.put(Model.MAIN_BASE_VERSION_PROP, 345 idFromString((String) properties.get(CoreSession.IMPORT_BASE_VERSION_ID))); 346 props.put(Model.MAIN_CHECKED_IN_PROP, properties.get(CoreSession.IMPORT_CHECKED_IN)); 347 } 348 } 349 return importChild(uuid, parentNode, name, pos, typeName, props); 350 } 351 352 protected static final Pattern ORDER_BY_PATH_ASC = Pattern.compile( 353 "(.*)\\s+ORDER\\s+BY\\s+" + NXQL.ECM_PATH + "\\s*$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); 354 355 protected static final Pattern ORDER_BY_PATH_DESC = Pattern.compile( 356 "(.*)\\s+ORDER\\s+BY\\s+" + NXQL.ECM_PATH + "\\s+DESC\\s*$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); 357 358 @Override 359 public PartialList<Document> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) { 360 // do ORDER BY ecm:path by hand in SQLQueryResult as we can't 361 // do it in SQL (and has to do limit/offset as well) 362 Boolean orderByPath; 363 Matcher matcher = ORDER_BY_PATH_ASC.matcher(query); 364 if (matcher.matches()) { 365 orderByPath = Boolean.TRUE; // ASC 366 } else { 367 matcher = ORDER_BY_PATH_DESC.matcher(query); 368 if (matcher.matches()) { 369 orderByPath = Boolean.FALSE; // DESC 370 } else { 371 orderByPath = null; 372 } 373 } 374 long limit = 0; 375 long offset = 0; 376 if (orderByPath != null) { 377 query = matcher.group(1); 378 limit = queryFilter.getLimit(); 379 offset = queryFilter.getOffset(); 380 queryFilter = QueryFilter.withoutLimitOffset(queryFilter); 381 } 382 PartialList<Serializable> pl = session.query(query, queryType, queryFilter, countUpTo); 383 384 // get Documents in bulk, returns a newly-allocated ArrayList 385 List<Document> list = getDocumentsById(pl); 386 387 // order / limit 388 if (orderByPath != null) { 389 Collections.sort(list, new PathComparator(orderByPath.booleanValue())); 390 } 391 if (limit != 0) { 392 // do limit/offset by hand 393 int size = list.size(); 394 list.subList(0, (int) (offset > size ? size : offset)).clear(); 395 size = list.size(); 396 if (limit < size) { 397 list.subList((int) limit, size).clear(); 398 } 399 } 400 return new PartialList<>(list, pl.totalSize()); 401 } 402 403 public static class PathComparator implements Comparator<Document> { 404 405 private final int sign; 406 407 public PathComparator(boolean asc) { 408 this.sign = asc ? 1 : -1; 409 } 410 411 @Override 412 public int compare(Document doc1, Document doc2) { 413 String p1 = doc1.getPath(); 414 String p2 = doc2.getPath(); 415 if (p1 == null && p2 == null) { 416 return sign * doc1.getUUID().compareTo(doc2.getUUID()); 417 } else if (p1 == null) { 418 return sign; 419 } else if (p2 == null) { 420 return -1 * sign; 421 } 422 return sign * p1.compareTo(p2); 423 } 424 } 425 426 @Override 427 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 428 boolean distinctDocuments, Object[] params) { 429 return session.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params); 430 } 431 432 @Override 433 public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType, 434 QueryFilter queryFilter, boolean distinctDocuments, long countUpTo, Object[] params) { 435 return session.queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params); 436 } 437 438 /* 439 * ----- called by SQLDocument ----- 440 */ 441 442 private Document newDocument(Node node) { 443 return newDocument(node, true); 444 } 445 446 // "readonly" meaningful for proxies and versions, used for import 447 private Document newDocument(Node node, boolean readonly) { 448 if (node == null) { 449 // root's parent 450 return null; 451 } 452 453 Node targetNode = null; 454 String typeName = node.getPrimaryType(); 455 if (node.isProxy()) { 456 Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 457 if (targetId == null) { 458 throw new DocumentNotFoundException("Proxy has null target"); 459 } 460 targetNode = session.getNodeById(targetId); 461 typeName = targetNode.getPrimaryType(); 462 } 463 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 464 DocumentType type = schemaManager.getDocumentType(typeName); 465 if (type == null) { 466 throw new DocumentNotFoundException("Unknown document type: " + typeName); 467 } 468 469 if (node.isProxy()) { 470 // proxy seen as a normal document 471 Document proxy = new SQLDocumentLive(node, type, this, false); 472 Document target = newDocument(targetNode, readonly); 473 return new SQLDocumentProxy(proxy, target); 474 } else if (node.isVersion()) { 475 return new SQLDocumentVersion(node, type, this, readonly); 476 } else { 477 return new SQLDocumentLive(node, type, this, false); 478 } 479 } 480 481 // called by SQLQueryResult iterator & others 482 protected Document getDocumentById(Serializable id) { 483 Node node = session.getNodeById(id); 484 return node == null ? null : newDocument(node); 485 } 486 487 // called by SQLQueryResult iterator 488 protected List<Document> getDocumentsById(List<Serializable> ids) { 489 List<Document> docs = new ArrayList<>(ids.size()); 490 List<Node> nodes = session.getNodesByIds(ids); 491 for (int index = 0; index < ids.size(); ++index) { 492 Node eachNode = nodes.get(index); 493 if (eachNode == null) { 494 if (log.isTraceEnabled()) { 495 Serializable id = ids.get(index); 496 log.trace("Cannot fetch document with id: " + id, new Throwable("debug stack trace")); 497 } 498 continue; 499 } 500 Document doc; 501 try { 502 doc = newDocument(eachNode); 503 } catch (DocumentNotFoundException e) { 504 // unknown type in db, ignore 505 continue; 506 } 507 docs.add(doc); 508 } 509 return docs; 510 } 511 512 protected Document getParent(Node node) { 513 return newDocument(session.getParentNode(node)); 514 } 515 516 protected String getPath(Node node) { 517 return session.getPath(node); 518 } 519 520 protected Document getChild(Node node, String name) throws DocumentNotFoundException { 521 Node childNode = session.getChildNode(node, name, false); 522 Document doc = newDocument(childNode); 523 if (doc == null) { 524 throw new DocumentNotFoundException(name); 525 } 526 return doc; 527 } 528 529 protected Node getChildProperty(Node node, String name, String typeName) { 530 // all complex property children have already been created by SessionImpl.addChildNode or 531 // SessionImpl.addMixinType 532 // if one is missing here, it means that it was concurrently deleted and we're only now finding out 533 // or that a schema change was done and we now expect a new child 534 // return null in that case 535 return session.getChildNode(node, name, true); 536 } 537 538 protected Node getChildPropertyForWrite(Node node, String name, String typeName) { 539 Node childNode = getChildProperty(node, name, typeName); 540 if (childNode == null) { 541 // create the needed complex property immediately 542 childNode = session.addChildNode(node, name, null, typeName, true); 543 } 544 return childNode; 545 } 546 547 protected List<Document> getChildren(Node node) { 548 List<Node> nodes = session.getChildren(node, null, false); 549 List<Document> children = new ArrayList<>(nodes.size()); 550 for (Node n : nodes) { 551 try { 552 children.add(newDocument(n)); 553 } catch (DocumentNotFoundException e) { 554 // ignore error retrieving one of the children 555 continue; 556 } 557 } 558 return children; 559 } 560 561 protected boolean hasChild(Node node, String name) { 562 return session.hasChildNode(node, name, false); 563 } 564 565 protected boolean hasChildren(Node node) { 566 return session.hasChildren(node, false); 567 } 568 569 protected Document addChild(Node parent, String name, Long pos, String typeName) { 570 return newDocument(session.addChildNode(parent, name, pos, typeName, false)); 571 } 572 573 protected Node addChildProperty(Node parent, String name, Long pos, String typeName) { 574 return session.addChildNode(parent, name, pos, typeName, true); 575 } 576 577 protected Document importChild(String uuid, Node parent, String name, Long pos, String typeName, 578 Map<String, Serializable> props) { 579 Serializable id = idFromString(uuid); 580 Node node = session.addChildNode(id, parent, name, pos, typeName, false); 581 for (Entry<String, Serializable> entry : props.entrySet()) { 582 node.setSimpleProperty(entry.getKey(), entry.getValue()); 583 } 584 return newDocument(node, false); // not readonly 585 } 586 587 protected boolean addMixinType(Node node, String mixin) { 588 return session.addMixinType(node, mixin); 589 } 590 591 protected boolean removeMixinType(Node node, String mixin) { 592 return session.removeMixinType(node, mixin); 593 } 594 595 protected List<Node> getComplexList(Node node, String name) { 596 List<Node> nodes = session.getChildren(node, name, true); 597 return nodes; 598 } 599 600 protected void remove(Node node) { 601 session.removeNode(node); 602 } 603 604 protected void removeProperty(Node node) { 605 session.removePropertyNode(node); 606 } 607 608 protected Document checkIn(Node node, String label, String checkinComment) { 609 Node versionNode = session.checkIn(node, label, checkinComment); 610 return versionNode == null ? null : newDocument(versionNode); 611 } 612 613 protected void checkOut(Node node) { 614 session.checkOut(node); 615 } 616 617 protected void restore(Node node, Node version) { 618 session.restore(node, version); 619 } 620 621 protected Document getVersionByLabel(String versionSeriesId, String label) { 622 Serializable vid = idFromString(versionSeriesId); 623 Node versionNode = session.getVersionByLabel(vid, label); 624 return versionNode == null ? null : newDocument(versionNode); 625 } 626 627 protected List<Document> getVersions(String versionSeriesId) { 628 Serializable vid = idFromString(versionSeriesId); 629 List<Node> versionNodes = session.getVersions(vid); 630 List<Document> versions = new ArrayList<>(versionNodes.size()); 631 for (Node versionNode : versionNodes) { 632 versions.add(newDocument(versionNode)); 633 } 634 return versions; 635 } 636 637 public Document getLastVersion(String versionSeriesId) { 638 Serializable vid = idFromString(versionSeriesId); 639 Node versionNode = session.getLastVersion(vid); 640 if (versionNode == null) { 641 return null; 642 } 643 return newDocument(versionNode); 644 } 645 646 protected Node getNodeById(Serializable id) { 647 return session.getNodeById(id); 648 } 649 650 @Override 651 public LockManager getLockManager() { 652 return session.getLockManager(); 653 } 654 655 @Override 656 public boolean isNegativeAclAllowed() { 657 return negativeAclAllowed; 658 } 659 660 @Override 661 public void updateReadACLs(Collection<String> docIds) { 662 throw new UnsupportedOperationException(); 663 } 664 665 @Override 666 public void setACP(Document doc, ACP acp, boolean overwrite) { 667 if (!overwrite && acp == null) { 668 return; 669 } 670 checkNegativeAcl(acp); 671 Node node = ((SQLDocument) doc).getNode(); 672 ACLRow[] aclrows; 673 if (overwrite) { 674 aclrows = acp == null ? null : acpToAclRows(acp); 675 } else { 676 aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue(); 677 aclrows = updateAclRows(aclrows, acp); 678 } 679 node.getCollectionProperty(Model.ACL_PROP).setValue(aclrows); 680 session.requireReadAclsUpdate(); 681 } 682 683 protected void checkNegativeAcl(ACP acp) { 684 if (negativeAclAllowed) { 685 return; 686 } 687 if (acp == null) { 688 return; 689 } 690 for (ACL acl : acp.getACLs()) { 691 if (acl.getName().equals(ACL.INHERITED_ACL)) { 692 continue; 693 } 694 for (ACE ace : acl.getACEs()) { 695 if (ace.isGranted()) { 696 continue; 697 } 698 String permission = ace.getPermission(); 699 if (permission.equals(SecurityConstants.EVERYTHING) 700 && ace.getUsername().equals(SecurityConstants.EVERYONE)) { 701 continue; 702 } 703 // allow Write, as we're sure it doesn't include Read/Browse 704 if (permission.equals(SecurityConstants.WRITE)) { 705 continue; 706 } 707 throw new IllegalArgumentException("Negative ACL not allowed: " + ace); 708 } 709 } 710 } 711 712 @Override 713 public ACP getMergedACP(Document doc) { 714 Document base = doc.isVersion() ? doc.getSourceDocument() : doc; 715 if (base == null) { 716 return null; 717 } 718 ACP acp = getACP(base); 719 if (doc.getParent() == null) { 720 return acp; 721 } 722 // get inherited acls only if no blocking inheritance ACE exists in the top level acp. 723 ACL acl = null; 724 if (acp == null || acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) != Access.DENY) { 725 acl = getInheritedACLs(doc); 726 } 727 if (acp == null) { 728 if (acl == null) { 729 return null; 730 } 731 acp = new ACPImpl(); 732 } 733 if (acl != null) { 734 acp.addACL(acl); 735 } 736 return acp; 737 } 738 739 /* 740 * ----- internal methods ----- 741 */ 742 743 protected ACP getACP(Document doc) { 744 Node node = ((SQLDocument) doc).getNode(); 745 ACLRow[] aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue(); 746 return aclRowsToACP(aclrows); 747 } 748 749 // unit tested 750 protected static ACP aclRowsToACP(ACLRow[] acls) { 751 ACP acp = new ACPImpl(); 752 ACL acl = null; 753 String name = null; 754 for (ACLRow aclrow : acls) { 755 if (!aclrow.name.equals(name)) { 756 if (acl != null) { 757 acp.addACL(acl); 758 } 759 name = aclrow.name; 760 acl = new ACLImpl(name); 761 } 762 // XXX should prefix user/group 763 String user = aclrow.user; 764 if (user == null) { 765 user = aclrow.group; 766 } 767 acl.add(ACE.builder(user, aclrow.permission) 768 .isGranted(aclrow.grant) 769 .creator(aclrow.creator) 770 .begin(aclrow.begin) 771 .end(aclrow.end) 772 .build()); 773 } 774 if (acl != null) { 775 acp.addACL(acl); 776 } 777 return acp; 778 } 779 780 // unit tested 781 protected static ACLRow[] acpToAclRows(ACP acp) { 782 List<ACLRow> aclrows = new LinkedList<>(); 783 for (ACL acl : acp.getACLs()) { 784 String name = acl.getName(); 785 if (name.equals(ACL.INHERITED_ACL)) { 786 continue; 787 } 788 for (ACE ace : acl.getACEs()) { 789 addACLRow(aclrows, name, ace); 790 } 791 } 792 ACLRow[] array = new ACLRow[aclrows.size()]; 793 return aclrows.toArray(array); 794 } 795 796 // unit tested 797 protected static ACLRow[] updateAclRows(ACLRow[] aclrows, ACP acp) { 798 List<ACLRow> newaclrows = new LinkedList<>(); 799 Map<String, ACL> aclmap = new HashMap<>(); 800 for (ACL acl : acp.getACLs()) { 801 String name = acl.getName(); 802 if (ACL.INHERITED_ACL.equals(name)) { 803 continue; 804 } 805 aclmap.put(name, acl); 806 } 807 List<ACE> aces = Collections.emptyList(); 808 Set<String> aceKeys = null; 809 String name = null; 810 for (ACLRow aclrow : aclrows) { 811 // new acl? 812 if (!aclrow.name.equals(name)) { 813 // finish remaining aces 814 for (ACE ace : aces) { 815 addACLRow(newaclrows, name, ace); 816 } 817 // start next round 818 name = aclrow.name; 819 ACL acl = aclmap.remove(name); 820 aces = acl == null ? Collections.<ACE> emptyList() : new LinkedList<>(Arrays.asList(acl.getACEs())); 821 aceKeys = new HashSet<>(); 822 for (ACE ace : aces) { 823 aceKeys.add(getACEkey(ace)); 824 } 825 } 826 if (!aceKeys.contains(getACLrowKey(aclrow))) { 827 // no match, keep the aclrow info instead of the ace 828 newaclrows.add(new ACLRow(newaclrows.size(), name, aclrow.grant, aclrow.permission, aclrow.user, 829 aclrow.group, aclrow.creator, aclrow.begin, aclrow.end, aclrow.status)); 830 } 831 } 832 // finish remaining aces for last acl done 833 for (ACE ace : aces) { 834 addACLRow(newaclrows, name, ace); 835 } 836 // do non-done acls 837 for (ACL acl : aclmap.values()) { 838 name = acl.getName(); 839 for (ACE ace : acl.getACEs()) { 840 addACLRow(newaclrows, name, ace); 841 } 842 } 843 ACLRow[] array = new ACLRow[newaclrows.size()]; 844 return newaclrows.toArray(array); 845 } 846 847 /** Key to distinguish ACEs */ 848 protected static String getACEkey(ACE ace) { 849 // TODO separate user/group 850 return ace.getUsername() + '|' + ace.getPermission(); 851 } 852 853 /** Key to distinguish ACLRows */ 854 protected static String getACLrowKey(ACLRow aclrow) { 855 // TODO separate user/group 856 String user = aclrow.user; 857 if (user == null) { 858 user = aclrow.group; 859 } 860 return user + '|' + aclrow.permission; 861 } 862 863 protected static void addACLRow(List<ACLRow> aclrows, String name, ACE ace) { 864 // XXX should prefix user/group 865 String user = ace.getUsername(); 866 if (user == null) { 867 // JCR implementation logs null and skips it 868 return; 869 } 870 String group = null; // XXX all in user for now 871 aclrows.add(new ACLRow(aclrows.size(), name, ace.isGranted(), ace.getPermission(), user, group, 872 ace.getCreator(), ace.getBegin(), ace.getEnd(), ace.getLongStatus())); 873 } 874 875 protected ACL getInheritedACLs(Document doc) { 876 doc = doc.getParent(); 877 ACL merged = null; 878 while (doc != null) { 879 ACP acp = getACP(doc); 880 if (acp != null) { 881 ACL acl = acp.getMergedACLs(ACL.INHERITED_ACL); 882 if (merged == null) { 883 merged = acl; 884 } else { 885 merged.addAll(acl); 886 } 887 if (acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) == Access.DENY) { 888 break; 889 } 890 } 891 doc = doc.getParent(); 892 } 893 return merged; 894 } 895 896 @Override 897 public Map<String, String> getBinaryFulltext(String id) { 898 return session.getBinaryFulltext(idFromString(id)); 899 } 900 901 public boolean isChangeTokenEnabled() { 902 return session.isChangeTokenEnabled(); 903 } 904 905 public void markUserChange(Serializable id) { 906 session.markUserChange(id); 907 } 908 909}