Commit 17c4724e authored by Sonia Zorba's avatar Sonia Zorba
Browse files

#4 Implemented invited registration

parent ba6b02eb
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);
}
}
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);
}
}
}
......@@ -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");
}
/**
......
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";
}
......
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;
}
}
......@@ -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);
......
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);
}
}
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);
}
}
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"));