Skip to content
JWTWebServiceController.java 13.5 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.exception.UnauthorizedException;
import it.inaf.ia2.gms.model.Permission;
import it.inaf.ia2.gms.persistence.GroupsDAO;
import it.inaf.ia2.gms.persistence.LoggingDAO;
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.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.MembersService;
import it.inaf.ia2.gms.service.PermissionUtils;
import it.inaf.ia2.gms.service.PermissionsService;
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.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.RestController;

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

    @Autowired
    private MembershipsDAO membershipsDAO;

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

    @Autowired
    private GroupsDAO groupsDAO;

    @Autowired
    private GroupsService groupsService;

    @Autowired
    private MembersService membersService;

    @Autowired
    private PermissionsService permissionsService;

    @Autowired
    private PermissionsDAO permissionsDAO;

    @Autowired
    private LoggingDAO loggingDAO;

    /**
     * This endpoint is compliant with the IVOA GMS standard.
     */
    @GetMapping(value = "/search", produces = MediaType.TEXT_PLAIN_VALUE)
    public void getGroups(Principal principal, HttpServletResponse response) throws IOException {

        List<GroupEntity> memberships = membershipsDAO.getUserMemberships(principal.getName());

        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, Principal principal, HttpServletResponse response) throws IOException {

        String userId = principal.getName();

        List<String> groupNames = extractGroupNames(group);

        boolean isMember = membershipsDAO.isMemberOf(userId, "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 = membershipsDAO.isMemberOf(userId, 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)) {
                if (group.isPresent()) {
                    String shortName = groupName.substring(group.get().length() + 1);
                    pw.println(shortName);
                } else {
                    pw.println(groupName);
                }
            }
        }
    }

    @PostMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
    public void createGroup(@PathVariable("group") String group, Principal principal, HttpServletRequest request, HttpServletResponse response) throws IOException {

        String userId = principal.getName();

        List<String> groupNames = extractGroupNames(group);
        GroupEntity parent = getParentFromNames(groupNames);

        String newGroupName = groupNames.get(groupNames.size() - 1);

        if (permissionsService.getUserPermissionForGroup(parent, userId) != Permission.ADMIN) {
            loggingDAO.logAction("Unauthorized create group request, group_name=" + newGroupName);
            throw new UnauthorizedException("Missing admin permission");
        }

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

        groupsService.addGroup(parent, newGroupName, leaf);
        loggingDAO.logAction("Added group: parent_path=" + parent.getPath() + ", group_name=" + newGroupName);

        response.setStatus(HttpServletResponse.SC_CREATED);
        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
            pw.println(group);
        }
    }

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

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(group));

        String userId = principal.getName();
        membersService.verifyUserCanManageMembers(groupEntity, userId);

        String targetUserId = request.getParameter("user_id");
        if (targetUserId == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing user_id parameter");
            return;
        }
        membersService.addMember(groupEntity.getId(), targetUserId);
        loggingDAO.logAction("Added member, group_id=" + groupEntity.getId() + ", user_id=" + targetUserId);
    }

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

        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));

        String userId = principal.getName();
        permissionsService.verifyUserCanManagePermissions(groupEntity, userId);

        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;
        }
        Permission permission = Permission.valueOf(permissionParam);
        permissionsService.addPermission(groupEntity, targetUserId, permission);
        loggingDAO.logAction("Permission added, group_id=" + groupEntity.getId() + ", user_id="
                + targetUserId + ", permission=" + permission);
    }

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

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

    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 IllegalArgumentException("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.
     */
    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);
    }
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);
    }