/* * Copyright 2012 SURFnet bv, The Netherlands * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package teams.control; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.NoSuchMessageException; import org.springframework.core.env.Environment; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.support.SessionStatus; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.view.RedirectView; import teams.Application; import teams.domain.*; import teams.interceptor.LoginInterceptor; import teams.service.*; import teams.util.AuditLog; import teams.util.ControllerUtil; import teams.util.TokenUtil; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.*; import java.util.stream.Collectors; import static java.net.URLDecoder.decode; import static java.nio.charset.StandardCharsets.UTF_8; import static teams.interceptor.LoginInterceptor.PERSON_SESSION_KEY; import static teams.util.TokenUtil.*; import static teams.util.ViewUtil.escapeViewParameters; /** * {@link Controller} that handles the detail team page of a logged in user. */ @Controller @SessionAttributes(TokenUtil.TOKENCHECK) public class DetailTeamController { private static final Logger LOG = LoggerFactory.getLogger(DetailTeamController.class); private static final String ADMIN = "0"; private static final String ADMIN_LEAVE_TEAM = "error.AdminCannotLeaveTeam"; private static final String NOT_AUTHORIZED_DELETE_MEMBER = "error.NotAuthorizedToDeleteMember"; protected static final String MEMBER_PARAM = "member"; protected static final String ROLE_PARAM = "role"; protected static final String PENDING_REQUESTS_PARAM = "pendingRequests"; protected static final String INVITATIONS_PARAM = "invitations"; private static final int PAGESIZE = 10; @Autowired private VootClient vootClient; @Autowired private GrouperTeamService grouperTeamService; @Autowired private TeamInviteService teamInviteService; @Autowired private JoinTeamRequestService joinTeamRequestService; @Autowired private TeamExternalGroupDao teamExternalGroupDao; @Autowired private LocaleResolver localeResolver; @Autowired private ControllerUtil controllerUtil; @Autowired private MessageSource messageSource; @Value("${grouperPowerUser}") private String grouperPowerUser; @Value("${maxInvitations}") private Integer maxInvitations; @Autowired private Environment environment; @RequestMapping("/detailteam.shtml") public String detailTeam(ModelMap modelMap, HttpServletRequest request, Locale locale, @RequestParam("team") String teamId, @RequestParam(value = "mes", required = false) String message) throws IOException { Person person = (Person) request.getSession().getAttribute(LoginInterceptor.PERSON_SESSION_KEY); Team team = findTeam(teamId, () -> {}); Set<Role> roles = team.getMembers().stream() .filter(m -> m.getId().equals(person.getId())) .findFirst() .map(Member::getRoles) .orElse(Collections.emptySet()); if (StringUtils.hasText(message) && messageExists(message, locale)) { modelMap.addAttribute("message", message); } // Check if there is only one admin for a team boolean onlyAdmin = grouperTeamService.findAdmins(team).size() <= 1; modelMap.addAttribute("onlyAdmin", onlyAdmin); modelMap.addAttribute(INVITATIONS_PARAM, teamInviteService.findInvitationsForTeamExcludeAccepted(team)); int offset = getOffset(request); modelMap.addAttribute("pager", new Pager(team.getMembers().size(), offset, PAGESIZE)); modelMap.addAttribute("team", team); modelMap.addAttribute("adminRole", Role.Admin); modelMap.addAttribute("managerRole", Role.Manager); modelMap.addAttribute("memberRole", Role.Member); modelMap.addAttribute("noRole", Role.None); modelMap.addAttribute(TOKENCHECK, generateSessionToken()); modelMap.addAttribute("maxInvitations", maxInvitations); if (roles.contains(Role.Admin)) { modelMap.addAttribute(PENDING_REQUESTS_PARAM, getRequesters(team)); modelMap.addAttribute(ROLE_PARAM, Role.Admin); } else if (roles.contains(Role.Manager)) { modelMap.addAttribute(PENDING_REQUESTS_PARAM, getRequesters(team)); modelMap.addAttribute(ROLE_PARAM, Role.Manager); } else if (roles.contains(Role.Member)) { modelMap.addAttribute(ROLE_PARAM, Role.Member); } else { modelMap.addAttribute(ROLE_PARAM, Role.None); } if (!Role.None.equals(modelMap.get(ROLE_PARAM))) { addLinkedExternalGroupsToModelMap(person.getId(), teamId, modelMap); } modelMap.addAttribute("groupzyEnabled", environment.acceptsProfiles(Application.GROUPZY_PROFILE_NAME)); return "detailteam"; } private boolean messageExists(String message, Locale locale) { try { messageSource.getMessage(message, new Object[] {}, locale); return true; } catch (NoSuchMessageException e) { return false; } } private int getOffset(HttpServletRequest request) { int offset = 0; String offsetParam = request.getParameter("offset"); if (StringUtils.hasText(offsetParam)) { try { offset = Integer.parseInt(offsetParam); } catch (NumberFormatException e) { // do nothing } } return offset; } private List<Person> getRequesters(Team team) { List<JoinTeamRequest> pendingRequests = joinTeamRequestService.findPendingRequests(team.getId()); return pendingRequests.stream() .map(request -> new Person(request.getPersonId(), null, request.getEmail(), null, null, request.getDisplayName())).collect(Collectors.toList()); } private void addLinkedExternalGroupsToModelMap(String personId, String teamId, ModelMap modelMap) { List<TeamExternalGroup> teamExternalGroups = teamExternalGroupDao.getByTeamIdentifier(teamId); if (!teamExternalGroups.isEmpty()) { List<ExternalGroup> groups = vootClient.groups(personId); Map<String, ExternalGroupProvider> groupProviderMap = new HashMap<>(); for (ExternalGroup group : groups) { groupProviderMap.put(group.getGroupProviderIdentifier(), group.getGroupProvider()); } modelMap.addAttribute("groupProviderMap", groupProviderMap); modelMap.addAttribute("teamExternalGroups", teamExternalGroups); } } @RequestMapping(value = "/doleaveteam.shtml", method = RequestMethod.POST) public RedirectView leaveTeam(ModelMap modelMap, HttpServletRequest request, @ModelAttribute(TokenUtil.TOKENCHECK) String sessionToken, @RequestParam String token, @RequestParam("team") String teamId, SessionStatus status) { Person person = (Person) request.getSession().getAttribute(LoginInterceptor.PERSON_SESSION_KEY); String personId = person.getId(); Runnable endingRequest = () -> { status.setComplete(); modelMap.clear(); }; Team team = findTeam(teamId, endingRequest); Set<Member> admins = grouperTeamService.findAdmins(team); if (admins.size() == 1 && admins.iterator().next().getId().equals(personId)) { status.setComplete(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s&mes=%s", teamId, ADMIN_LEAVE_TEAM), false, true, false); } grouperTeamService.deleteMember(team, personId); AuditLog.log("User {} left team {}", personId, teamId); endingRequest.run(); return new RedirectView("home.shtml?teams=my"); } @RequestMapping(value = "/dodeleteteam.shtml", method = RequestMethod.POST) public RedirectView deleteTeam(ModelMap modelMap, HttpServletRequest request, @ModelAttribute(TokenUtil.TOKENCHECK) String sessionToken, @RequestParam String token, @RequestParam("team") String teamId, SessionStatus status) throws UnsupportedEncodingException { checkTokens(sessionToken, token, status); Runnable endingRequest = () -> { status.setComplete(); modelMap.clear(); }; Person person = (Person) request.getSession().getAttribute(LoginInterceptor.PERSON_SESSION_KEY); String personId = person.getId(); validateArgument(teamId, endingRequest); Team team = grouperTeamService.findTeamById(teamId); Member member = grouperTeamService.findMember(team, personId); if (member.getRoles().contains(Role.Admin)) { // Delete the team List<Invitation> invitationsForTeam = teamInviteService.findAllInvitationsForTeam(team); for (Invitation invitation : invitationsForTeam) { teamInviteService.delete(invitation); } List<TeamExternalGroup> teamExternalGroups = teamExternalGroupDao.getByTeamIdentifier(teamId); for (TeamExternalGroup teamExternalGroup : teamExternalGroups) { teamExternalGroupDao.delete(teamExternalGroup); } grouperTeamService.deleteTeam(teamId); AuditLog.log("User {} deleted team {}", personId, teamId); status.setComplete(); return new RedirectView("home.shtml?teams=my", false, true, false); } endingRequest.run(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s", teamId)); } @RequestMapping(value = "/dodeletemember.shtml", method = RequestMethod.GET) public RedirectView deleteMember(ModelMap modelMap, HttpServletRequest request, @ModelAttribute(TokenUtil.TOKENCHECK) String sessionToken, @RequestParam String token, @RequestParam("team") String teamId, SessionStatus status) throws UnsupportedEncodingException { checkTokens(sessionToken, token, status); String personId = decode(request.getParameter(MEMBER_PARAM), UTF_8.name()); Person ownerPerson = (Person) request.getSession().getAttribute(LoginInterceptor.PERSON_SESSION_KEY); String ownerId = ownerPerson.getId(); if (!StringUtils.hasText(teamId) || !StringUtils.hasText(personId)) { status.setComplete(); modelMap.clear(); throw new RuntimeException("Parameter error."); } Team team = grouperTeamService.findTeamById(teamId); // fetch the logged in member Member owner = grouperTeamService.findMember(team, ownerId); Member member = grouperTeamService.findMember(team, personId); // Check whether the owner is admin and thus is granted to delete the // member. // Check whether the member that should be deleted is the logged in user. // This should not be possible, a logged in user should click the resign // from team button. if (owner.getRoles().contains(Role.Admin) && !personId.equals(ownerId)) { grouperTeamService.deleteMember(team, personId); AuditLog.log("Admin user {} deleted user {} from team {}", ownerId, personId, teamId); status.setComplete(); modelMap.clear(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s", teamId)); // if the owner is manager and the member is not an admin he can delete the member } else if (owner.getRoles().contains(Role.Manager) && !member.getRoles().contains(Role.Admin) && !personId.equals(ownerId)) { grouperTeamService.deleteMember(team, personId); AuditLog.log("Manager user {} deleted user {} from team {}", ownerId, personId, teamId); status.setComplete(); modelMap.clear(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s", teamId)); } status.setComplete(); modelMap.clear(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s&mes=%s", teamId, NOT_AUTHORIZED_DELETE_MEMBER)); } @RequestMapping(value = "/doaddremoverole.shtml", method = RequestMethod.POST) public RedirectView addOrRemoveRole(ModelMap modelMap, HttpServletRequest request, @ModelAttribute(TokenUtil.TOKENCHECK) String sessionToken, @RequestParam String token, SessionStatus status) throws IOException { checkTokens(sessionToken, token, status); String teamId = request.getParameter("teamId"); String memberId = request.getParameter("memberId"); String roleString = request.getParameter("roleId"); int offset = getOffset(request); String action = request.getParameter("doAction"); if (!StringUtils.hasText(teamId)) { status.setComplete(); modelMap.clear(); return new RedirectView("home.shtml?teams=my"); } if (!StringUtils.hasText(memberId) || !StringUtils.hasText(roleString) || !validAction(action)) { status.setComplete(); modelMap.clear(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s&mes=no.role.action&offset=%s", teamId, offset)); } Person person = (Person) request.getSession().getAttribute(PERSON_SESSION_KEY); Team team = grouperTeamService.findTeamById(teamId); if (team == null) { status.setComplete(); modelMap.clear(); return new RedirectView("home.shtml?teams=my"); } String message; if (action.equalsIgnoreCase("remove")) { // is the team null? return error message = removeRole(team, memberId, roleString, person.getId()); } else { message = addRole(team, memberId, roleString, person.getId()); } status.setComplete(); modelMap.clear(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s&mes=%s&offset=%d", teamId, message, offset)); } private boolean validAction(String action) { return StringUtils.hasText(action) && (action.equalsIgnoreCase("remove") || action.equalsIgnoreCase("add")); } private String removeRole(Team team, String memberId, String roleString, String loggedInUserId) { // The role admin can only be removed if there are more then one admins in a team. if (roleString.equals(ADMIN) && grouperTeamService.findAdmins(team).size() == 1) { return "no.role.added.admin.status"; } Role role = roleString.equals(ADMIN) ? Role.Admin : Role.Manager; if (grouperTeamService.removeMemberRole(team, memberId, role, loggedInUserId)) { AuditLog.log("User {} removed role {} from user {} in team {}", loggedInUserId, role, memberId, team.getId()); return "role.removed"; } else { return "no.role.removed"; } } private String addRole(Team team, String memberId, String roleString, String loggedInUserId) { Role role = roleString.equals(ADMIN) ? Role.Admin : Role.Manager; Member other = grouperTeamService.findMember(team, memberId); // Guests may not become admin if (other.isGuest() && role == Role.Admin) { return "no.role.added.guest.status"; } if (grouperTeamService.addMemberRole(team, memberId, role, loggedInUserId)) { AuditLog.log("User {} added role {} to user {} in team {}", loggedInUserId, role, memberId, team.getId()); return "role.added"; } else { return "no.role.added"; } } @RequestMapping(value = "/dodeleterequest.shtml", method = RequestMethod.POST) public RedirectView deleteJoinRequest(HttpServletRequest request, ModelMap modelMap, @ModelAttribute(TokenUtil.TOKENCHECK) String sessionToken, @RequestParam String token, @RequestParam("team") String teamId, SessionStatus status) throws UnsupportedEncodingException { return doHandleJoinRequest(modelMap, request, sessionToken, token, teamId, status, false); } @RequestMapping(value = "/doapproverequest.shtml", method = RequestMethod.POST) public RedirectView approveJoinRequest(HttpServletRequest request, ModelMap modelMap, @ModelAttribute(TokenUtil.TOKENCHECK) String sessionToken, @RequestParam() String token, @RequestParam("team") String teamId, SessionStatus status) throws UnsupportedEncodingException { return doHandleJoinRequest(modelMap, request, sessionToken, token, teamId, status, true); } private RedirectView doHandleJoinRequest(ModelMap modelMap, HttpServletRequest request, String sessionToken, String token, String teamId, SessionStatus status, boolean approve) throws UnsupportedEncodingException { checkTokens(sessionToken, token, status); Runnable endingRequest = () -> { status.setComplete(); modelMap.clear(); }; String memberId = decode(request.getParameter(MEMBER_PARAM), UTF_8.name()); Team team = findTeam(teamId, endingRequest); JoinTeamRequest pendingRequest = findJoinTeamRequest(memberId, team, endingRequest); Person loggedInPerson = (Person) request.getSession().getAttribute(LoginInterceptor.PERSON_SESSION_KEY); // Check if the user has the correct privileges if (!controllerUtil.hasUserAdministrativePrivileges(loggedInPerson, team)) { endingRequest.run(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s&mes=error.NotAuthorizedForAction", teamId)); } Person requester = new Person(pendingRequest.getPersonId(), null, pendingRequest.getEmail(), null, null, pendingRequest.getDisplayName()); if (approve) { grouperTeamService.addMember(team, requester); grouperTeamService.addMemberRole(team, memberId, Role.Member, grouperPowerUser); AuditLog.log("User {} approved join-team-request of user {} in team {}", loggedInPerson.getId(), requester.getId(), teamId); } joinTeamRequestService.delete(pendingRequest); AuditLog.log("Deleted join-team-request for user {} in team {}", pendingRequest.getPersonId(), teamId); trySendEmailToRequester(approve, requester, team, localeResolver.resolveLocale(request)); endingRequest.run(); return new RedirectView(escapeViewParameters("detailteam.shtml?team=%s", teamId)); } private void validateArgument(String argument, Runnable argumentMissing) { if (Strings.isNullOrEmpty(argument)) { argumentMissing.run(); throw new RuntimeException("Missing parameters for team or member"); } } private Team findTeam(String teamId, Runnable missing) { validateArgument(teamId, missing); Optional<Team> team = Optional.ofNullable(grouperTeamService.findTeamById(teamId)); if (!team.isPresent()) { missing.run(); } return team.orElseThrow(() -> new RuntimeException("Cannot find team with id " + teamId)); } private JoinTeamRequest findJoinTeamRequest(String memberId, Team team, Runnable missing) { validateArgument(memberId, missing); Optional<JoinTeamRequest> pendingRequest = Optional.ofNullable(joinTeamRequestService.findPendingRequest(memberId, team.getId())); if (!pendingRequest.isPresent()) { missing.run(); } return pendingRequest.orElseThrow(() -> new RuntimeException(String.format("Could not find join team request for %s", memberId))); } private void trySendEmailToRequester(boolean approve, Person person, Team team, Locale locale) { if (Strings.isNullOrEmpty(person.getEmail())) { LOG.debug("Could not send email, because {} has no email", person.getName()); return; } if (approve) { controllerUtil.sendAcceptMail(person, team, locale); } else { controllerUtil.sendDeclineMail(person, team, locale); } } }