From 17c4724e9133744b2d095449cf26e1823eb253cd Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Fri, 17 Apr 2020 16:29:50 +0200 Subject: [PATCH] #4 Implemented invited registration --- .../it/inaf/ia2/gms/client/GmsClient.java | 6 + .../call/AddInvitedRegistrationCall.java | 56 +++++++++ .../it/inaf/ia2/gms/authn/SecurityConfig.java | 2 +- .../gms/controller/HomePageController.java | 18 ++- .../InvitedRegistrationController.java | 74 +++++++++++ .../controller/JWTWebServiceController.java | 96 +++++--------- .../ia2/gms/exception/NotFoundException.java | 12 ++ .../manager/InvitedRegistrationManager.java | 119 ++++++++++++++++++ .../persistence/InvitedRegistrationDAO.java | 36 +++--- .../ia2/gms/persistence/MembershipsDAO.java | 3 +- .../model/InvitedRegistration.java | 26 ++-- .../ia2/gms/service/GroupNameService.java | 63 ++++++++++ gms/src/main/resources/sql/init.sql | 6 +- .../templates/invited-registration.html | 18 +++ .../templates/registration-completed.html | 17 +++ 15 files changed, 458 insertions(+), 94 deletions(-) create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java create mode 100644 gms/src/main/resources/templates/invited-registration.html create mode 100644 gms/src/main/resources/templates/registration-completed.html 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 7be236d..481e4c3 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 @@ -1,5 +1,6 @@ package it.inaf.ia2.gms.client; +import it.inaf.ia2.gms.client.call.AddInvitedRegistrationCall; import it.inaf.ia2.gms.client.call.HttpClientWrapper; import it.inaf.ia2.gms.client.call.AddMemberCall; import it.inaf.ia2.gms.client.call.AddPermissionCall; @@ -13,6 +14,7 @@ import it.inaf.ia2.gms.client.call.RemovePermissionCall; import it.inaf.ia2.gms.client.model.Permission; import it.inaf.ia2.gms.client.model.UserPermission; import java.util.List; +import java.util.Map; public class GmsClient { @@ -71,4 +73,8 @@ public class GmsClient { public List<UserPermission> getUserPermissions(String userId) { return new GetUserPermissionsCall(httpClientWrapper).getUserPermissions(userId); } + + public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) { + new AddInvitedRegistrationCall(httpClientWrapper).addInvitedRegistration(token, email, groupsPermissions); + } } diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java new file mode 100644 index 0000000..0aefd71 --- /dev/null +++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java @@ -0,0 +1,56 @@ +package it.inaf.ia2.gms.client.call; + +import it.inaf.ia2.gms.client.model.Permission; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Map; +import java.util.stream.Collectors; + +public class AddInvitedRegistrationCall extends BaseGmsCall { + + public AddInvitedRegistrationCall(HttpClientWrapper clientWrapper) { + super(clientWrapper); + } + + public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) { + + String tokenHash = getTokenHash(token); + + String endpoint = "invited-registration"; + + String bodyParams = "token_hash=" + tokenHash + + "&email=" + email + "&groups=" + + String.join("\n", groupsPermissions.entrySet() + .stream().map(e -> e.getKey() + " " + e.getValue()) + .collect(Collectors.toList())); + + HttpRequest groupsRequest = newHttpRequest(endpoint) + .header("Accept", "text/plain") + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(bodyParams)) + .build(); + + getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream()) + .thenApply(response -> { + if (response.statusCode() == 201) { + return true; + } + logServerErrorInputStream(groupsRequest, response); + throw new IllegalStateException("Unable to create invited registration"); + }).join(); + } + + private String getTokenHash(String token) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java index 0f561c6..63b992e 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java +++ b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java @@ -64,7 +64,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { */ @Override public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/ws/basic/**", "/ws/jwt/**", "/error", "/logout"); + web.ignoring().antMatchers("/ws/jwt/**", "/error", "/logout", "/invited-registration"); } /** diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java index fc3f0a4..ced3a3d 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java @@ -1,10 +1,15 @@ package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.authn.SessionData; +import it.inaf.ia2.gms.manager.InvitedRegistrationManager; import it.inaf.ia2.gms.model.request.GroupsRequest; import it.inaf.ia2.gms.model.response.GroupsTabResponse; import it.inaf.ia2.gms.model.response.HomePageResponse; +import it.inaf.ia2.gms.persistence.model.InvitedRegistration; import java.io.IOException; +import java.util.Optional; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.validation.Valid; @@ -25,6 +30,9 @@ public class HomePageController { @Autowired private GroupsTabResponseBuilder groupsTabResponseBuilder; + @Autowired + private InvitedRegistrationManager invitedRegistrationManager; + @ResponseBody @GetMapping(value = "/home", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity<HomePageResponse> getMainPage(@Valid GroupsRequest request) { @@ -42,7 +50,15 @@ public class HomePageController { } @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) - public String index() { + public String index(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + Optional<InvitedRegistration> optReg = invitedRegistrationManager.completeInvitedRegistrationIfNecessary(); + if (optReg.isPresent()) { + request.setAttribute("invited-registration", optReg.get()); + return "/registration-completed"; + //request.getRequestDispatcher("/registration-completed").forward(request, response); + } + return "index.html"; } diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java new file mode 100644 index 0000000..0a4554c --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java @@ -0,0 +1,74 @@ +package it.inaf.ia2.gms.controller; + +import it.inaf.ia2.gms.manager.InvitedRegistrationManager; +import it.inaf.ia2.gms.persistence.model.InvitedRegistration; +import it.inaf.ia2.gms.service.GroupNameService; +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@Controller +public class InvitedRegistrationController { + + @Autowired + private InvitedRegistrationManager invitedRegistrationManager; + + @Autowired + private GroupNameService groupNameService; + + @GetMapping(value = "/invited-registration", produces = MediaType.TEXT_HTML_VALUE) + public void index(@RequestParam("token") String token, HttpServletRequest request, HttpServletResponse response) throws IOException { + InvitedRegistration invitedRegistration = invitedRegistrationManager.getInvitedRegistrationFromToken(token); + + String html = getFileContent("invited-registration.html") + .replace("#EMAIL#", invitedRegistration.getEmail()) + .replace("#GROUPS#", getGroupsList(invitedRegistration)) + .replace("#HOME#", request.getContextPath()); + + response.getOutputStream().print(html); + } + + @GetMapping(value = "/registration-completed", produces = MediaType.TEXT_HTML_VALUE) + public void completed(HttpServletRequest request, HttpServletResponse response) throws IOException { + + response.setContentType("text/html;charset=UTF-8"); + + InvitedRegistration invitedRegistration = (InvitedRegistration) request.getAttribute("invited-registration"); + if (invitedRegistration == null) { + // redirect to home + String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString(); + response.sendRedirect(baseUrl); + } else { + String html = getFileContent("registration-completed.html") + .replace("#GROUPS#", getGroupsList(invitedRegistration)) + .replace("#HOME#", request.getContextPath()); + + response.getOutputStream().print(html); + } + } + + private String getFileContent(String templateFileName) throws IOException { + try (InputStream in = InvitedRegistrationController.class.getClassLoader().getResourceAsStream("templates/" + templateFileName)) { + Scanner s = new Scanner(in).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + } + + private String getGroupsList(InvitedRegistration invitedRegistration) { + String groups = "<ul>"; + for (String groupName : groupNameService.getGroupsNamesFromIdentifiers( + invitedRegistration.getGroupsPermissions().keySet())) { + groups += "<li>" + groupName + "</li>"; + } + groups += "</ul>"; + return groups; + } +} 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 4a7a2d6..ea828d3 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 @@ -3,6 +3,7 @@ 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.manager.GroupsManager; +import it.inaf.ia2.gms.manager.InvitedRegistrationManager; import it.inaf.ia2.gms.manager.MembershipManager; import it.inaf.ia2.gms.manager.PermissionsManager; import it.inaf.ia2.gms.model.Permission; @@ -11,6 +12,7 @@ import it.inaf.ia2.gms.persistence.GroupsDAO; import it.inaf.ia2.gms.persistence.PermissionsDAO; import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.persistence.model.PermissionEntity; +import it.inaf.ia2.gms.service.GroupNameService; import it.inaf.ia2.gms.service.GroupsService; import it.inaf.ia2.gms.service.JoinService; import it.inaf.ia2.gms.service.PermissionUtils; @@ -20,11 +22,9 @@ import java.io.PrintWriter; import java.security.Principal; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -56,6 +56,9 @@ public class JWTWebServiceController { @Autowired private GroupsService groupsService; + + @Autowired + private GroupNameService groupNameService; @Autowired private MembershipManager membershipManager; @@ -69,6 +72,9 @@ public class JWTWebServiceController { @Autowired private SearchService searchService; + @Autowired + private InvitedRegistrationManager invitedRegistrationManager; + /** * This endpoint is compliant with the IVOA GMS standard. */ @@ -77,7 +83,7 @@ public class JWTWebServiceController { List<GroupEntity> memberships = membershipManager.getCurrentUserMemberships(); - List<String> names = getGroupsNames(memberships); + List<String> names = groupNameService.getGroupsNames(memberships); try (PrintWriter pw = new PrintWriter(response.getOutputStream())) { @@ -145,7 +151,7 @@ public class JWTWebServiceController { } try (PrintWriter pw = new PrintWriter(response.getOutputStream())) { - for (String groupName : getGroupsNames(visibleSubgroups)) { + for (String groupName : groupNameService.getGroupsNames(visibleSubgroups)) { pw.println(getShortGroupName(groupName, group)); } } @@ -195,7 +201,7 @@ public class JWTWebServiceController { List<GroupEntity> groups = membershipManager.getUserGroups(parent, userId); try (PrintWriter pw = new PrintWriter(response.getOutputStream())) { - for (String groupName : getGroupsNames(groups)) { + for (String groupName : groupNameService.getGroupsNames(groups)) { pw.println(getShortGroupName(groupName, group)); } } @@ -265,6 +271,27 @@ public class JWTWebServiceController { response.setStatus(HttpServletResponse.SC_NO_CONTENT); } + @PostMapping(value = "/invited-registration", produces = MediaType.TEXT_PLAIN_VALUE) + public void addInvitedRegistration(@RequestParam("token_hash") String tokenHash, @RequestParam("email") String email, + @RequestParam("groups") String groupNamesAndPermissionsParam, HttpServletResponse response) { + + Map<GroupEntity, Permission> groupsPermissions = new HashMap<>(); + + for (String param : groupNamesAndPermissionsParam.split("\n")) { + if (!param.isEmpty()) { + int lastSpaceIndex = param.lastIndexOf(" "); + String groupName = param.substring(0, lastSpaceIndex); + Permission permission = Permission.valueOf(param.substring(lastSpaceIndex + 1)); + GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupName)); + groupsPermissions.put(groupEntity, permission); + } + } + + invitedRegistrationManager.addInvitedRegistration(tokenHash, email, groupsPermissions); + + response.setStatus(HttpServletResponse.SC_CREATED); + } + private GroupEntity getGroupFromNames(List<String> groupNames) { if (groupNames.isEmpty()) { return getRoot(); @@ -292,27 +319,6 @@ public class JWTWebServiceController { .orElseThrow(() -> new IllegalStateException("Missing root group")); } - /** - * Returns the list of the group complete names, given a list of GroupEntity - * objects. TODO: probably this logic is duplicated inside GroupNameService. - * Check this. - */ - private List<String> getGroupsNames(List<GroupEntity> groups) { - - // We need to return the complete group name, so it is necessary to load - // all the parents too. - Map<String, String> idNameMap = new HashMap<>(); - Set<String> allIdentifiers = getAllIdentifiers(groups); - for (GroupEntity group : groupsDAO.findGroupsByIds(allIdentifiers)) { - idNameMap.put(group.getId(), group.getName()); - } - - List<String> names = new ArrayList<>(); - for (GroupEntity group : groups) { - names.add(getGroupCompleteName(group, idNameMap)); - } - return names; - } private List<String> extractGroupNames(Optional<String> group) { return extractGroupNames(group.orElse(null)); @@ -341,44 +347,6 @@ public class JWTWebServiceController { return names; } - private Set<String> getAllIdentifiers(List<GroupEntity> groups) { - - Set<String> allIdentifiers = new HashSet<>(); - for (GroupEntity group : groups) { - if (!"".equals(group.getPath())) { - String[] ids = group.getPath().split("\\."); - for (String id : ids) { - allIdentifiers.add(id); - } - } - } - - return allIdentifiers; - } - - private String getGroupCompleteName(GroupEntity group, Map<String, String> idNameMap) { - - if ("ROOT".equals(group.getId())) { - return group.getName(); - } - - List<String> names = new ArrayList<>(); - - for (String groupId : group.getPath().split("\\.")) { - - String groupName = idNameMap.get(groupId); - - // Dot inside names is considered a special character (because it is - // used to separate the group from its parents), so we use a - // backslash to escape it (client apps need to be aware of this). - groupName = groupName.replace("\\.", "\\\\."); - - names.add(groupName); - } - - return String.join(".", names); - } - private String getShortGroupName(String completeGroupName, Optional<String> groupPrefix) { if (groupPrefix.isPresent()) { return completeGroupName.substring(groupPrefix.get().length() + 1); diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java new file mode 100644 index 0000000..f04bc1f --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java @@ -0,0 +1,12 @@ +package it.inaf.ia2.gms.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java new file mode 100644 index 0000000..a8547e3 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java @@ -0,0 +1,119 @@ +package it.inaf.ia2.gms.manager; + +import it.inaf.ia2.gms.exception.NotFoundException; +import it.inaf.ia2.gms.exception.UnauthorizedException; +import it.inaf.ia2.gms.model.Permission; +import it.inaf.ia2.gms.persistence.GroupsDAO; +import it.inaf.ia2.gms.persistence.InvitedRegistrationDAO; +import it.inaf.ia2.gms.persistence.LoggingDAO; +import it.inaf.ia2.gms.persistence.MembershipsDAO; +import it.inaf.ia2.gms.persistence.model.GroupEntity; +import it.inaf.ia2.gms.persistence.model.InvitedRegistration; +import it.inaf.ia2.gms.persistence.model.MembershipEntity; +import it.inaf.ia2.gms.service.PermissionsService; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import javax.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class InvitedRegistrationManager extends UserAwareComponent { + + private static final String INVITED_REGISTRATION = "invited-registration"; + + @Autowired + private GroupsDAO groupsDAO; + + @Autowired + private MembershipsDAO membershipsDAO; + + @Autowired + private PermissionsService permissionsService; + + @Autowired + private PermissionsManager permissionsManager; + + @Autowired + private InvitedRegistrationDAO invitedRegistrationDAO; + + @Autowired + private LoggingDAO loggingDAO; + + @Autowired + private HttpSession httpSession; + + public void addInvitedRegistration(String tokenHash, String email, Map<GroupEntity, Permission> groupsPermissions) { + + Map<String, Permission> groupIdsPermissions = new HashMap<>(); + for (Map.Entry<GroupEntity, Permission> entry : groupsPermissions.entrySet()) { + GroupEntity group = entry.getKey(); + if (permissionsManager.getCurrentUserPermission(group) != Permission.ADMIN) { + throw new UnauthorizedException("You don't have the permission to perform invited registrations"); + } + groupIdsPermissions.put(group.getId(), entry.getValue()); + } + + InvitedRegistration invitedRegistration = new InvitedRegistration() + .setId(UUID.randomUUID().toString().replaceAll("-", "")) + .setEmail(email) + .setTokenHash(tokenHash) + .setGroupsPermissions(groupIdsPermissions); + + invitedRegistrationDAO.addInvitedRegistration(invitedRegistration); + } + + public InvitedRegistration getInvitedRegistrationFromToken(String token) { + + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8)); + String tokenHash = Base64.getEncoder().encodeToString(hash); + + InvitedRegistration invitedRegistration = invitedRegistrationDAO.getInvitedRegistrationFromToken(tokenHash) + .orElseThrow(() -> new NotFoundException("No invited registrations found for this token")); + + httpSession.setAttribute(INVITED_REGISTRATION, invitedRegistration); + + loggingDAO.logAction("Started invited registration for email " + invitedRegistration.getEmail()); + + return invitedRegistration; + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + + public Optional<InvitedRegistration> completeInvitedRegistrationIfNecessary() { + + InvitedRegistration invitedRegistration = (InvitedRegistration) httpSession.getAttribute(INVITED_REGISTRATION); + + if (invitedRegistration != null) { + + for (Map.Entry<String, Permission> entry : invitedRegistration.getGroupsPermissions().entrySet()) { + String groupId = entry.getKey(); + String userId = getCurrentUserId(); + + GroupEntity groupEntity = groupsDAO.findGroupById(groupId).get(); + + MembershipEntity membershipEntity = new MembershipEntity(); + membershipEntity.setUserId(userId); + membershipEntity.setGroupId(groupId); + membershipsDAO.addMember(membershipEntity); + + permissionsService.addPermission(groupEntity, userId, entry.getValue()); + } + + invitedRegistrationDAO.setRegistrationDone(invitedRegistration); + + httpSession.removeAttribute(INVITED_REGISTRATION); + } + + return Optional.ofNullable(invitedRegistration); + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java index e7047fe..3824015 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java @@ -1,9 +1,11 @@ package it.inaf.ia2.gms.persistence; +import it.inaf.ia2.gms.model.Permission; import it.inaf.ia2.gms.persistence.model.InvitedRegistration; import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.List; +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; @@ -34,13 +36,15 @@ public class InvitedRegistrationDAO { return ps; }); - for (String groupId : invitedRegistration.getGroupIds()) { - String sqlReqGroup = "INSERT INTO invited_registration_request_group (request_id, group_id) VALUES (?, ?)"; + for (Map.Entry<String, Permission> entry : invitedRegistration.getGroupsPermissions().entrySet()) { + + String sqlReqGroup = "INSERT INTO invited_registration_request_group (request_id, group_id, permission) VALUES (?, ?, ?)"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sqlReqGroup); ps.setString(1, invitedRegistration.getId()); - ps.setString(2, groupId); + ps.setString(2, entry.getKey()); + ps.setObject(3, entry.getValue().toString(), Types.OTHER); return ps; }); } @@ -48,7 +52,7 @@ public class InvitedRegistrationDAO { public Optional<InvitedRegistration> getInvitedRegistrationFromToken(String tokenHash) { - String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND !done"; + String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND done IS NOT true"; InvitedRegistration registration = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sqlReq); @@ -66,33 +70,35 @@ public class InvitedRegistrationDAO { if (registration != null) { - String sqlReqGroup = "SELECT group_id FROM invited_registration_request_group WHERE request_id = ?"; + String sqlReqGroup = "SELECT group_id, permission FROM invited_registration_request_group WHERE request_id = ?"; - List<String> groupIds = jdbcTemplate.query(conn -> { + Map<String, Permission> groupsPermissions = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sqlReqGroup); ps.setString(1, registration.getId()); return ps; }, resultSet -> { - List<String> groups = new ArrayList<>(); + Map<String, Permission> map = new HashMap<>(); while (resultSet.next()) { - groups.add(resultSet.getString("group_id")); + String groupId = resultSet.getString("group_id"); + Permission permission = Permission.valueOf(resultSet.getString("permission")); + map.put(groupId, permission); } - return groups; + return map; }); - registration.setGroupIds(groupIds); + registration.setGroupsPermissions(groupsPermissions); } return Optional.ofNullable(registration); } - public void setRegistrationDone(String tokenHash) { + public void setRegistrationDone(InvitedRegistration invitedRegistration) { - String sql = "UPDATE invited_registration_request SET done = true WHERE token_hash = ?"; + String sql = "UPDATE invited_registration_request SET done = true WHERE id = ?"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); - ps.setString(1, tokenHash); + ps.setString(1, invitedRegistration.getId()); 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 aedfbf8..6f0b4c6 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 @@ -99,7 +99,8 @@ public class MembershipsDAO { public MembershipEntity addMember(MembershipEntity membership) { - String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES (?, ?)"; + String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES (?, ?)\n" + + "ON CONFLICT (group_id, user_id) DO NOTHING"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java index da709f7..f0e4769 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java @@ -1,6 +1,7 @@ package it.inaf.ia2.gms.persistence.model; -import java.util.List; +import it.inaf.ia2.gms.model.Permission; +import java.util.Map; public class InvitedRegistration { @@ -8,45 +9,50 @@ public class InvitedRegistration { private String tokenHash; private String email; private boolean done; - private List<String> groupIds; + private Map<String, Permission> groupsPermissions; public String getId() { return id; } - public void setId(String id) { + public InvitedRegistration setId(String id) { this.id = id; + return this; } public String getTokenHash() { return tokenHash; } - public void setTokenHash(String tokenHash) { + public InvitedRegistration setTokenHash(String tokenHash) { this.tokenHash = tokenHash; + return this; } public String getEmail() { return email; } - public void setEmail(String email) { + public InvitedRegistration setEmail(String email) { this.email = email; + return this; } public boolean isDone() { return done; } - public void setDone(boolean done) { + public InvitedRegistration setDone(boolean done) { this.done = done; + return this; } - public List<String> getGroupIds() { - return groupIds; + public Map<String, Permission> getGroupsPermissions() { + return groupsPermissions; } - public void setGroupIds(List<String> groupIds) { - this.groupIds = groupIds; + public InvitedRegistration setGroupsPermissions(Map<String, Permission> groupsPermissions) { + this.groupsPermissions = groupsPermissions; + return this; } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java index 20c04e9..0cb329b 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java +++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java @@ -22,6 +22,69 @@ public class GroupNameService { @Autowired private GroupsDAO groupsDAO; + public List<String> getGroupsNamesFromIdentifiers(Set<String> groupIdentifiers) { + return getGroupsNames(groupsDAO.findGroupsByIds(groupIdentifiers)); + } + + /** + * Returns the list of the group complete names, given a list of GroupEntity + * objects. + */ + public List<String> getGroupsNames(List<GroupEntity> groups) { + + // We need to return the complete group name, so it is necessary to load + // all the parents too. + Map<String, String> idNameMap = new HashMap<>(); + Set<String> allIdentifiers = getAllIdentifiers(groups); + for (GroupEntity group : groupsDAO.findGroupsByIds(allIdentifiers)) { + idNameMap.put(group.getId(), group.getName()); + } + + List<String> names = new ArrayList<>(); + for (GroupEntity group : groups) { + names.add(getGroupCompleteName(group, idNameMap)); + } + return names; + } + + private Set<String> getAllIdentifiers(List<GroupEntity> groups) { + + Set<String> allIdentifiers = new HashSet<>(); + for (GroupEntity group : groups) { + if (!"".equals(group.getPath())) { + String[] ids = group.getPath().split("\\."); + for (String id : ids) { + allIdentifiers.add(id); + } + } + } + + return allIdentifiers; + } + + private String getGroupCompleteName(GroupEntity group, Map<String, String> idNameMap) { + + if ("ROOT".equals(group.getId())) { + return group.getName(); + } + + List<String> names = new ArrayList<>(); + + for (String groupId : group.getPath().split("\\.")) { + + String groupName = idNameMap.get(groupId); + + // Dot inside names is considered a special character (because it is + // used to separate the group from its parents), so we use a + // backslash to escape it (client apps need to be aware of this). + groupName = groupName.replace("\\.", "\\\\."); + + names.add(groupName); + } + + return String.join(".", names); + } + /** * @param groupsIdPath map having group id as keys and group paths as values * @return map having group id as keys and group names as values diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql index b9c0a94..2849c2f 100644 --- a/gms/src/main/resources/sql/init.sql +++ b/gms/src/main/resources/sql/init.sql @@ -56,6 +56,8 @@ CREATE TABLE invited_registration_request ( CREATE TABLE invited_registration_request_group ( request_id varchar NOT NULL, group_id varchar NOT NULL, - PRIMARY KEY (request_id, group_id), - FOREIGN KEY (request_id) REFERENCES invited_registration_request(id) + permission permission_type NOT NULL, + PRIMARY KEY (request_id, group_id, permission), + FOREIGN KEY (request_id) REFERENCES invited_registration_request(id), + FOREIGN KEY (group_id) REFERENCES gms_group(id) ); diff --git a/gms/src/main/resources/templates/invited-registration.html b/gms/src/main/resources/templates/invited-registration.html new file mode 100644 index 0000000..32e9a46 --- /dev/null +++ b/gms/src/main/resources/templates/invited-registration.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <title>Invited registration</title> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" /> + </head> + <body> + <div class="container mt-4"> + <h1 class="mb-3">Invited registration</h1> + <p>Hi <strong>#EMAIL#</strong>, at the end of this procedure you will be added to the following groups:</p> + #GROUPS# + <p>Just perform a login!</p> + <p><a class="btn btn-primary" href="#HOME#">Login</a></p> + </div> + </body> +</html> diff --git a/gms/src/main/resources/templates/registration-completed.html b/gms/src/main/resources/templates/registration-completed.html new file mode 100644 index 0000000..5ded067 --- /dev/null +++ b/gms/src/main/resources/templates/registration-completed.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <title>Invited registration completed</title> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" /> + </head> + <body> + <div class="container mt-4"> + <h1 class="mb-3">Invited registration completed!</h1> + <p>You are now member of the following groups:</p> + #GROUPS# + <p><a class="btn btn-primary" href="#HOME#">Ok</a></p> + </div> + </body> +</html> -- GitLab