Skip to content
Snippets Groups Projects
Commit a4fd95a8 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Handled permissions on UI

parent 93f1d1f0
No related branches found
No related tags found
No related merge requests found
Pipeline #963 passed
Showing
with 222 additions and 131 deletions
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;
}
}
}
......@@ -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();
......
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/**"})
......
......@@ -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());
}
......
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;
}
}
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) {
......@@ -47,22 +57,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) {
for (Property property : node.getProperties()) {
......@@ -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;
}
}
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()) {
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,17 +70,24 @@ 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.isFile() && nodeInfo.isBusy()) {
html += "gear";
} else {
if (nodeInfo.isFolder()) {
html += "folder";
} else {
......@@ -78,12 +96,13 @@ public class NodesService {
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()) {
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;
}
}
......@@ -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;
......@@ -28,7 +29,7 @@ public class NodesControllerTest {
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());
}
}
<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>
{
"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>"
}
<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>
{
"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>"
}
<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>
{
"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>"
}
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';
......
......@@ -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() {
......
......@@ -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() {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment