001/* 002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012package org.nuxeo.ecm.core.storage.sql.coremodel; 013 014import java.io.Serializable; 015import java.text.DateFormat; 016import java.text.ParseException; 017import java.util.ArrayList; 018import java.util.Arrays; 019import java.util.Calendar; 020import java.util.Collections; 021import java.util.Comparator; 022import java.util.GregorianCalendar; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Set; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import javax.resource.ResourceException; 034 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.nuxeo.ecm.core.api.CoreSession; 038import org.nuxeo.ecm.core.api.DocumentNotFoundException; 039import org.nuxeo.ecm.core.api.IterableQueryResult; 040import org.nuxeo.ecm.core.api.PartialList; 041import org.nuxeo.ecm.core.api.VersionModel; 042import org.nuxeo.ecm.core.api.security.ACE; 043import org.nuxeo.ecm.core.api.security.ACL; 044import org.nuxeo.ecm.core.api.security.ACP; 045import org.nuxeo.ecm.core.api.security.Access; 046import org.nuxeo.ecm.core.api.security.SecurityConstants; 047import org.nuxeo.ecm.core.api.security.impl.ACLImpl; 048import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 049import org.nuxeo.ecm.core.model.Document; 050import org.nuxeo.ecm.core.model.LockManager; 051import org.nuxeo.ecm.core.model.Repository; 052import org.nuxeo.ecm.core.model.Session; 053import org.nuxeo.ecm.core.query.QueryFilter; 054import org.nuxeo.ecm.core.query.sql.NXQL; 055import org.nuxeo.ecm.core.schema.DocumentType; 056import org.nuxeo.ecm.core.schema.SchemaManager; 057import org.nuxeo.ecm.core.storage.sql.ACLRow; 058import org.nuxeo.ecm.core.storage.sql.Model; 059import org.nuxeo.ecm.core.storage.sql.Node; 060import org.nuxeo.runtime.api.Framework; 061 062/** 063 * This class is the bridge between the Nuxeo SPI Session and the actual low-level implementation of the SQL storage 064 * session. 065 * 066 * @author Florent Guillaume 067 */ 068public class SQLSession implements Session { 069 070 protected final Log log = LogFactory.getLog(SQLSession.class); 071 072 /** 073 * Framework property to control whether negative ACLs (deny) are allowed. 074 * 075 * @since 6.0 076 */ 077 public static final String ALLOW_NEGATIVE_ACL_PROPERTY = "nuxeo.security.allowNegativeACL"; 078 079 /** 080 * Framework property to disabled free-name collision detection for copy. This is useful when constraints have been 081 * added to the database to detect collisions at the database level and raise a ConcurrentUpdateException, thus 082 * letting the high-level application deal with collisions. 083 * 084 * @since 7.3 085 */ 086 public static final String COPY_FINDFREENAME_DISABLED_PROP = "nuxeo.vcs.copy.findFreeName.disabled"; 087 088 private final Repository repository; 089 090 private final org.nuxeo.ecm.core.storage.sql.Session session; 091 092 private Document root; 093 094 private final boolean negativeAclAllowed; 095 096 private final boolean copyFindFreeNameDisabled; 097 098 public SQLSession(org.nuxeo.ecm.core.storage.sql.Session session, Repository repository) { 099 this.session = session; 100 this.repository = repository; 101 Node rootNode = session.getRootNode(); 102 root = newDocument(rootNode); 103 negativeAclAllowed = Framework.isBooleanPropertyTrue(ALLOW_NEGATIVE_ACL_PROPERTY); 104 copyFindFreeNameDisabled = Framework.isBooleanPropertyTrue(COPY_FINDFREENAME_DISABLED_PROP); 105 } 106 107 /* 108 * ----- org.nuxeo.ecm.core.model.Session ----- 109 */ 110 111 @Override 112 public Document getRootDocument() { 113 return root; 114 } 115 116 @Override 117 public Document getNullDocument() { 118 return new SQLDocumentLive(null, null, this, true); 119 } 120 121 @Override 122 public void close() { 123 root = null; 124 try { 125 session.close(); 126 } catch (ResourceException e) { 127 throw new RuntimeException(e); 128 } 129 } 130 131 @Override 132 public void save() { 133 session.save(); 134 } 135 136 @Override 137 public boolean isLive() { 138 // session can become non-live behind our back 139 // through ConnectionAwareXAResource that closes 140 // all handles (sessions) at tx end() time 141 return session != null && session.isLive(); 142 } 143 144 @Override 145 public boolean isStateSharedByAllThreadSessions() { 146 return session.isStateSharedByAllThreadSessions(); 147 } 148 149 @Override 150 public String getRepositoryName() { 151 return repository.getName(); 152 } 153 154 protected String idToString(Serializable id) { 155 return session.getModel().idToString(id); 156 } 157 158 protected Serializable idFromString(String id) { 159 return session.getModel().idFromString(id); 160 } 161 162 @Override 163 public Document getDocumentByUUID(String uuid) throws DocumentNotFoundException { 164 /* 165 * Document ids coming from higher level have been turned into strings (by {@link SQLDocument#getUUID}) but the 166 * backend may actually expect them to be Longs (for database-generated integer ids). 167 */ 168 Document doc = getDocumentById(idFromString(uuid)); 169 if (doc == null) { 170 // required by callers such as AbstractSession.exists 171 throw new DocumentNotFoundException(uuid); 172 } 173 return doc; 174 } 175 176 @Override 177 public Document resolvePath(String path) throws DocumentNotFoundException { 178 if (path.endsWith("/") && path.length() > 1) { 179 path = path.substring(0, path.length() - 1); 180 } 181 Node node = session.getNodeByPath(path, session.getRootNode()); 182 Document doc = newDocument(node); 183 if (doc == null) { 184 throw new DocumentNotFoundException(path); 185 } 186 return doc; 187 } 188 189 protected void orderBefore(Node node, Node src, Node dest) { 190 session.orderBefore(node, src, dest); 191 } 192 193 @Override 194 public Document move(Document source, Document parent, String name) { 195 assert source instanceof SQLDocument; 196 assert parent instanceof SQLDocument; 197 if (name == null) { 198 name = source.getName(); 199 } 200 Node result = session.move(((SQLDocument) source).getNode(), ((SQLDocument) parent).getNode(), name); 201 return newDocument(result); 202 } 203 204 private static final Pattern dotDigitsPattern = Pattern.compile("(.*)\\.[0-9]+$"); 205 206 protected String findFreeName(Node parentNode, String name) { 207 if (session.hasChildNode(parentNode, name, false)) { 208 Matcher m = dotDigitsPattern.matcher(name); 209 if (m.matches()) { 210 // remove trailing dot and digits 211 name = m.group(1); 212 } 213 // add dot + unique digits 214 name += "." + System.nanoTime(); 215 } 216 return name; 217 } 218 219 @Override 220 public Document copy(Document source, Document parent, String name) { 221 assert source instanceof SQLDocument; 222 assert parent instanceof SQLDocument; 223 if (name == null) { 224 name = source.getName(); 225 } 226 Node parentNode = ((SQLDocument) parent).getNode(); 227 if (!copyFindFreeNameDisabled) { 228 name = findFreeName(parentNode, name); 229 } 230 Node copy = session.copy(((SQLDocument) source).getNode(), parentNode, name); 231 return newDocument(copy); 232 } 233 234 @Override 235 public Document getVersion(String versionableId, VersionModel versionModel) { 236 Serializable vid = idFromString(versionableId); 237 Node versionNode = session.getVersionByLabel(vid, versionModel.getLabel()); 238 if (versionNode == null) { 239 return null; 240 } 241 versionModel.setDescription(versionNode.getSimpleProperty(Model.VERSION_DESCRIPTION_PROP).getString()); 242 versionModel.setCreated((Calendar) versionNode.getSimpleProperty(Model.VERSION_CREATED_PROP).getValue()); 243 return newDocument(versionNode); 244 } 245 246 @Override 247 public Document createProxy(Document doc, Document folder) { 248 Node folderNode = ((SQLDocument) folder).getNode(); 249 Node targetNode = ((SQLDocument) doc).getNode(); 250 Serializable targetId = targetNode.getId(); 251 Serializable versionableId; 252 if (doc.isVersion()) { 253 versionableId = targetNode.getSimpleProperty(Model.VERSION_VERSIONABLE_PROP).getValue(); 254 } else if (doc.isProxy()) { 255 // copy the proxy 256 targetId = targetNode.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 257 versionableId = targetNode.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue(); 258 } else { 259 // working copy (live document) 260 versionableId = targetId; 261 } 262 String name = findFreeName(folderNode, doc.getName()); 263 Node proxy = session.addProxy(targetId, versionableId, folderNode, name, null); 264 return newDocument(proxy); 265 } 266 267 @Override 268 public List<Document> getProxies(Document document, Document parent) { 269 List<Node> proxyNodes = session.getProxies(((SQLDocument) document).getNode(), 270 parent == null ? null : ((SQLDocument) parent).getNode()); 271 List<Document> proxies = new ArrayList<Document>(proxyNodes.size()); 272 for (Node proxyNode : proxyNodes) { 273 proxies.add(newDocument(proxyNode)); 274 } 275 return proxies; 276 } 277 278 @Override 279 public void setProxyTarget(Document proxy, Document target) { 280 Node proxyNode = ((SQLDocument) proxy).getNode(); 281 Serializable targetId = idFromString(target.getUUID()); 282 session.setProxyTarget(proxyNode, targetId); 283 } 284 285 // returned document is r/w even if a version or a proxy, so that normal 286 // props can be set 287 @Override 288 public Document importDocument(String uuid, Document parent, String name, String typeName, 289 Map<String, Serializable> properties) { 290 assert Model.PROXY_TYPE == CoreSession.IMPORT_PROXY_TYPE; 291 boolean isProxy = typeName.equals(Model.PROXY_TYPE); 292 Map<String, Serializable> props = new HashMap<String, Serializable>(); 293 Long pos = null; // TODO pos 294 if (!isProxy) { 295 // version & live document 296 props.put(Model.MISC_LIFECYCLE_POLICY_PROP, properties.get(CoreSession.IMPORT_LIFECYCLE_POLICY)); 297 props.put(Model.MISC_LIFECYCLE_STATE_PROP, properties.get(CoreSession.IMPORT_LIFECYCLE_STATE)); 298 // compat with old lock import 299 @SuppressWarnings("deprecation") 300 String key = (String) properties.get(CoreSession.IMPORT_LOCK); 301 if (key != null) { 302 String[] values = key.split(":"); 303 if (values.length == 2) { 304 String owner = values[0]; 305 Calendar created = new GregorianCalendar(); 306 try { 307 created.setTimeInMillis( 308 DateFormat.getDateInstance(DateFormat.MEDIUM).parse(values[1]).getTime()); 309 } catch (ParseException e) { 310 // use current date 311 } 312 props.put(Model.LOCK_OWNER_PROP, owner); 313 props.put(Model.LOCK_CREATED_PROP, created); 314 } 315 } 316 317 Serializable importLockOwnerProp = properties.get(CoreSession.IMPORT_LOCK_OWNER); 318 if (importLockOwnerProp != null) { 319 props.put(Model.LOCK_OWNER_PROP, importLockOwnerProp); 320 } 321 Serializable importLockCreatedProp = properties.get(CoreSession.IMPORT_LOCK_CREATED); 322 if (importLockCreatedProp != null) { 323 props.put(Model.LOCK_CREATED_PROP, importLockCreatedProp); 324 } 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.list); 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, Object[] params) { 435 return session.queryAndFetch(query, queryType, queryFilter, 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.getLocalService(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<Document>(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 Serializable eachId = ids.get(index); 495 log.warn("Cannot fetch document by id " + eachId, new Throwable("debug stack trace")); 496 continue; 497 } 498 Document doc; 499 try { 500 doc = newDocument(eachNode); 501 } catch (DocumentNotFoundException e) { 502 // unknown type in db, ignore 503 continue; 504 } 505 docs.add(doc); 506 } 507 return docs; 508 } 509 510 protected Document getParent(Node node) { 511 return newDocument(session.getParentNode(node)); 512 } 513 514 protected String getPath(Node node) { 515 return session.getPath(node); 516 } 517 518 protected Document getChild(Node node, String name) throws DocumentNotFoundException { 519 Node childNode = session.getChildNode(node, name, false); 520 Document doc = newDocument(childNode); 521 if (doc == null) { 522 throw new DocumentNotFoundException(name); 523 } 524 return doc; 525 } 526 527 protected Node getChildProperty(Node node, String name, String typeName) { 528 // all complex property children have already been created by SessionImpl.addChildNode or 529 // SessionImpl.addMixinType 530 // if one is missing here, it means that it was concurrently deleted and we're only now finding out 531 // or that a schema change was done and we now expect a new child 532 // return null in that case 533 return session.getChildNode(node, name, true); 534 } 535 536 protected Node getChildPropertyForWrite(Node node, String name, String typeName) { 537 Node childNode = getChildProperty(node, name, typeName); 538 if (childNode == null) { 539 // create the needed complex property immediately 540 childNode = session.addChildNode(node, name, null, typeName, true); 541 } 542 return childNode; 543 } 544 545 protected List<Document> getChildren(Node node) { 546 List<Node> nodes = session.getChildren(node, null, false); 547 List<Document> children = new ArrayList<Document>(nodes.size()); 548 for (Node n : nodes) { 549 try { 550 children.add(newDocument(n)); 551 } catch (DocumentNotFoundException e) { 552 // ignore error retrieving one of the children 553 continue; 554 } 555 } 556 return children; 557 } 558 559 protected boolean hasChild(Node node, String name) { 560 return session.hasChildNode(node, name, false); 561 } 562 563 protected boolean hasChildren(Node node) { 564 return session.hasChildren(node, false); 565 } 566 567 protected Document addChild(Node parent, String name, Long pos, String typeName) { 568 return newDocument(session.addChildNode(parent, name, pos, typeName, false)); 569 } 570 571 protected Node addChildProperty(Node parent, String name, Long pos, String typeName) { 572 return session.addChildNode(parent, name, pos, typeName, true); 573 } 574 575 protected Document importChild(String uuid, Node parent, String name, Long pos, String typeName, 576 Map<String, Serializable> props) { 577 Serializable id = idFromString(uuid); 578 Node node = session.addChildNode(id, parent, name, pos, typeName, false); 579 for (Entry<String, Serializable> entry : props.entrySet()) { 580 node.setSimpleProperty(entry.getKey(), entry.getValue()); 581 } 582 return newDocument(node, false); // not readonly 583 } 584 585 protected boolean addMixinType(Node node, String mixin) { 586 return session.addMixinType(node, mixin); 587 } 588 589 protected boolean removeMixinType(Node node, String mixin) { 590 return session.removeMixinType(node, mixin); 591 } 592 593 protected List<Node> getComplexList(Node node, String name) { 594 List<Node> nodes = session.getChildren(node, name, true); 595 return nodes; 596 } 597 598 protected void remove(Node node) { 599 session.removeNode(node); 600 } 601 602 protected void removeProperty(Node node) { 603 session.removePropertyNode(node); 604 } 605 606 protected Document checkIn(Node node, String label, String checkinComment) { 607 Node versionNode = session.checkIn(node, label, checkinComment); 608 return versionNode == null ? null : newDocument(versionNode); 609 } 610 611 protected void checkOut(Node node) { 612 session.checkOut(node); 613 } 614 615 protected void restore(Node node, Node version) { 616 session.restore(node, version); 617 } 618 619 protected Document getVersionByLabel(String versionSeriesId, String label) { 620 Serializable vid = idFromString(versionSeriesId); 621 Node versionNode = session.getVersionByLabel(vid, label); 622 return versionNode == null ? null : newDocument(versionNode); 623 } 624 625 protected List<Document> getVersions(String versionSeriesId) { 626 Serializable vid = idFromString(versionSeriesId); 627 List<Node> versionNodes = session.getVersions(vid); 628 List<Document> versions = new ArrayList<Document>(versionNodes.size()); 629 for (Node versionNode : versionNodes) { 630 versions.add(newDocument(versionNode)); 631 } 632 return versions; 633 } 634 635 public Document getLastVersion(String versionSeriesId) { 636 Serializable vid = idFromString(versionSeriesId); 637 Node versionNode = session.getLastVersion(vid); 638 if (versionNode == null) { 639 return null; 640 } 641 return newDocument(versionNode); 642 } 643 644 protected Node getNodeById(Serializable id) { 645 return session.getNodeById(id); 646 } 647 648 @Override 649 public LockManager getLockManager() { 650 return session.getLockManager(); 651 } 652 653 @Override 654 public boolean isNegativeAclAllowed() { 655 return negativeAclAllowed; 656 } 657 658 @Override 659 public void setACP(Document doc, ACP acp, boolean overwrite) { 660 if (!overwrite && acp == null) { 661 return; 662 } 663 checkNegativeAcl(acp); 664 Node node = ((SQLDocument) doc).getNode(); 665 ACLRow[] aclrows; 666 if (overwrite) { 667 aclrows = acp == null ? null : acpToAclRows(acp); 668 } else { 669 aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue(); 670 aclrows = updateAclRows(aclrows, acp); 671 } 672 node.getCollectionProperty(Model.ACL_PROP).setValue(aclrows); 673 session.requireReadAclsUpdate(); 674 } 675 676 protected void checkNegativeAcl(ACP acp) { 677 if (negativeAclAllowed) { 678 return; 679 } 680 if (acp == null) { 681 return; 682 } 683 for (ACL acl : acp.getACLs()) { 684 if (acl.getName().equals(ACL.INHERITED_ACL)) { 685 continue; 686 } 687 for (ACE ace : acl.getACEs()) { 688 if (ace.isGranted()) { 689 continue; 690 } 691 String permission = ace.getPermission(); 692 if (permission.equals(SecurityConstants.EVERYTHING) 693 && ace.getUsername().equals(SecurityConstants.EVERYONE)) { 694 continue; 695 } 696 // allow Write, as we're sure it doesn't include Read/Browse 697 if (permission.equals(SecurityConstants.WRITE)) { 698 continue; 699 } 700 throw new IllegalArgumentException("Negative ACL not allowed: " + ace); 701 } 702 } 703 } 704 705 @Override 706 public ACP getMergedACP(Document doc) { 707 Document base = doc.isVersion() ? doc.getSourceDocument() : doc; 708 if (base == null) { 709 return null; 710 } 711 ACP acp = getACP(base); 712 if (doc.getParent() == null) { 713 return acp; 714 } 715 // get inherited acls only if no blocking inheritance ACE exists in the top level acp. 716 ACL acl = null; 717 if (acp == null || acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) != Access.DENY) { 718 acl = getInheritedACLs(doc); 719 } 720 if (acp == null) { 721 if (acl == null) { 722 return null; 723 } 724 acp = new ACPImpl(); 725 } 726 if (acl != null) { 727 acp.addACL(acl); 728 } 729 return acp; 730 } 731 732 /* 733 * ----- internal methods ----- 734 */ 735 736 protected ACP getACP(Document doc) { 737 Node node = ((SQLDocument) doc).getNode(); 738 ACLRow[] aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue(); 739 return aclRowsToACP(aclrows); 740 } 741 742 // unit tested 743 protected static ACP aclRowsToACP(ACLRow[] acls) { 744 ACP acp = new ACPImpl(); 745 ACL acl = null; 746 String name = null; 747 for (ACLRow aclrow : acls) { 748 if (!aclrow.name.equals(name)) { 749 if (acl != null) { 750 acp.addACL(acl); 751 } 752 name = aclrow.name; 753 acl = new ACLImpl(name); 754 } 755 // XXX should prefix user/group 756 String user = aclrow.user; 757 if (user == null) { 758 user = aclrow.group; 759 } 760 acl.add(ACE.builder(user, aclrow.permission) 761 .isGranted(aclrow.grant) 762 .creator(aclrow.creator) 763 .begin(aclrow.begin) 764 .end(aclrow.end) 765 .build()); 766 } 767 if (acl != null) { 768 acp.addACL(acl); 769 } 770 return acp; 771 } 772 773 // unit tested 774 protected static ACLRow[] acpToAclRows(ACP acp) { 775 List<ACLRow> aclrows = new LinkedList<ACLRow>(); 776 for (ACL acl : acp.getACLs()) { 777 String name = acl.getName(); 778 if (name.equals(ACL.INHERITED_ACL)) { 779 continue; 780 } 781 for (ACE ace : acl.getACEs()) { 782 addACLRow(aclrows, name, ace); 783 } 784 } 785 ACLRow[] array = new ACLRow[aclrows.size()]; 786 return aclrows.toArray(array); 787 } 788 789 // unit tested 790 protected static ACLRow[] updateAclRows(ACLRow[] aclrows, ACP acp) { 791 List<ACLRow> newaclrows = new LinkedList<ACLRow>(); 792 Map<String, ACL> aclmap = new HashMap<String, ACL>(); 793 for (ACL acl : acp.getACLs()) { 794 String name = acl.getName(); 795 if (ACL.INHERITED_ACL.equals(name)) { 796 continue; 797 } 798 aclmap.put(name, acl); 799 } 800 List<ACE> aces = Collections.emptyList(); 801 Set<String> aceKeys = null; 802 String name = null; 803 for (ACLRow aclrow : aclrows) { 804 // new acl? 805 if (!aclrow.name.equals(name)) { 806 // finish remaining aces 807 for (ACE ace : aces) { 808 addACLRow(newaclrows, name, ace); 809 } 810 // start next round 811 name = aclrow.name; 812 ACL acl = aclmap.remove(name); 813 aces = acl == null ? Collections.<ACE> emptyList() : new LinkedList<ACE>(Arrays.asList(acl.getACEs())); 814 aceKeys = new HashSet<String>(); 815 for (ACE ace : aces) { 816 aceKeys.add(getACEkey(ace)); 817 } 818 } 819 if (!aceKeys.contains(getACLrowKey(aclrow))) { 820 // no match, keep the aclrow info instead of the ace 821 newaclrows.add(new ACLRow(newaclrows.size(), name, aclrow.grant, aclrow.permission, aclrow.user, 822 aclrow.group, aclrow.creator, aclrow.begin, aclrow.end, aclrow.status)); 823 } 824 } 825 // finish remaining aces for last acl done 826 for (ACE ace : aces) { 827 addACLRow(newaclrows, name, ace); 828 } 829 // do non-done acls 830 for (ACL acl : aclmap.values()) { 831 name = acl.getName(); 832 for (ACE ace : acl.getACEs()) { 833 addACLRow(newaclrows, name, ace); 834 } 835 } 836 ACLRow[] array = new ACLRow[newaclrows.size()]; 837 return newaclrows.toArray(array); 838 } 839 840 /** Key to distinguish ACEs */ 841 protected static String getACEkey(ACE ace) { 842 // TODO separate user/group 843 return ace.getUsername() + '|' + ace.getPermission(); 844 } 845 846 /** Key to distinguish ACLRows */ 847 protected static String getACLrowKey(ACLRow aclrow) { 848 // TODO separate user/group 849 String user = aclrow.user; 850 if (user == null) { 851 user = aclrow.group; 852 } 853 return user + '|' + aclrow.permission; 854 } 855 856 protected static void addACLRow(List<ACLRow> aclrows, String name, ACE ace) { 857 // XXX should prefix user/group 858 String user = ace.getUsername(); 859 if (user == null) { 860 // JCR implementation logs null and skips it 861 return; 862 } 863 String group = null; // XXX all in user for now 864 aclrows.add(new ACLRow(aclrows.size(), name, ace.isGranted(), ace.getPermission(), user, group, 865 ace.getCreator(), ace.getBegin(), ace.getEnd(), ace.getLongStatus())); 866 } 867 868 protected ACL getInheritedACLs(Document doc) { 869 doc = doc.getParent(); 870 ACL merged = null; 871 while (doc != null) { 872 ACP acp = getACP(doc); 873 if (acp != null) { 874 ACL acl = acp.getMergedACLs(ACL.INHERITED_ACL); 875 if (merged == null) { 876 merged = acl; 877 } else { 878 merged.addAll(acl); 879 } 880 if (acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) == Access.DENY) { 881 break; 882 } 883 } 884 doc = doc.getParent(); 885 } 886 return merged; 887 } 888 889 @Override 890 public Map<String, String> getBinaryFulltext(String id) { 891 return session.getBinaryFulltext(idFromString(id)); 892 } 893 894}