From 3aecd0509cff4666aabf3100600ab185eb318381 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Fri, 12 Nov 2021 14:15:31 +0100
Subject: [PATCH] Added generation of 'Shared files' folder

---
 .../ia2/vospace/ui/client/VOSpaceClient.java  | 33 +++++--
 .../ui/controller/NodesController.java        | 20 +++-
 .../ia2/vospace/ui/data/ShareRequest.java     |  9 ++
 .../ui/service/MainNodesHtmlGenerator.java    | 19 +++-
 .../inaf/ia2/vospace/ui/service/NodeInfo.java | 11 ++-
 .../ui/service/NodesHtmlGenerator.java        |  2 +
 .../vospace/ui/service/SharingService.java    | 92 ++++++++++++++++++-
 .../vospace/ui/client/VOSpaceClientTest.java  |  3 +-
 .../ui/service/NodesHtmlGeneratorTest.java    | 30 ++++++
 .../ui/service/SharingServiceTest.java        | 70 +++++++++++++-
 vospace-ui-frontend/src/assets/css/fonts.css  |  4 +
 .../src/components/modal/ShareModal.vue       | 22 ++++-
 12 files changed, 289 insertions(+), 26 deletions(-)

diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
index c7c5735..4a33a42 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
@@ -12,6 +12,7 @@ import it.inaf.ia2.vospace.ui.data.Job;
 import it.inaf.ia2.vospace.ui.exception.BadRequestException;
 import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
 import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
+import it.inaf.oats.vospace.datamodel.NodeUtils;
 import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
 import java.io.IOException;
 import java.io.InputStream;
@@ -25,6 +26,7 @@ import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.Scanner;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ForkJoinPool;
@@ -56,9 +58,6 @@ public class VOSpaceClient {
     @Value("${use-json}")
     private boolean useJson;
 
-    @Value("${vospace-authority}")
-    private String authority;
-
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
     private final HttpClient httpClient;
@@ -84,8 +83,12 @@ public class VOSpaceClient {
     }
 
     public Node getNode(String path) {
+        return getNode(path, Optional.empty());
+    }
 
-        HttpRequest request = getRequest("/nodes" + urlEncodePath(path))
+    public Node getNode(String path, Optional<String> adminToken) {
+
+        HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken)
                 .header("Accept", useJson ? "application/json" : "text/xml")
                 .build();
 
@@ -140,10 +143,14 @@ public class VOSpaceClient {
     }
 
     public Node createNode(Node node) {
+        return createNode(node, Optional.empty());
+    }
 
-        String path = node.getUri().substring(("vos://" + authority).length());
+    public Node createNode(Node node, Optional<String> adminToken) {
 
-        HttpRequest request = getRequest("/nodes" + path)
+        String path = NodeUtils.getVosPath(node);
+
+        HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken)
                 .header("Accept", useJson ? "application/json" : "text/xml")
                 .header("Content-Type", useJson ? "application/json" : "text/xml")
                 .PUT(HttpRequest.BodyPublishers.ofString(marshal(node)))
@@ -164,10 +171,14 @@ public class VOSpaceClient {
     }
 
     public Node setNode(Node node, boolean recursive) {
+        return setNode(node, recursive, Optional.empty());
+    }
+
+    public Node setNode(Node node, boolean recursive, Optional<String> adminToken) {
 
-        String path = node.getUri().substring(("vos://" + authority).length());
+        String path = NodeUtils.getVosPath(node);
 
-        HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive)
+        HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, adminToken)
                 .header("Accept", useJson ? "application/json" : "text/xml")
                 .header("Content-Type", useJson ? "application/json" : "text/xml")
                 .POST(HttpRequest.BodyPublishers.ofString(marshal(node)))
@@ -281,8 +292,12 @@ public class VOSpaceClient {
     }
 
     private HttpRequest.Builder getRequest(String path) {
+        return getRequest(path, Optional.empty());
+    }
+
+    private HttpRequest.Builder getRequest(String path, Optional<String> adminToken) {
         HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path));
-        String token = getToken();
+        String token = adminToken.orElseGet(() -> getToken());
         if (token != null) {
             builder.setHeader("Authorization", "Bearer " + token);
         }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
index abcb6dd..50be555 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
@@ -17,6 +17,9 @@ import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator;
 import it.inaf.ia2.vospace.ui.service.MoveOrCopyNodeModalHtmlGenerator;
 import it.inaf.oats.vospace.datamodel.NodeUtils;
 import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -43,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -132,7 +136,9 @@ public class NodesController extends BaseController {
             for (LinkNode linkNode : linksToLoad) {
                 String prefix = "vos://" + authority;
                 if (linkNode.getTarget().startsWith(prefix)) {
-                    String linkPath = linkNode.getTarget().substring(prefix.length());
+                    String linkPath = StringUtils.uriDecode(linkNode.getTarget(), StandardCharsets.UTF_8)
+                            .substring(prefix.length());
+
                     nodesCalls.add(CompletableFuture.supplyAsync(() -> client.getNode(linkPath), Runnable::run)
                             .exceptionally(ex -> null)); // null is returned in case of broken link
                 } else {
@@ -225,10 +231,10 @@ public class NodesController extends BaseController {
         transfer.setView(view);
 
         if (paths.size() == 1) {
-            transfer.setTarget("vos://" + authority + paths.get(0));
+            transfer.setTarget(getUri(paths.get(0)));
         } else {
             String parent = getCommonParent(paths);
-            transfer.setTarget("vos://" + authority + parent);
+            transfer.setTarget(getUri(parent));
             for (String path : paths) {
                 String childName = path.substring(parent.length() + 1);
                 Param param = new Param();
@@ -246,6 +252,14 @@ public class NodesController extends BaseController {
         return new Job(client.startTransferJob(transfer), Job.JobType.ARCHIVE);
     }
 
+    private String getUri(String path) {
+        try {
+            return new URI("vos", authority, path, null, null).toASCIIString();
+        } catch (URISyntaxException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
     private String getCommonParent(List<String> vosPaths) {
         String commonParent = null;
         for (String vosPath : vosPaths) {
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java
index 7591f79..a2f20c6 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java
@@ -15,6 +15,7 @@ public class ShareRequest {
     private List<String> userRead;
     private List<String> userWrite;
     private boolean recursive;
+    private List<String> newPeople;
 
     public String getPath() {
         return path;
@@ -63,4 +64,12 @@ public class ShareRequest {
     public void setRecursive(boolean recursive) {
         this.recursive = recursive;
     }
+
+    public List<String> getNewPeople() {
+        return newPeople;
+    }
+
+    public void setNewPeople(List<String> newPeople) {
+        this.newPeople = newPeople;
+    }
 }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java
index 4a6ffd2..fe00b1e 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java
@@ -173,6 +173,10 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator {
     }
 
     private boolean isDownloadable(NodeInfo nodeInfo, User user) {
+        if (nodeInfo.isBrokenLink()) {
+            return false;
+        }
+
         if (nodeInfo.isFile()) {
             if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) {
                 return false;
@@ -186,11 +190,16 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator {
             return true;
         }
 
-        if (user.getGroups() != null && !user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) {
-            List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" "));
-            for (String group : groupRead) {
-                if (user.getGroups().contains(group)) {
-                    return true;
+        if (user.getGroups() != null) {
+            if (user.getGroups().contains("VOSpace.ADMIN")) {
+                return true;
+            }
+            if (!user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) {
+                List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" "));
+                for (String group : groupRead) {
+                    if (user.getGroups().contains(group)) {
+                        return true;
+                    }
                 }
             }
         }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java
index cb7cc33..b47ef7f 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java
@@ -30,6 +30,7 @@ public class NodeInfo {
     private final String path;
     private final String name;
     private final String size;
+    private boolean brokenLink;
     private String type;
     private final String creator;
     private final String groupRead;
@@ -63,7 +64,11 @@ public class NodeInfo {
             this.type = linkedNode.getType();
         } else if (node instanceof LinkNode) {
             this.target = ((LinkNode) node).getTarget();
-            this.type = "vos:DataNode"; // data link
+            if (this.target.startsWith("vos://" + authority)) {
+                this.brokenLink = true;
+            } else {
+                this.type = "vos:DataNode"; // external data link
+            }
         }
     }
 
@@ -215,4 +220,8 @@ public class NodeInfo {
     public boolean isLink() {
         return target != null;
     }
+
+    public boolean isBrokenLink() {
+        return brokenLink;
+    }
 }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java
index 8ba5b50..9f3daa4 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java
@@ -74,6 +74,8 @@ public abstract class NodesHtmlGenerator {
         if (nodeInfo.isLink()) {
             if (nodeInfo.isFolder()) {
                 icon.addClass("folder-link-icon");
+            } else if(nodeInfo.isBrokenLink()) {
+                icon.addClass("question-icon");
             } else {
                 icon.addClass("link-icon");
             }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java
index aa6759a..0bfedd6 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java
@@ -16,18 +16,26 @@ import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.data.ShareRequest;
 import it.inaf.ia2.vospace.ui.data.SharingInfo;
 import it.inaf.ia2.vospace.ui.exception.BadRequestException;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
 import it.inaf.oats.vospace.datamodel.NodeProperties;
+import it.inaf.oats.vospace.datamodel.NodeUtils;
+import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
-import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.ContainerNode;
+import net.ivoa.xml.vospace.v2.LinkNode;
 import net.ivoa.xml.vospace.v2.Node;
 import net.ivoa.xml.vospace.v2.Property;
 import org.slf4j.Logger;
@@ -41,9 +49,14 @@ public class SharingService {
 
     private static final Logger LOG = LoggerFactory.getLogger(SharingService.class);
 
+    private static final String SHARED_FILES_DIR_NAME = "Shared Files";
+
     @Value("${trusted.eppn.scope}")
     protected String trustedEppnScope;
 
+    @Value("${vospace-authority}")
+    protected String authority;
+
     private final Set<String> existingGroups = new CopyOnWriteArraySet<>();
     private final Set<String> existingPeopleGroups = new CopyOnWriteArraySet<>();
     private final Map<String, String> existingUsers = new ConcurrentHashMap<>();
@@ -55,9 +68,6 @@ public class SharingService {
     private TokenContext tokenContext;
     private Date lastUpdate;
 
-    @Autowired
-    private HttpServletRequest request;
-
     @Autowired
     public SharingService(GmsClient gmsClient, ClientCredentialsRapClient gmsRapClient, VOSpaceClient vospaceClient) {
         this.gmsClient = gmsClient;
@@ -100,6 +110,80 @@ public class SharingService {
         getNodeProperty(node, NodeProperties.GROUP_WRITE_URI).setValue(groupWrite);
 
         vospaceClient.setNode(node, shareRequest.isRecursive());
+
+        if (shareRequest.getNewPeople() != null && !shareRequest.getNewPeople().isEmpty()) {
+            createSharedLinks(shareRequest);
+        }
+    }
+
+    private void createSharedLinks(ShareRequest shareRequest) {
+
+        for (String person : shareRequest.getNewPeople()) {
+
+            loadAccessToken();
+
+            if (nodeExists("/" + person)) {
+
+                createSharedDir(person);
+
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH.mm.ss");
+                String linkNodeName = NodeUtils.getNodeName(shareRequest.getPath()) + " (" + sdf.format(new Date()) + ")";
+
+                LinkNode sharedLink = new LinkNode();
+                sharedLink.setUri("vos://" + authority + urlEncodePath("/" + person + "/" + SHARED_FILES_DIR_NAME + "/" + linkNodeName));
+
+                try {
+                    URI taregtUri = new URI("vos", authority, shareRequest.getPath(), null, null);
+                    sharedLink.setTarget(taregtUri.toASCIIString());
+                } catch (URISyntaxException ex) {
+                    throw new RuntimeException(ex);
+                }
+
+                addPersonGroups(sharedLink, person);
+
+                vospaceClient.createNode(sharedLink, Optional.of(tokenContext.getAccessToken()));
+            }
+        }
+    }
+
+    private boolean nodeExists(String path) {
+        try {
+            vospaceClient.getNode(path, Optional.of(tokenContext.getAccessToken()));
+            return true;
+        } catch (VOSpaceStatusException ex) {
+            if (ex.getHttpStatus() == 404) {
+                return false;
+            } else {
+                throw ex;
+            }
+        }
+    }
+
+    private void createSharedDir(String person) {
+        ContainerNode sharedFilesDir = new ContainerNode();
+        sharedFilesDir.setUri("vos://" + authority + urlEncodePath("/" + person + "/" + SHARED_FILES_DIR_NAME));
+        addPersonGroups(sharedFilesDir, person);
+
+        try {
+            vospaceClient.createNode(sharedFilesDir, Optional.of(tokenContext.getAccessToken()));
+        } catch (VOSpaceStatusException ex) {
+            if (ex.getHttpStatus() != 200 && ex.getHttpStatus() != 409) { // created or already existing
+                throw ex;
+            }
+        }
+    }
+
+    private void addPersonGroups(Node node, String person) {
+
+        Property groupReadProp = new Property();
+        groupReadProp.setUri(NodeProperties.GROUP_READ_URI);
+        groupReadProp.setValue("people." + person.replace(".", "\\."));
+        node.getProperties().add(groupReadProp);
+
+        Property groupWriteProp = new Property();
+        groupWriteProp.setUri(NodeProperties.GROUP_WRITE_URI);
+        groupWriteProp.setValue("people." + person.replace(".", "\\."));
+        node.getProperties().add(groupWriteProp);
     }
 
     private Property getNodeProperty(Node node, String uri) {
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java
index bcf643d..8998550 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java
@@ -57,7 +57,6 @@ public class VOSpaceClientTest {
         try ( MockedStatic<HttpClient> staticMock = Mockito.mockStatic(HttpClient.class)) {
             staticMock.when(HttpClient::newBuilder).thenReturn(builder);
             voSpaceClient = new VOSpaceClient("http://localhost/vospace");
-            ReflectionTestUtils.setField(voSpaceClient, "authority", "ia2.inaf.it!vospace");
         }
 
         voSpaceClient.servletRequest = mock(HttpServletRequest.class);
@@ -101,7 +100,7 @@ public class VOSpaceClientTest {
         try {
             voSpaceClient.createNode(newNode);
             fail("Exception was expected");
-        } catch (IllegalArgumentException ex) {
+        } catch (RuntimeException ex) {
             assertTrue(ex.getCause() instanceof URISyntaxException);
         }
     }
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java
index b9d22fa..9738399 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java
@@ -49,6 +49,11 @@ public class NodesHtmlGeneratorTest {
         setGroups(link3, "group1", "group2 people.name\\.surname");
         parent.getNodes().add(link3);
 
+        LinkNode brokenLink = new LinkNode();
+        brokenLink.setUri("vos://example.com!vospace/mynode/broken-link");
+        brokenLink.setTarget("vos://example.com!vospace/mynode/not-found");
+        parent.getNodes().add(brokenLink);
+
         List<Node> linkedNodes = List.of(parent, file1);
 
         User user = new User();
@@ -66,6 +71,31 @@ public class NodesHtmlGeneratorTest {
         assertTrue(html.contains("<span class=\"icon folder-link-icon\"></span>&nbsp;<a href=\"#/nodes/mynode\">link1</a>"));
         assertTrue(html.contains("<span class=\"icon link-icon\"></span>&nbsp;<a target=\"blank_\" href=\"download/mynode/link2\">link2</a>"));
         assertTrue(html.contains("<span class=\"icon link-icon\"></span>&nbsp;<a target=\"blank_\" href=\"download/mynode/link3\">link3</a>"));
+        assertTrue(html.contains("<span class=\"icon question-icon\"></span>&nbsp;broken-link"));
+    }
+
+    @Test
+    public void testAdminDownloadable() {
+
+        User user = new User();
+        user.setUserId("user_id");
+        user.setGroups(Arrays.asList("VOSpace.ADMIN"));
+
+        ContainerNode parent = new ContainerNode();
+        parent.setUri("vos://example.com!vospace/mynode");
+        setGroups(parent, "group1", "group1");
+
+        DataNode file1 = new DataNode();
+        file1.setUri("vos://example.com!vospace/mynode/file1");
+        parent.getNodes().add(file1);
+
+        MainNodesHtmlGenerator generator = new MainNodesHtmlGenerator(parent, user, "example.com!vospace", List.of());
+
+        String html = generator.generateNodes();
+
+        System.out.println(html);
+
+        assertTrue(html.contains("<input type=\"checkbox\" data-node=\"/mynode/file1\" class=\"deletable\">"));
     }
 
     private void setGroups(Node node, String groupRead, String groupWrite) {
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java
index 3f2767d..df918a0 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java
@@ -14,12 +14,16 @@ import it.inaf.ia2.rap.data.RapUser;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.data.ShareRequest;
 import it.inaf.ia2.vospace.ui.data.SharingInfo;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
 import it.inaf.oats.vospace.datamodel.NodeProperties;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.DataNode;
+import net.ivoa.xml.vospace.v2.LinkNode;
 import net.ivoa.xml.vospace.v2.Property;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -33,6 +37,8 @@ import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -59,8 +65,12 @@ public class SharingServiceTest {
     @BeforeEach
     public void setUp() {
         sharingService.trustedEppnScope = "inaf.it";
+        sharingService.authority = "example.com!vospace";
 
-        when(rapClient.getAccessTokenFromClientCredentials()).thenReturn(new AccessTokenResponse());
+        AccessTokenResponse tokenResponse = new AccessTokenResponse();
+        tokenResponse.setAccessToken("<admin-token>");
+
+        when(rapClient.getAccessTokenFromClientCredentials()).thenReturn(tokenResponse);
 
         when(rapClient.getUsers(anyString(), any())).thenReturn(getRapUsers());
 
@@ -123,6 +133,64 @@ public class SharingServiceTest {
         }), anyBoolean());
     }
 
+    @Test
+    public void testCreateSharedFolder() {
+
+        DataNode node = new DataNode();
+        node.setUri("vos://example.com!vospace/anna.bianchi/mynode");
+
+        when(vospaceClient.getNode(any())).thenReturn(node);
+
+        ShareRequest shareRequest = new ShareRequest();
+        shareRequest.setPath("/anna.bianchi/mynode");
+        shareRequest.setUserRead(List.of("mario.rossi", "paolo.gialli", "bianca.verdi"));
+        shareRequest.setUserWrite(List.of());
+        shareRequest.setGroupRead(List.of());
+        shareRequest.setGroupWrite(List.of());
+        shareRequest.setNewPeople(List.of("mario.rossi", "paolo.gialli", "bianca.verdi"));
+
+        ContainerNode marioHome = new ContainerNode();
+        marioHome.setUri("vos://example.com!vospace/mario.rossi");
+        ContainerNode biancaHome = new ContainerNode();
+        biancaHome.setUri("vos://example.com!vospace/bianca.verdi");
+
+        // Mario and Bianca have their home, Paolo not
+        when(vospaceClient.getNode(eq("/mario.rossi"), adminToken())).thenReturn(marioHome);
+        when(vospaceClient.getNode(eq("/bianca.verdi"), adminToken())).thenReturn(biancaHome);
+        doThrow(new VOSpaceStatusException("Not found", 404)).when(vospaceClient).getNode(eq("/paolo.gialli"), any());
+
+        // Mario's Shared Files folder has to be created
+        ContainerNode marioShared = new ContainerNode();
+        marioShared.setUri("vos://example.com!vospace/mario.rossi/Shared%20Files");
+        when(vospaceClient.createNode(argThat(n -> n.getUri().equals(marioShared.getUri())), adminToken())).thenReturn(marioShared);
+
+        // Bianca's Shared Files folder already exists
+        doThrow(new VOSpaceStatusException("Conflict", 409)).when(vospaceClient).createNode(
+                argThat(n -> n.getUri().equals("vos://example.com!vospace/bianca.verdi/Shared%20Files")), any());
+
+        doAnswer(inv -> inv.getArgument(0)).when(vospaceClient).createNode(argThat(n -> {
+            return n instanceof LinkNode
+                    && ((LinkNode) n).getTarget().equals("vos://example.com!vospace/anna.bianchi/mynode");
+        }), adminToken());
+
+        // Share node
+        sharingService.setNodeGroups(shareRequest);
+
+        // Check Mario's shared link
+        verify(vospaceClient, times(1)).createNode(argThat(n -> {
+            return n.getUri().startsWith("vos://example.com!vospace/mario.rossi/Shared%20Files/mynode");
+        }), adminToken());
+
+        // Check Bianca's shared link
+        verify(vospaceClient, times(1)).createNode(argThat(n -> {
+            return n.getUri().startsWith("vos://example.com!vospace/bianca.verdi/Shared%20Files/mynode");
+        }), adminToken());
+    }
+
+    private Optional<String> adminToken() {
+        return argThat(op -> ((Optional) op).isPresent() && "<admin-token>".equals(((Optional) op).get()));
+    }
+
     private List<RapUser> getRapUsers() {
         List<RapUser> users = new ArrayList<>();
 
diff --git a/vospace-ui-frontend/src/assets/css/fonts.css b/vospace-ui-frontend/src/assets/css/fonts.css
index e5de317..a03bb16 100644
--- a/vospace-ui-frontend/src/assets/css/fonts.css
+++ b/vospace-ui-frontend/src/assets/css/fonts.css
@@ -52,3 +52,7 @@
 .link-icon {
   background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='link45deg' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-link45deg mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M4.715 6.542L3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.001 1.001 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z'%3E%3C/path%3E%3Cpath d='M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 0 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 0 0-4.243-4.243L6.586 4.672z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
 }
+
+.question-icon {
+  background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='question circle' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-question-circle mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'%3E%3C/path%3E%3Cpath d='M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
+}
diff --git a/vospace-ui-frontend/src/components/modal/ShareModal.vue b/vospace-ui-frontend/src/components/modal/ShareModal.vue
index 1e4eec5..7fa3f23 100644
--- a/vospace-ui-frontend/src/components/modal/ShareModal.vue
+++ b/vospace-ui-frontend/src/components/modal/ShareModal.vue
@@ -39,6 +39,7 @@ export default {
       userRead: [],
       groupWrite: [],
       userWrite: [],
+      currentPeople: [],
       people: [],
       groups: [],
       recursive: true
@@ -48,6 +49,11 @@ export default {
     onShow() {
       this.setGroups(this.userRead, this.groupRead, this.node.groupRead);
       this.setGroups(this.userWrite, this.groupWrite, this.node.groupWrite);
+
+      this.currentPeople.splice(0, this.currentPeople.length);
+      this.addCurrentPeople(this.currentPeople, this.userRead);
+      this.addCurrentPeople(this.currentPeople, this.userWrite);
+
       client.getSharingInfo()
         .then(res => {
           setArray(this.people, res.people);
@@ -65,6 +71,13 @@ export default {
         }
       }
     },
+    addCurrentPeople(targetArr, sourceArr) {
+      for (let person of sourceArr) {
+        if (!targetArr.includes(person)) {
+          targetArr.push(person);
+        }
+      }
+    },
     share() {
       client.setNodeGroups({
         path: this.$store.state.nodeToShare.path,
@@ -72,12 +85,19 @@ export default {
         groupWrite: this.groupWrite,
         userRead: this.userRead,
         userWrite: this.userWrite,
-        recursive: this.recursive
+        recursive: this.recursive,
+        newPeople: this.getNewPeople()
       }).then(() => {
         // Reload current path
         this.$bvModal.hide('share-modal');
         this.$store.dispatch('setPath', this.$store.state.path);
       });
+    },
+    getNewPeople() {
+      let currentPeople = [];
+      this.addCurrentPeople(currentPeople, this.userRead);
+      this.addCurrentPeople(currentPeople, this.userWrite);
+      return currentPeople.filter(p => !this.currentPeople.includes(p));
     }
   }
 }
-- 
GitLab