Skip to content
JWTWebServiceController.java 17 KiB
Newer Older
package it.inaf.ia2.gms.controller;

Sonia Zorba's avatar
Sonia Zorba committed
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;
import it.inaf.ia2.gms.model.RapUser;
import it.inaf.ia2.gms.model.response.UserPermission;
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;
Sonia Zorba's avatar
Sonia Zorba committed
import it.inaf.ia2.gms.service.JoinService;
import it.inaf.ia2.gms.service.PermissionUtils;
import it.inaf.ia2.gms.service.SearchService;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
Sonia Zorba's avatar
Sonia Zorba committed
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Sonia Zorba's avatar
Sonia Zorba committed
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Web service called by other web applications using JWT (delegation).
 */
@RestController
@RequestMapping("/ws/jwt")
public class JWTWebServiceController {

Sonia Zorba's avatar
Sonia Zorba committed
    @Autowired
    private JoinService joinService;

    @Autowired
    private GroupsDAO groupsDAO;

    private GroupsManager groupsManager;
    private GroupsService groupsService;
    @Autowired
    private GroupNameService groupNameService;
    private MembershipManager membershipManager;
    private PermissionsManager permissionsManager;
    private PermissionsDAO permissionsDAO;
    @Autowired
    private SearchService searchService;

    @Autowired
    private InvitedRegistrationManager invitedRegistrationManager;

    /**
     * This endpoint is compliant with the IVOA GMS standard.
     */
    @GetMapping(value = "/search", produces = MediaType.TEXT_PLAIN_VALUE)
    public void getGroups(HttpServletResponse response) throws IOException {
        List<GroupEntity> memberships = membershipManager.getCurrentUserMemberships();
        List<String> names = groupNameService.getGroupsNames(memberships);

        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {

            for (String name : names) {
                pw.println(name);
    /**
     * This endpoint is compliant with the IVOA GMS standard. Warning: for
     * supporting the groups of groups (with dots inside) the path variable must
     * be defined adding ".+", otherwise Spring will think it is a file
     * extension (thanks https://stackoverflow.com/a/16333149/771431)
     */
    @GetMapping(value = "/search/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
    public void isMemberOf(@PathVariable("group") String group, HttpServletResponse response) throws IOException {

        List<String> groupNames = extractGroupNames(group);

        boolean isMember = membershipManager.isCurrentUserMemberOf("ROOT");
        if (!isMember) {
            String parentPath = ""; // starting from ROOT
            for (String groupName : groupNames) {
                Optional<GroupEntity> optionalGroup = groupsDAO.findGroupByParentAndName(parentPath, groupName);
                if (optionalGroup.isPresent()) {
                    GroupEntity groupEntity = optionalGroup.get();
                    parentPath = groupEntity.getPath();
                    isMember = membershipManager.isCurrentUserMemberOf(groupEntity.getId());
                    if (isMember) {
                        break;
                    }
                } else {
        if (isMember) {
            try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
                pw.println(group);
            }
        }
        // else: empty response (as defined by GMS standard)
    }

    @GetMapping(value = {"/list/{group:.+}", "/list"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void listGroups(@PathVariable("group") Optional<String> group, Principal principal, HttpServletResponse response) throws IOException {

        String userId = principal.getName();

        List<String> groupNames = extractGroupNames(group);
        GroupEntity parentGroup = getGroupFromNames(groupNames);

        List<GroupEntity> allSubGroups = groupsDAO.getDirectSubGroups(parentGroup.getPath());

        // Select only the groups visible to the user
        List<PermissionEntity> permissions = permissionsDAO.findUserPermissions(userId);
        List<GroupEntity> visibleSubgroups = new ArrayList<>();

        for (GroupEntity subgroup : allSubGroups) {
            PermissionUtils.getGroupPermission(subgroup, permissions).ifPresent(permission -> {
                visibleSubgroups.add(subgroup);
            });
        }

        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
            for (String groupName : groupNameService.getGroupsNames(visibleSubgroups)) {
                pw.println(getShortGroupName(groupName, group));
    /**
     * Creates a group and its ancestors if they are missing. It doesn't fail if
     * the last group already exists.
     */
    @PostMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
    public void createGroup(@PathVariable("group") String groupParam, HttpServletRequest request, HttpServletResponse response) throws IOException {
        List<String> groupNames = extractGroupNames(groupParam);

        String leafParam = request.getParameter("leaf");
        boolean leaf = leafParam == null ? false : Boolean.valueOf(leafParam);

        GroupEntity group = groupsManager.getRoot();
        for (int i = 0; i < groupNames.size(); i++) {
            String name = groupNames.get(i);
            Optional<GroupEntity> optGroup = groupsService.findGroupByParentAndName(group, name);
            if (optGroup.isPresent()) {
                group = optGroup.get();
            } else {
                group = groupsManager.createGroup(group, name, i == groupNames.size() - 1 ? leaf : false);
            }
        }

        response.setStatus(HttpServletResponse.SC_CREATED);
        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
    @DeleteMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
    public void deleteGroup(@PathVariable("group") String groupParam, HttpServletResponse response) {
        GroupEntity group = getGroupFromNames(extractGroupNames(groupParam));
        groupsDAO.deleteGroupById(group.getId());
        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
    }

    @GetMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void getMembership(@PathVariable("group") Optional<String> group, @RequestParam("user_id") String userId, HttpServletResponse response) throws IOException {

        GroupEntity parent = getGroupFromNames(extractGroupNames(group));

        List<GroupEntity> groups = membershipManager.getUserGroups(parent, userId);

        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
            for (String groupName : groupNameService.getGroupsNames(groups)) {
                pw.println(getShortGroupName(groupName, group));
            }
        }
    }

    @PostMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void addMember(@PathVariable("group") Optional<String> group, HttpServletRequest request, HttpServletResponse response) throws IOException {

        String targetUserId = request.getParameter("user_id");
        if (targetUserId == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing user_id parameter");
            return;
        }

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(group));
        membershipManager.addMember(groupEntity, targetUserId);
    }
    @DeleteMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void removeMember(@PathVariable("group") Optional<String> group, @RequestParam("user_id") String userId,
            HttpServletRequest request, HttpServletResponse response) throws IOException {

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(group));
        membershipManager.removeMember(groupEntity, userId);

        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
    }

Sonia Zorba's avatar
Sonia Zorba committed
    @GetMapping(value = {"/permission/{group:.+}", "/permission"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void getUserPermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") Optional<String> userId, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (userId.isPresent()) {
            try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
                for (UserPermission userPermission : searchService.getUserPermission(userId.get(), permissionsManager.getCurrentUserPermissions(getRoot()))) {
                    String group = String.join(".", userPermission.getGroupCompleteName());
                    pw.println(group + " " + userPermission.getPermission());
                }
            }
        } else {
            GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
            try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
                for (it.inaf.ia2.gms.model.UserPermission up : permissionsManager.getAllPermissions(groupEntity)) {
                    pw.println(up.getUser().getId() + " " + up.getPermission());
                }
    @PostMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public void addPermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String targetUserId, @RequestParam("permission") Permission permission) throws IOException {
        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
        permissionsManager.addPermission(groupEntity, targetUserId, permission);
    }
    @PutMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public void setPermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String targetUserId, @RequestParam("permission") Permission permission) throws IOException {
        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
        permissionsManager.createOrUpdatePermission(groupEntity, targetUserId, permission);
    @DeleteMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void removePermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String userId,
            HttpServletRequest request, HttpServletResponse response) throws IOException {

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
        permissionsManager.removePermission(groupEntity, userId);

        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);
    }

    @GetMapping(value = "/email/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
    public void getEmailOfMembers(@PathVariable("group") String groupNames, @RequestParam("permission") Optional<Permission> permission, HttpServletResponse response) throws IOException {

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));

        Set<String> selectedUserIds = null;
        if (permission.isPresent()) {
            Permission desiredPermission = permission.get();
            selectedUserIds = new HashSet<>();
            for (PermissionEntity groupsPermission : permissionsDAO.getGroupsPermissions(groupEntity.getId())) {
                if (Permission.includes(groupsPermission.getPermission(), desiredPermission)) {
                    selectedUserIds.add(groupsPermission.getUserId());
                }
            }
        }

        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
            for (RapUser member : membershipManager.getMembers(groupEntity)) {
                if (selectedUserIds == null || selectedUserIds.contains(member.getId())) {
                    pw.println(member.getPrimaryEmail());
                }
            }
        }
    }

    private GroupEntity getGroupFromNames(List<String> groupNames) {
        if (groupNames.isEmpty()) {
            return getRoot();
        }
        return getGroupFromNamesAndIndex(groupNames, groupNames.size() - 1);
    }

    private GroupEntity getGroupFromNamesAndIndex(List<String> groupNames, int index) {
        String parentPath = ""; // starting from ROOT
        GroupEntity group = null;
        for (int i = 0; i < index + 1; i++) {
            String groupName = groupNames.get(i);
            group = groupsDAO.findGroupByParentAndName(parentPath, groupName)
                    .orElseThrow(() -> new BadRequestException("Unable to find group " + groupName));
            parentPath = group.getPath();
        }
        if (group == null) {
            throw new IllegalStateException();
        }
        return group;
    }

    private GroupEntity getRoot() {
        return groupsDAO.findGroupById("ROOT")
                .orElseThrow(() -> new IllegalStateException("Missing root group"));
    }

    private List<String> extractGroupNames(Optional<String> group) {
        return extractGroupNames(group.orElse(null));
    }

    private List<String> extractGroupNames(String groupStr) {

        if (groupStr == null || groupStr.isEmpty()) {
            return new ArrayList<>();
        }

        List<String> names = new ArrayList<>();
        String currentName = "";
        for (int i = 0; i < groupStr.length(); i++) {
            char c = groupStr.charAt(i);
            // dot is the group separator and it must be escaped if used inside
            // group names
            if (c == '.' && groupStr.charAt(i - 1) != '\\') {
                names.add(currentName.replace("\\.", "."));
                currentName = "";
            } else {
                currentName += c;
            }
        }
        names.add(currentName);
        return names;
    }

    private String getShortGroupName(String completeGroupName, Optional<String> groupPrefix) {
        if (groupPrefix.isPresent()) {
            return completeGroupName.substring(groupPrefix.get().length() + 1);
        }
        return completeGroupName;
    }

Sonia Zorba's avatar
Sonia Zorba committed
    @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);
    }