Skip to content
JWTWebServiceController.java 15.6 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.MembershipManager;
import it.inaf.ia2.gms.manager.PermissionsManager;
import it.inaf.ia2.gms.model.Permission;
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.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.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;
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.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;
    private MembershipManager membershipManager;
    private PermissionsManager permissionsManager;
    private PermissionsDAO permissionsDAO;
    @Autowired
    private SearchService searchService;

    /**
     * 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 = 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 : 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 : 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);
    }

    @GetMapping(value = "/permission", produces = MediaType.TEXT_PLAIN_VALUE)
    public void getUserPermission(@RequestParam("user_id") String userId, HttpServletRequest request, HttpServletResponse response) throws IOException {

        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
            for (UserPermission userPermission : searchService.getUserPermission(userId, permissionsManager.getCurrentUserPermissions(getRoot()))) {
                String group = String.join(".", userPermission.getGroupCompleteName());
                pw.println(group + " " + userPermission.getPermission());
            }
        }
    }

    @PostMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public void addPermission(@PathVariable("group") Optional<String> groupNames, 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;
        }
        String permissionParam = request.getParameter("permission");
        if (permissionParam == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing permission parameter");
            return;
        }

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));

        permissionsManager.addPermission(groupEntity, targetUserId, Permission.valueOf(permissionParam));
    @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);
    }

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

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

    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 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) {

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