def edit_helper(self, param_list, user_id) -> ResponseTuple: """ Edit the properties of a specific team. Team leads can only edit the teams that they are a part of, but admins can edit any teams. :param param_list: List of parameters for editing team :param user_id: Slack ID of user who called command :return: error message if user has insufficient permission level or team edited unsuccessfully, otherwise return success message """ try: command_user = self.facade.retrieve(User, user_id) command_team = param_list['team_name'] team = get_team_by_name(self.facade, command_team) if not check_permissions(command_user, team): return self.permission_error, 200 msg = f"Team edited: {command_team}, " if param_list['name'] is not None: msg += f"name: {param_list['name']}, " team.display_name = param_list['name'] if param_list['platform'] is not None: msg += f"platform: {param_list['platform']}" team.platform = param_list['platform'] if param_list['folder'] is not None: msg += f"folder: {param_list['folder']}" team.folder = param_list['folder'] self.facade.store(team) ret = {'attachments': [team.get_attachment()], 'text': msg} return ret, 200 except LookupError: return self.lookup_error, 200
def handle_added(self, github_id: str, github_username: str, organization: str) -> ResponseTuple: """Help organization function if payload action is added.""" logging.info(f"user {github_username} added to {organization}") all_name = self._conf.github_team_all try: # Try to add the user to the 'all' team if it exists team_all = get_team_by_name(self._facade, all_name) self._gh.add_team_member(github_username, team_all.github_team_id) team_all.add_member(github_id) self._facade.store(team_all) except LookupError: # If that team doesn't exist, make it exist t_id = str(self._gh.org_create_team(self._conf.github_team_all)) self._gh.add_team_member(github_username, t_id) logging.info(f'team {all_name} created for {organization}') team = Team(t_id, all_name, all_name) team.add_member(github_id) self._facade.store(team) except RuntimeError as e: # If there are any other kinds of errors, we log it logging.error(e) return '', 200 logging.info(f'user {github_username} added to team {all_name}') return f"user {github_username} added to {organization}", 200
def view_helper(self, team_name) -> ResponseTuple: """ Return display information and members of specified team. :param team_name: name of team being viewed :return: error message if team not found, otherwise return team information """ try: team = get_team_by_name(self.facade, team_name) team_leads_set = team.team_leads team_leads_list = list( map(lambda i: ('github_user_id', str(i)), team_leads_set)) team_leads: List[User] = [] if team_leads_list: team_leads = self.facade.query_or(User, team_leads_list) names = set(map(lambda m: m.github_username, team_leads)) team.team_leads = names members_set = team.members members_list = list( map(lambda i: ('github_user_id', str(i)), members_set)) members: List[User] = [] if members_list: members = self.facade.query_or(User, members_list) names = set(map(lambda m: m.github_username, members)) team.members = names return {'attachments': [team.get_attachment()]}, 200 except LookupError: return self.lookup_error, 200
def team_delete(self, caller_id: str, gh_team_name: str) -> None: """ Permanently delete a team. :param gh_team_name: Github team name of the team to delete :param caller_id: Slack ID of user who called command :raises: LookupError if the calling user or the team to delete could not be found :raises: RuntimeError if more than one team has the specified team name :raises: PermissionError if the calling user does not have sufficient permissions to delete the specified team """ logging.info("Team delete command API called") command_user = self._db_facade.retrieve(User, caller_id) logging.debug(f"Calling user: {command_user.__str__()}") team = db_utils.get_team_by_name(self._db_facade, gh_team_name) if not check_permissions(command_user, team): msg = f"User with ID {caller_id} has insufficient permissions" \ f" to delete team {gh_team_name}" logging.error(msg) raise PermissionError(msg) self._gh_interface.org_delete_team(int(team.github_team_id)) self._db_facade.delete(Team, team.github_team_id) logging.info(f"{gh_team_name} successfully deleted")
def team_lead(self, caller_id: str, lead_id: str, gh_team_name: str, remove: bool = False) -> bool: """ Add a user as a team lead, and add them to team if not already added. :param caller_id: Slack ID of user who called command :param lead_id: Slack ID of user to declare as team lead :param gh_team_name: Github team name of team to add a lead to :param remove: if True, removes the user as team lead of the team :raises: LookupError if the calling user, the team to add a lead to could not be found, the user is not on the team, or the user is not a lead on the team :raises: RuntimeError if more than one team has the specified team name :raises: PermissionError if the calling user does not have sufficient permissions to add a lead to the specified team :returns: True if removal was successful, False otherwise """ logging.info("Team lead command API called") command_user = self._db_facade.retrieve(User, caller_id) logging.debug(f"Calling user: {command_user.__str__()}") team = db_utils.get_team_by_name(self._db_facade, gh_team_name) if not check_permissions(command_user, team): msg = f"User with ID {caller_id} has insufficient permissions" \ f" to add lead to team {gh_team_name}" logging.error(msg) raise PermissionError(msg) lead_user = self._db_facade.retrieve(User, lead_id) logging.debug(f"User to add as lead: {lead_user.__str__()}") if remove: if not team.has_member(lead_user.github_id): msg = f"User with Github ID {lead_user.github_id} not a " \ "member of specified team" logging.error(msg) raise LookupError(msg) if team.has_team_lead(lead_user.github_id): team.discard_team_lead(lead_user.github_id) discarded = self._db_facade.store(team) return discarded else: msg = f"User with Github ID {lead_user.github_id} not a " \ "lead of specified team" logging.error(msg) raise LookupError(msg) else: if not team.has_member(lead_user.github_id): team.add_member(lead_user.github_id) self._gh_interface.add_team_member(lead_user.github_username, team.github_team_id) team.add_team_lead(lead_user.github_id) added = self._db_facade.store(team) return added
def add_helper(self, args: Namespace, user_id: str) -> ResponseTuple: """ Add user to team. If user is not admin or team lead of specified team, the user will not be added and an error message is returned. :param args: Parameters for adding user :param user_id: Slack ID of user who called command :return: error message if user added unsuccessfully or if user has insufficient permission level, otherwise returns success message """ try: command_user = self.facade.retrieve(User, user_id) command_team = args.team_name team = get_team_by_name(self.facade, command_team) if not check_permissions(command_user, team): return self.permission_error, 200 user = self.facade.retrieve(User, args.username) if len(user.github_id) == 0: return self.no_ghusername_error, 200 team.add_member(user.github_id) self.gh.add_team_member(user.github_username, team.github_team_id) self.facade.store(team) msg = "Added User to " + command_team # Update drive shares sync_team_email_perms(self.gcp, self.facade, team) # If this team is a team with special permissions, promote the # user to the appropriate permission promoted_level = Permissions.member if command_team == self.config.github_team_admin: promoted_level = Permissions.admin elif command_team == self.config.github_team_leads: promoted_level = Permissions.team_lead # Only perform promotion if it is actually a promotion. if promoted_level > user.permissions_level: logging.info(f"Promoting {command_user} to {promoted_level}") user.permissions_level = promoted_level self.facade.store(user) msg += f" and promoted user to {promoted_level}" ret = {'attachments': [team.get_attachment()], 'text': msg} return ret, 200 except LookupError: return self.lookup_error, 200 except GithubAPIException as e: logging.error("user added unsuccessfully to team") return f"User added unsuccessfully with the " \ f"following error: {e.data}", 200
def team_view(self, gh_team_name: str) -> Team: """ View team information from the database. :param gh_team_name: name of the team to view :raises: LookupError if a team with the specified team name does not exist :raises: RuntimeError if more than one team has the specified team name :return: ``Team`` object if found """ logging.info("Team view command API called") return db_utils.get_team_by_name(self._db_facade, gh_team_name)
def team_remove(self, caller_id: str, gh_team_name: str, rem_user_id: str) -> bool: """ Remove the specified user from a team. If the user is also a team lead, removes team lead status from Team. :param caller_id: Slack ID of user who called command :param gh_team_name: Github team name of the team to remove user from :param rem_user_id: Slack ID of user to remove from team :raises: LookupError if the calling user, user to remove, or specified team cannot be found in the database :raises: RuntimeError if more than one team has the specified team name :raises: PermissionError if the calling user has insufficient permission to remove members to the specified team :raises: GithubAPIException if an error occured removing the user from the Github team :return: True if user was removed from team successfully, False otherwise """ logging.info("Team remove command API called") command_user = self._db_facade.retrieve(User, caller_id) logging.debug(f"Calling user: {command_user.__str__()}") team = db_utils.get_team_by_name(self._db_facade, gh_team_name) if not check_permissions(command_user, team): msg = f"User with ID {caller_id} has insufficient permissions" \ f" to remove members to team {gh_team_name}" logging.error(msg) raise PermissionError(msg) rem_user = self._db_facade.retrieve(User, rem_user_id) logging.debug(f"User to remove: {rem_user.__str__()}") if not self._gh_interface.has_team_member(rem_user.github_username, team.github_team_id): msg = f"Github user {rem_user.github_username} not a member" \ f" of Github team with ID {team.github_team_id}" logging.error(msg) raise GithubAPIException(msg) self._gh_interface.remove_team_member(rem_user.github_username, team.github_team_id) team.discard_member(rem_user.github_id) if team.has_team_lead(rem_user.github_id): team.discard_team_lead(rem_user.github_id) removed = self._db_facade.store(team) return removed
def lead_helper(self, param_list, user_id) -> ResponseTuple: """ Add a user as team lead, and add them to team if not already added. If ``--remove`` flag is used, user is instead demoted from being a team lead, but not from the team. :param param_list: List of parameters for editing leads :param user_id: Slack ID of user who called command :return: error message if user has insufficient permission level or lead demoted unsuccessfully, otherwise return success message """ try: command_user = self.facade.retrieve(User, user_id) command_team = param_list['team_name'] team = get_team_by_name(self.facade, command_team) if not check_permissions(command_user, team): return self.permission_error, 200 user = self.facade.retrieve(User, param_list["username"]) msg = "" if param_list["remove"]: if not team.has_member(user.github_id): return "User not in team!", 200 if team.has_team_lead(user.github_id): team.discard_team_lead(user.github_id) self.facade.store(team) msg = f"User removed as team lead from" \ f" {command_team}" else: if not team.has_member(user.github_id): team.add_member(user.github_id) self.gh.add_team_member(user.github_username, team.github_team_id) team.add_team_lead(user.github_id) self.facade.store(team) msg = f"User added as team lead to" \ f" {command_team}" ret = {'attachments': [team.get_attachment()], 'text': msg} return ret, 200 except LookupError: return self.lookup_error, 200 except GithubAPIException as e: logging.error("team lead edit unsuccessful") return f"Edit team lead was unsuccessful " \ f"with the following error: {e.data}", 200
def team_edit(self, caller_id: str, gh_team_name: str, display_name: str = None, platform: str = None) -> bool: """ Edit the properties of a specific team. Team leads can only edit the teams that they are a part of, but admins can edit any team. :param caller_id: Slack ID of user who called command :param display_name: display name to change to if not None :param platform: platform to change to if not None :raises: LookupError if the calling user or team to edit could not be found :raises: RuntimeError if more than one team has the specified team name :raises: PermissionError if the calling user does not have sufficient permissions to edit the specified team :return: True if the edit was successful, False otherwise """ logging.info("Team edit command API called") command_user = self._db_facade.retrieve(User, caller_id) logging.debug(f"Calling user: {command_user.__str__()}") team = db_utils.get_team_by_name(self._db_facade, gh_team_name) if not check_permissions(command_user, team): msg = f"User with ID {caller_id} has insufficient permissions" \ f" to edit team {gh_team_name}" logging.error(msg) raise PermissionError(msg) if display_name is not None: logging.debug(f"Attaching display name {display_name} " f"to {gh_team_name}") team.display_name = display_name if platform is not None: logging.debug(f"Attaching platform {platform} to {gh_team_name}") team.platform = platform edited = self._db_facade.store(team) return edited
def refresh_all_rocket_permissions(self): """ Refresh Rocket permissions for members in teams like GITHUB_ADMIN_TEAM_NAME and GITHUB_LEADS_TEAM_NAME. It only ever promotes users, and does not demote users. """ # provide teams from low permissions level to high teams = [ { 'name': self.config.github_team_leads, 'permission': Permissions.team_lead, }, { 'name': self.config.github_team_admin, 'permission': Permissions.admin, }, ] logging.info(f'refreshing Rocket permissions for teams {teams}') for t in teams: team_name = t['name'] if len(team_name) == 0: continue team = None try: team = get_team_by_name(self.facade, team_name) except LookupError: t_id = str(self.gh.org_create_team(team_name)) logging.info(f'team {team_name} created') self.facade.store(Team(t_id, team_name, team_name)) if team is not None: team_members = get_team_members(self.facade, team) updated = [] for user in team_members: if user.permissions_level < t['permission']: user.permissions_level = t['permission'] updated.append(user) self.facade.store(user) if len(updated) > 0: logging.info(f'updated users {updated}') else: logging.info('no users updated')
def edit_helper(self, args: Namespace, user_id: str) -> ResponseTuple: """ Edit the properties of a specific team. Team leads can only edit the teams that they are a part of, but admins can edit any teams. :param args: Parameters for editing team :param user_id: Slack ID of user who called command :return: error message if user has insufficient permission level or team edited unsuccessfully, otherwise return success message """ try: command_user = self.facade.retrieve(User, user_id) command_team = args.team_name team = get_team_by_name(self.facade, command_team) if not check_permissions(command_user, team): return self.permission_error, 200 msg = f"Team edited: {command_team}, " if args.displayname is not None: msg += f"displayname: {args.displayname}, " team.displayname = args.displayname if args.platform is not None: msg += f"platform: {args.platform}" team.platform = args.platform if args.folder is not None: msg += f"folder: {args.folder}" team.folder = args.folder if args.github is not None: msg += f"new github team name: {args.github}" team.github_team_name = args.github self.gh.org_edit_team( int(team.github_team_id), team.github_team_name) self.facade.store(team) # Update drive shares if folder was changed if args.folder: sync_team_email_perms(self.gcp, self.facade, team) ret = {'attachments': [team.get_attachment()], 'text': msg} return ret, 200 except LookupError: return self.lookup_error, 200
def team_add(self, caller_id: str, add_user_id: str, gh_team_name: str) -> bool: """ Add a user to a team. :param caller_id: Slack ID of user who called the API :param add_user_id: Slack ID of user to add to a team :param gh_team_name: Github team name of the team to add a user to :raises: LookupError if the calling user, user to add, or specified team cannot be found in the database :raises: RuntimeError if more than one team has the specified team name :raises: PermissionError if the calling user has insufficient permission to add members to the specified team :raises: GithubAPIException if an error occurs when adding the user to the Github team :return: True if adding the member to the team was successful, False otherwise """ logging.info("Team add command API called") command_user = self._db_facade.retrieve(User, caller_id) logging.debug(f"Calling user: {command_user.__str__()}") team = db_utils.get_team_by_name(self._db_facade, gh_team_name) if not check_permissions(command_user, team): msg = f"User with ID {caller_id} has insufficient permissions" \ f" to add members to team {gh_team_name}" logging.error(msg) raise PermissionError(msg) add_user = self._db_facade.retrieve(User, add_user_id) logging.debug(f"User to add: {add_user.__str__()}") self._gh_interface.add_team_member(add_user.github_username, team.github_team_id) team.add_member(add_user.github_id) added = self._db_facade.store(team) return added
def delete_helper(self, team_name, user_id) -> ResponseTuple: """ Permanently delete a team. :param team_name: Name of team to be deleted :param user_id: Slack ID of user who called command :return: error message if user has insufficient permission level or team deleted unsuccessfully, otherwise return success message """ try: command_user = self.facade.retrieve(User, user_id) team = get_team_by_name(self.facade, team_name) if not check_permissions(command_user, team): return self.permission_error, 200 self.facade.delete(Team, team.github_team_id) self.gh.org_delete_team(int(team.github_team_id)) return f"Team {team_name} deleted", 200 except LookupError: return self.lookup_error, 200 except GithubAPIException as e: logging.error("team delete unsuccessful") return f"Team delete was unsuccessful with " \ f"the following error: {e.data}", 200
def refresh_all_team(self): """ Refresh the 'all' team - this team is used to track all members. Should only be called after the teams have all synced, or bugs will probably occur. See https://github.com/orgs/ubclaunchpad/teams/all """ all_name = self.config.github_team_all team_all = None if len(all_name) == 0: logging.info('no "all" team configured, skipping refresh') return logging.info(f'refreshing all team {all_name}') try: team_all = get_team_by_name(self.facade, all_name) except LookupError: t_id = str(self.gh.org_create_team(all_name)) logging.info(f'team {all_name} created') team_all = Team(t_id, all_name, all_name) if team_all is not None: all_members = self.facade.query(User) for m in all_members: if len(m.github_id) > 0 and\ not team_all.has_member(m.github_id): # The only way for this to be true is if both locally and # remotely the member (who is part of launchpad) is not # part of the 'all' team. self.gh.add_team_member(m.github_username, team_all.github_team_id) team_all.add_member(m.github_id) self.facade.store(team_all) else: logging.error(f'Could not create {all_name}. Aborting.')
def get_team_users(self, team_name): team = get_team_by_name(self.facade, team_name) return get_team_members(self.facade, team)
def test_get_team_by_name_lots_of_teams_same_name(self): db = MemoryDB(teams=[self.t0, self.t1]) with self.assertRaises(RuntimeError): get_team_by_name(db, 'some-team')
def test_get_team_by_name_no_team_name(self): with self.assertRaises(LookupError): get_team_by_name(self.db, 'random-team')
def remove_helper(self, param_list, user_id) -> ResponseTuple: """ Remove specified user from a team. If the user is also a team lead, removes team lead status from Team. If user is not admin or team lead of specified team, user will not be removed and an error message is returned. :param param_list: List of parameters for removing user :param user_id: Slack ID of user who called command :return: error message if user removed unsuccessfully, if user is not in team, or if user has insufficient permission level, otherwise returns success message """ try: command_user = self.facade.retrieve(User, user_id) command_team = param_list['team_name'] team = get_team_by_name(self.facade, command_team) if not check_permissions(command_user, team): return self.permission_error, 200 user = self.facade.retrieve(User, param_list['username']) if not self.gh.has_team_member(user.github_username, team.github_team_id): return "User not in team!", 200 team.discard_member(user.github_id) if team.has_team_lead(user.github_id): team.discard_team_lead(user.github_id) self.gh.remove_team_member(user.github_username, team.github_team_id) self.facade.store(team) msg = "Removed User from " + command_team # If the user is being removed from a team with special # permisisons, figure out a demotion strategy. demoted_level = None if command_team == self.config.github_team_leads: # If the user is currently an admin, we only demote this user # if it is currently NOT and admin team member if user.permissions_level == Permissions.admin \ and len(self.config.github_team_admin) > 0: admins = get_team_by_name(self.facade, self.config.github_team_admin) if not admins.has_member(user.github_id): demoted_level = Permissions.member else: demoted_level = Permissions.member if command_team == self.config.github_team_admin: # If the user is being removed from the admin team, we demote # this user to team_lead if this user is a member of the leads # team, otherwise we demote to member. demoted_level = Permissions.member if len(self.config.github_team_leads) > 0: leads = get_team_by_name(self.facade, self.config.github_team_leads) if leads.has_member(user.github_id): demoted_level = Permissions.team_lead if demoted_level is not None: logging.info(f"Demoting {command_user} to member") user.permissions_level = demoted_level self.facade.store(user) msg += " and demoted user" ret = {'attachments': [team.get_attachment()], 'text': msg} return ret, 200 except LookupError: return self.lookup_error, 200 except GithubAPIException as e: logging.error("user removed unsuccessfully from team") return f"User removed unsuccessfully with " \ f"the following error: {e.data}", 200