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