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