From 82262c2715873733df90e3ade432d9c81c1df2c5 Mon Sep 17 00:00:00 2001 From: Sonia Zorba Date: Wed, 17 Mar 2021 18:55:10 +0100 Subject: [PATCH] Implemented logic for retrieving list of groups and users from GMS and RAP and updating the groupRead and groupWrite properties creating the single-user groups when needed --- vospace-ui-backend/pom.xml | 5 + .../ia2/vospace/ui/VOSpaceUiApplication.java | 20 ++ .../ia2/vospace/ui/client/VOSpaceClient.java | 13 ++ .../ui/controller/SharingController.java | 31 +++ .../ia2/vospace/ui/data/ShareRequest.java | 52 +++++ .../inaf/ia2/vospace/ui/data/SharingInfo.java | 25 +++ .../vospace/ui/service/SharingService.java | 204 ++++++++++++++++++ .../src/main/resources/application.properties | 2 + .../ui/controller/SharingControllerTest.java | 37 ++++ .../ui/service/SharingServiceTest.java | 145 +++++++++++++ 10 files changed, 534 insertions(+) create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/SharingController.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/SharingInfo.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java create mode 100644 vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/SharingControllerTest.java create mode 100644 vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java diff --git a/vospace-ui-backend/pom.xml b/vospace-ui-backend/pom.xml index fcbf7be..04dab28 100644 --- a/vospace-ui-backend/pom.xml +++ b/vospace-ui-backend/pom.xml @@ -55,6 +55,11 @@ mockito-inline test + + ${project.groupId} + gms-client + 1.0-SNAPSHOT + 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 8d16a8a..80129a3 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 @@ -1,8 +1,11 @@ package it.inaf.ia2.vospace.ui; +import it.inaf.ia2.aa.AuthConfig; import it.inaf.ia2.aa.LoginFilter; import it.inaf.ia2.aa.ServiceLocator; import it.inaf.ia2.aa.UserManager; +import it.inaf.ia2.gms.client.GmsClient; +import it.inaf.ia2.rap.client.ClientCredentialsRapClient; import java.util.concurrent.ForkJoinPool; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -61,4 +64,21 @@ public class VOSpaceUiApplication { public RestTemplate restTemplate() { return new RestTemplate(); } + + @Bean + public ClientCredentialsRapClient gmsRapClient() { + + AuthConfig config = ServiceLocator.getInstance().getConfig(); + + ClientCredentialsRapClient rapClient = new ClientCredentialsRapClient(config.getRapBaseUri()); + rapClient.setClientId(config.getClientId()); + rapClient.setClientSecret(config.getClientSecret()); + return rapClient; + } + + @Bean + public GmsClient gmsClient() { + String gmsBaseUri = ServiceLocator.getInstance().getConfig().getGmsUri(); + return new GmsClient(gmsBaseUri); + } } 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 a67829d..e023156 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 @@ -126,6 +126,19 @@ public class VOSpaceClient { call(request, BodyHandlers.ofInputStream(), 200, res -> null); } + public Node setNode(Node node) { + + String path = node.getUri().substring(("vos://" + authority).length()); + + HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) + .header("Accept", useJson ? "application/json" : "text/xml") + .header("Content-Type", useJson ? "application/json" : "text/xml") + .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) + .build(); + + return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); + } + public List getJobs() { HttpRequest request = getRequest("/transfers?direction=pullToVoSpace") diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/SharingController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/SharingController.java new file mode 100644 index 0000000..2284e52 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/SharingController.java @@ -0,0 +1,31 @@ +package it.inaf.ia2.vospace.ui.controller; + +import it.inaf.ia2.vospace.ui.data.ShareRequest; +import it.inaf.ia2.vospace.ui.data.SharingInfo; +import it.inaf.ia2.vospace.ui.service.SharingService; +import javax.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SharingController { + + @Autowired + private SharingService sharingService; + + @GetMapping(value = "/sharing", produces = MediaType.APPLICATION_JSON_VALUE) + public SharingInfo getSharingInfo() { + return sharingService.getSharingInfo(); + } + + @PostMapping(value = "/sharing", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity setNodeGroups(@Valid @RequestBody ShareRequest shareRequest) { + sharingService.setNodeGroups(shareRequest); + return ResponseEntity.ok().build(); + } +} 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 new file mode 100644 index 0000000..360a3f1 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java @@ -0,0 +1,52 @@ +package it.inaf.ia2.vospace.ui.data; + +import java.util.List; + +public class ShareRequest { + + private String path; + private List groupRead; + private List groupWrite; + private List userRead; + private List userWrite; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getGroupRead() { + return groupRead; + } + + public void setGroupRead(List groupRead) { + this.groupRead = groupRead; + } + + public List getGroupWrite() { + return groupWrite; + } + + public void setGroupWrite(List groupWrite) { + this.groupWrite = groupWrite; + } + + public List getUserRead() { + return userRead; + } + + public void setUserRead(List userRead) { + this.userRead = userRead; + } + + public List getUserWrite() { + return userWrite; + } + + public void setUserWrite(List userWrite) { + this.userWrite = userWrite; + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/SharingInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/SharingInfo.java new file mode 100644 index 0000000..7805f01 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/SharingInfo.java @@ -0,0 +1,25 @@ +package it.inaf.ia2.vospace.ui.data; + +import java.util.List; + +public class SharingInfo { + + private List people; + private List groups; + + public List getPeople() { + return people; + } + + public void setPeople(List people) { + this.people = people; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } +} 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 new file mode 100644 index 0000000..535ef43 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java @@ -0,0 +1,204 @@ +package it.inaf.ia2.vospace.ui.service; + +import it.inaf.ia2.gms.client.GmsClient; +import it.inaf.ia2.gms.client.model.Permission; +import it.inaf.ia2.rap.client.ClientCredentialsRapClient; +import it.inaf.ia2.rap.data.Identity; +import it.inaf.ia2.rap.data.IdentityType; +import it.inaf.ia2.rap.data.RapUser; +import it.inaf.ia2.rap.data.TokenContext; +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.oats.vospace.datamodel.NodeProperties; +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.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.Node; +import net.ivoa.xml.vospace.v2.Property; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class SharingService { + + private static final Logger LOG = LoggerFactory.getLogger(SharingService.class); + + @Value("${trusted.eppn.scope}") + protected String trustedEppnScope; + + private final Set existingGroups = new CopyOnWriteArraySet<>(); + private final Set existingPeopleGroups = new CopyOnWriteArraySet<>(); + private final Map existingUsers = new ConcurrentHashMap<>(); + + private final GmsClient gmsClient; + private final ClientCredentialsRapClient rapClient; + private final VOSpaceClient vospaceClient; + + private TokenContext tokenContext; + private Date lastUpdate; + + @Autowired + private HttpServletRequest request; + + @Autowired + public SharingService(GmsClient gmsClient, ClientCredentialsRapClient gmsRapClient, VOSpaceClient vospaceClient) { + this.gmsClient = gmsClient; + this.rapClient = gmsRapClient; + this.vospaceClient = vospaceClient; + } + + public SharingInfo getSharingInfo() { + + loadInfoFromServices(); + + SharingInfo sharingInfo = new SharingInfo(); + + Set people = new HashSet<>(existingUsers.values()); + people.addAll(existingPeopleGroups); + + sharingInfo.setPeople(new ArrayList<>(people)); + Collections.sort(sharingInfo.getPeople()); + + sharingInfo.setGroups(new ArrayList<>(existingGroups)); + Collections.sort(sharingInfo.getGroups()); + + return sharingInfo; + } + + public void setNodeGroups(ShareRequest shareRequest) { + + loadInfoFromServices(); + + createPeopleGroupIfNeeded(shareRequest.getUserRead()); + createPeopleGroupIfNeeded(shareRequest.getUserWrite()); + validateGroups(shareRequest.getGroupRead()); + validateGroups(shareRequest.getGroupWrite()); + + String groupRead = getGroupString(shareRequest.getGroupRead(), shareRequest.getUserRead()); + String groupWrite = getGroupString(shareRequest.getGroupWrite(), shareRequest.getUserWrite()); + + Node node = vospaceClient.getNode(shareRequest.getPath()); + getNodeProperty(node, NodeProperties.GROUP_READ_URI).setValue(groupRead); + getNodeProperty(node, NodeProperties.GROUP_WRITE_URI).setValue(groupWrite); + + vospaceClient.setNode(node); + } + + private Property getNodeProperty(Node node, String uri) { + for (Property property : node.getProperties()) { + if (uri.equals(property.getUri())) { + return property; + } + } + Property property = new Property(); + property.setUri(uri); + node.getProperties().add(property); + return property; + } + + private void createPeopleGroupIfNeeded(List users) { + for (String username : users) { + if (!existingPeopleGroups.contains(username)) { + + String completeGroupName = "people." + username.replace(".", "\\."); + String rapId = getRapId(username); + + gmsClient.createGroup(completeGroupName, true); + gmsClient.addMember(completeGroupName, rapId); + gmsClient.addPermission(completeGroupName, rapId, Permission.VIEW_MEMBERS); + + existingPeopleGroups.add(username); + } + } + } + + private String getRapId(String username) { + for (Map.Entry entry : existingUsers.entrySet()) { + if (entry.getValue().equals(username)) { + return entry.getKey(); + } + } + throw new BadRequestException("Unable to find an identifier for user " + username); + } + + private void validateGroups(List groups) { + for (String group : groups) { + if (!existingGroups.contains(group)) { + throw new BadRequestException("Group " + group + " doesn't exist"); + } + } + } + + private String getGroupString(List groups, List users) { + List values = new ArrayList<>(groups); + for (String user : users) { + values.add("people." + user.replace(".", "\\.")); + } + return String.join(" ", values); + } + + private void loadInfoFromServices() { + loadAccessToken(); + + if (lastUpdate == null || new Date().getTime() - lastUpdate.getTime() > 3600 * 100) { + LOG.trace("Loading existing users and groups from services"); + CompletableFuture.allOf( + CompletableFuture.runAsync(() -> loadGroups()), + CompletableFuture.runAsync(() -> loadUsers()) + ).join(); + } + } + + public void loadAccessToken() { + if (tokenContext == null) { + tokenContext = new TokenContext(); + } + if (tokenContext.isTokenExpired()) { + tokenContext.setAccessTokenResponse(rapClient.getAccessTokenFromClientCredentials()); + gmsClient.setAccessToken(tokenContext.getAccessToken()); + } + } + + private void loadGroups() { + List groups = new ArrayList<>(); + List peopleGroups = new ArrayList<>(); + + for (String group : gmsClient.listGroups("", true)) { + if (group.startsWith("people.")) { + String singleUserGroup = group.substring("people.".length()).replace("\\.", "."); + peopleGroups.add(singleUserGroup); + } else { + groups.add(group); + } + } + + existingGroups.addAll(groups); + existingPeopleGroups.addAll(peopleGroups); + } + + private void loadUsers() { + for (RapUser user : rapClient.getUsers(trustedEppnScope, tokenContext)) { + for (Identity identity : user.getIdentities()) { + if (identity.getType() == IdentityType.EDU_GAIN + && identity.getEppn().endsWith("@" + trustedEppnScope)) { + String username = identity.getEppn().substring(0, identity.getEppn().indexOf("@")); + existingUsers.put(user.getId(), username); + break; + } + } + } + } +} diff --git a/vospace-ui-backend/src/main/resources/application.properties b/vospace-ui-backend/src/main/resources/application.properties index 200742e..2b7cd11 100644 --- a/vospace-ui-backend/src/main/resources/application.properties +++ b/vospace-ui-backend/src/main/resources/application.properties @@ -9,3 +9,5 @@ spring.profiles.active=dev cors.allowed.origin=http://localhost:8080 logging.level.it.inaf=TRACE + +trusted.eppn.scope=inaf.it diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/SharingControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/SharingControllerTest.java new file mode 100644 index 0000000..b471b20 --- /dev/null +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/SharingControllerTest.java @@ -0,0 +1,37 @@ +package it.inaf.ia2.vospace.ui.controller; + +import it.inaf.ia2.vospace.ui.data.SharingInfo; +import it.inaf.ia2.vospace.ui.service.SharingService; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class SharingControllerTest { + + @MockBean + private SharingService service; + + @Autowired + private MockMvc mockMvc; + + @Test + public void testListNodesEmpty() throws Exception { + + when(service.getSharingInfo()).thenReturn(new SharingInfo()); + + mockMvc.perform(get("/sharing")) + .andExpect(status().isOk()); + + verify(service, times(1)).getSharingInfo(); + } +} 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 new file mode 100644 index 0000000..23fd9da --- /dev/null +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java @@ -0,0 +1,145 @@ +package it.inaf.ia2.vospace.ui.service; + +import it.inaf.ia2.gms.client.GmsClient; +import it.inaf.ia2.rap.client.ClientCredentialsRapClient; +import it.inaf.ia2.rap.data.AccessTokenResponse; +import it.inaf.ia2.rap.data.Identity; +import it.inaf.ia2.rap.data.IdentityType; +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.oats.vospace.datamodel.NodeProperties; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.vospace.v2.DataNode; +import net.ivoa.xml.vospace.v2.Property; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SharingServiceTest { + + @Mock + private GmsClient gmsClient; + + @Mock + private ClientCredentialsRapClient rapClient; + + @Mock + private HttpServletRequest servletRequest; + + @Mock + private VOSpaceClient vospaceClient; + + @InjectMocks + private SharingService sharingService; + + @BeforeEach + public void setUp() { + sharingService.trustedEppnScope = "inaf.it"; + + when(rapClient.getAccessTokenFromClientCredentials()).thenReturn(new AccessTokenResponse()); + + when(rapClient.getUsers(anyString(), any())).thenReturn(getRapUsers()); + + when(gmsClient.listGroups(eq(""), eq(true))).thenReturn( + Arrays.asList("group1", "group2", "people.mario\\.rossi", "people.john\\.doe")); + } + + @Test + public void testGetSharingInfo() { + + SharingInfo sharingInfo = sharingService.getSharingInfo(); + + assertEquals(2, sharingInfo.getGroups().size()); + assertTrue(sharingInfo.getGroups().contains("group1")); + assertTrue(sharingInfo.getGroups().contains("group2")); + + assertEquals(4, sharingInfo.getPeople().size()); + assertTrue(sharingInfo.getPeople().contains("mario.rossi")); + assertTrue(sharingInfo.getPeople().contains("bianca.verdi")); + assertTrue(sharingInfo.getPeople().contains("paolo.gialli")); + assertTrue(sharingInfo.getPeople().contains("john.doe")); + } + + @Test + public void testSetNodeGroups() { + + DataNode node = new DataNode(); + node.setUri("vos://example.com!vospace/mynode"); + Property groupReadProperty = new Property(); + groupReadProperty.setUri(NodeProperties.GROUP_READ_URI); + groupReadProperty.setValue("group1"); + node.getProperties().add(groupReadProperty); + + when(vospaceClient.getNode(any())).thenReturn(node); + + ShareRequest shareRequest = new ShareRequest(); + shareRequest.setPath("/mynode"); + shareRequest.setUserRead(Arrays.asList("bianca.verdi", "john.doe")); + shareRequest.setGroupRead(Arrays.asList("group1", "group2")); + shareRequest.setUserWrite(Arrays.asList("bianca.verdi")); + shareRequest.setGroupWrite(Arrays.asList("group2")); + + sharingService.setNodeGroups(shareRequest); + + verify(gmsClient, times(1)).createGroup(eq("people.bianca\\.verdi"), eq(true)); + + verify(vospaceClient, times(1)).setNode(argThat(n -> { + List groupRead = NodeProperties.getNodePropertyAsListByURI(n, NodeProperties.GROUP_READ_URI); + assertEquals(4, groupRead.size()); + assertTrue(groupRead.contains("people.bianca\\.verdi")); + assertTrue(groupRead.contains("people.john\\.doe")); + assertTrue(groupRead.contains("group1")); + assertTrue(groupRead.contains("group2")); + + List groupWrite = NodeProperties.getNodePropertyAsListByURI(n, NodeProperties.GROUP_WRITE_URI); + assertEquals(2, groupWrite.size()); + assertTrue(groupWrite.contains("people.bianca\\.verdi")); + assertTrue(groupWrite.contains("group2")); + return true; + })); + } + + private List getRapUsers() { + List users = new ArrayList<>(); + + users.add(getRapUser("1", "mario.rossi")); + users.add(getRapUser("2", "bianca.verdi")); + users.add(getRapUser("3", "paolo.gialli")); + + RapUser ignored = getRapUser("4", "name.surname"); + ignored.getIdentities().get(0).setType(IdentityType.GOOGLE); + users.add(ignored); + + return users; + } + + private RapUser getRapUser(String id, String name) { + RapUser user = new RapUser(); + user.setId(id); + List identities = new ArrayList<>(); + Identity identity = new Identity(); + identity.setType(IdentityType.EDU_GAIN); + identity.setEppn(name + "@inaf.it"); + identities.add(identity); + user.setIdentities(identities); + return user; + } +} -- GitLab