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