diff --git a/source/java/org/alfresco/rest/api/impl/NodesImpl.java b/source/java/org/alfresco/rest/api/impl/NodesImpl.java index c3508e3db7..206f43dd94 100644 --- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -40,6 +40,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; import org.alfresco.model.ApplicationModel; import org.alfresco.model.ContentModel; @@ -55,6 +56,9 @@ import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.repo.site.SiteServiceInternal; +import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.version.VersionModel; import org.alfresco.rest.antlr.WhereClauseParser; import org.alfresco.rest.api.Nodes; @@ -166,6 +170,7 @@ public class NodesImpl implements Nodes private PersonService personService; private OwnableService ownableService; private AuthorityService authorityService; + private SiteServiceInternal siteServiceInternal; private BehaviourFilter behaviourFilter; @@ -179,6 +184,8 @@ public class NodesImpl implements Nodes // ignore types/aspects private Set ignoreQNames; + private ConcurrentHashMap ddCache = new ConcurrentHashMap(); + private Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf public void setNonAttachContentTypes(Set nonAttachWhiteList) @@ -807,13 +814,19 @@ public class NodesImpl implements Nodes String perm = kv.getKey(); String op = kv.getValue(); - // special case: do not return "create" for file - if (! (perm.equals(PermissionService.ADD_CHILDREN) && type.equals(Type.DOCUMENT))) + if (perm.equals(PermissionService.ADD_CHILDREN) && type.equals(Type.DOCUMENT)) { - if (permissionService.hasPermission(nodeRef, perm) == AccessStatus.ALLOWED) - { - allowableOperations.add(op); - } + // special case: do not return "create" (as an allowable op) for file/content types + continue; + } + else if (perm.equals(PermissionService.DELETE) && (isSpecialNodeDoNotDelete(nodeRef, nodeTypeQName))) + { + // special case: do not return "delete" (as an allowable op) for specific system nodes + continue; + } + else if (permissionService.hasPermission(nodeRef, perm) == AccessStatus.ALLOWED) + { + allowableOperations.add(op); } } @@ -1226,7 +1239,12 @@ public class NodesImpl implements Nodes @Override public void deleteNode(String nodeId, Parameters parameters) { - NodeRef nodeRef = validateNode(nodeId); + NodeRef nodeRef = validateOrLookupNode(nodeId, null); + + if (isSpecialNodeDoNotDelete(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Cannot delete: " + nodeId); + } // default false (if not provided) boolean permanentDelete = Boolean.valueOf(parameters.getParameter(PARAM_PERMANENT)); @@ -1390,6 +1408,47 @@ public class NodesImpl implements Nodes } } + // special case: additional delete validation (pending common lower-level service support) + // for blacklist of system nodes that should not be deleted, eg. Company Home, Sites, Data Dictionary + private boolean isSpecialNodeDoNotDelete(NodeRef nodeRef, QName type) + { + // Check for Company Home, Sites and Data Dictionary (note: must be tenant-aware) + + if (nodeRef.equals(repositoryHelper.getCompanyHome())) + { + return true; + } + else if (type.equals(SiteModel.TYPE_SITES)) + { + // note: alternatively, we could inject SiteServiceInternal and use getSitesRoot (or indirectly via node locator) + return true; + } + else + { + String tenantDomain = TenantUtil.getCurrentDomain(); + NodeRef ddNodeRef = ddCache.get(tenantDomain); + if (ddNodeRef == null) + { + List ddAssocs = nodeService.getChildAssocs( + repositoryHelper.getCompanyHome(), + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "dictionary")); + if (ddAssocs.size() == 1) + { + ddNodeRef = ddAssocs.get(0).getChildRef(); + ddCache.put(tenantDomain, ddNodeRef); + } + } + + if (nodeRef.equals(ddNodeRef)) + { + return true; + } + } + + return false; + } + @Override public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters) { @@ -1565,10 +1624,17 @@ public class NodesImpl implements Nodes { if (isCopy) { + // copy return fileFolderService.copy(nodeRef, parentNodeRef, name); } else { + // move + if ((! nodeRef.equals(parentNodeRef)) && isSpecialNodeDoNotDelete(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Cannot move: "+nodeRef.getId()); + } + // updating "parentId" means moving primary parent ! // note: in the future (as and when we support secondary parent/child assocs) we may also // wish to select which parent to "move from" (in case where the node resides in multiple locations) diff --git a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java index 1d094ea604..0032f644a8 100644 --- a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -1209,6 +1209,33 @@ public class NodeApiTest extends AbstractBaseApiTest publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); response = publicApiClient.delete(getScope(), 1, URL_NODES, folder6Id, null, null, params); checkStatus(204, response.getStatusCode()); + + // -ve - cannot delete Company Home root node + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.delete(getScope(), 1, URL_NODES, rootNodeId, null, null, params); + checkStatus(403, response.getStatusCode()); + + params = new HashMap<>(); + params.put("relativePath", "/Sites"); + response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String sitesNodeId = nodeResp.getId(); + + // -ve - cannot delete Sites node + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.delete(getScope(), 1, URL_NODES, sitesNodeId, null, null, params); + checkStatus(403, response.getStatusCode()); + + params = new HashMap<>(); + params.put("relativePath", "/Data Dictionary"); + response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String ddNodeId = nodeResp.getId(); + + // -ve - cannot delete Data Dictionary node + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.delete(getScope(), 1, URL_NODES, ddNodeId, null, null, params); + checkStatus(403, response.getStatusCode()); } private boolean existsArchiveNode(String userId, String nodeId) @@ -1328,6 +1355,35 @@ public class NodeApiTest extends AbstractBaseApiTest tgt = new NodeTarget(); tgt.setTargetParentId(my2NodeId); post("nodes/"+f1Id+"/move", user2, toJsonAsStringNonNull(tgt), null, 403); + + // TODO improve - admin-related tests + + // -ve - cannot move (delete) Company Home root node + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.post(getScope(), "nodes/"+rootNodeId+"/move", null, null, null, toJsonAsStringNonNull(tgt)); + checkStatus(403, response.getStatusCode()); + + Map params = new HashMap<>(); + params.put("relativePath", "/Sites"); + response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + Node nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String sitesNodeId = nodeResp.getId(); + + // -ve - cannot move (delete) Sites node + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.post(getScope(), "nodes/"+sitesNodeId+"/move", null, null, null, toJsonAsStringNonNull(tgt)); + checkStatus(403, response.getStatusCode()); + + params = new HashMap<>(); + params.put("relativePath", "/Data Dictionary"); + response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String ddNodeId = nodeResp.getId(); + + // -ve - cannot move (delete) Data Dictionary node + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.post(getScope(), "nodes/"+ddNodeId+"/move", null, null, null, toJsonAsStringNonNull(tgt)); + checkStatus(403, response.getStatusCode()); } /** @@ -2690,21 +2746,54 @@ public class NodeApiTest extends AbstractBaseApiTest @Test public void testAllowableOps() throws Exception { - String sharedNodeId = getSharedNodeId(user1); - // as user1 ... - // create folder - Node nodeResp = createFolder(user1, sharedNodeId, "folder 1 - "+RUNID); - String folderId = nodeResp.getId(); - assertNull(nodeResp.getAllowableOperations()); + String rootNodeId = getRootNodeId(user1); + String sharedNodeId = getSharedNodeId(user1); - HttpResponse response = getSingle(NodesEntityResource.class, user1, folderId, null, 200); + Map params = new HashMap<>(); + params.put("relativePath", "/Sites"); + HttpResponse response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + Node nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String sitesNodeId = nodeResp.getId(); + + params = new HashMap<>(); + params.put("relativePath", "/Data Dictionary"); + response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + String ddNodeId = nodeResp.getId(); + + + params = new HashMap<>(); + params.put("include", "allowableOperations"); + + response = getSingle(NodesEntityResource.class, user1, rootNodeId, params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNull(nodeResp.getAllowableOperations()); + + response = getSingle(NodesEntityResource.class, user1, sharedNodeId, params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNotNull(nodeResp.getAllowableOperations()); + assertEquals(1, nodeResp.getAllowableOperations().size()); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); + + response = getSingle(NodesEntityResource.class, user1, getMyNodeId(user1), params, 200); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNotNull(nodeResp.getAllowableOperations()); + assertEquals(3, nodeResp.getAllowableOperations().size()); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_DELETE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); + + // create folder + nodeResp = createFolder(user1, sharedNodeId, "folder 1 - "+RUNID); + String folderId = nodeResp.getId(); + assertNull(nodeResp.getAllowableOperations()); + + response = getSingle(NodesEntityResource.class, user1, folderId, null, 200); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); assertNull(nodeResp.getAllowableOperations()); - Map params = new HashMap<>(); - params.put("include", "allowableOperations"); response = getSingle(NodesEntityResource.class, user1, folderId, params, 200); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); assertNotNull(nodeResp.getAllowableOperations()); @@ -2722,8 +2811,7 @@ public class NodeApiTest extends AbstractBaseApiTest nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); assertNull(nodeResp.getAllowableOperations()); - params = new HashMap<>(); - params.put("include", "allowableOperations"); + // a file - no create response = getSingle(NodesEntityResource.class, user1, fileId, params, 200); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); assertNotNull(nodeResp.getAllowableOperations()); @@ -2733,16 +2821,12 @@ public class NodeApiTest extends AbstractBaseApiTest // as user2 ... - params = new HashMap<>(); - params.put("include", "allowableOperations"); response = getSingle(NodesEntityResource.class, user2, folderId, params, 200); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); assertNotNull(nodeResp.getAllowableOperations()); assertEquals(1, nodeResp.getAllowableOperations().size()); assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); - params = new HashMap<>(); - params.put("include", "allowableOperations"); response = getSingle(NodesEntityResource.class, user2, fileId, params, 200); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); assertNull(nodeResp.getAllowableOperations()); @@ -2750,8 +2834,6 @@ public class NodeApiTest extends AbstractBaseApiTest // as admin ... // TODO improve - admin-related tests - params = new HashMap<>(); - params.put("include", "allowableOperations"); publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); response = publicApiClient.get(NodesEntityResource.class, folderId, null, params); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); @@ -2761,8 +2843,7 @@ public class NodeApiTest extends AbstractBaseApiTest assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); - params = new HashMap<>(); - params.put("include", "allowableOperations"); + // a file - no create publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); response = publicApiClient.get(NodesEntityResource.class, fileId, null, params); nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); @@ -2771,6 +2852,43 @@ public class NodeApiTest extends AbstractBaseApiTest assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_DELETE)); assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.get(NodesEntityResource.class, sharedNodeId, null, params); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNotNull(nodeResp.getAllowableOperations()); + assertEquals(3, nodeResp.getAllowableOperations().size()); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_DELETE)); + + // Company Home - no delete + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.get(NodesEntityResource.class, rootNodeId, null, params); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNotNull(nodeResp.getAllowableOperations()); + assertEquals(2, nodeResp.getAllowableOperations().size()); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); + + // Sites - no delete + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.get(NodesEntityResource.class, sitesNodeId, null, params); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNotNull(nodeResp.getAllowableOperations()); + assertEquals(2, nodeResp.getAllowableOperations().size()); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); + + // Data Dictionary - no delete + publicApiClient.setRequestContext(new RequestContext("-default-", "admin", "admin")); + response = publicApiClient.get(NodesEntityResource.class, ddNodeId, null, params); + nodeResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Node.class); + assertNotNull(nodeResp.getAllowableOperations()); + assertEquals(2, nodeResp.getAllowableOperations().size()); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_CREATE)); + assertTrue(nodeResp.getAllowableOperations().contains(Nodes.OP_UPDATE)); + + publicApiClient.setRequestContext(null); // as user1 ...