From a4fd95a8046f4a86f0f716bab42d1871f4c1c1d6 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Wed, 3 Feb 2021 16:17:14 +0100
Subject: [PATCH] Handled permissions on UI

---
 .../it/inaf/ia2/vospace/ui/UserFilter.java    | 48 ++++++++++++
 .../ia2/vospace/ui/VOSpaceUiApplication.java  |  8 ++
 .../ui/controller/NodesController.java        | 16 ++--
 .../java/it/inaf/ia2/vospace/ui/data/Job.java |  3 +
 .../ia2/vospace/ui/data/ListNodeData.java     | 23 ++++++
 .../inaf/ia2/vospace/ui/service/NodeInfo.java | 51 ++++++++++++-
 .../ia2/vospace/ui/service/NodesService.java  | 76 ++++++++++++++-----
 .../ui/controller/NodesControllerTest.java    |  9 ++-
 .../src/api/mock/data/nodes/folder1.html      | 35 ---------
 .../src/api/mock/data/nodes/folder1.json      |  4 +
 .../src/api/mock/data/nodes/folder2.html      | 24 ------
 .../src/api/mock/data/nodes/folder2.json      |  4 +
 .../src/api/mock/data/nodes/root.html         | 26 -------
 .../src/api/mock/data/nodes/root.json         |  4 +
 vospace-ui-frontend/src/api/mock/index.js     |  6 +-
 vospace-ui-frontend/src/components/Main.vue   |  7 +-
 vospace-ui-frontend/src/store.js              |  9 ++-
 17 files changed, 222 insertions(+), 131 deletions(-)
 create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/UserFilter.java
 create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java
 delete mode 100644 vospace-ui-frontend/src/api/mock/data/nodes/folder1.html
 create mode 100644 vospace-ui-frontend/src/api/mock/data/nodes/folder1.json
 delete mode 100644 vospace-ui-frontend/src/api/mock/data/nodes/folder2.html
 create mode 100644 vospace-ui-frontend/src/api/mock/data/nodes/folder2.json
 delete mode 100644 vospace-ui-frontend/src/api/mock/data/nodes/root.html
 create mode 100644 vospace-ui-frontend/src/api/mock/data/nodes/root.json

diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/UserFilter.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/UserFilter.java
new file mode 100644
index 0000000..40e7293
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/UserFilter.java
@@ -0,0 +1,48 @@
+package it.inaf.ia2.vospace.ui;
+
+import it.inaf.ia2.aa.data.User;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.ArrayList;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpSession;
+
+/**
+ * Extracts user from the session and set it as request Principal.
+ */
+public class UserFilter implements Filter {
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) throws IOException, ServletException {
+        HttpServletRequestWrapper requestWithPrincipal = new RequestWithPrincipal((HttpServletRequest) req);
+        fc.doFilter(requestWithPrincipal, res);
+    }
+
+    private static class RequestWithPrincipal extends HttpServletRequestWrapper {
+
+        private final User user;
+
+        public RequestWithPrincipal(HttpServletRequest request) {
+            super(request);
+            HttpSession session = request.getSession(false);
+            if (session == null || session.getAttribute("user_data") == null) {
+                this.user = new User()
+                        .setUserId("anonymous").setUserLabel("Anonymous")
+                        .setGroups(new ArrayList<>());
+            } else {
+                this.user = (User) session.getAttribute("user_data");
+            }
+        }
+
+        @Override
+        public Principal getUserPrincipal() {
+            return user;
+        }
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
index a015518..8d16a8a 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
@@ -35,6 +35,14 @@ public class VOSpaceUiApplication {
         return registration;
     }
 
+    @Bean
+    public FilterRegistrationBean userFilterRegistration() {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new UserFilter());
+        registration.addUrlPatterns("/*");
+        return registration;
+    }
+
     @Bean
     public UserManager userManager() {
         return ServiceLocator.getInstance().getUserManager();
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 73f1299..4e12e12 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
@@ -1,6 +1,8 @@
 package it.inaf.ia2.vospace.ui.controller;
 
+import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import it.inaf.ia2.vospace.ui.data.ListNodeData;
 import it.inaf.ia2.vospace.ui.service.NodesService;
 import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
@@ -35,20 +37,12 @@ public class NodesController extends BaseController {
     @Autowired
     private HttpServletRequest servletRequest;
 
-    /**
-     * This is the only API endpoint that returns HTML code instead of JSON. The
-     * reason is that JavaScript frameworks are not very efficient in handling
-     * very long lists and tables, so this part of the code is generated
-     * server-side. The content type is set to text/plain even if it is an HTML
-     * fragment to avoid browser parsing issues since it is not a complete HTML
-     * document.
-     */
-    @GetMapping(value = {"/nodes", "/nodes/**"}, produces = MediaType.TEXT_PLAIN_VALUE)
-    public String listNodes() throws Exception {
+    @GetMapping(value = {"/nodes", "/nodes/**"}, produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<ListNodeData> listNodes(User principal) throws Exception {
 
         String path = getPath("/nodes/");
 
-        return nodesService.generateNodesHtml(path);
+        return ResponseEntity.ok(nodesService.generateNodesHtml(path, principal));
     }
 
     @DeleteMapping(value = {"/nodes", "/nodes/**"})
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java
index 180fc32..6515a50 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java
@@ -29,6 +29,9 @@ public class Job {
     }
 
     private String formatCreationTime(XMLGregorianCalendar calendar) {
+        if (calendar == null) {
+            return null;
+        }
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         return sdf.format(calendar.toGregorianCalendar().getTime());
     }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java
new file mode 100644
index 0000000..2692ef4
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java
@@ -0,0 +1,23 @@
+package it.inaf.ia2.vospace.ui.data;
+
+public class ListNodeData {
+
+    private String htmlTable;
+    private boolean writable;
+
+    public String getHtmlTable() {
+        return htmlTable;
+    }
+
+    public void setHtmlTable(String htmlTable) {
+        this.htmlTable = htmlTable;
+    }
+
+    public boolean isWritable() {
+        return writable;
+    }
+
+    public void setWritable(boolean writable) {
+        this.writable = writable;
+    }
+}
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 82d66fd..987886a 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
@@ -1,9 +1,13 @@
 package it.inaf.ia2.vospace.ui.service;
 
 import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
+import it.inaf.oats.vospace.datamodel.NodeProperties;
+import java.util.List;
 import java.util.Optional;
+import net.ivoa.xml.vospace.v2.DataNode;
 import net.ivoa.xml.vospace.v2.Node;
 import net.ivoa.xml.vospace.v2.Property;
+import net.ivoa.xml.vospace.v2.View;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -17,10 +21,13 @@ public class NodeInfo {
     private final String name;
     private final String size;
     private final String type;
+    private final String creator;
     private final String groupRead;
     private final String groupWrite;
     private final boolean isPublic;
     private final boolean asyncTrans;
+    private final boolean busy;
+    private final boolean listOfFiles;
 
     public NodeInfo(Node node, String authority) {
         this.authority = authority;
@@ -28,10 +35,13 @@ public class NodeInfo {
         this.name = path.substring(path.lastIndexOf("/") + 1);
         this.size = getSize(node);
         this.type = node.getType();
+        this.creator = getCreator(node);
         this.groupRead = getGroupRead(node);
         this.groupWrite = getGroupWrite(node);
         this.isPublic = isPublic(node);
         this.asyncTrans = isAsyncTrans(node);
+        this.busy = isBusy(node);
+        this.listOfFiles = isListOfFiles(node);
     }
 
     private String getPath(Node node) {
@@ -46,22 +56,45 @@ public class NodeInfo {
 
         return uri.substring(prefix.length());
     }
+    
+    private String getCreator(Node node) {
+        return getProperty(node, NodeProperties.CREATOR_URI).orElse("");
+    }
 
     private String getGroupRead(Node node) {
-        return getProperty(node, "ivo://ivoa.net/vospace/core#groupread").orElse("");
+        return getProperty(node, NodeProperties.GROUP_READ_URI).orElse("");
     }
 
     private String getGroupWrite(Node node) {
-        return getProperty(node, "ivo://ivoa.net/vospace/core#groupwrite").orElse("");
+        return getProperty(node, NodeProperties.GROUP_WRITE_URI).orElse("");
     }
 
     private boolean isPublic(Node node) {
-        return getProperty(node, "ivo://ivoa.net/vospace/core#ispublic").map(value -> "t".equals(value)).orElse(false);
+        return getProperty(node, NodeProperties.PUBLIC_READ_URI).map(value -> "t".equals(value)).orElse(false);
     }
 
     private boolean isAsyncTrans(Node node) {
         return getProperty(node, "urn:async_trans").map(value -> "t".equals(value)).orElse(false);
     }
+    
+    private boolean isBusy(Node node) {
+        return node instanceof DataNode && ((DataNode) node).isBusy();
+    }
+
+    private boolean isListOfFiles(Node node) {
+        if (node instanceof DataNode) {
+            DataNode dataNode = (DataNode) node;
+            List<View> provides = dataNode.getProvides();
+            if (provides != null) {
+                for (View provide : provides) {
+                    if ("urn:list-of-files".equals(provide.getUri())) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
 
     private Optional<String> getProperty(Node node, String uri) {
         if (node.getProperties() != null && node.getProperties() != null) {
@@ -122,6 +155,10 @@ public class NodeInfo {
         return size;
     }
 
+    public String getCreator() {
+        return creator;
+    }
+
     public String getGroupRead() {
         return groupRead;
     }
@@ -137,4 +174,12 @@ public class NodeInfo {
     public boolean isAsyncTrans() {
         return asyncTrans;
     }
+
+    public boolean isBusy() {
+        return busy;
+    }
+
+    public boolean isListOfFiles() {
+        return listOfFiles;
+    }
 }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
index 5d347db..4c3b4bf 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
@@ -1,9 +1,14 @@
 package it.inaf.ia2.vospace.ui.service;
 
+import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import it.inaf.ia2.vospace.ui.data.ListNodeData;
+import it.inaf.oats.vospace.datamodel.NodeUtils;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.List;
 import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.Node;
 import org.slf4j.Logger;
@@ -23,32 +28,38 @@ public class NodesService {
     @Value("${vospace-authority}")
     private String authority;
 
-    public String generateNodesHtml(String path) {
+    public ListNodeData generateNodesHtml(String path, User user) {
 
+        ListNodeData listNodeData = new ListNodeData();
+        
         Node node = client.getNode(path);
+        
+        listNodeData.setWritable(NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()));
 
-        try ( StringWriter sw = new StringWriter()) {
+        try (StringWriter sw = new StringWriter()) {
 
             if (node instanceof ContainerNode) {
                 ContainerNode folder = (ContainerNode) node;
                 sw.write("<tbody id=\"nodes\">");
                 for (Node child : folder.getNodes()) {
-                    sw.write(getNodeHtml(child));
+                    sw.write(getNodeHtml(child, user));
                 }
                 sw.write("</tbody>");
             }
 
-            return sw.toString();
+            listNodeData.setHtmlTable(sw.toString());
+            
+            return listNodeData;
         } catch (IOException ex) {
             throw new UncheckedIOException(ex);
         }
     }
 
-    private String getNodeHtml(Node node) {
+    private String getNodeHtml(Node node, User user) {
 
         NodeInfo nodeInfo = new NodeInfo(node, authority);
 
-        if (nodeInfo.getName().startsWith(".")) {
+        if (nodeInfo.isListOfFiles()) {
             // hidden file
             return "";
         }
@@ -59,31 +70,39 @@ public class NodesService {
             html += "class=\"async\"";
         }
         html += "/></td>";
-        html += "<td>" + getIcon(nodeInfo) + getLink(nodeInfo) + "</td>";
+        html += "<td>" + getIcon(nodeInfo) + getLink(nodeInfo, user) + "</td>";
         html += "<td>" + nodeInfo.getSize() + "</td>";
         html += "<td>" + nodeInfo.getGroupRead() + "</td>";
         html += "<td>" + nodeInfo.getGroupWrite() + "</td>";
-        html += "<td><span class=\"icon trash-icon pointer\" onclick=\"deleteNode('" + nodeInfo.getPath() + "')\"></span></td>";
+        html += "<td>";
+        if (NodeUtils.checkIfWritable(node, user.getName(), user.getGroups())) {
+            html += "<span class=\"icon trash-icon pointer\" onclick=\"deleteNode('" + nodeInfo.getPath() + "')\"></span>";
+        }
+        html += "</td>";
         html += "</tr>";
         return html;
     }
 
     private String getIcon(NodeInfo nodeInfo) {
         String html = "<span class=\"icon ";
-        if (nodeInfo.isFolder()) {
-            html += "folder";
+        if (nodeInfo.isFile() && nodeInfo.isBusy()) {
+            html += "gear";
         } else {
-            html += "file";
-        }
-        if (nodeInfo.isAsyncTrans()) {
-            html += "-x";
+            if (nodeInfo.isFolder()) {
+                html += "folder";
+            } else {
+                html += "file";
+            }
+            if (nodeInfo.isAsyncTrans()) {
+                html += "-x";
+            }
         }
         html += "-icon\"></span>&nbsp;";
         return html;
     }
 
-    private String getLink(NodeInfo nodeInfo) {
-        if (isDownloadable(nodeInfo)) {
+    private String getLink(NodeInfo nodeInfo, User user) {
+        if (isDownloadable(nodeInfo, user)) {
             if (nodeInfo.isFolder()) {
                 return "<a href=\"#/nodes" + nodeInfo.getPath() + "\">" + nodeInfo.getName() + "</a>";
             } else {
@@ -93,14 +112,29 @@ public class NodesService {
         return nodeInfo.getName();
     }
 
-    private boolean isDownloadable(NodeInfo nodeInfo) {
-        if (nodeInfo.isFile() && nodeInfo.isAsyncTrans()) {
-            return false;
+    private boolean isDownloadable(NodeInfo nodeInfo, User user) {
+        if (nodeInfo.isFile()) {
+            if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) {
+                return false;
+            }
         }
         if (nodeInfo.isPublic()) {
             return true;
         }
-        // TODO: check user group
-        return true; // temporary always true
+        
+        if (nodeInfo.getCreator().equals(user.getName())) {
+            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;
+                }
+            }
+        }
+
+        return false;
     }
 }
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
index d4291fb..085b3f2 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
@@ -2,6 +2,7 @@ package it.inaf.ia2.vospace.ui.controller;
 
 import it.inaf.ia2.vospace.ui.service.NodesService;
 import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,14 +22,14 @@ public class NodesControllerTest {
 
     @Autowired
     private MockMvc mockMvc;
-
+    
     @Test
     public void testListNodesEmpty() throws Exception {
 
         mockMvc.perform(get("/nodes"))
                 .andExpect(status().isOk());
 
-        verify(nodesService).generateNodesHtml(eq("/"));
+        verify(nodesService).generateNodesHtml(eq("/"), any());
     }
 
     @Test
@@ -37,7 +38,7 @@ public class NodesControllerTest {
         mockMvc.perform(get("/nodes/"))
                 .andExpect(status().isOk());
 
-        verify(nodesService).generateNodesHtml(eq("/"));
+        verify(nodesService).generateNodesHtml(eq("/"), any());
     }
 
     @Test
@@ -46,6 +47,6 @@ public class NodesControllerTest {
         mockMvc.perform(get("/nodes/a/b/c"))
                 .andExpect(status().isOk());
 
-        verify(nodesService).generateNodesHtml(eq("/a/b/c"));
+        verify(nodesService).generateNodesHtml(eq("/a/b/c"), any());
     }
 }
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html
deleted file mode 100644
index cacb3e1..0000000
--- a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<tbody id="nodes">
-  <tr>
-    <td><input type="checkbox" class="async" data-node="/folder1/folder2" /></td>
-    <td>
-      <span class="icon folder-x-icon"></span>
-      <a href="#/nodes/folder1/folder2">folder2</a>
-    </td>
-    <td>0 B</td>
-    <td>group1</td>
-    <td>group2</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td><input type="checkbox" data-node="/folder1/file2" /></td>
-    <td>
-      <span class="icon file-icon"></span>
-      <a href="download/file2">file2</a>
-    </td>
-    <td>30 KB</td>
-    <td>group1</td>
-    <td>group2</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td><input type="checkbox" class="async" data-node="/folder1/file3" /></td>
-    <td>
-      <span class="icon file-x-icon"></span>
-      file3
-    </td>
-    <td>12 MB</td>
-    <td>group3</td>
-    <td>group4</td>
-    <td></td>
-  </tr>
-</tbody>
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json
new file mode 100644
index 0000000..2527b19
--- /dev/null
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json
@@ -0,0 +1,4 @@
+{
+  "writable": true,
+  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2\" /></td>    <td>      <span class=\"icon folder-x-icon\"></span>      <a href=\"#/nodes/folder1/folder2\">folder2</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/folder1/file2\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file2\">file2</a>    </td>    <td>30 KB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/file3\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file3    </td>    <td>12 MB</td>    <td>group3</td>    <td>group4</td>    <td></td>  </tr></tbody>"
+}
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html
deleted file mode 100644
index 2c52dd9..0000000
--- a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<tbody id="nodes">
-  <tr>
-    <td><input type="checkbox" class="async" data-node="/folder1/folder2/file4" /></td>
-    <td>
-      <span class="icon file-x-icon"></span>
-      file4
-    </td>
-    <td>10 KB</td>
-    <td>group1</td>
-    <td>group2</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td><input type="checkbox" class="async" data-node="/folder1/folder2/file5" /></td>
-    <td>
-      <span class="icon file-x-icon"></span>
-      file5
-    </td>
-    <td>15 MB</td>
-    <td>group3</td>
-    <td>group4</td>
-    <td></td>
-  </tr>
-</tbody>
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json
new file mode 100644
index 0000000..be52e82
--- /dev/null
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json
@@ -0,0 +1,4 @@
+{
+  "writable": false,
+  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2/file4\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file4    </td>    <td>10 KB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2/file5\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file5    </td>    <td>15 MB</td>    <td>group3</td>    <td>group4</td>    <td></td>  </tr></tbody>"
+}
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/root.html b/vospace-ui-frontend/src/api/mock/data/nodes/root.html
deleted file mode 100644
index d6143ad..0000000
--- a/vospace-ui-frontend/src/api/mock/data/nodes/root.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<tbody id="nodes">
-  <tr>
-    <td><input type="checkbox" data-node="/folder1" /></td>
-    <td>
-      <span class="icon folder-icon"></span>
-      <a href="#/nodes/folder1">folder1</a>
-    </td>
-    <td>0 B</td>
-    <td>group1</td>
-    <td>group2</td>
-    <td>
-      <span class="icon trash-icon pointer" onclick="deleteNode('/folder1')"></span>
-    </td>
-  </tr>
-  <tr>
-    <td><input type="checkbox" data-node="/file1" /></td>
-    <td>
-      <span class="icon file-icon"></span>
-      <a href="download/file1">file1</a>
-    </td>
-    <td>12 MB</td>
-    <td>group1</td>
-    <td>group2</td>
-    <td></td>
-  </tr>
-</tbody>
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/root.json b/vospace-ui-frontend/src/api/mock/data/nodes/root.json
new file mode 100644
index 0000000..dcd4c04
--- /dev/null
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/root.json
@@ -0,0 +1,4 @@
+{
+  "writable": true,
+  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td>      <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
+}
diff --git a/vospace-ui-frontend/src/api/mock/index.js b/vospace-ui-frontend/src/api/mock/index.js
index 9de8cec..9924ec9 100644
--- a/vospace-ui-frontend/src/api/mock/index.js
+++ b/vospace-ui-frontend/src/api/mock/index.js
@@ -1,6 +1,6 @@
-import root from 'raw-loader!./data/nodes/root.html';
-import folder1 from 'raw-loader!./data/nodes/folder1.html';
-import folder2 from 'raw-loader!./data/nodes/folder2.html';
+import root from './data/nodes/root';
+import folder1 from './data/nodes/folder1';
+import folder2 from './data/nodes/folder2';
 import job from './data/job';
 import jobs from './data/jobs';
 import user from './data/user';
diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue
index c1ce55f..e9147d7 100644
--- a/vospace-ui-frontend/src/components/Main.vue
+++ b/vospace-ui-frontend/src/components/Main.vue
@@ -2,8 +2,8 @@
 <div class="container">
   <b-breadcrumb :items="breadcrumbs"></b-breadcrumb>
   <div class="mb-3">
-    <b-button variant="success" class="mr-2" :disabled="false" v-b-modal.create-folder-modal>New folder</b-button>
-    <b-button variant="success" class="mr-2" :disabled="false" v-b-modal.upload-files-modal>Upload files</b-button>
+    <b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.create-folder-modal>New folder</b-button>
+    <b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.upload-files-modal>Upload files</b-button>
     <b-button variant="primary" class="mr-2" v-if="asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-button>
   </div>
   <b-card>
@@ -68,6 +68,9 @@ export default {
     },
     asyncButtonEnabled() {
       return this.$store.state.asyncButtonEnabled;
+    },
+    writable() {
+      return this.$store.state.writable;
     }
   },
   created() {
diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js
index 628d91c..8cd9bd8 100644
--- a/vospace-ui-frontend/src/store.js
+++ b/vospace-ui-frontend/src/store.js
@@ -14,7 +14,8 @@ export default new Vuex.Store({
     asyncButtonEnabled: false,
     jobs: [],
     user: 'anonymous',
-    nodeToDelete: null
+    nodeToDelete: null,
+    writable: false
   },
   mutations: {
     setLoading(state, loading) {
@@ -45,6 +46,9 @@ export default new Vuex.Store({
     },
     setNodeToDelete(state, path) {
       state.nodeToDelete = path;
+    },
+    setWritable(state, value) {
+      state.writable = value;
     }
   },
   actions: {
@@ -52,7 +56,8 @@ export default new Vuex.Store({
       commit('setPath', path);
       client.getNode(state.path)
         .then(res => {
-          document.getElementById('nodes').outerHTML = res;
+          commit('setWritable', res.writable);
+          document.getElementById('nodes').outerHTML = res.htmlTable;
           let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]');
           for (let i = 0; i < checkboxes.length; i++) {
             checkboxes[i].addEventListener('change', function() {
-- 
GitLab