From 784f041ce59125ac2087572c2b09fea22e172315 Mon Sep 17 00:00:00 2001 From: Ancuta Morarasu Date: Wed, 11 May 2016 11:17:42 +0000 Subject: [PATCH] Merged HEAD (5.2) to 5.2.N (5.2.1) 126427 jkaabimofrad: Merged FILE-FOLDER-API (5.2.0) to HEAD (5.2) 121844 jvonka: Quick Share Link API - initial commit (work-in-progress) - demonstrate WebApiNoAuth - auth required to create &/or delete quick share link - no auth required to get &/or download content for a quick share link - TODO review detailed api & impl (+ add tests) RA-775, RA-773, RA-750, RA-708, RA-776 git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@126773 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/public-rest-context.xml | 11 + .../api/PublicApiDeclarativeRegistry.java | 292 +++++++++++++++++- .../rest/api/model/QuickShareLink.java | 129 ++++++++ .../QuickShareLinkEntityResource.java | 266 ++++++++++++++++ .../alfresco/rest/framework/WebApiNoAuth.java | 38 +++ .../core/ActionResourceMetaData.java | 22 +- .../framework/core/ResourceInspector.java | 153 ++++++--- .../rest/framework/core/ResourceMetadata.java | 23 +- .../webscripts/AbstractResourceWebScript.java | 26 +- 9 files changed, 910 insertions(+), 50 deletions(-) create mode 100644 source/java/org/alfresco/rest/api/model/QuickShareLink.java create mode 100644 source/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java create mode 100644 source/java/org/alfresco/rest/framework/WebApiNoAuth.java diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 6117ba3c4f..43565b0fa7 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -29,6 +29,10 @@ webscript.default + + + + @@ -696,6 +700,13 @@ + + + + + + + diff --git a/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java b/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java index 14923b79fa..a88409172d 100644 --- a/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java +++ b/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java @@ -25,19 +25,45 @@ */ package org.alfresco.rest.api; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.core.ResourceLocator; +import org.alfresco.rest.framework.core.ResourceWithMetadata; +import org.alfresco.rest.framework.core.exceptions.DeletedResourceException; +import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.ResourceAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.apache.commons.lang.StringUtils; +import org.springframework.extensions.webscripts.ArgumentTypeDescription; import org.springframework.extensions.webscripts.Container; import org.springframework.extensions.webscripts.DeclarativeRegistry; +import org.springframework.extensions.webscripts.Description; import org.springframework.extensions.webscripts.Description.FormatStyle; import org.springframework.extensions.webscripts.Description.RequiredAuthentication; import org.springframework.extensions.webscripts.Description.RequiredTransaction; import org.springframework.extensions.webscripts.Description.TransactionCapability; import org.springframework.extensions.webscripts.DescriptionImpl; import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.NegotiatedFormat; +import org.springframework.extensions.webscripts.Path; import org.springframework.extensions.webscripts.TransactionParameters; +import org.springframework.extensions.webscripts.TypeDescription; +import org.springframework.extensions.webscripts.URLModelFactory; import org.springframework.extensions.webscripts.WebScript; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.http.HttpMethod; public class PublicApiDeclarativeRegistry extends DeclarativeRegistry { @@ -45,6 +71,13 @@ public class PublicApiDeclarativeRegistry extends DeclarativeRegistry private WebScript getNetworkWebScript; private Container container; + private ResourceLocator locator; + + public void setLocator(ResourceLocator locator) + { + this.locator = locator; + } + public void setGetNetworksWebScript(WebScript getNetworksWebScript) { this.getNetworksWebScript = getNetworksWebScript; @@ -86,10 +119,265 @@ public class PublicApiDeclarativeRegistry extends DeclarativeRegistry } else { - return super.findWebScript(method, uri); + Match match = super.findWebScript(method, uri); + + HttpMethod httpMethod = HttpMethod.valueOf(method); + + if (httpMethod.equals(HttpMethod.GET)) + { + // TODO - review (experimental) + + // noAuth currently only exposed for GET + Map templateVars = match.getTemplateVars(); + Api api = determineApi(templateVars); + + // TODO can we avoid locating resource more than once ? + ResourceWithMetadata rwm = locator.locateResource(api, templateVars, HttpMethod.valueOf(method)); + + Class resAction = null; + + switch (rwm.getMetaData().getType()) + { + case ENTITY: + // TODO check params for entity id (for now - assume there is) + if (EntityResourceAction.ReadById.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = EntityResourceAction.ReadById.class; + } + break; + case PROPERTY: + // TODO check params for entity id (for now - assume there is) + if (BinaryResourceAction.Read.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = BinaryResourceAction.Read.class; + } + break; + default: + break; + } + + final boolean noAuth = (resAction != null && rwm.getMetaData().isNoAuth(resAction)); + + if (noAuth) + { + final WebScript webScript = match.getWebScript(); + + // hack ! - is there a better way (to dynamically override "requiredAuthentication") or handle noAuth check earlier ? + WebScript noAuthWebScriptWrapper = new WebScript() + { + @Override + public void init(Container container, Description description) + { + webScript.init(container, description); + } + + @Override + public Description getDescription() + { + final Description d = webScript.getDescription(); + return new Description() + { + @Override + public String getStorePath() + { + return d.getStorePath(); + } + + @Override + public String getScriptPath() + { + return d.getScriptPath(); + } + + @Override + public Path getPackage() + { + return d.getPackage(); + } + + @Override + public String getDescPath() + { + return d.getDescPath(); + } + + @Override + public InputStream getDescDocument() throws IOException + { + return d.getDescDocument(); + } + + @Override + public String getKind() + { + return d.getKind(); + } + + @Override + public Set getFamilys() + { + return d.getFamilys(); + } + + @Override + public RequiredAuthentication getRequiredAuthentication() + { + return RequiredAuthentication.none; + } + + @Override + public String getRunAs() + { + return d.getRunAs(); + } + + @Override + public RequiredTransaction getRequiredTransaction() + { + return d.getRequiredTransaction(); + } + + @Override + public RequiredTransactionParameters getRequiredTransactionParameters() + { + return d.getRequiredTransactionParameters(); + } + + @Override + public RequiredCache getRequiredCache() + { + return d.getRequiredCache(); + } + + @Override + public String getMethod() + { + return d.getMethod(); + } + + @Override + public String[] getURIs() + { + return d.getURIs(); + } + + @Override + public FormatStyle getFormatStyle() + { + return d.getFormatStyle(); + } + + @Override + public String getDefaultFormat() + { + return d.getDefaultFormat(); + } + + @Override + public NegotiatedFormat[] getNegotiatedFormats() + { + return d.getNegotiatedFormats(); + } + + @Override + public Map getExtensions() + { + return d.getExtensions(); + } + + @Override + public Lifecycle getLifecycle() + { + return d.getLifecycle(); + } + + @Override + public boolean getMultipartProcessing() + { + return d.getMultipartProcessing(); + } + + @Override + public void setMultipartProcessing(boolean b) + { + d.setMultipartProcessing(b); + } + + @Override + public ArgumentTypeDescription[] getArguments() + { + return d.getArguments(); + } + + @Override + public TypeDescription[] getRequestTypes() + { + return d.getRequestTypes(); + } + + @Override + public TypeDescription[] getResponseTypes() + { + return d.getResponseTypes(); + } + + @Override + public String getId() + { + return d.getId(); + } + + @Override + public String getShortName() + { + return d.getShortName(); + } + + @Override + public String getDescription() + { + return d.getDescription(); + } + }; + } + + @Override + public ResourceBundle getResources() + { + return webScript.getResources(); + } + + @Override + public void execute(WebScriptRequest webScriptRequest, WebScriptResponse webScriptResponse) throws IOException + { + webScript.execute(webScriptRequest, webScriptResponse); + } + + @Override + public void setURLModelFactory(URLModelFactory urlModelFactory) + { + webScript.setURLModelFactory(urlModelFactory); + } + }; + + match = new Match(match.getTemplate(), match.getTemplateVars(), match.getPath(), noAuthWebScriptWrapper); + } + + } + + return match; } } - + + // note: same as ApiWebscript + private Api determineApi(Map templateVars) + { + String apiScope = templateVars.get("apiScope"); + String apiVersion = templateVars.get("apiVersion"); + String apiName = templateVars.get("apiName"); + return Api.valueOf(apiName,apiScope,apiVersion); + } + private void initWebScript(WebScript webScript, String name) { DescriptionImpl serviceDesc = new DescriptionImpl(name, name, name, name); diff --git a/source/java/org/alfresco/rest/api/model/QuickShareLink.java b/source/java/org/alfresco/rest/api/model/QuickShareLink.java new file mode 100644 index 0000000000..6f2b52b632 --- /dev/null +++ b/source/java/org/alfresco/rest/api/model/QuickShareLink.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2005-2016 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.rest.api.model; + +import java.util.Date; + +/** + * Representation of quick share link + * + * The "sharedId" provides a short link/url that is easy to copy/paste/send (via email or other). + * As of now, these links are public in that they provide unauthenticated access to the + * node's content and limited metadata info, such as file name and last modifer/modification. + * + * In the future, the QuickShareService *could* be enhanced to provide additional features, + * such as link expiry &/or "password" protection, etc. + * + * @author janv + * + */ +public class QuickShareLink +{ + // unique "short" link (ie. shorter than a guid, 22 vs 36 chars) + private String sharedId; + + private String nodeId; + + private String name; + private ContentInfo content; + + protected Date modifiedAt; + protected UserInfo modifiedByUser; + + public QuickShareLink() + { + } + + public QuickShareLink(String sharedId, String nodeId) + { + this.sharedId = sharedId; + this.nodeId = nodeId; + } + + public String getSharedId() { + return sharedId; + } + + public void setSharedId(String sharedId) { + this.sharedId = sharedId; + } + + public String getNodeId() { + return nodeId; + } + + public void setNodeId(String nodeId) { + this.nodeId = nodeId; + } + + public ContentInfo getContent() + { + return content; + } + + public void setContent(ContentInfo content) + { + this.content = content; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public Date getModifiedAt() + { + return modifiedAt; + } + + public void setModifiedAt(Date modifiedAt) + { + this.modifiedAt = modifiedAt; + } + + public UserInfo getModifiedByUser() + { + return modifiedByUser; + } + + public void setModifiedByUser(UserInfo modifiedByUser) + { + this.modifiedByUser = modifiedByUser; + } + + // eg. for debug logging etc + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("QuickShareLink [sharedId=").append(getSharedId()); + sb.append(", nodeId=").append(getNodeId()); + sb.append(", name=").append(getName()); + sb.append(", modifiedAt=").append(getModifiedAt()); + sb.append(", modifiedByUser=").append(getModifiedByUser()); + sb.append(", content=").append(getContent()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/source/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java b/source/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java new file mode 100644 index 0000000000..03fc49be3d --- /dev/null +++ b/source/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2005-2016 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.rest.api.quicksharelinks; + +import org.alfresco.model.QuickShareModel; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.ContentInfo; +import org.alfresco.rest.api.model.QuickShareLink; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.quickshare.InvalidSharedIdException; +import org.alfresco.service.cmr.quickshare.QuickShareDTO; +import org.alfresco.service.cmr.quickshare.QuickShareService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * An implementation of an Entity Resource for a QuickShareLink (name TBC !!) + * + * @author janv + */ +@EntityResource(name="quicksharelinks", title = "QuickShareLinks") +public class QuickShareLinkEntityResource implements EntityResourceAction.ReadById, + BinaryResourceAction.Read, EntityResourceAction.Delete, + EntityResourceAction.Create, InitializingBean +{ + // TODO move impl into QuickShare REST service (especially if & when we need to span more than one resource) .... + + private static final Log logger = LogFactory.getLog(QuickShareLinkEntityResource.class); + + private final static String DISABLED = "QuickShare is disabled system-wide"; + private boolean enabled = true; + + private QuickShareService quickShareService; + private Nodes nodes; + private NodeService nodeService; + + public void setQuickShareService(QuickShareService quickShareService) + { + this.quickShareService = quickShareService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("quickShareService", this.quickShareService); + ParameterCheck.mandatory("nodes", this.nodes); + ParameterCheck.mandatory("nodeService", this.nodeService); + } + + /** + * Returns limited metadata regarding the sharedId. + * + * Note: does not require authenticated access ! + */ + @Override + @WebApiDescription(title="Returns quick share information for given sharedId.") + @WebApiNoAuth + public QuickShareLink readById(String sharedId, Parameters parameters) + { + if (! enabled) + { + throw new PermissionDeniedException(DISABLED); + } + + return getQuickShareInfo(sharedId); + } + + /** + * Download content via sharedId. + * + * Note: does not require authenticated access ! + * + * @param sharedId + * @param parameters {@link Parameters} + * @return + * @throws EntityNotFoundException + */ + @Override + @WebApiDescription(title = "Download content", description = "Download content") + @WebApiNoAuth + @BinaryProperties({"content"}) + public BinaryResource readProperty(String sharedId, final Parameters parameters) throws EntityNotFoundException + { + if (! enabled) + { + throw new PermissionDeniedException(DISABLED); + } + + try + { + Pair pair = quickShareService.getTenantNodeRefFromSharedId(sharedId); + + String networkTenantDomain = pair.getFirst(); + final NodeRef nodeRef = pair.getSecond(); + + return TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork() + { + public BinaryResource doWork() throws Exception + { + // belt-and-braces (similar to QuickSjareContentGet) + if (! nodeService.hasAspect(nodeRef, QuickShareModel.ASPECT_QSHARE)) + { + throw new InvalidNodeRefException(nodeRef); + } + + return nodes.getContent(nodeRef.getId(), parameters); + } + }, networkTenantDomain); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: "+sharedId); + throw new EntityNotFoundException("Unable to find: "+sharedId); + } + catch (InvalidNodeRefException inre){ + logger.warn("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new EntityNotFoundException("Unable to find: "+sharedId); + } + } + + /** + * Delete the specified quick share. + * + * Requires authenticated access. + * + * @param sharedId String id of the quick share + */ + @Override + @WebApiDescription(title = "Delete quick share", description="Delete the quick share reference") + public void delete(String sharedId, Parameters parameters) + { + if (! enabled) + { + throw new PermissionDeniedException(DISABLED); + } + + try + { + quickShareService.unshareContent(sharedId); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: "+sharedId); + throw new EntityNotFoundException("Unable to find: "+sharedId); + } + catch (InvalidNodeRefException inre){ + logger.warn("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new EntityNotFoundException("Unable to find: "+sharedId); + } + } + + /** + * Create quick share. + * + * Requires authenticated access. + * + * @param nodeIds + * @param parameters + * @return + */ + @Override + @WebApiDescription(title="Create quick share") + public List create(List nodeIds, Parameters parameters) + { + List result = new ArrayList<>(nodeIds.size()); + + for (QuickShareLink qs : nodeIds) + { + String nodeId = qs.getNodeId(); + QuickShareDTO qsDto = quickShareService.shareContent(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId)); + + // TODO should we skip errors (eg. broken share) ? + result.add(getQuickShareInfo(qsDto.getId())); + } + + return result; + } + + private QuickShareLink getQuickShareInfo(String sharedId) + { + try + { + Map map = (Map)quickShareService.getMetaData(sharedId).get("item"); + + String nodeId = new NodeRef((String)map.get("nodeRef")).getId(); + + ContentInfo contentInfo = new ContentInfo((String)map.get("mimetype"), null, (Long)map.get("size"), null); + + // note: we do not return modifier user id (to be consistent with v0 internal - limited disclosure) + UserInfo modifier = new UserInfo(null,(String)map.get("modifierFirstName"), (String)map.get("modifierLastName")); + + // TODO other "properties" (if needed) - eg. cm:title, cm:lastThumbnailModificationData, ... thumbnail info ... + + QuickShareLink qs = new QuickShareLink(sharedId, nodeId); + qs.setName((String)map.get("name")); + qs.setContent(contentInfo); + qs.setModifiedAt((Date)map.get("modified")); + qs.setModifiedByUser(modifier); + + return qs; + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: "+sharedId); + throw new EntityNotFoundException("Unable to find: "+sharedId); + } + catch (InvalidNodeRefException inre){ + logger.warn("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new EntityNotFoundException("Unable to find: "+sharedId); + } + } +} diff --git a/source/java/org/alfresco/rest/framework/WebApiNoAuth.java b/source/java/org/alfresco/rest/framework/WebApiNoAuth.java new file mode 100644 index 0000000000..baa06e5b0d --- /dev/null +++ b/source/java/org/alfresco/rest/framework/WebApiNoAuth.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2016 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.rest.framework; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates this Web api is does *not* require authentication ! + * + * @author janv + */ +@Target({ElementType.TYPE,ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebApiNoAuth +{ + +} diff --git a/source/java/org/alfresco/rest/framework/core/ActionResourceMetaData.java b/source/java/org/alfresco/rest/framework/core/ActionResourceMetaData.java index e461cd057a..7d41897f8d 100644 --- a/source/java/org/alfresco/rest/framework/core/ActionResourceMetaData.java +++ b/source/java/org/alfresco/rest/framework/core/ActionResourceMetaData.java @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2005-2016 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.rest.framework.core; import org.alfresco.rest.framework.Api; @@ -26,7 +44,7 @@ public class ActionResourceMetaData extends ResourceMetadata */ public ActionResourceMetaData(String uniqueId, List operations, Api api, Method actionMethod) { - super(uniqueId, RESOURCE_TYPE.ACTION, operations, api, null, null); + super(uniqueId, RESOURCE_TYPE.ACTION, operations, api, null, null, null); if (operations.size()!= 1) { throw new IllegalArgumentException("Only 1 action per url is supported for an entity"); @@ -42,7 +60,7 @@ public class ActionResourceMetaData extends ResourceMetadata */ public ActionResourceMetaData(String uniqueId, Api api, Set> apiDeleted) { - super(uniqueId, RESOURCE_TYPE.ACTION, null, api, apiDeleted, null); + super(uniqueId, RESOURCE_TYPE.ACTION, null, api, apiDeleted, null, null); this.actionMethod = null; } diff --git a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java index 677588536c..a8efb4d436 100644 --- a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java +++ b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java @@ -43,6 +43,7 @@ import org.alfresco.rest.framework.BinaryProperties; import org.alfresco.rest.framework.WebApi; import org.alfresco.rest.framework.WebApiDeleted; import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; import org.alfresco.rest.framework.WebApiParam; import org.alfresco.rest.framework.WebApiParameters; import org.alfresco.rest.framework.core.ResourceMetadata.RESOURCE_TYPE; @@ -57,6 +58,7 @@ import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartResource import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartRelationshipResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.ResourceAction; +import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -122,17 +124,20 @@ public class ResourceInspector findOperation(EntityResourceAction.Delete.class, HttpMethod.DELETE, helper); findOperation(MultiPartResourceAction.Create.class, HttpMethod.POST, helper); + boolean noAuth = resource.isAnnotationPresent(WebApiNoAuth.class); + Set> apiNoAuth = (noAuth ? ALL_ENTITY_RESOURCE_INTERFACES : helper.apiNoAuth); + if (resource.isAnnotationPresent(WebApiDeleted.class)) { metainfo.add(new ResourceMetadata(ResourceDictionary.resourceKey(urlPath,null), RESOURCE_TYPE.ENTITY, - null, api, ALL_ENTITY_RESOURCE_INTERFACES, null)); + null, api, ALL_ENTITY_RESOURCE_INTERFACES, apiNoAuth, null)); } else { if (!helper.apiDeleted.isEmpty() || !helper.operations.isEmpty()) { metainfo.add(new ResourceMetadata(ResourceDictionary.resourceKey(urlPath,null), RESOURCE_TYPE.ENTITY, - helper.operations, api, helper.apiDeleted, null)); + helper.operations, api, helper.apiDeleted, apiNoAuth, null)); } } @@ -151,56 +156,29 @@ public class ResourceInspector public static void inspectAddressedProperties(Api api, Class resource, final String entityPath, List metainfo) { final Map> operationGroupedByProperty = new HashMap>(); - MetaHelperCallback helperForAddressProps = new MetaHelperCallback(resource) { - @Override - public void whenNewOperation(ResourceOperation operation, Method aMethod) - { - Annotation addressableProps = AnnotationUtils.findAnnotation(aMethod, BinaryProperties.class); - if (addressableProps != null) - { - Map annotAttribs = AnnotationUtils.getAnnotationAttributes(addressableProps); - String[] props = (String[]) annotAttribs.get("value"); - for (String property : props) - { - String propKey = ResourceDictionary.propertyResourceKey(entityPath,property); - if (!operationGroupedByProperty.containsKey(propKey)) - { - List ops = new ArrayList(); - operationGroupedByProperty.put(propKey, ops); - } - List operations = operationGroupedByProperty.get(propKey); - operations.add(operation); - } - - } - else - { - logger.warn("Resource "+resource.getCanonicalName()+" should declare a @BinaryProperties annotation."); - } - } + MetaHelperAddressable helperForAddressProps = new MetaHelperAddressable(resource, entityPath, operationGroupedByProperty); - @Override - public void whenOperationDeleted(Class deleted, Method aMethod) - { - } - }; findOperation(BinaryResourceAction.Read.class, HttpMethod.GET, helperForAddressProps); findOperation(BinaryResourceAction.Delete.class, HttpMethod.DELETE, helperForAddressProps); findOperation(BinaryResourceAction.Update.class, HttpMethod.PUT, helperForAddressProps); - + + boolean noAuth = resource.isAnnotationPresent(WebApiNoAuth.class); + Set> apiNoAuth = (noAuth ? ALL_PROPERTY_RESOURCE_INTERFACES : helperForAddressProps.apiNoAuth); + if (resource.isAnnotationPresent(WebApiDeleted.class)) { metainfo.add(new ResourceMetadata(ResourceDictionary.propertyResourceKey(entityPath,"FIX_ME"), RESOURCE_TYPE.PROPERTY, - null, inspectApi(resource), ALL_PROPERTY_RESOURCE_INTERFACES, null)); + null, inspectApi(resource), ALL_PROPERTY_RESOURCE_INTERFACES, apiNoAuth, null)); } else { for (Entry> groupedOps : operationGroupedByProperty.entrySet()) { - metainfo.add(new ResourceMetadata(groupedOps.getKey(), RESOURCE_TYPE.PROPERTY, groupedOps.getValue(), api, null, null)); - } + metainfo.add(new ResourceMetadata(groupedOps.getKey(), RESOURCE_TYPE.PROPERTY, groupedOps.getValue(), api, null, apiNoAuth, null)); + } } + } /** @@ -222,14 +200,17 @@ public class ResourceInspector findOperation(RelationshipResourceAction.Update.class, HttpMethod.PUT, helper); findOperation(RelationshipResourceAction.Delete.class, HttpMethod.DELETE, helper); findOperation(MultiPartRelationshipResourceAction.Create.class, HttpMethod.POST, helper); - + + boolean noAuth = resource.isAnnotationPresent(WebApiNoAuth.class); + Set> apiNoAuth = (noAuth ? ALL_RELATIONSHIP_RESOURCE_INTERFACES : helper.apiNoAuth); + if (resource.isAnnotationPresent(WebApiDeleted.class)) { - return Arrays.asList(new ResourceMetadata(ResourceDictionary.resourceKey(entityPath,urlPath), RESOURCE_TYPE.RELATIONSHIP, null, inspectApi(resource), ALL_RELATIONSHIP_RESOURCE_INTERFACES, entityPath)); + return Arrays.asList(new ResourceMetadata(ResourceDictionary.resourceKey(entityPath,urlPath), RESOURCE_TYPE.RELATIONSHIP, null, inspectApi(resource), ALL_RELATIONSHIP_RESOURCE_INTERFACES, apiNoAuth, entityPath)); } else { - return Arrays.asList(new ResourceMetadata(ResourceDictionary.resourceKey(entityPath,urlPath), RESOURCE_TYPE.RELATIONSHIP, helper.operations, inspectApi(resource), helper.apiDeleted, entityPath)); + return Arrays.asList(new ResourceMetadata(ResourceDictionary.resourceKey(entityPath,urlPath), RESOURCE_TYPE.RELATIONSHIP, helper.operations, inspectApi(resource), helper.apiDeleted, apiNoAuth, entityPath)); } } @@ -246,6 +227,7 @@ public class ResourceInspector { Method aMethod = findMethod(resourceInterfaceWithOneMethod, helper.resource); ResourceOperation operation = inspectOperation(helper.resource, aMethod, httpMethod); + if (isDeleted(aMethod)) { helper.whenOperationDeleted(resourceInterfaceWithOneMethod, aMethod); @@ -254,6 +236,11 @@ public class ResourceInspector { helper.whenNewOperation(operation, aMethod); } + + if (isNoAuth(aMethod)) + { + helper.whenOperationNoAuth(resourceInterfaceWithOneMethod, aMethod); + } } } @@ -448,6 +435,17 @@ public class ResourceInspector WebApiDeleted deleted = AnnotationUtils.getAnnotation(method, WebApiDeleted.class); return (deleted!=null); } + + /** + * Returns true if the method has been marked as no auth required. + * @param method the method + * @return true - if is is marked as no auth required. + */ + public static boolean isNoAuth(Method method) + { + WebApiNoAuth noAuth = AnnotationUtils.getAnnotation(method, WebApiNoAuth.class); + return (noAuth!=null); + } /** * Returns the method for the interface @@ -655,6 +653,10 @@ public class ResourceInspector Object id = ResourceInspectorUtil.invokeMethod(annotatedMethod, obj); if (id != null) { + if (id instanceof NodeRef) + { + return ((NodeRef)id).getId(); + } return String.valueOf(id); } else @@ -704,6 +706,66 @@ public class ResourceInspector } return UniqueId.UNIQUE_NAME; } + + private static class MetaHelperAddressable extends MetaHelperCallback { + + private Set> apiNoAuth = new HashSet>(); + + private String entityPath; + private Map> operationGroupedByProperty; + + public MetaHelperAddressable(Class resource, String entityPath, Map> operationGroupedByProperty) + { + super(resource); + + this.entityPath = entityPath; + this.operationGroupedByProperty = operationGroupedByProperty; + } + + public MetaHelperAddressable(Class resource) + { + super(resource); + } + + @Override + public void whenNewOperation(ResourceOperation operation, Method aMethod) + { + Annotation addressableProps = AnnotationUtils.findAnnotation(aMethod, BinaryProperties.class); + if (addressableProps != null) + { + Map annotAttribs = AnnotationUtils.getAnnotationAttributes(addressableProps); + String[] props = (String[]) annotAttribs.get("value"); + for (String property : props) + { + String propKey = ResourceDictionary.propertyResourceKey(entityPath,property); + if (!operationGroupedByProperty.containsKey(propKey)) + { + List ops = new ArrayList(); + operationGroupedByProperty.put(propKey, ops); + } + List operations = operationGroupedByProperty.get(propKey); + operations.add(operation); + } + + } + else + { + logger.warn("Resource "+resource.getCanonicalName()+" should declare a @BinaryProperties annotation."); + } + } + + @Override + public void whenOperationDeleted(Class deleted, Method aMethod) + { + } + + @Override + public void whenOperationNoAuth(Class noAuth, Method aMethod) + { + // TODO review - is this right ? + apiNoAuth.add(noAuth); + } + } /** * Little container of a subset of metadata @@ -718,7 +780,9 @@ public class ResourceInspector } private List operations = new ArrayList(); + private Set> apiDeleted = new HashSet>(); + private Set> apiNoAuth = new HashSet>(); @Override public void whenNewOperation(ResourceOperation operation, Method aMethod) @@ -731,6 +795,12 @@ public class ResourceInspector { apiDeleted.add(deleted); } + + @Override + public void whenOperationNoAuth(Class noAuth, Method aMethod) + { + apiNoAuth.add(noAuth); + } } /** @@ -750,6 +820,7 @@ public class ResourceInspector public abstract void whenNewOperation(ResourceOperation operation, Method aMethod); public abstract void whenOperationDeleted(Class deleted, Method aMethod); + public abstract void whenOperationNoAuth(Class noAuth, Method aMethod); } } diff --git a/source/java/org/alfresco/rest/framework/core/ResourceMetadata.java b/source/java/org/alfresco/rest/framework/core/ResourceMetadata.java index 8f753385a2..0df8a4ad7d 100644 --- a/source/java/org/alfresco/rest/framework/core/ResourceMetadata.java +++ b/source/java/org/alfresco/rest/framework/core/ResourceMetadata.java @@ -39,6 +39,7 @@ import org.springframework.http.HttpMethod; * the resource can perform and what properties it has. * * @author Gethin James + * @author janv */ public class ResourceMetadata { @@ -50,10 +51,15 @@ public class ResourceMetadata @JsonIgnore private final Api api; + private final Set> apiDeleted; + private Set> apiNoAuth; @SuppressWarnings("unchecked") - public ResourceMetadata(String uniqueId, RESOURCE_TYPE type, List operations, Api api, Set> apiDeleted, String parentResource) + public ResourceMetadata(String uniqueId, RESOURCE_TYPE type, List operations, Api api, + Set> apiDeleted, + Set> apiNoAuth, + String parentResource) { super(); this.uniqueId = uniqueId; @@ -61,6 +67,7 @@ public class ResourceMetadata this.operations = (List) (operations==null?Collections.emptyList():operations); this.api = api; this.apiDeleted = (Set>) (apiDeleted==null?Collections.emptySet():apiDeleted); + this.apiNoAuth = (Set>) (apiNoAuth==null?Collections.emptySet():apiNoAuth); this.parentResource = parentResource!=null?(parentResource.startsWith("/")?parentResource:"/"+parentResource):null; } @@ -110,7 +117,17 @@ public class ResourceMetadata { return apiDeleted.contains(resourceAction); } - + + /** + * Indicates if this resource action supports unauthenticated access. + * @param resourceAction + * @return + */ + public boolean isNoAuth(Class resourceAction) + { + return apiNoAuth.contains(resourceAction); + } + /** * URL uniqueId to the resource * @@ -158,6 +175,8 @@ public class ResourceMetadata builder.append(this.operations); builder.append(", apiDeleted="); builder.append(this.apiDeleted); + builder.append(", apiNoAuth="); + builder.append(this.apiNoAuth); builder.append("]"); return builder.toString(); } diff --git a/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java b/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java index 88652b18ae..df189b5f6b 100644 --- a/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java +++ b/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.web.scripts.content.ContentStreamer; import org.alfresco.rest.framework.Api; import org.alfresco.rest.framework.core.HttpMethodSupport; @@ -37,6 +38,7 @@ import org.alfresco.rest.framework.core.ResourceWithMetadata; import org.alfresco.rest.framework.core.exceptions.ApiException; import org.alfresco.rest.framework.jacksonextensions.JacksonHelper; import org.alfresco.rest.framework.resource.actions.ActionExecutor; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.content.ContentInfo; import org.alfresco.rest.framework.resource.content.FileBinaryResource; @@ -49,6 +51,7 @@ import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.extensions.surf.util.URLEncoder; +import org.springframework.extensions.webscripts.Description; import org.springframework.extensions.webscripts.Status; import org.springframework.extensions.webscripts.WebScriptException; import org.springframework.extensions.webscripts.WebScriptRequest; @@ -124,7 +127,7 @@ public abstract class AbstractResourceWebScript extends ApiWebScript implements }); //Outside the transaction. - Object toSerialize = respons.get("toSerialize"); + final Object toSerialize = respons.get("toSerialize"); ContentInfo contentInfo = (ContentInfo) respons.get("contentInfo"); // set caching (MNT-13938) @@ -137,7 +140,25 @@ public abstract class AbstractResourceWebScript extends ApiWebScript implements { if (toSerialize instanceof BinaryResource) { - streamResponse(req, res, (BinaryResource) toSerialize); + // TODO review (experimental) - can we move earlier & wrap complete execute ? Also for QuickShare (in MT/Cloud) needs to be tenant for the nodeRef (TBC). + boolean noAuth = resource.getMetaData().isNoAuth(BinaryResourceAction.Read.class); + if (noAuth) + { + String networkTenantDomain = TenantUtil.getCurrentDomain(); + + TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork() + { + public Void doWork() throws Exception + { + streamResponse(req, res, (BinaryResource) toSerialize); + return null; + } + }, networkTenantDomain); + } + else + { + streamResponse(req, res, (BinaryResource) toSerialize); + } } else { @@ -250,5 +271,4 @@ public abstract class AbstractResourceWebScript extends ApiWebScript implements { this.streamer = streamer; } - }