From d6ee290f482be6eb2bd9ac7cffb035aa66bfca9f Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Fri, 29 Nov 2019 17:33:25 +0100
Subject: [PATCH] Join implementation changes

---
 .../main/java/it/inaf/ia2/gms/cli/CLI.java    |  10 +-
 .../it/inaf/ia2/gms/client/GmsClient.java     |  14 ---
 .../it/inaf/ia2/gms/client/GmsClientTest.java |  22 ----
 .../java/it/inaf/ia2/gms/GmsApplication.java  |   4 +
 .../java/it/inaf/ia2/gms/authn/JWTFilter.java |  14 +--
 .../it/inaf/ia2/gms/authn/RapPrincipal.java   |  27 +++++
 .../BasicAuthWebServiceController.java        |  10 --
 .../controller/JWTWebServiceController.java   |  25 ++++
 .../ia2/gms/model/PrepareToJoinRequest.java   |  23 ----
 .../it/inaf/ia2/gms/persistence/JoinDAO.java  | 107 ++++++++++++++++++
 .../ia2/gms/persistence/MembershipsDAO.java   |  12 --
 .../ia2/gms/persistence/PermissionsDAO.java   |  12 --
 .../persistence/model/MembershipEntity.java   |  27 +++++
 .../persistence/model/PermissionEntity.java   |  35 ++++++
 .../it/inaf/ia2/gms/service/JoinService.java  |  76 +++++++++++++
 .../inaf/ia2/gms/service/MembersService.java  |   4 -
 .../ia2/gms/service/PermissionsService.java   |   4 -
 gms/src/main/resources/application.properties |   3 +-
 .../BasicAuthWebServiceControllerTest.java    |  17 ---
 .../ia2/gms/persistence/GroupsDAOTest.java    |   1 -
 .../inaf/ia2/gms/persistence/JoinDAOTest.java |  99 ++++++++++++++++
 .../inaf/ia2/gms/service/JoinServiceTest.java | 104 +++++++++++++++++
 22 files changed, 513 insertions(+), 137 deletions(-)
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/authn/RapPrincipal.java
 delete mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/service/JoinService.java
 create mode 100644 gms/src/test/java/it/inaf/ia2/gms/persistence/JoinDAOTest.java
 create mode 100644 gms/src/test/java/it/inaf/ia2/gms/service/JoinServiceTest.java

diff --git a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
index 356bfa3..b183e2c 100644
--- a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
+++ b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
@@ -93,13 +93,6 @@ public class CLI implements CommandLineRunner {
                 client.removePermission(getNames(args, 1, args.length - 2), args[args.length - 1]);
                 System.out.println("Permission removed");
                 break;
-            case "prepare-join":
-                if (args.length != 3) {
-                    displayUsage();
-                }
-                client.prepareToJoin(args[1], args[2]);
-                System.out.println("Join prepared");
-                break;
             default:
                 displayUsage();
                 break;
@@ -113,8 +106,7 @@ public class CLI implements CommandLineRunner {
                 + "    add-member <name1 name2 name3> <user_id>\n"
                 + "    remove-member <name1 name2 name3> <user_id>\n"
                 + "    add-permission <name1 name2 name3> <user_id> <permission>\n"
-                + "    delete-permission <name1 name2 name3> <user_id>\n"
-                + "    prepare-join <from_user_id> <to_user_id>");
+                + "    delete-permission <name1 name2 name3> <user_id>");
         System.exit(0);
     }
 
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
index d788dd1..ab11cee 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
@@ -116,20 +116,6 @@ public class GmsClient {
         restTemplate.exchange(url, HttpMethod.DELETE, getEntity(), Void.class);
     }
 
-    public void prepareToJoin(String fromUserId, String toUserId) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("prepare-join")
-                .toUriString();
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("fromUserId", fromUserId);
-        params.put("toUserId", toUserId);
-        HttpEntity<Map<String, Object>> httpEntity = getEntity(params);
-
-        restTemplate.exchange(url, HttpMethod.POST, httpEntity, Void.class);
-    }
-
     private HttpEntity<?> getEntity() {
         return new HttpEntity<>(getHeaders());
     }
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
index 7771a98..e0eeb8d 100644
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
+++ b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
@@ -150,28 +150,6 @@ public class GmsClientTest {
         verifyAuthHeaders(entity);
     }
 
-    @Test
-    public void testPrepareToJoin() {
-
-        String fromUserId = "from_user_id";
-        String toUserId = "to_user_id";
-
-        client.prepareToJoin(fromUserId, toUserId);
-
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/prepare-join"),
-                eq(HttpMethod.POST), entityCaptor.capture(), eq(Void.class));
-
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
-
-        Map<String, Object> expectedBody = new HashMap<>();
-        expectedBody.put("fromUserId", fromUserId);
-        expectedBody.put("toUserId", toUserId);
-
-        verifyBody(entity, expectedBody);
-    }
-
     private void verifyAuthHeaders(HttpEntity<?> entity) {//
         String authHeader = entity.getHeaders().getFirst("Authorization");
         assertEquals("Basic dGVzdDp0ZXN0", authHeader);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java b/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java
index 5833724..a3b2a8f 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java
@@ -2,8 +2,12 @@ package it.inaf.ia2.gms;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 @SpringBootApplication
+@Configuration
+@EnableTransactionManagement
 public class GmsApplication {
 
     public static void main(String[] args) {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java b/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java
index 88b44ca..5310b24 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java
@@ -11,7 +11,6 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.oauth2.common.OAuth2AccessToken;
 import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
 
@@ -45,29 +44,28 @@ public class JWTFilter implements Filter {
 
         Map<String, Object> claims = accessToken.getAdditionalInformation();
 
-        String principal = (String) claims.get("sub");
-        if (principal == null) {
+        if (claims.get("sub") == null) {
             response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid access token: missing sub claim");
             return;
         }
 
-        ServletRequest wrappedRequest = new ServletRequestWithJWTPrincipal(request, principal);
+        ServletRequest wrappedRequest = new ServletRequestWithJWTPrincipal(request, claims);
 
         fc.doFilter(wrappedRequest, res);
     }
 
     private static class ServletRequestWithJWTPrincipal extends HttpServletRequestWrapper {
 
-        private final String principal;
+        private final Principal principal;
 
-        public ServletRequestWithJWTPrincipal(HttpServletRequest request, String principal) {
+        public ServletRequestWithJWTPrincipal(HttpServletRequest request, Map<String, Object> jwtClaims) {
             super(request);
-            this.principal = principal;
+            this.principal = new RapPrincipal(jwtClaims);
         }
 
         @Override
         public Principal getUserPrincipal() {
-            return new UsernamePasswordAuthenticationToken(principal, null);
+            return principal;
         }
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/RapPrincipal.java b/gms/src/main/java/it/inaf/ia2/gms/authn/RapPrincipal.java
new file mode 100644
index 0000000..174ff2f
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/RapPrincipal.java
@@ -0,0 +1,27 @@
+package it.inaf.ia2.gms.authn;
+
+import java.security.Principal;
+import java.util.Map;
+
+public class RapPrincipal implements Principal {
+
+    private final String sub;
+    private final String altSub;
+
+    public RapPrincipal(Map<String, Object> jwtClaims) {
+        sub = (String) jwtClaims.get("sub");
+        altSub = (String) jwtClaims.get("alt_sub");
+    }
+
+    @Override
+    public String getName() {
+        return sub;
+    }
+
+    /**
+     * Alternative subject identifier: used during a join.
+     */
+    public String getAlternativeName() {
+        return altSub;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceController.java
index 49530ba..9b9d27f 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceController.java
@@ -3,7 +3,6 @@ package it.inaf.ia2.gms.controller;
 import it.inaf.ia2.gms.exception.BadRequestException;
 import it.inaf.ia2.gms.model.request.AddMemberWsRequest;
 import it.inaf.ia2.gms.model.request.AddPermissionWsRequest;
-import it.inaf.ia2.gms.model.PrepareToJoinRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
@@ -112,15 +111,6 @@ public class BasicAuthWebServiceController {
         return ResponseEntity.noContent().build();
     }
 
-    @PostMapping(value = "/prepare-join", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
-    public ResponseEntity<?> prepareToJoin(@Valid @RequestBody PrepareToJoinRequest request) {
-
-        permissionsService.movePermissions(request.getFromUserId(), request.getToUserId());
-        membersService.moveMemberships(request.getFromUserId(), request.getToUserId());
-
-        return ResponseEntity.ok().build();
-    }
-
     private GroupEntity getGroupByNames(List<String> names) {
         return groupsService.findGroupByNames(names)
                 .orElseThrow(() -> new BadRequestException("Unable to find requested group"));
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
index 53f2d75..4f63277 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
@@ -1,8 +1,11 @@
 package it.inaf.ia2.gms.controller;
 
+import it.inaf.ia2.gms.authn.RapPrincipal;
+import it.inaf.ia2.gms.exception.BadRequestException;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.service.JoinService;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.security.Principal;
@@ -15,7 +18,9 @@ import java.util.Set;
 import javax.servlet.http.HttpServletResponse;
 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.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -29,6 +34,9 @@ public class JWTWebServiceController {
     @Autowired
     private MembershipsDAO membershipsDAO;
 
+    @Autowired
+    private JoinService joinService;
+
     @Autowired
     private GroupsDAO groupsDAO;
 
@@ -93,4 +101,21 @@ public class JWTWebServiceController {
 
         return String.join(".", names);
     }
+
+    @PostMapping(value = "/join", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<?> join(RapPrincipal principal) {
+
+        String fromUser = principal.getName();
+        String toUser = principal.getAlternativeName();
+
+        if (toUser == null) {
+            throw new BadRequestException("Missing alternative subject");
+        }
+
+        joinService.join(fromUser, toUser);
+
+        Map<String, String> responseBody = new HashMap<>();
+        responseBody.put("mergedId", fromUser);
+        return ResponseEntity.ok(responseBody);
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java
deleted file mode 100644
index 84681f7..0000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-public class PrepareToJoinRequest {
-
-    private String fromUserId;
-    private String toUserId;
-
-    public String getFromUserId() {
-        return fromUserId;
-    }
-
-    public void setFromUserId(String fromUserId) {
-        this.fromUserId = fromUserId;
-    }
-
-    public String getToUserId() {
-        return toUserId;
-    }
-
-    public void setToUserId(String toUserId) {
-        this.toUserId = toUserId;
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java
new file mode 100644
index 0000000..a62f315
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java
@@ -0,0 +1,107 @@
+package it.inaf.ia2.gms.persistence;
+
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import java.sql.PreparedStatement;
+import java.sql.Types;
+import java.util.Collections;
+import java.util.Set;
+import javax.sql.DataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class JoinDAO {
+
+    private static final Logger LOG = LoggerFactory.getLogger(JoinDAO.class);
+
+    private final JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    public JoinDAO(DataSource dataSource) {
+        jdbcTemplate = new JdbcTemplate(dataSource);
+    }
+
+    @Transactional
+    public void join(Set<MembershipEntity> membershipsToAdd, Set<PermissionEntity> permissionsToAdd, String userToDelete) {
+
+        if (!membershipsToAdd.isEmpty()) {
+            addMemberships(membershipsToAdd);
+        }
+        if (!permissionsToAdd.isEmpty()) {
+            addPermissions(permissionsToAdd);
+        }
+        deleteUserMemberships(userToDelete);
+        deleteUserPermissions(userToDelete);
+    }
+
+    private void addMemberships(Set<MembershipEntity> membershipsToAdd) {
+
+        String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES "
+                + String.join(", ", Collections.nCopies(membershipsToAdd.size(), "(?, ?)"));
+
+        LOG.trace("Executing {}", sql);
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            int i = 0;
+            for (MembershipEntity membership : membershipsToAdd) {
+                ps.setString(++i, membership.getGroupId());
+                ps.setString(++i, membership.getUserId());
+            }
+            return ps;
+        });
+    }
+
+    private void addPermissions(Set<PermissionEntity> permissionsToAdd) {
+
+        String sql = "INSERT INTO gms_permission (group_id, user_id, permission, group_path) VALUES "
+                + String.join(", ", Collections.nCopies(permissionsToAdd.size(), "(?, ?, ?, ?)")) + "\n"
+                + "ON CONFLICT (group_id, user_id) DO UPDATE\n"
+                + "SET permission = EXCLUDED.permission";;
+
+        LOG.trace("Executing {}", sql);
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            int i = 0;
+            for (PermissionEntity permission : permissionsToAdd) {
+                ps.setString(++i, permission.getGroupId());
+                ps.setString(++i, permission.getUserId());
+                ps.setObject(++i, permission.getPermission().toString(), Types.OTHER);
+                ps.setObject(++i, permission.getGroupPath(), Types.OTHER);
+            }
+            return ps;
+        });
+    }
+
+    private void deleteUserMemberships(String userId) {
+
+        String sql = "DELETE FROM gms_membership WHERE user_id = ?";
+
+        LOG.trace("Executing {}", sql);
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, userId);
+            return ps;
+        });
+    }
+
+    private void deleteUserPermissions(String userId) {
+
+        String sql = "DELETE FROM gms_permission WHERE user_id = ?";
+
+        LOG.trace("Executing {}", sql);
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, userId);
+            return ps;
+        });
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
index 1d31707..d8ff1ab 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
@@ -109,18 +109,6 @@ public class MembershipsDAO {
         });
     }
 
-    public void moveMemberships(String fromUserId, String toUserId) {
-
-        String sql = "UPDATE gms_membership SET user_id = ? WHERE user_id = ?";
-
-        jdbcTemplate.update(conn -> {
-            PreparedStatement ps = conn.prepareStatement(sql);
-            ps.setString(1, toUserId);
-            ps.setString(2, fromUserId);
-            return ps;
-        });
-    }
-
     public void deleteAllGroupsMembership(List<String> groupIds) {
 
         String sql = "DELETE FROM gms_membership WHERE group_id IN ("
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
index 07c2774..d8d417e 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
@@ -144,18 +144,6 @@ public class PermissionsDAO {
         });
     }
 
-    public void movePermissions(String fromUserId, String toUserId) {
-
-        String sql = "UPDATE gms_permission SET user_id = ? WHERE user_id = ?";
-
-        jdbcTemplate.update(conn -> {
-            PreparedStatement ps = conn.prepareStatement(sql);
-            ps.setString(1, toUserId);
-            ps.setString(2, fromUserId);
-            return ps;
-        });
-    }
-
     public void deleteAllGroupsPermissions(List<String> groupIds) {
 
         String sql = "DELETE FROM gms_permission WHERE group_id IN ("
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
index 81c793c..c71bccb 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
@@ -1,5 +1,6 @@
 package it.inaf.ia2.gms.persistence.model;
 
+import java.util.Objects;
 import javax.validation.constraints.NotEmpty;
 
 public class MembershipEntity {
@@ -24,4 +25,30 @@ public class MembershipEntity {
     public void setUserId(String userId) {
         this.userId = userId;
     }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 67 * hash + Objects.hashCode(this.groupId);
+        hash = 67 * hash + Objects.hashCode(this.userId);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final MembershipEntity other = (MembershipEntity) obj;
+        if (!Objects.equals(this.groupId, other.groupId)) {
+            return false;
+        }
+        return Objects.equals(this.userId, other.userId);
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
index 98bfc6f..ca6c623 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
@@ -1,6 +1,7 @@
 package it.inaf.ia2.gms.persistence.model;
 
 import it.inaf.ia2.gms.model.Permission;
+import java.util.Objects;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
@@ -47,4 +48,38 @@ public class PermissionEntity {
     public void setGroupPath(String groupPath) {
         this.groupPath = groupPath;
     }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 41 * hash + Objects.hashCode(this.userId);
+        hash = 41 * hash + Objects.hashCode(this.groupId);
+        hash = 41 * hash + Objects.hashCode(this.permission);
+        hash = 41 * hash + Objects.hashCode(this.groupPath);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PermissionEntity other = (PermissionEntity) obj;
+        if (!Objects.equals(this.userId, other.userId)) {
+            return false;
+        }
+        if (!Objects.equals(this.groupId, other.groupId)) {
+            return false;
+        }
+        if (!Objects.equals(this.groupPath, other.groupPath)) {
+            return false;
+        }
+        return this.permission == other.permission;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/JoinService.java b/gms/src/main/java/it/inaf/ia2/gms/service/JoinService.java
new file mode 100644
index 0000000..ebad2a8
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/JoinService.java
@@ -0,0 +1,76 @@
+package it.inaf.ia2.gms.service;
+
+import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.persistence.JoinDAO;
+import it.inaf.ia2.gms.persistence.MembershipsDAO;
+import it.inaf.ia2.gms.persistence.PermissionsDAO;
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JoinService {
+
+    @Autowired
+    private MembershipsDAO membershipsDAO;
+
+    @Autowired
+    private PermissionsDAO permissionsDAO;
+
+    @Autowired
+    private JoinDAO joinDAO;
+
+    public void join(String userId1, String userId2) {
+
+        Set<MembershipEntity> existingMemberships
+                = membershipsDAO.getUserMemberships(userId1).stream()
+                        .map(g -> getMembershipEntity(g.getId(), userId1))
+                        .collect(Collectors.toSet());
+
+        Set<MembershipEntity> membershipsToAdd
+                = membershipsDAO.getUserMemberships(userId2).stream()
+                        .map(g -> getMembershipEntity(g.getId(), userId1))
+                        .filter(m -> !existingMemberships.contains(m))
+                        .collect(Collectors.toSet());
+
+        Set<PermissionEntity> existingPermissions
+                = permissionsDAO.findUserPermissions(userId1).stream()
+                        .collect(Collectors.toSet());
+
+        Set<PermissionEntity> permissionsToAdd
+                = permissionsDAO.findUserPermissions(userId2).stream()
+                        .map(p -> {
+                            p.setUserId(userId1);
+                            return p;
+                        })
+                        .filter(p -> isPermissionToAdd(existingPermissions, p))
+                        .collect(Collectors.toSet());
+
+        joinDAO.join(membershipsToAdd, permissionsToAdd, userId2);
+    }
+
+    private MembershipEntity getMembershipEntity(String groupId, String userId) {
+        MembershipEntity entity = new MembershipEntity();
+        entity.setGroupId(groupId);
+        entity.setUserId(userId);
+        return entity;
+    }
+
+    private boolean isPermissionToAdd(Set<PermissionEntity> existingPermissions, PermissionEntity permissionToCheck) {
+        for (PermissionEntity permission : existingPermissions) {
+            if (permission.getGroupId().equals(permissionToCheck.getGroupId())
+                    && permission.getUserId().equals(permissionToCheck.getUserId())) {
+                if (permission.getPermission() == permissionToCheck.getPermission()) {
+                    return false;
+                }
+                Permission strongerPermission = Permission.addPermission(
+                        permission.getPermission(), permissionToCheck.getPermission());
+                return permission.getPermission() != strongerPermission;
+            }
+        }
+        return true;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java b/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java
index b05fb52..4adfa77 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java
@@ -42,8 +42,4 @@ public class MembersService {
     public void removeMember(String groupId, String userId) {
         membershipsDAO.removeMembership(groupId, userId);
     }
-
-    public void moveMemberships(String fromUserId, String toUserId) {
-        membershipsDAO.moveMemberships(fromUserId, toUserId);
-    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
index 80ef491..9912849 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
@@ -87,8 +87,4 @@ public class PermissionsService {
 
         return permissionsDAO.createOrUpdatePermission(permissionEntity);
     }
-
-    public void movePermissions(String fromUserId, String toUserId) {
-        permissionsDAO.movePermissions(fromUserId, toUserId);
-    }
 }
diff --git a/gms/src/main/resources/application.properties b/gms/src/main/resources/application.properties
index a4dec9a..eaa17ff 100644
--- a/gms/src/main/resources/application.properties
+++ b/gms/src/main/resources/application.properties
@@ -1,4 +1,4 @@
-server.port=8081
+server.port=8082
 server.servlet.context-path=/gms
 
 security.oauth2.client.client-id=gms
@@ -9,6 +9,7 @@ security.oauth2.resource.token-info-uri=http://localhost/rap-ia2/auth/oauth2/che
 security.oauth2.client.scope=openid,email,profile
 security.oauth2.resource.jwk.key-set-uri=http://localhost/rap-ia2/auth/oidc/jwks
 
+logging.level.it.inaf=TRACE
 logging.level.org.springframework.security=DEBUG
 logging.level.org.springframework.jdbc=TRACE
 logging.level.org.springframework.web=TRACE
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceControllerTest.java
index 0061ca3..8594bdb 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceControllerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/BasicAuthWebServiceControllerTest.java
@@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.gms.model.request.AddMemberWsRequest;
 import it.inaf.ia2.gms.model.request.AddPermissionWsRequest;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.PrepareToJoinRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
@@ -194,22 +193,6 @@ public class BasicAuthWebServiceControllerTest {
         verify(permissionsService, times(1)).removePermission(eq(inaf), eq("user_id"));
     }
 
-    @Test
-    public void testPrepareToJoin() throws Exception {
-
-        PrepareToJoinRequest request = new PrepareToJoinRequest();
-        request.setFromUserId("from_user");
-        request.setToUserId("to_user");
-
-        mockMvc.perform(post("/ws/basic/prepare-join")
-                .content(mapper.writeValueAsString(request))
-                .contentType(MediaType.APPLICATION_JSON_UTF8))
-                .andExpect(status().isOk());
-
-        verify(permissionsService, times(1)).movePermissions(request.getFromUserId(), request.getToUserId());
-        verify(membersService, times(1)).moveMemberships(request.getFromUserId(), request.getToUserId());
-    }
-
     private GroupEntity getInafGroup() {
         GroupEntity inaf = new GroupEntity();
         inaf.setId("inaf_id");
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
index ff5d365..271779e 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
@@ -34,7 +34,6 @@ public class GroupsDAOTest {
     }
 
     @Test
-    //@Sql("/sql/init.sql")
     public void testAll() {
 
         // Create groups
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/JoinDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/JoinDAOTest.java
new file mode 100644
index 0000000..c1ca308
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/JoinDAOTest.java
@@ -0,0 +1,99 @@
+package it.inaf.ia2.gms.persistence;
+
+import it.inaf.ia2.gms.DataSourceConfig;
+import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import java.util.HashSet;
+import java.util.Set;
+import javax.sql.DataSource;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = DataSourceConfig.class)
+public class JoinDAOTest {
+
+    private static final String USER_1 = "user-1";
+    private static final String USER_2 = "user-2";
+
+    @Autowired
+    private DataSource dataSource;
+
+    private GroupsDAO groupsDAO;
+    private MembershipsDAO membershipsDAO;
+    private PermissionsDAO permissionsDAO;
+    private JoinDAO joinDAO;
+
+    @Before
+    public void setUp() {
+        groupsDAO = new GroupsDAO(dataSource);
+        membershipsDAO = new MembershipsDAO(dataSource);
+        permissionsDAO = new PermissionsDAO(dataSource);
+        joinDAO = new JoinDAO(dataSource);
+    }
+
+    @Test
+    public void testJoin() {
+
+        groupsDAO.createGroup(groupEntity("A"));
+        groupsDAO.createGroup(groupEntity("B"));
+
+        permissionsDAO.createOrUpdatePermission(permissionEntity(USER_1, "A", Permission.VIEW_MEMBERS));
+        permissionsDAO.createOrUpdatePermission(permissionEntity(USER_1, "B", Permission.VIEW_MEMBERS));
+        permissionsDAO.createOrUpdatePermission(permissionEntity(USER_2, "A", Permission.ADMIN));
+
+        membershipsDAO.addMember(membershipEntity(USER_2, "A"));
+
+        Set<MembershipEntity> membershipsToAdd = new HashSet<>();
+        membershipsToAdd.add(membershipEntity(USER_1, "A"));
+
+        Set<PermissionEntity> permissionsToAdd = new HashSet<>();
+        permissionsToAdd.add(permissionEntity(USER_1, "A", Permission.ADMIN));
+
+        assertEquals(1, membershipsDAO.getUserMemberships(USER_2).size());
+        assertEquals(2, permissionsDAO.findUserPermissions(USER_1).size());
+        assertEquals(1, permissionsDAO.findUserPermissions(USER_2).size());
+
+        joinDAO.join(membershipsToAdd, permissionsToAdd, USER_2);
+
+        assertTrue(membershipsDAO.getUserMemberships(USER_2).isEmpty());
+        assertTrue(permissionsDAO.findUserPermissions(USER_2).isEmpty());
+        assertEquals(2, permissionsDAO.findUserPermissions(USER_1).size());
+        assertTrue(permissionsDAO.findUserPermissions(USER_1)
+                .contains(permissionEntity(USER_1, "A", Permission.ADMIN)));
+        assertEquals(1, membershipsDAO.getUserMemberships(USER_1).size());
+    }
+
+    private GroupEntity groupEntity(String groupId) {
+        GroupEntity groupEntity = new GroupEntity();
+        groupEntity.setId(groupId);
+        groupEntity.setName(groupId);
+        groupEntity.setPath(groupId);
+        groupEntity.setLeaf(false);
+        return groupEntity;
+    }
+
+    private MembershipEntity membershipEntity(String userId, String groupId) {
+        MembershipEntity membershipEntity = new MembershipEntity();
+        membershipEntity.setUserId(userId);
+        membershipEntity.setGroupId(groupId);
+        return membershipEntity;
+    }
+
+    private PermissionEntity permissionEntity(String userId, String groupId, Permission permission) {
+        PermissionEntity permissionEntity = new PermissionEntity();
+        permissionEntity.setGroupId(groupId);
+        permissionEntity.setUserId(userId);
+        permissionEntity.setPermission(permission);
+        permissionEntity.setGroupPath(groupId);
+        return permissionEntity;
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/service/JoinServiceTest.java b/gms/src/test/java/it/inaf/ia2/gms/service/JoinServiceTest.java
new file mode 100644
index 0000000..87d0402
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/service/JoinServiceTest.java
@@ -0,0 +1,104 @@
+package it.inaf.ia2.gms.service;
+
+import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.persistence.JoinDAO;
+import it.inaf.ia2.gms.persistence.MembershipsDAO;
+import it.inaf.ia2.gms.persistence.PermissionsDAO;
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import java.util.Arrays;
+import java.util.Set;
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+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.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JoinServiceTest {
+
+    private static final String USER_1 = "user-1";
+    private static final String USER_2 = "user-2";
+
+    @Mock
+    private MembershipsDAO membershipsDAO;
+
+    @Mock
+    private PermissionsDAO permissionsDAO;
+
+    @Mock
+    private JoinDAO joinDAO;
+
+    @InjectMocks
+    private JoinService joinService;
+
+    @Test
+    public void testJoin() {
+
+        when(membershipsDAO.getUserMemberships(eq(USER_1)))
+                .thenReturn(Arrays.asList(groupEntity("A")));
+
+        when(membershipsDAO.getUserMemberships(eq(USER_2)))
+                .thenReturn(Arrays.asList(groupEntity("A"), groupEntity("B")));
+
+        when(permissionsDAO.findUserPermissions(eq(USER_1)))
+                .thenReturn(Arrays.asList(
+                        permissionEntity("A", USER_1, Permission.ADMIN),
+                        permissionEntity("B", USER_1, Permission.VIEW_MEMBERS),
+                        permissionEntity("C", USER_1, Permission.MANAGE_MEMBERS)
+                ));
+
+        when(permissionsDAO.findUserPermissions(eq(USER_2)))
+                .thenReturn(Arrays.asList(
+                        permissionEntity("A", USER_2, Permission.VIEW_MEMBERS),
+                        permissionEntity("B", USER_2, Permission.ADMIN),
+                        permissionEntity("C", USER_2, Permission.MANAGE_MEMBERS),
+                        permissionEntity("D", USER_2, Permission.VIEW_MEMBERS)
+                ));
+
+        joinService.join(USER_1, USER_2);
+
+        verify(joinDAO, times(1)).join(argThat(verifyMembershipsToAdd()),
+                argThat(verifyPermissionsToAdd()), eq(USER_2));
+    }
+
+    private GroupEntity groupEntity(String groupId) {
+        GroupEntity group = new GroupEntity();
+        group.setId(groupId);
+        return group;
+    }
+
+    private PermissionEntity permissionEntity(String groupId, String userId, Permission permission) {
+        PermissionEntity permissionEntity = new PermissionEntity();
+        permissionEntity.setGroupId(groupId);
+        permissionEntity.setUserId(userId);
+        permissionEntity.setPermission(permission);
+        return permissionEntity;
+    }
+
+    private ArgumentMatcher<Set<MembershipEntity>> verifyMembershipsToAdd() {
+        return memberships -> {
+            assertEquals(1, memberships.size());
+            MembershipEntity entity = memberships.iterator().next();
+            assertEquals("B", entity.getGroupId());
+            assertEquals(USER_1, entity.getUserId());
+            return true;
+        };
+    }
+
+    private ArgumentMatcher<Set<PermissionEntity>> verifyPermissionsToAdd() {
+        return permissions -> {
+            assertEquals(2, permissions.size());
+            return permissions.contains(permissionEntity("B", USER_1, Permission.ADMIN))
+                    && permissions.contains(permissionEntity("D", USER_1, Permission.VIEW_MEMBERS));
+        };
+    }
+}
-- 
GitLab