Loading src/main/java/it/inaf/oats/vospace/CopyService.java 0 → 100644 +76 −0 Original line number Original line Diff line number Diff line /* * This file is part of vospace-rest * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.CannotSerializeTransactionException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class CopyService { @Autowired private NodeDAO nodeDao; @Value("${vospace-authority}") private String authority; @Autowired private HttpServletRequest servletRequest; @Autowired private NodeBranchService nodeBranchService; public void processCopyJob(Transfer transfer, String jobId) { if (transfer.getTarget().size() != 1) { throw new InvalidArgumentException("Invalid target size for copyNode: " + transfer.getTarget().size()); } // Get Source Vos Path String sourcePath = URIUtils.returnVosPathFromNodeURI(transfer.getTarget().get(0), authority); // Get Destination Vos Path (it's in transfer direction) String destinationPath = URIUtils.returnVosPathFromNodeURI(transfer.getDirection(), authority); // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); this.validatePath(sourcePath); this.validatePath(destinationPath); if (sourcePath.equals(destinationPath)) { return; } // check source branch for read and lock it nodeBranchService.checkBranchForReadAndLock(sourcePath, jobId); } private void validatePath(String path) { if (path.equals("/")) { throw new IllegalArgumentException("Cannot move root node or to root node"); } } } src/main/java/it/inaf/oats/vospace/JobService.java +19 −7 Original line number Original line Diff line number Diff line Loading @@ -33,6 +33,9 @@ public class JobService { @Autowired @Autowired private MoveService moveService; private MoveService moveService; @Autowired private CopyService copyService; @Autowired @Autowired private AsyncTransferService asyncTransfService; private AsyncTransferService asyncTransfService; Loading Loading @@ -108,6 +111,10 @@ public class JobService { case moveNode: case moveNode: handleMoveNode(transfer); handleMoveNode(transfer); break; break; case copyNode: handleCopyNode(transfer, job.getJobId()); break; default: default: throw new UnsupportedOperationException("Not implemented yet"); throw new UnsupportedOperationException("Not implemented yet"); } } Loading Loading @@ -152,11 +159,16 @@ public class JobService { uriService.setTransferJobResult(job, transfer); uriService.setTransferJobResult(job, transfer); } } private void handleMoveNode(Transfer transfer) private void handleMoveNode(Transfer transfer) { { moveService.processMoveJob(transfer); moveService.processMoveJob(transfer); } } private void handleCopyNode(Transfer transfer, String jobId) { copyService.processCopyJob(transfer, jobId); } private JobDirection getJobDirection(Transfer transfer) { private JobDirection getJobDirection(Transfer transfer) { return JobDirection.getJobDirectionEnumFromTransfer(transfer); return JobDirection.getJobDirectionEnumFromTransfer(transfer); } } Loading src/main/java/it/inaf/oats/vospace/NodeBranchService.java 0 → 100644 +71 −0 Original line number Original line Diff line number Diff line /* * This file is part of vospace-rest * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.CannotSerializeTransactionException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class NodeBranchService { @Autowired private NodeDAO nodeDao; @Autowired private HttpServletRequest servletRequest; @Transactional(rollbackFor = {Exception.class}, isolation = Isolation.REPEATABLE_READ) public void checkBranchForReadAndLock(String sourcePath, String jobId) { // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); try { // Get source node Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath); long sourceId = sourceIdOpt.orElseThrow(() -> new NodeNotFoundException(sourcePath)); if (nodeDao.isBranchBusy(sourceId)) { throw new NodeBusyException(sourcePath); } if (!nodeDao.isBranchReadable(sourceId, user.getName(), user.getGroups())) { throw new PermissionDeniedException(sourcePath); } this.lockBranch(sourceId, jobId); } catch (CannotSerializeTransactionException ex) { // Concurrent transactions attempted to modify this set of nodes throw new NodeBusyException(sourcePath); } } private void lockBranch(Long sourceRootId, String jobId) { nodeDao.setBranchJobId(sourceRootId, jobId); } } src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java +87 −0 Original line number Original line Diff line number Diff line Loading @@ -320,6 +320,51 @@ public class NodeDAO { }); }); } } public void copySingleNode(Long sourceId, Long destId, String jobId) { // Select source node String selectSourceSQL = "SELECT\n" + "c.path, c.node_id, c.parent_path, c.parent_relative_path, c.name, c.os_name, c.tstamp_wrapper_dir,\n" + "c.type, c.location_id, c.format, c.async_trans, c.sticky, c.creator_id,\n" + "c.group_read, c.group_write, c.is_public, c.delta, c.content_type, c.content_encoding,\n" + "c.content_length, c.content_md5, c.accept_views, c.provide_views, c.protocols\n" + "FROM node c\n" + "WHERE c.node_id = ?"; // Select destination node necessary information String selectDestinationSQL = "SELECT\n" + "d.path, d.parent_path, d.parent_relative_path\n" + "FROM node d\n" + "WHERE d.node_id = ?"; // Insert branch String insertSQL = "INSERT INTO node\n" + "(parent_path, parent_relative_path, job_id, name, os_name, tstamp_wrapper_dir,\n" + "type, location_id, format, async_trans, sticky, creator_id,\n" + "group_read, group_write, is_public, delta, content_type, content_encoding,\n" + "content_length, content_md5, accept_views, provide_views, protocols)\n" + "VALUES\n" + "(cte_dest.path, COALESCE(cte_dest.parent_relative_path, cte_dest.path), ?,\n" + "cte_source.name, cte_source.os_name, cte_source.tstamp_wrapper_dir,\n" + "cte_source.type, cte_source.location_id, cte_source.format, cte_source.async_trans, cte_source.sticky, cte_source.creator_id,\n" + "cte_source.group_read, cte_source.group_write, cte_source.is_public, cte_source.delta, cte_source.content_type, cte_source.content_encoding,\n" + "cte_source.content_length, cte_source.content_md5, cte_source.accept_views, cte_source.provide_views, cte_source.protocols)\n"; String cteSQL = "WITH cte_source AS (" + selectSourceSQL + "),\n" + "cte_dest AS (" + selectDestinationSQL + ")\n" + insertSQL; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(cteSQL); ps.setLong(1, sourceId); ps.setLong(2, destId); ps.setString(3, jobId); return ps; }); } public boolean isBranchBusy(long parentNodeId) { public boolean isBranchBusy(long parentNodeId) { String sql = "SELECT COUNT(c.node_id) > 0 " String sql = "SELECT COUNT(c.node_id) > 0 " Loading @@ -330,6 +375,18 @@ public class NodeDAO { return jdbcTemplate.queryForObject(sql, new Object[]{parentNodeId}, new int[]{Types.BIGINT}, Boolean.class); return jdbcTemplate.queryForObject(sql, new Object[]{parentNodeId}, new int[]{Types.BIGINT}, Boolean.class); } } public void setBranchJobId(Long rootNodeId, String jobId) { String sql = "UPDATE node SET job_id = ?\n" + "WHERE path ~ ('*.' || ? || '.*')::lquery"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, jobId); ps.setLong(2, rootNodeId); return ps; }); } public boolean isBranchWritable(long parentNodeId, String userId, List<String> userGroups) { public boolean isBranchWritable(long parentNodeId, String userId, List<String> userGroups) { String sql = "SELECT COUNT(c.node_id) = 0 " String sql = "SELECT COUNT(c.node_id) = 0 " Loading Loading @@ -361,6 +418,36 @@ public class NodeDAO { }); }); } } public boolean isBranchReadable(long parentNodeId, String userId, List<String> userGroups) { String sql = "SELECT COUNT(c.node_id) = 0 " + "FROM node n " + "JOIN node c ON c.path <@ n.path " + "WHERE n.node_id = ? " + "(NOT COALESCE(c.is_public, FALSE) " + "AND (SELECT COUNT(*) FROM (SELECT UNNEST(?) INTERSECT SELECT UNNEST(c.group_read)) AS allowed_groups) = 0 " + "AND c.creator_id <> ?"; return jdbcTemplate.query(sql, ps -> { ps.setLong(1, parentNodeId); String[] groups; if (userGroups == null) { groups = new String[0]; } else { groups = userGroups.toArray(String[]::new); } ps.setArray(2, ps.getConnection().createArrayOf("varchar", groups)); ps.setString(3, userId); }, row -> { if (!row.next()) { throw new IllegalStateException("Expected one result"); } return row.getBoolean(1); }); } public void deleteNode(String path) { public void deleteNode(String path) { int nodesWithPath = countNodesWithPath(path); int nodesWithPath = countNodesWithPath(path); if (nodesWithPath == 0) { if (nodesWithPath == 0) { Loading Loading
src/main/java/it/inaf/oats/vospace/CopyService.java 0 → 100644 +76 −0 Original line number Original line Diff line number Diff line /* * This file is part of vospace-rest * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.CannotSerializeTransactionException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class CopyService { @Autowired private NodeDAO nodeDao; @Value("${vospace-authority}") private String authority; @Autowired private HttpServletRequest servletRequest; @Autowired private NodeBranchService nodeBranchService; public void processCopyJob(Transfer transfer, String jobId) { if (transfer.getTarget().size() != 1) { throw new InvalidArgumentException("Invalid target size for copyNode: " + transfer.getTarget().size()); } // Get Source Vos Path String sourcePath = URIUtils.returnVosPathFromNodeURI(transfer.getTarget().get(0), authority); // Get Destination Vos Path (it's in transfer direction) String destinationPath = URIUtils.returnVosPathFromNodeURI(transfer.getDirection(), authority); // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); this.validatePath(sourcePath); this.validatePath(destinationPath); if (sourcePath.equals(destinationPath)) { return; } // check source branch for read and lock it nodeBranchService.checkBranchForReadAndLock(sourcePath, jobId); } private void validatePath(String path) { if (path.equals("/")) { throw new IllegalArgumentException("Cannot move root node or to root node"); } } }
src/main/java/it/inaf/oats/vospace/JobService.java +19 −7 Original line number Original line Diff line number Diff line Loading @@ -33,6 +33,9 @@ public class JobService { @Autowired @Autowired private MoveService moveService; private MoveService moveService; @Autowired private CopyService copyService; @Autowired @Autowired private AsyncTransferService asyncTransfService; private AsyncTransferService asyncTransfService; Loading Loading @@ -108,6 +111,10 @@ public class JobService { case moveNode: case moveNode: handleMoveNode(transfer); handleMoveNode(transfer); break; break; case copyNode: handleCopyNode(transfer, job.getJobId()); break; default: default: throw new UnsupportedOperationException("Not implemented yet"); throw new UnsupportedOperationException("Not implemented yet"); } } Loading Loading @@ -152,11 +159,16 @@ public class JobService { uriService.setTransferJobResult(job, transfer); uriService.setTransferJobResult(job, transfer); } } private void handleMoveNode(Transfer transfer) private void handleMoveNode(Transfer transfer) { { moveService.processMoveJob(transfer); moveService.processMoveJob(transfer); } } private void handleCopyNode(Transfer transfer, String jobId) { copyService.processCopyJob(transfer, jobId); } private JobDirection getJobDirection(Transfer transfer) { private JobDirection getJobDirection(Transfer transfer) { return JobDirection.getJobDirectionEnumFromTransfer(transfer); return JobDirection.getJobDirectionEnumFromTransfer(transfer); } } Loading
src/main/java/it/inaf/oats/vospace/NodeBranchService.java 0 → 100644 +71 −0 Original line number Original line Diff line number Diff line /* * This file is part of vospace-rest * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.CannotSerializeTransactionException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class NodeBranchService { @Autowired private NodeDAO nodeDao; @Autowired private HttpServletRequest servletRequest; @Transactional(rollbackFor = {Exception.class}, isolation = Isolation.REPEATABLE_READ) public void checkBranchForReadAndLock(String sourcePath, String jobId) { // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); try { // Get source node Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath); long sourceId = sourceIdOpt.orElseThrow(() -> new NodeNotFoundException(sourcePath)); if (nodeDao.isBranchBusy(sourceId)) { throw new NodeBusyException(sourcePath); } if (!nodeDao.isBranchReadable(sourceId, user.getName(), user.getGroups())) { throw new PermissionDeniedException(sourcePath); } this.lockBranch(sourceId, jobId); } catch (CannotSerializeTransactionException ex) { // Concurrent transactions attempted to modify this set of nodes throw new NodeBusyException(sourcePath); } } private void lockBranch(Long sourceRootId, String jobId) { nodeDao.setBranchJobId(sourceRootId, jobId); } }
src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java +87 −0 Original line number Original line Diff line number Diff line Loading @@ -320,6 +320,51 @@ public class NodeDAO { }); }); } } public void copySingleNode(Long sourceId, Long destId, String jobId) { // Select source node String selectSourceSQL = "SELECT\n" + "c.path, c.node_id, c.parent_path, c.parent_relative_path, c.name, c.os_name, c.tstamp_wrapper_dir,\n" + "c.type, c.location_id, c.format, c.async_trans, c.sticky, c.creator_id,\n" + "c.group_read, c.group_write, c.is_public, c.delta, c.content_type, c.content_encoding,\n" + "c.content_length, c.content_md5, c.accept_views, c.provide_views, c.protocols\n" + "FROM node c\n" + "WHERE c.node_id = ?"; // Select destination node necessary information String selectDestinationSQL = "SELECT\n" + "d.path, d.parent_path, d.parent_relative_path\n" + "FROM node d\n" + "WHERE d.node_id = ?"; // Insert branch String insertSQL = "INSERT INTO node\n" + "(parent_path, parent_relative_path, job_id, name, os_name, tstamp_wrapper_dir,\n" + "type, location_id, format, async_trans, sticky, creator_id,\n" + "group_read, group_write, is_public, delta, content_type, content_encoding,\n" + "content_length, content_md5, accept_views, provide_views, protocols)\n" + "VALUES\n" + "(cte_dest.path, COALESCE(cte_dest.parent_relative_path, cte_dest.path), ?,\n" + "cte_source.name, cte_source.os_name, cte_source.tstamp_wrapper_dir,\n" + "cte_source.type, cte_source.location_id, cte_source.format, cte_source.async_trans, cte_source.sticky, cte_source.creator_id,\n" + "cte_source.group_read, cte_source.group_write, cte_source.is_public, cte_source.delta, cte_source.content_type, cte_source.content_encoding,\n" + "cte_source.content_length, cte_source.content_md5, cte_source.accept_views, cte_source.provide_views, cte_source.protocols)\n"; String cteSQL = "WITH cte_source AS (" + selectSourceSQL + "),\n" + "cte_dest AS (" + selectDestinationSQL + ")\n" + insertSQL; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(cteSQL); ps.setLong(1, sourceId); ps.setLong(2, destId); ps.setString(3, jobId); return ps; }); } public boolean isBranchBusy(long parentNodeId) { public boolean isBranchBusy(long parentNodeId) { String sql = "SELECT COUNT(c.node_id) > 0 " String sql = "SELECT COUNT(c.node_id) > 0 " Loading @@ -330,6 +375,18 @@ public class NodeDAO { return jdbcTemplate.queryForObject(sql, new Object[]{parentNodeId}, new int[]{Types.BIGINT}, Boolean.class); return jdbcTemplate.queryForObject(sql, new Object[]{parentNodeId}, new int[]{Types.BIGINT}, Boolean.class); } } public void setBranchJobId(Long rootNodeId, String jobId) { String sql = "UPDATE node SET job_id = ?\n" + "WHERE path ~ ('*.' || ? || '.*')::lquery"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, jobId); ps.setLong(2, rootNodeId); return ps; }); } public boolean isBranchWritable(long parentNodeId, String userId, List<String> userGroups) { public boolean isBranchWritable(long parentNodeId, String userId, List<String> userGroups) { String sql = "SELECT COUNT(c.node_id) = 0 " String sql = "SELECT COUNT(c.node_id) = 0 " Loading Loading @@ -361,6 +418,36 @@ public class NodeDAO { }); }); } } public boolean isBranchReadable(long parentNodeId, String userId, List<String> userGroups) { String sql = "SELECT COUNT(c.node_id) = 0 " + "FROM node n " + "JOIN node c ON c.path <@ n.path " + "WHERE n.node_id = ? " + "(NOT COALESCE(c.is_public, FALSE) " + "AND (SELECT COUNT(*) FROM (SELECT UNNEST(?) INTERSECT SELECT UNNEST(c.group_read)) AS allowed_groups) = 0 " + "AND c.creator_id <> ?"; return jdbcTemplate.query(sql, ps -> { ps.setLong(1, parentNodeId); String[] groups; if (userGroups == null) { groups = new String[0]; } else { groups = userGroups.toArray(String[]::new); } ps.setArray(2, ps.getConnection().createArrayOf("varchar", groups)); ps.setString(3, userId); }, row -> { if (!row.next()) { throw new IllegalStateException("Expected one result"); } return row.getBoolean(1); }); } public void deleteNode(String path) { public void deleteNode(String path) { int nodesWithPath = countNodesWithPath(path); int nodesWithPath = countNodesWithPath(path); if (nodesWithPath == 0) { if (nodesWithPath == 0) { Loading