diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 56f8d1e251..f64f63f569 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -243,6 +243,8 @@ ${system.readpermissions.bulkfetchsize} + + @@ -250,6 +252,7 @@ + diff --git a/source/java/org/alfresco/repo/blog/BlogService.java b/source/java/org/alfresco/repo/blog/BlogService.java index b4a9621196..1caca65c8b 100644 --- a/source/java/org/alfresco/repo/blog/BlogService.java +++ b/source/java/org/alfresco/repo/blog/BlogService.java @@ -1,157 +1,159 @@ -/* - * Copyright (C) 2005-2011 Alfresco Software Limited. - * - * This file is part of Alfresco - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - */ -package org.alfresco.repo.blog; - -import java.util.Date; - -import org.alfresco.model.ContentModel; -import org.alfresco.query.PagingRequest; -import org.alfresco.query.PagingResults; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.site.SiteService; - -/** - * The Blog Service handles the management (CRUD) of Alfresco blog data, namely the blog posts which are - * exposed in the Share UI under the "Blog" heading. The {@link BlogIntegrationService}, a separate service, is - * concerned with the integration of Alfresco blog content with external Blog-hosting sites. - *

- * Please note that this service is a work in progress and currently exists primarily to support the blogs REST API. - * - * @author Neil Mc Erlean (based on existing webscript controllers in the REST API) - * @since 4.0 - */ -public interface BlogService -{ - /** - * Creates a new blog post within the specified container node. - * - * @param blogContainerNode the container node for blog posts (under the site). - * @param blogTitle the title of the blog post. - * @param blogContent text/html content of the blog post. - * @param isDraft true if the blog post is a draft post, else false. - * - * @return The {@link ChildAssociationRef} of the newly created blog post. - * - * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode - */ - ChildAssociationRef createBlogPost(NodeRef blogContainerNode, String blogTitle, - String blogContent, boolean isDraft); - - /** - * Gets the draft blog posts created by the specified user. - * - * @param blogContainerNode the container node for blog posts (under the site). - * @param username to limit results to blogs with this cm:creator. null means all users. - * @param pagingReq an object defining the paging parameters for the result set. - * - * @return a {@link PagingResults} object containing some or all of the results (subject to paging). - * - * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode - */ - PagingResults getDrafts(NodeRef blogContainerNode, String username, PagingRequest pagingReq); - - /** - * Gets the (internally, Alfresco-) published blog posts. - * - * @param blogContainerNode the container node for blog posts (under the site). - * @param fromDate an inclusive date limit for the results (more recent than). - * @param toDate an inclusive date limit for the results (before). - * @param byUser if not null limits results to posts by the specified user. - * if null results will be by all users. - * @param pagingReq an object defining the paging parameters for the result set. - * - * @return a {@link PagingResults} object containing some or all of the results (subject to paging). - * - * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode - */ - PagingResults getPublished(NodeRef blogContainerNode, Date fromDate, Date toDate, String byUser, PagingRequest pagingReq); - - /** - * Gets blog posts published externally (i.e. to an external blog hosting site). - * - * @param blogContainerNode the container node for blog posts (under the site). - * @param pagingReq an object defining the paging parameters for the result set. - * - * @return a {@link PagingResults} object containing some or all of the results (subject to paging). - * - * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode - */ - PagingResults getPublishedExternally(NodeRef blogContainerNode, PagingRequest pagingReq); - - /** - * Gets draft blog posts by the currently authenticated user along with all published posts. - * - * @param blogContainerNode the container node for blog posts (under the site). - * @param fromDate an inclusive date limit for the results (more recent than). - * @param toDate an inclusive date limit for the results (before). - * @param tag if specified, only returns posts tagged with this tag. - * @param pagingReq an object defining the paging parameters for the result set. - * - * @return a {@link PagingResults} object containing some or all of the results (subject to paging). - * - * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode - * - * @deprecated This method is a domain-specific query used by the Blog REST API and is not considered suitable for general use. - */ - PagingResults getMyDraftsAndAllPublished(NodeRef blogContainerNode, Date fromDate, Date toDate, - String tag, PagingRequest pagingReq); - - /** - * Returns true if the specified blog-post node is a 'draft' blog post. - * - * @param blogPostNode a NodeRef representing a blog-post. - * @return true if it is a draft post, else false. - */ - boolean isDraftBlogPost(NodeRef blogPostNode); - - /** - * A simple data object for storage of blog-related data. - * - * @author Neil Mc Erlean - * @since 4.0 - */ - public class BlogPostInfo - { - private final NodeRef nodeRef; - private final String name; - - public BlogPostInfo(NodeRef nodeRef, String name) - { - this.nodeRef = nodeRef; - this.name = name; - } - /** - * Gets the NodeRef representing this blog-post. - */ - public NodeRef getNodeRef() - { - return nodeRef; - } - - /** - * Gets the {@link ContentModel#PROP_NAME cm:name} of the blog post. - * @return - */ - public String getName() - { - return name; - } - } -} +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.blog; + +import java.util.Date; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.permissions.PermissionCheckValue; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteService; + +/** + * The Blog Service handles the management (CRUD) of Alfresco blog data, namely the blog posts which are + * exposed in the Share UI under the "Blog" heading. The {@link BlogIntegrationService}, a separate service, is + * concerned with the integration of Alfresco blog content with external Blog-hosting sites. + *

+ * Please note that this service is a work in progress and currently exists primarily to support the blogs REST API. + * + * @author Neil Mc Erlean (based on existing webscript controllers in the REST API) + * @since 4.0 + */ +public interface BlogService +{ + /** + * Creates a new blog post within the specified container node. + * + * @param blogContainerNode the container node for blog posts (under the site). + * @param blogTitle the title of the blog post. + * @param blogContent text/html content of the blog post. + * @param isDraft true if the blog post is a draft post, else false. + * + * @return The {@link ChildAssociationRef} of the newly created blog post. + * + * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode + */ + ChildAssociationRef createBlogPost(NodeRef blogContainerNode, String blogTitle, + String blogContent, boolean isDraft); + + /** + * Gets the draft blog posts created by the specified user. + * + * @param blogContainerNode the container node for blog posts (under the site). + * @param username to limit results to blogs with this cm:creator. null means all users. + * @param pagingReq an object defining the paging parameters for the result set. + * + * @return a {@link PagingResults} object containing some or all of the results (subject to paging). + * + * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode + */ + PagingResults getDrafts(NodeRef blogContainerNode, String username, PagingRequest pagingReq); + + /** + * Gets the (internally, Alfresco-) published blog posts. + * + * @param blogContainerNode the container node for blog posts (under the site). + * @param fromDate an inclusive date limit for the results (more recent than). + * @param toDate an inclusive date limit for the results (before). + * @param byUser if not null limits results to posts by the specified user. + * if null results will be by all users. + * @param pagingReq an object defining the paging parameters for the result set. + * + * @return a {@link PagingResults} object containing some or all of the results (subject to paging). + * + * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode + */ + PagingResults getPublished(NodeRef blogContainerNode, Date fromDate, Date toDate, String byUser, PagingRequest pagingReq); + + /** + * Gets blog posts published externally (i.e. to an external blog hosting site). + * + * @param blogContainerNode the container node for blog posts (under the site). + * @param pagingReq an object defining the paging parameters for the result set. + * + * @return a {@link PagingResults} object containing some or all of the results (subject to paging). + * + * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode + */ + PagingResults getPublishedExternally(NodeRef blogContainerNode, PagingRequest pagingReq); + + /** + * Gets draft blog posts by the currently authenticated user along with all published posts. + * + * @param blogContainerNode the container node for blog posts (under the site). + * @param fromDate an inclusive date limit for the results (more recent than). + * @param toDate an inclusive date limit for the results (before). + * @param tag if specified, only returns posts tagged with this tag. + * @param pagingReq an object defining the paging parameters for the result set. + * + * @return a {@link PagingResults} object containing some or all of the results (subject to paging). + * + * @see SiteService#getContainer(String, String) to retrieve the blogContainerNode + * + * @deprecated This method is a domain-specific query used by the Blog REST API and is not considered suitable for general use. + */ + PagingResults getMyDraftsAndAllPublished(NodeRef blogContainerNode, Date fromDate, Date toDate, + String tag, PagingRequest pagingReq); + + /** + * Returns true if the specified blog-post node is a 'draft' blog post. + * + * @param blogPostNode a NodeRef representing a blog-post. + * @return true if it is a draft post, else false. + */ + boolean isDraftBlogPost(NodeRef blogPostNode); + + /** + * A simple data object for storage of blog-related data. + * + * @author Neil Mc Erlean + * @since 4.0 + */ + public class BlogPostInfo implements PermissionCheckValue + { + private final NodeRef nodeRef; + private final String name; + + public BlogPostInfo(NodeRef nodeRef, String name) + { + this.nodeRef = nodeRef; + this.name = name; + } + /** + * Gets the NodeRef representing this blog-post. + */ + @Override + public NodeRef getNodeRef() + { + return nodeRef; + } + + /** + * Gets the {@link ContentModel#PROP_NAME cm:name} of the blog post. + * @return + */ + public String getName() + { + return name; + } + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java index 33fbae2a51..8eb4ec7644 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -28,9 +28,9 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.ResourceBundle.Control; import java.util.Set; import java.util.Stack; -import java.util.ResourceBundle.Control; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; @@ -41,7 +41,7 @@ import org.alfresco.query.PagingResults; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; import org.alfresco.repo.node.getchildren.GetChildrenCannedQueryFactory; import org.alfresco.repo.search.QueryParameterDefImpl; -import org.alfresco.repo.security.permissions.impl.acegi.WrappedList; +import org.alfresco.repo.security.permissions.PermissionCheckedValue.PermissionCheckedValueMixin; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.model.FileExistsException; @@ -409,11 +409,6 @@ public class FileFolderServiceImpl implements FileFolderService { return totalCount; } - @Override - public boolean permissionsApplied() - { - return results.permissionsApplied(); - } }; } @@ -703,7 +698,7 @@ public class FileFolderServiceImpl implements FileFolderService List results = toFileInfo(nodeRefs); // avoid re-applying permissions (for "list" canned queries) - return new WrappedList(results, cq.permissionsApplied(), cq.hasMoreItems()); + return PermissionCheckedValueMixin.create(results); } private Set buildTypes(boolean files, boolean folders, Set ignoreQNameTypes) diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java index 4f1ebe16b9..2712ee66b0 100644 --- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java +++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQuery.java @@ -48,9 +48,9 @@ import org.alfresco.repo.domain.node.ReferenceablePropertiesEntity; import org.alfresco.repo.domain.qname.QNameDAO; import org.alfresco.repo.domain.query.CannedQueryDAO; import org.alfresco.repo.node.getchildren.FilterPropString.FilterTypeString; +import org.alfresco.repo.security.permissions.PermissionCheckedValue.PermissionCheckedValueMixin; import org.alfresco.repo.security.permissions.impl.acegi.AbstractCannedQueryPermissions; import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityBean; -import org.alfresco.repo.security.permissions.impl.acegi.WrappedList; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.InvalidNodeRefException; @@ -156,7 +156,7 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions> sortPairs = (List)sortDetails.getSortPairs(); // Set sort / filter params @@ -234,21 +234,9 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions 0) && (requestTotalCountMax > requestedCount)) - { - requestedCount = requestTotalCountMax; - } - - if (requestedCount != Integer.MAX_VALUE) - { - requestedCount++; // add one for "hasMoreItems" - } - - final WrappedList rawResult = new WrappedList(new ArrayList(100), requestedCount); + final int requestedCount = parameters.getResultsRequired(); + final List rawResult = new ArrayList(Math.min(1000, requestedCount)); UnsortedChildQueryCallback callback = new UnsortedChildQueryCallback() { public boolean handle(NodeRef nodeRef) @@ -256,7 +244,7 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions(rawResult.getWrapped(), true, (rawResult.size() == requestedCount)); + result = PermissionCheckedValueMixin.create(rawResult); } if (start != null) @@ -482,7 +470,7 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions 0) && (requestTotalCountMax > requestedCount)) ? requestTotalCountMax : requestedCount); int cnt = results.size(); @@ -693,7 +681,7 @@ public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions results = applyPermissions(nodeRefs, nodeRefs.size()); + List results = applyPostQueryPermissions(nodeRefs, nodeRefs.size()); for (NodeRef nodeRef : results) { diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java index 0b6a854005..f940f95b6c 100644 --- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java +++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryTest.java @@ -498,7 +498,6 @@ public class GetChildrenCannedQueryTest extends TestCase PagingResults results = list(parentNodeRef, -1, -1, 0); assertFalse(results.hasMoreItems()); - assertTrue(results.permissionsApplied()); List nodeRefs = results.getPage(); @@ -516,7 +515,6 @@ public class GetChildrenCannedQueryTest extends TestCase results = list(parentNodeRef, -1, -1, 0); assertFalse(results.hasMoreItems()); - assertTrue(results.permissionsApplied()); nodeRefs = results.getPage(); @@ -767,7 +765,7 @@ public class GetChildrenCannedQueryTest extends TestCase totalCount = results.getTotalResultCount().getFirst(); } - return new PagingNodeRefResultsImpl(nodeRefs, results.hasMoreItems(), totalCount, false, true); + return new PagingNodeRefResultsImpl(nodeRefs, results.hasMoreItems(), totalCount, false); } private class PagingNodeRefResultsImpl implements PagingResults @@ -775,18 +773,16 @@ public class GetChildrenCannedQueryTest extends TestCase private List nodeRefs; private boolean hasMorePages; - private boolean permissionsApplied; private Integer totalResultCount; // null => not requested (or unknown) private Boolean isTotalResultCountCutoff; // null => unknown - public PagingNodeRefResultsImpl(List nodeRefs, boolean hasMorePages, Integer totalResultCount, Boolean isTotalResultCountCutoff, boolean permissionsApplied) + public PagingNodeRefResultsImpl(List nodeRefs, boolean hasMorePages, Integer totalResultCount, Boolean isTotalResultCountCutoff) { this.nodeRefs = nodeRefs; this.hasMorePages = hasMorePages; this.totalResultCount= totalResultCount; this.isTotalResultCountCutoff = isTotalResultCountCutoff; - this.permissionsApplied = permissionsApplied; } public List getPage() @@ -799,11 +795,6 @@ public class GetChildrenCannedQueryTest extends TestCase return hasMorePages; } - public boolean permissionsApplied() - { - return permissionsApplied; - } - public Pair getTotalResultCount() { return new Pair(totalResultCount, (isTotalResultCountCutoff ? null : totalResultCount)); diff --git a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java index 9a47684d76..c22ea0b0f9 100644 --- a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java +++ b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java @@ -358,11 +358,6 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor { return results.getTotalResultCount(); } - @Override - public boolean permissionsApplied() - { - return results.permissionsApplied(); - } }; if (start != null) @@ -428,11 +423,6 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor { return ppr.getTotalResultCount(); } - @Override - public boolean permissionsApplied() - { - return ppr.permissionsApplied(); - } }; } diff --git a/source/java/org/alfresco/repo/security/authority/AuthorityServiceImpl.java b/source/java/org/alfresco/repo/security/authority/AuthorityServiceImpl.java index 891a0d2c55..5bce944791 100644 --- a/source/java/org/alfresco/repo/security/authority/AuthorityServiceImpl.java +++ b/source/java/org/alfresco/repo/security/authority/AuthorityServiceImpl.java @@ -370,11 +370,6 @@ public class AuthorityServiceImpl implements AuthorityService, InitializingBean { return new Pair(auths.size(), auths.size()); } - @Override - public boolean permissionsApplied() - { - return true; // note: for now, assume ACL_ALLOW for "other" authorities (as per public-services-security.xml) - } }; } diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionCheckCollection.java b/source/java/org/alfresco/repo/security/permissions/PermissionCheckCollection.java new file mode 100644 index 0000000000..b2aa22637c --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionCheckCollection.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions; + +import java.util.Collection; + +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; + +/** + * Interface for collection-based results that describe permission filtering + * behaviour around cut-off limits. + * + * @author Derek Hulley + * @since 4.0 + */ +public interface PermissionCheckCollection +{ + /** + * Get the desired number of results. Permission checks can stop once the number of + * return objects reaches this number. + * + * @return the number of results desired + */ + int getTargetResultCount(); + + /** + * Get the maximum time for permission checks to execute before cutting the results off. + *
Zero: Ignore this value. + * + * @return the time allowed for permission checks before cutoff + */ + long getCutOffAfterTimeMs(); + + /** + * Get the maximum number of permission checks to perform before cutting the results off + * + * @return the maximum number of permission checks before cutoff + */ + int getCutOffAfterCount(); + + /** + * Helper 'introduction' to allow simple addition of the {@link PermissionCheckCollection} interface to + * existing collections. + * + * @param the type of the Collection in use + * + * @author Derek Hulley + * @since 4.0 + */ + @SuppressWarnings("serial") + public static class PermissionCheckCollectionMixin extends DelegatingIntroductionInterceptor implements PermissionCheckCollection + { + private final int targetResultCount; + private final long cutOffAfterTimeMs; + private final int cutOffAfterCount; + + private PermissionCheckCollectionMixin(int targetResultCount, long cutOffAfterTimeMs, int cutOffAfterCount) + { + super(); + this.targetResultCount = targetResultCount; + this.cutOffAfterTimeMs = cutOffAfterTimeMs; + this.cutOffAfterCount = cutOffAfterCount; + if (cutOffAfterTimeMs <= 0) + { + cutOffAfterTimeMs = 0; + } + if (cutOffAfterCount <= 0) + { + cutOffAfterCount = 0; + } + } + + @Override + public int getTargetResultCount() + { + return targetResultCount; + } + + @Override + public long getCutOffAfterTimeMs() + { + return cutOffAfterTimeMs; + } + + @Override + public int getCutOffAfterCount() + { + return cutOffAfterCount; + } + + /** + * Helper method to create a {@link PermissionCheckCollection} from an existing Collection + * + * @param the type of the Collection + * @param collection the Collection to proxy + * @param targetResultCount the desired number of results or default to the collection size + * @param cutOffAfterTimeMs the number of milliseconds to wait before cut-off or zero to use the system default + * time-based cut-off. + * @param cutOffAfterCount the number of permission checks to process before cut-off or zero to use the system default + * count-based cut-off. + * @return a Collection of the same type but including the + * {@link PermissionCheckCollection} interface + */ + @SuppressWarnings("unchecked") + public static final Collection create( + Collection collection, + int targetResultCount, long cutOffAfterTimeMs, int cutOffAfterCount) + { + if (targetResultCount <= 0) + { + targetResultCount = collection.size(); + } + // Create the mixin + DelegatingIntroductionInterceptor mixin = new PermissionCheckCollectionMixin( + targetResultCount, + cutOffAfterTimeMs, + cutOffAfterCount); + // Create the advisor + IntroductionAdvisor advisor = new DefaultIntroductionAdvisor(mixin, PermissionCheckCollection.class); + // Proxy + ProxyFactory pf = new ProxyFactory(collection); + pf.addAdvisor(advisor); + Object proxiedObject = pf.getProxy(); + + // Done + return (Collection) proxiedObject; + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionCheckCollectionTest.java b/source/java/org/alfresco/repo/security/permissions/PermissionCheckCollectionTest.java new file mode 100644 index 0000000000..156b8d3256 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionCheckCollectionTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.repo.security.permissions.PermissionCheckCollection.PermissionCheckCollectionMixin; + +/** + * Tests {@link PermissionCheckCollection} + * + * @author Derek Hulley + * @since 4.0 + */ +public class PermissionCheckCollectionTest extends TestCase +{ + @Override + protected void setUp() throws Exception + { + } + + public void testBasicWrapping() throws Exception + { + List list = new ArrayList(); + list.add(1); + list.add(2); + list.add(3); + + final int targetResultCount = 2000; + final long cutOffAfterTimeMs = 10000L; + final int cutOffAfterCount = 1000; + + Collection proxiedList = PermissionCheckCollectionMixin.create(list, targetResultCount, cutOffAfterTimeMs, cutOffAfterCount); + + // Check + assertTrue("Proxied object must still be a List", proxiedList instanceof List); + assertEquals("List values incorrect", 3, proxiedList.size()); + assertTrue("Proxied object must also be a PermissionCheckCollection", proxiedList instanceof PermissionCheckCollection); + @SuppressWarnings("unchecked") + PermissionCheckCollection proxiedPermissionCheckCollection = (PermissionCheckCollection) proxiedList; + assertEquals("targetResultCount value incorrect", targetResultCount, proxiedPermissionCheckCollection.getTargetResultCount()); + assertEquals("cutOffAfterTimeMs value incorrect", cutOffAfterTimeMs, proxiedPermissionCheckCollection.getCutOffAfterTimeMs()); + assertEquals("cutOffAfterCount value incorrect", cutOffAfterCount, proxiedPermissionCheckCollection.getCutOffAfterCount()); + } + + public void testVolumeWrapping() throws Exception + { + int count = 10000; + long before = System.nanoTime(); + for (int i = 0; i < count; i++) + { + testBasicWrapping(); + } + long after = System.nanoTime(); + double average = ((double) (after - before) / (double) count) / (double) 1.0E6; + System.out.println("Average is " + average + "ms per wrap."); + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionCheckValue.java b/source/java/org/alfresco/repo/security/permissions/PermissionCheckValue.java new file mode 100644 index 0000000000..57b7122aac --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionCheckValue.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Interface for objects that can provide information to allow permission checks + *

+ * Implement this interface to enable the permission filtering layers to extract + * information in order to check permissions. + * + * @author Derek Hulley + * @since 4.0 + */ +public interface PermissionCheckValue +{ + /** + * Get the underlying node value that needs to be permission checked. + * + * @return the underlying value to filter + */ + NodeRef getNodeRef(); +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionCheckedCollection.java b/source/java/org/alfresco/repo/security/permissions/PermissionCheckedCollection.java new file mode 100644 index 0000000000..8c9aaf734c --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionCheckedCollection.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions; + +import java.util.Collection; + +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; + +/** + * Interface for collection-based results that carry extra information + * about the state of permission cut-offs. + * + * @author Derek Hulley + * @since 4.0 + */ +public interface PermissionCheckedCollection +{ + /** + * Check if the results have been truncated by permission check limits. + * This can only be called when {@link #isFiltered()} is true. + * + * @return true - if the results (usually a collection) have been + * cut off by permission check limits + */ + boolean isCutOff(); + + /** + * Get the number of objects in the original (unfiltered) collection that did + * not have any permission checks. + * + * @return number of entries from the original collection that were not checked + */ + int sizeUnchecked(); + + /** + * Get the number of objects in the original (unfiltered) collection. + * + * @return number of entries in the original, pre-checked collection + */ + int sizeOriginal(); + + /** + * Helper 'introduction' to allow simple addition of the {@link PermissionCheckedCollection} interface to + * existing collections. + * + * @param the type of the Collection in use + * + * @author Derek Hulley + * @since 4.0 + */ + @SuppressWarnings("serial") + public static class PermissionCheckedCollectionMixin extends DelegatingIntroductionInterceptor implements PermissionCheckedCollection + { + private final boolean isCutOff; + private final int sizeUnchecked; + private final int sizeOriginal; + private PermissionCheckedCollectionMixin(boolean isCutOff, int sizeUnchecked, int sizeOriginal) + { + super(); + this.isCutOff = isCutOff; + this.sizeUnchecked = sizeUnchecked; + this.sizeOriginal = sizeOriginal; + } + @Override + public boolean isCutOff() + { + return isCutOff; + } + @Override + public int sizeUnchecked() + { + return sizeUnchecked; + } + @Override + public int sizeOriginal() + { + return sizeOriginal; + } + /** + * Helper method to create a {@link PermissionCheckedCollection} from an existing Collection + * + * @param the type of the Collection + * @param collection the Collection to proxy + * @param isCutOff true if permission checking was cut off before completion + * @param sizeUnchecked number of entries from the original collection that were not checked + * @param sizeOriginal number of entries in the original, pre-checked collection + * @return a Collection of the same type but including the + * {@link PermissionCheckedCollection} interface + */ + @SuppressWarnings("unchecked") + public static final Collection create( + Collection collection, + boolean isCutOff, int sizeUnchecked, int sizeOriginal) + { + // Create the mixin + DelegatingIntroductionInterceptor mixin = new PermissionCheckedCollectionMixin( + isCutOff, + sizeUnchecked, + sizeOriginal + ); + // Create the advisor + IntroductionAdvisor advisor = new DefaultIntroductionAdvisor(mixin, PermissionCheckedCollection.class); + // Proxy + ProxyFactory pf = new ProxyFactory(collection); + pf.addAdvisor(advisor); + Object proxiedObject = pf.getProxy(); + + // Done + return (Collection) proxiedObject; + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionCheckedCollectionTest.java b/source/java/org/alfresco/repo/security/permissions/PermissionCheckedCollectionTest.java new file mode 100644 index 0000000000..feb6ce7aa0 --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionCheckedCollectionTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.repo.security.permissions.PermissionCheckedCollection.PermissionCheckedCollectionMixin; + +/** + * Tests {@link PermissionCheckedCollection} + * + * @author Derek Hulley + * @since 4.0 + */ +public class PermissionCheckedCollectionTest extends TestCase +{ + @Override + protected void setUp() throws Exception + { + } + + public void testBasicWrapping() throws Exception + { + List list = new ArrayList(); + list.add(1); + list.add(2); + list.add(3); + + final boolean isCutOff = true; + final int sizeUnchecked = 100; + final int sizeOriginal = 900; + + Collection proxiedList = PermissionCheckedCollectionMixin.create( + list, isCutOff, sizeUnchecked, sizeOriginal); + + // Check + assertTrue("Proxied object must still be a List", proxiedList instanceof List); + assertEquals("List values incorrect", 3, proxiedList.size()); + assertTrue("Proxied object must also be a PermissionCheckedCollection", proxiedList instanceof PermissionCheckedCollection); + @SuppressWarnings("unchecked") + PermissionCheckedCollection proxiedPermissionCheckedCollection = (PermissionCheckedCollection) proxiedList; + assertEquals("cutOff value incorrect", isCutOff, proxiedPermissionCheckedCollection.isCutOff()); + assertEquals("sizeUnchecked value incorrect", sizeUnchecked, proxiedPermissionCheckedCollection.sizeUnchecked()); + assertEquals("sizeOriginal value incorrect", sizeOriginal, proxiedPermissionCheckedCollection.sizeOriginal()); + } + + public void testVolumeWrapping() throws Exception + { + int count = 10000; + long before = System.nanoTime(); + for (int i = 0; i < count; i++) + { + testBasicWrapping(); + } + long after = System.nanoTime(); + double average = ((double) (after - before) / (double) count) / (double) 1.0E6; + System.out.println("Average is " + average + "ms per wrap."); + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/PermissionCheckedValue.java b/source/java/org/alfresco/repo/security/permissions/PermissionCheckedValue.java new file mode 100644 index 0000000000..71c67616ee --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/PermissionCheckedValue.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions; + +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; + +/** + * Marker interface for objects that have already passed permission checking. + * + * @author Derek Hulley + * @since 4.0 + */ +public interface PermissionCheckedValue +{ + /** + * Helper 'introduction' to allow simple addition of the {@link PermissionCheckedValue} interface to + * existing objects. + * + * @author Derek Hulley + * @since 4.0 + */ + @SuppressWarnings("serial") + public static class PermissionCheckedValueMixin extends DelegatingIntroductionInterceptor implements PermissionCheckedValue + { + private PermissionCheckedValueMixin() + { + super(); + } + /** + * Helper method to create a {@link PermissionCheckedValue} from an existing Object. + * + * @param collection the Object to proxy + * @return a Object of the same type but including the + * {@link PermissionCheckedValue} interface + */ + @SuppressWarnings("unchecked") + public static final T create(T object) + { + // Create the mixin + DelegatingIntroductionInterceptor mixin = new PermissionCheckedValueMixin(); + // Create the advisor + IntroductionAdvisor advisor = new DefaultIntroductionAdvisor(mixin, PermissionCheckedValue.class); + // Proxy + ProxyFactory pf = new ProxyFactory(object); + pf.addAdvisor(advisor); + Object proxiedObject = pf.getProxy(); + + // Done + return (T) proxiedObject; + } + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java index ef0ca49719..fc45aa7125 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationProvider.java @@ -36,17 +36,16 @@ import net.sf.acegisecurity.ConfigAttributeDefinition; import net.sf.acegisecurity.afterinvocation.AfterInvocationProvider; import org.alfresco.cmis.CMISResultSet; -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.query.PagingResults; -import org.alfresco.query.PermissionedResults; -import org.alfresco.repo.blog.BlogService.BlogPostInfo; import org.alfresco.repo.search.SimpleResultSetMetaData; import org.alfresco.repo.search.impl.lucene.PagingLuceneResultSet; import org.alfresco.repo.search.impl.lucene.SolrJSONResultSet; import org.alfresco.repo.search.impl.querymodel.QueryEngineResults; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.PermissionCheckCollection; +import org.alfresco.repo.security.permissions.PermissionCheckValue; +import org.alfresco.repo.security.permissions.PermissionCheckedCollection.PermissionCheckedCollectionMixin; +import org.alfresco.repo.security.permissions.PermissionCheckedValue; import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; -import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -57,7 +56,6 @@ import org.alfresco.service.cmr.search.ResultSet; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.security.PersonService.PersonInfo; import org.alfresco.service.namespace.NamespacePrefixResolver; import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; @@ -255,125 +253,57 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, } return null; } + else if (PermissionCheckedValue.class.isAssignableFrom(returnedObject.getClass())) + { + // The security provider was not already present + return decide(authentication, object, config, (PermissionCheckedValue) returnedObject); + } + else if (PermissionCheckValue.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (PermissionCheckValue) returnedObject); + } else if (StoreRef.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("Store access"); - } return decide(authentication, object, config, nodeService.getRootNode((StoreRef) returnedObject)).getStoreRef(); } else if (NodeRef.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("Node access"); - } return decide(authentication, object, config, (NodeRef) returnedObject); } - else if (FileInfo.class.isAssignableFrom(returnedObject.getClass())) - { - return decide(authentication, object, config, (FileInfo) returnedObject); - } - else if (PagingResults.class.isAssignableFrom(returnedObject.getClass())) - { - if (PermissionedResults.class.isAssignableFrom(returnedObject.getClass()) && - (! ((PermissionedResults)returnedObject).permissionsApplied())) - { - throw new AlfrescoRuntimeException("Not implemented"); - /* - if (log.isDebugEnabled()) - { - log.debug("Paging Results access"); - } - return decide(authentication, object, config, ((PagingResults) returnedObject); - */ - } - else - { - if (log.isDebugEnabled()) - { - log.debug("Paging Results access - already checked permissions for " + object.getClass().getName()); - } - - return returnedObject; - } - } else if (Pair.class.isAssignableFrom(returnedObject.getClass())) { return decide(authentication, object, config, (Pair) returnedObject); } else if (ChildAssociationRef.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("Child Association access"); - } return decide(authentication, object, config, (ChildAssociationRef) returnedObject); } else if (SolrJSONResultSet.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("SolrJSONResultSet - already checked permissions for " + object.getClass().getName()); - } return returnedObject; } else if (CMISResultSet.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("CMIS Result Set - already checked permissions for " + object.getClass().getName()); - } return returnedObject; } else if (PagingLuceneResultSet.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("Result Set access"); - } return decide(authentication, object, config, (PagingLuceneResultSet) returnedObject); } else if (ResultSet.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("Result Set access"); - } return decide(authentication, object, config, (ResultSet) returnedObject); } else if (QueryEngineResults.class.isAssignableFrom(returnedObject.getClass())) { - if (log.isDebugEnabled()) - { - log.debug("Result Set access"); - } return decide(authentication, object, config, (QueryEngineResults) returnedObject); } else if (Collection.class.isAssignableFrom(returnedObject.getClass())) { - if (PermissionedResults.class.isAssignableFrom(returnedObject.getClass()) && - ((PermissionedResults)returnedObject).permissionsApplied()) - { - // Already checked - don't need to re-check (eg. WrappedList - used by unsorted GetChildren CQ) - return returnedObject; - } - else - { - if (log.isDebugEnabled()) - { - log.debug("Collection Access"); - } - return decide(authentication, object, config, (Collection) returnedObject); - } + return decide(authentication, object, config, (Collection) returnedObject); } else if (returnedObject.getClass().isArray()) { - if (log.isDebugEnabled()) - { - log.debug("Array Access"); - } return decide(authentication, object, config, (Object[]) returnedObject); } else @@ -477,16 +407,22 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, return false; } - private FileInfo decide(Authentication authentication, Object object, ConfigAttributeDefinition config, FileInfo returnedObject) throws AccessDeniedException + private PermissionCheckedValue decide(Authentication authentication, Object object, ConfigAttributeDefinition config, PermissionCheckedValue returnedObject) throws AccessDeniedException { - // Filter check done later - NodeRef nodeRef = returnedObject.getNodeRef(); - // this is virtually equivalent to the noderef - decide(authentication, object, config, nodeRef); - // the noderef was allowed + // This passes as it has already been filtered + // TODO: Get the filter that was applied and double-check return returnedObject; } - + + private PermissionCheckValue decide(Authentication authentication, Object object, ConfigAttributeDefinition config, PermissionCheckValue returnedObject) throws AccessDeniedException + { + // Get the wrapped value + NodeRef nodeRef = returnedObject.getNodeRef(); + decide(authentication, object, config, nodeRef); + // This passes + return returnedObject; + } + @SuppressWarnings("rawtypes") private Pair decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Pair returnedObject) throws AccessDeniedException { @@ -496,6 +432,7 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, return returnedObject; } + @SuppressWarnings("rawtypes") private List extractSupportedDefinitions(ConfigAttributeDefinition config) { List definitions = new ArrayList(); @@ -866,6 +803,7 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, return new QueryEngineResults(answer); } + @SuppressWarnings({ "rawtypes", "unchecked" }) private Collection decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Collection returnedObject) throws AccessDeniedException { if (returnedObject == null) @@ -874,68 +812,78 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, } List supportedDefinitions = extractSupportedDefinitions(config); - - if (supportedDefinitions.size() == 0) - { - if (returnedObject instanceof WrappedList) - { - ((WrappedList)returnedObject).setHasMoreItems(false); - ((WrappedList)returnedObject).setPermissionsApplied(true); - } - - return returnedObject; - } - - Set removed = new HashSet(); - if (log.isDebugEnabled()) { log.debug("Entries are " + supportedDefinitions); } - // record search start time + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + // Default to the system-wide values and we'll see if they need to be reduced + long targetResultCount = returnedObject.size(); + int maxPermissionChecks = Integer.MAX_VALUE; + long maxPermissionCheckTimeMillis = this.maxPermissionCheckTimeMillis; + if (returnedObject instanceof PermissionCheckCollection) + { + PermissionCheckCollection permissionCheckCollection = (PermissionCheckCollection) returnedObject; + // Get values + targetResultCount = permissionCheckCollection.getTargetResultCount(); + if (permissionCheckCollection.getCutOffAfterCount() > 0) + { + maxPermissionChecks = permissionCheckCollection.getCutOffAfterCount(); + } + if (permissionCheckCollection.getCutOffAfterTimeMs() > 0) + { + maxPermissionCheckTimeMillis = permissionCheckCollection.getCutOffAfterTimeMs(); + } + } + + // Start timer and counter for cut-off + boolean cutoff = false; long startTimeMillis = System.currentTimeMillis(); int count = 0; - boolean cutoff = false; + // Keep values explicitly + List keepValues = new ArrayList(returnedObject.size()); - int maxChecks = Integer.MAX_VALUE; - if ((returnedObject instanceof WrappedList) && ((WrappedList)returnedObject).getMaxChecks() > 0) + for (Object nextObject : returnedObject) { - maxChecks = ((WrappedList)returnedObject).getMaxChecks(); - } - - Iterator iterator = returnedObject.iterator(); - while (iterator.hasNext()) - { - Object nextObject = iterator.next(); - // if the maximum result size or time has been exceeded, then we have to remove only long currentTimeMillis = System.currentTimeMillis(); - // NOTE: for reference - the "maxPermissionChecks" has never been honoured by this loop (since previously the count was not being incremented) - if (count >= maxChecks || (currentTimeMillis - startTimeMillis) > maxPermissionCheckTimeMillis) + if (count >= targetResultCount) { - // just remove it - iterator.remove(); - - if (! cutoff) + // We have enough results. We stop without cutoff. + break; + } + else if (count >= maxPermissionChecks) + { + // We have been cut off by count + cutoff = true; + if (log.isDebugEnabled()) { - cutoff = true; - if (log.isDebugEnabled()) - { - log.debug("decide (collection) cut-off: "+(count >= maxChecks ? " maxChecks="+maxChecks : " ")+((currentTimeMillis - startTimeMillis) > maxPermissionCheckTimeMillis ? " maxCheckTime="+maxPermissionCheckTimeMillis : "")); - } + log.debug("decide (collection) cut-off: " + count + " checks exceeded " + maxPermissionChecks + " checks"); } - - continue; + break; + } + else if ((currentTimeMillis - startTimeMillis) > maxPermissionCheckTimeMillis) + { + // We have been cut off by time + cutoff = true; + if (log.isDebugEnabled()) + { + log.debug("decide (collection) cut-off: " + (currentTimeMillis - startTimeMillis) + "ms exceeded " + maxPermissionCheckTimeMillis + "ms"); + } + break; } boolean allowed = true; for (ConfigAttributeDefintion cad : supportedDefinitions) { NodeRef testNodeRef = null; - if (cad.typeString.equals(AFTER_ACL_NODE)) { if (StoreRef.class.isAssignableFrom(nextObject.getClass())) @@ -950,27 +898,17 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, { testNodeRef = ((ChildAssociationRef) nextObject).getChildRef(); } - else if (FileInfo.class.isAssignableFrom(nextObject.getClass())) - { - testNodeRef = ((FileInfo) nextObject).getNodeRef(); - } - else if (BlogPostInfo.class.isAssignableFrom(nextObject.getClass())) - { - testNodeRef = ((BlogPostInfo) nextObject).getNodeRef(); - } - else if (PersonInfo.class.isAssignableFrom(nextObject.getClass())) - { - testNodeRef = ((PersonInfo) nextObject).getNodeRef(); - } else if (Pair.class.isAssignableFrom(nextObject.getClass())) { testNodeRef = (NodeRef) ((Pair)nextObject).getSecond(); } + else if (PermissionCheckValue.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((PermissionCheckValue) nextObject).getNodeRef(); + } else { - throw new ACLEntryVoterException( - "The specified parameter is not a collection of " + - "StoreRefs, NodeRefs, ChildAssociationRefs, FileInfos, BlogPostInfos or Pair"); + throw new ACLEntryVoterException("The specified parameter is recognized: " + nextObject.getClass()); } } else if (cad.typeString.equals(AFTER_ACL_PARENT)) @@ -988,19 +926,18 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, { testNodeRef = ((ChildAssociationRef) nextObject).getParentRef(); } - else if (FileInfo.class.isAssignableFrom(nextObject.getClass())) - { - testNodeRef = ((FileInfo) nextObject).getNodeRef(); - } else if (Pair.class.isAssignableFrom(nextObject.getClass())) { testNodeRef = (NodeRef) ((Pair)nextObject).getSecond(); } + else if (PermissionCheckValue.class.isAssignableFrom(nextObject.getClass())) + { + NodeRef nodeRef = ((PermissionCheckValue) nextObject).getNodeRef(); + testNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + } else { - throw new ACLEntryVoterException( - "The specified parameter is not a collection of " + - "NodeRefs, FileInfos, ChildAssociationRefs or Pair"); + throw new ACLEntryVoterException("The specified parameter is recognized: " + nextObject.getClass()); } } @@ -1009,51 +946,55 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, log.debug("\t" + cad.typeString + " test on " + testNodeRef + " from " + nextObject.getClass().getName()); } - if(isUnfiltered(testNodeRef)) + if (isUnfiltered(testNodeRef)) // Null allows { - continue; + continue; // Continue to next ConfigAttributeDefintion } if (allowed && (testNodeRef != null) && (permissionService.hasPermission(testNodeRef, cad.required.toString()) == AccessStatus.DENIED)) { allowed = false; + break; // No point evaluating more ConfigAttributeDefintions } } - if (!allowed) + + // Failure or success, increase the count + count++; + + if (allowed) { - removed.add(nextObject); - } - else - { - count++; + keepValues.add(nextObject); } } - for (Object toRemove : removed) + // Work out how many were left unchecked (for whatever reason) + int sizeOriginal = returnedObject.size(); + int checksRemaining = sizeOriginal - count; + // Note: There are use-cases where unmodifiable collections are passing through. + // So make sure that the collection needs modification at all + if (keepValues.size() < sizeOriginal) { - while (returnedObject.remove(toRemove)) - ; + // There are values that need to be removed. We have to modify the collection. + try + { + returnedObject.clear(); + returnedObject.addAll(keepValues); + } + catch (UnsupportedOperationException e) + { + throw new AccessDeniedException("Permission-checked list must be modifiable", e); + } } - - if (returnedObject instanceof WrappedList) - { - ((WrappedList)returnedObject).setHasMoreItems(cutoff); - ((WrappedList)returnedObject).setPermissionsApplied(true); - } - - return returnedObject; + + // Attach the extra permission-check data to the collection + return PermissionCheckedCollectionMixin.create(returnedObject, cutoff, checksRemaining, sizeOriginal); } @SuppressWarnings("rawtypes") private Object[] decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Object[] returnedObject) throws AccessDeniedException - { + // Assumption: value is not null BitSet incudedSet = new BitSet(returnedObject.length); - if (returnedObject == null) - { - return null; - } - List supportedDefinitions = extractSupportedDefinitions(config); if (supportedDefinitions.size() == 0) @@ -1082,17 +1023,17 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, { testNodeRef = ((ChildAssociationRef) current).getChildRef(); } - else if (FileInfo.class.isAssignableFrom(current.getClass())) - { - testNodeRef = ((FileInfo) current).getNodeRef(); - } else if (Pair.class.isAssignableFrom(current.getClass())) { testNodeRef = (NodeRef) ((Pair)current).getSecond(); } + else if (PermissionCheckValue.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((PermissionCheckValue) current).getNodeRef(); + } else { - throw new ACLEntryVoterException("The specified array is not of NodeRef or ChildAssociationRef"); + throw new ACLEntryVoterException("The specified parameter is recognized: " + current.getClass()); } } else if (cad.typeString.equals(AFTER_ACL_PARENT)) @@ -1109,17 +1050,18 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, { testNodeRef = ((ChildAssociationRef) current).getParentRef(); } - else if (FileInfo.class.isAssignableFrom(current.getClass())) - { - testNodeRef = ((FileInfo) current).getNodeRef(); - } else if (Pair.class.isAssignableFrom(current.getClass())) { testNodeRef = (NodeRef) ((Pair)current).getSecond(); } + else if (PermissionCheckValue.class.isAssignableFrom(current.getClass())) + { + NodeRef nodeRef = ((PermissionCheckValue) current).getNodeRef(); + testNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + } else { - throw new ACLEntryVoterException("The specified array is not of NodeRef or ChildAssociationRef"); + throw new ACLEntryVoterException("The specified parameter is recognized: " + current.getClass()); } } @@ -1168,6 +1110,7 @@ public class ACLEntryAfterInvocationProvider implements AfterInvocationProvider, } } + @SuppressWarnings("rawtypes") public boolean supports(Class clazz) { return (MethodInvocation.class.isAssignableFrom(clazz)); diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java index 96a2f3f2f5..57df7d94b8 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryAfterInvocationTest.java @@ -33,6 +33,7 @@ import net.sf.acegisecurity.ConfigAttributeDefinition; import org.alfresco.model.ContentModel; import org.alfresco.repo.search.results.ChildAssocRefResultSet; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.PermissionCheckCollection.PermissionCheckCollectionMixin; import org.alfresco.repo.security.permissions.impl.AbstractPermissionTest; import org.alfresco.repo.security.permissions.impl.SimplePermissionEntry; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -466,7 +467,7 @@ public class ACLEntryAfterInvocationTest extends AbstractPermissionTest answer = method.invoke(proxy, new Object[] { nodeService.getPrimaryParent(systemNodeRef) }); assertEquals(answer, nodeService.getPrimaryParent(systemNodeRef)); } - + public void testBasicAllowNullResultSet() throws Exception { runAs("andy"); @@ -721,6 +722,17 @@ public class ACLEntryAfterInvocationTest extends AbstractPermissionTest assertEquals(4, answerArray.length); answerArray = (Object[]) methodArray.invoke(proxy, new Object[] { carArray }); assertEquals(4, answerArray.length); + + // Check cut-offs + answerCollection = (Collection) methodCollection.invoke( + proxy, new Object[] { PermissionCheckCollectionMixin.create(new ArrayList(nodeRefList), 1, 0, 0) }); + assertEquals(1, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke( + proxy, new Object[] { PermissionCheckCollectionMixin.create(new ArrayList(nodeRefList), 5, 0, 2) }); + assertEquals(2, answerCollection.size()); + answerCollection = (Collection) methodCollection.invoke( + proxy, new Object[] { PermissionCheckCollectionMixin.create(new ArrayList(nodeRefList), 5, 1, 0) }); + assertTrue("Too many results: " + answerCollection.size(), answerCollection.size() < 5); // Only 1ms to do the check permissionService.setPermission(new SimplePermissionEntry(n1, getPermission(PermissionService.READ), "andy", AccessStatus.DENIED)); diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/AbstractCannedQueryPermissions.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/AbstractCannedQueryPermissions.java index 31fdbe5462..37a2a80eb2 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/acegi/AbstractCannedQueryPermissions.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/AbstractCannedQueryPermissions.java @@ -18,7 +18,7 @@ */ package org.alfresco.repo.security.permissions.impl.acegi; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import net.sf.acegisecurity.Authentication; @@ -29,6 +29,8 @@ import net.sf.acegisecurity.context.security.SecureContext; import org.alfresco.query.AbstractCannedQuery; import org.alfresco.query.CannedQueryParameters; import org.alfresco.repo.security.authentication.AlfrescoSecureContext; +import org.alfresco.repo.security.permissions.PermissionCheckedCollection; +import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -50,15 +52,8 @@ public abstract class AbstractCannedQueryPermissions extends AbstractCannedQu this.methodSecurity = methodSecurity; } + @Override protected List applyPostQueryPermissions(List results, int requestedCount) - { - int requestTotalCountMax = getParameters().requestTotalResultCountMax(); - int maxChecks = (((requestTotalCountMax > 0) && (requestTotalCountMax > requestedCount)) ? requestTotalCountMax : requestedCount); - - return applyPermissions(results, maxChecks); - } - - protected List applyPermissions(List results, int maxChecks) { Context context = ContextHolder.getContext(); if ((context == null) || (! (context instanceof AlfrescoSecureContext))) @@ -67,13 +62,40 @@ public abstract class AbstractCannedQueryPermissions extends AbstractCannedQu { logger.debug("Unexpected context: "+(context == null ? "null" : context.getClass())+" - "+Thread.currentThread().getId()); } - - return new WrappedList(new ArrayList(0), true, false); // empty result + return Collections.emptyList(); } Authentication authentication = (((SecureContext) context).getAuthentication()); - List resultsOut = methodSecurity.applyPermissions(results, authentication, maxChecks); + List resultsOut = (List) methodSecurity.applyPermissions(results, authentication, requestedCount); // Done return resultsOut; } + + /** + * Overrides the default implementation to check for the permission data + * that will allow a good guess as to the maximum number of results in + * the event of a permission-based cut-off. + */ + @Override + protected Pair getTotalResultCount(List results) + { + // Start with the simplest + int size = results.size(); + int possibleSize = size; + + if (results instanceof PermissionCheckedCollection) + { + @SuppressWarnings("unchecked") + PermissionCheckedCollection pcc = (PermissionCheckedCollection) results; + if (pcc.isCutOff()) + { + // We didn't get all the results processed, so make a guess + double successRatio = (double)size/(double)pcc.sizeOriginal(); + int possiblyMissed = (int) (pcc.sizeUnchecked() * successRatio); + possibleSize = size + possiblyMissed; + } + } + // Done + return new Pair(size, possibleSize); + } } diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/MarkingAfterInvocationProvider.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/MarkingAfterInvocationProvider.java new file mode 100644 index 0000000000..a96f9ba58e --- /dev/null +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/MarkingAfterInvocationProvider.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.permissions.impl.acegi; + +import java.util.Collection; + +import net.sf.acegisecurity.AccessDeniedException; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.afterinvocation.AfterInvocationProvider; + +import org.alfresco.repo.security.permissions.PermissionCheckedValue; +import org.alfresco.repo.security.permissions.PermissionCheckedValue.PermissionCheckedValueMixin; + +/** + * Invocation provider that can be used to mark entries that have been permission checked. + * Use an instance of this class at the end of the 'after' invocations. + * + * @author Derek Hulley + * @since 4.0 + */ +public class MarkingAfterInvocationProvider implements AfterInvocationProvider +{ + + @Override + public Object decide( + Authentication authentication, + Object object, + ConfigAttributeDefinition config, + Object returnedObject) throws AccessDeniedException + { + // If this object has already been marked, then leave it + if (returnedObject == null) + { + return null; + } + else if (returnedObject instanceof PermissionCheckedValue) + { + return returnedObject; + } + else if (object instanceof Collection) + { + // Mark it + return PermissionCheckedValueMixin.create(returnedObject); + } + else + { + return returnedObject; + } + } + + @Override + public boolean supports(ConfigAttribute attribute) + { + return true; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean supports(Class clazz) + { + return true; + } +} diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityBean.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityBean.java index fe78a8e601..856959ca6d 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityBean.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/MethodSecurityBean.java @@ -20,12 +20,13 @@ package org.alfresco.repo.security.permissions.impl.acegi; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Method; -import java.util.List; +import java.util.Collection; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.ConfigAttributeDefinition; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.security.permissions.PermissionCheckCollection.PermissionCheckCollectionMixin; import org.alfresco.util.PropertyCheck; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; @@ -119,8 +120,25 @@ public class MethodSecurityBean implements InitializingBean } } + /** + * @see PermissionCheckCollectionMixin#create(Collection, int, long, int) + */ + public Collection applyPermissions( + Collection toCheck, + Authentication authentication, + int targetResultCount) + { + return applyPermissions(toCheck, authentication, targetResultCount, Long.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * @see PermissionCheckCollectionMixin#create(Collection, int, long, int) + */ @SuppressWarnings("unchecked") - public List applyPermissions(List results, Authentication authentication, int maxChecks) + public Collection applyPermissions( + Collection toCheck, + Authentication authentication, + int targetResultCount, long cutOffAfterTimeMs, int cutOffAfterCount) { if (cad == null) { @@ -129,14 +147,18 @@ public class MethodSecurityBean implements InitializingBean { logger.trace("applyPermissions ignored: " + this); } - return new WrappedList(results, true, false); + return toCheck; } + // Wrap the collection to pass the information to the interceptor + Collection wrappedToCheck = PermissionCheckCollectionMixin.create( + toCheck, + targetResultCount, cutOffAfterTimeMs, cutOffAfterCount); long start = System.currentTimeMillis(); - List ret = (WrappedList) methodSecurityInterceptor.getAfterInvocationManager().decide( + Collection ret = (Collection) methodSecurityInterceptor.getAfterInvocationManager().decide( authentication, null, cad, - new WrappedList(results, maxChecks)); + wrappedToCheck); if (logger.isTraceEnabled()) { logger.trace("applyPermissions: " + ret.size() + " items in " + (System.currentTimeMillis() - start) + " msecs"); diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java index 94d5a56e46..105681900f 100644 --- a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -1275,11 +1275,6 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per { return totalCount; } - @Override - public boolean permissionsApplied() - { - return results.permissionsApplied(); - } }; } diff --git a/source/java/org/alfresco/service/cmr/model/FileInfo.java b/source/java/org/alfresco/service/cmr/model/FileInfo.java index 1749bb0f4c..d5daf2a45e 100644 --- a/source/java/org/alfresco/service/cmr/model/FileInfo.java +++ b/source/java/org/alfresco/service/cmr/model/FileInfo.java @@ -22,6 +22,7 @@ import java.io.Serializable; import java.util.Date; import java.util.Map; +import org.alfresco.repo.security.permissions.PermissionCheckValue; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -33,11 +34,12 @@ import org.alfresco.service.namespace.QName; * * @author Derek Hulley */ -public interface FileInfo extends Serializable +public interface FileInfo extends PermissionCheckValue, Serializable { /** * @return Returns a reference to the low-level node representing this file */ + @Override public NodeRef getNodeRef(); /** diff --git a/source/java/org/alfresco/service/cmr/security/PersonService.java b/source/java/org/alfresco/service/cmr/security/PersonService.java index c19a00d25e..f49e5ea91b 100644 --- a/source/java/org/alfresco/service/cmr/security/PersonService.java +++ b/source/java/org/alfresco/service/cmr/security/PersonService.java @@ -25,6 +25,7 @@ import java.util.Set; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.permissions.PermissionCheckValue; import org.alfresco.service.Auditable; import org.alfresco.service.NotAuditable; import org.alfresco.service.cmr.repository.NodeRef; @@ -226,7 +227,7 @@ public interface PersonService * @author janv * @since 4.0 */ - public class PersonInfo + public class PersonInfo implements PermissionCheckValue { private final NodeRef nodeRef; private final String userName; @@ -241,6 +242,7 @@ public interface PersonService this.lastName = lastName; } + @Override public NodeRef getNodeRef() { return nodeRef; diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java b/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java index c722e517da..656fa4e219 100644 --- a/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java +++ b/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java @@ -63,10 +63,4 @@ public class PagingFollowingResultsImpl implements PagingFollowingResults { return null; } - - @Override - public boolean permissionsApplied() - { - return false; - } } diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java b/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java index e7c6b077a4..d9c77ba8bf 100644 --- a/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java +++ b/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java @@ -64,10 +64,4 @@ public class PagingSubscriptionResultsImpl implements PagingSubscriptionResults { return null; } - - @Override - public boolean permissionsApplied() - { - return false; - } }