diff --git a/data/model/legacy.py b/data/model/legacy.py index 4276775e1..111dd2d10 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -414,6 +414,28 @@ def convert_user_to_organization(user, admin_user): return user +def remove_organization_member(org, user): + org_admins = [u.username for u in __get_org_admin_users(org)] + if len(org_admins) == 1 and user.username in org_admins: + raise DataModelException('Cannot remove user as they are the only organization admin') + + with config.app_config['DB_TRANSACTION_FACTORY'](db): + # Find and remove the user from any repositorys under the org. + permissions = (RepositoryPermission.select(RepositoryPermission.id) + .join(Repository) + .where(Repository.namespace_user == org, + RepositoryPermission.user == user)) + + RepositoryPermission.delete().where(RepositoryPermission.id << permissions).execute() + + # Find and remove the user from any teams under the org. + members = (TeamMember.select(TeamMember.id) + .join(Team) + .where(Team.organization == org, TeamMember.user == user)) + + TeamMember.delete().where(TeamMember.id << members).execute() + + def create_team(name, org, team_role_name, description=''): (username_valid, username_issue) = validate_username(name) if not username_valid: @@ -428,6 +450,15 @@ def create_team(name, org, team_role_name, description=''): description=description) +def __get_org_admin_users(org): + return (User.select() + .join(TeamMember) + .join(Team) + .join(TeamRole) + .where(Team.organization == org, TeamRole.name == 'admin', User.robot == False) + .distinct()) + + def __get_user_admin_teams(org_name, teamname, username): Org = User.alias() user_teams = Team.select().join(TeamMember).join(User) @@ -877,6 +908,23 @@ def verify_user(username_or_email, password): # We weren't able to authorize the user return None +def list_organization_member_permissions(organization): + query = (RepositoryPermission.select(RepositoryPermission, Repository, User) + .join(Repository) + .switch(RepositoryPermission) + .join(User) + .where(Repository.namespace_user == organization) + .where(User.robot == False)) + return query + + +def list_organization_members_by_teams(organization): + query = (TeamMember.select(Team, User) + .annotate(Team) + .annotate(User) + .where(Team.organization == organization)) + return query + def get_user_organizations(username): UserAlias = User.alias() @@ -905,14 +953,6 @@ def get_organization_team(orgname, teamname): return result[0] - -def get_organization_members_with_teams(organization, membername = None): - joined = TeamMember.select().annotate(Team).annotate(User) - query = joined.where(Team.organization == organization) - if membername: - query = query.where(User.username == membername) - return query - def get_organization_team_members(teamid): joined = User.select().join(TeamMember).join(Team) query = joined.where(Team.id == teamid) diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 1836dcb1f..03255c22e 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -230,7 +230,7 @@ class OrganizationMemberList(ApiResource): @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationMembers') def get(self, orgname): - """ List the members of the specified organization. """ + """ List the human members of the specified organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): try: @@ -242,21 +242,41 @@ class OrganizationMemberList(ApiResource): # will return an entry for *every team* a member is on, so we will have # duplicate keys (which is why we pre-build the dictionary). members_dict = {} - members = model.get_organization_members_with_teams(org) + members = model.list_organization_members_by_teams(org) for member in members: + if member.user.robot: + continue + if not member.user.username in members_dict: - members_dict[member.user.username] = {'name': member.user.username, - 'kind': 'user', - 'is_robot': member.user.robot, - 'teams': []} + member_data = { + 'name': member.user.username, + 'kind': 'user', + 'avatar': avatar.get_data_for_user(member.user), + 'teams': [], + 'repositories': [] + } - members_dict[member.user.username]['teams'].append(member.team.name) + members_dict[member.user.username] = member_data - return {'members': members_dict} + members_dict[member.user.username]['teams'].append({ + 'name': member.team.name, + 'avatar': avatar.get_data_for_team(member.team), + }) + + # Loop to add direct repository permissions. + for permission in model.list_organization_member_permissions(org): + username = permission.user.username + if not username in members_dict: + continue + + members_dict[username]['repositories'].append(permission.repository.name) + + return {'members': members_dict.values()} raise Unauthorized() + @resource('/v1/organization//members/') @path_param('orgname', 'The name of the organization') @path_param('membername', 'The username of the organization member') @@ -264,31 +284,26 @@ class OrganizationMember(ApiResource): """ Resource for managing individual organization members. """ @require_scope(scopes.ORG_ADMIN) - @nickname('getOrganizationMember') - def get(self, orgname, membername): - """ Get information on the specific organization member. """ + @nickname('removeOrganizationMember') + def delete(self, orgname, membername): + """ Removes a member from an organization, revoking all its repository + priviledges and removing it from all teams in the organization. + """ permission = AdministerOrganizationPermission(orgname) if permission.can(): + # Lookup the user. + user = model.get_nonrobot_user(membername) + if not user: + raise NotFound() + try: org = model.get_organization(orgname) except model.InvalidOrganizationException: raise NotFound() - member_dict = None - member_teams = model.get_organization_members_with_teams(org, membername=membername) - for member in member_teams: - if not member_dict: - member_dict = {'name': member.user.username, - 'kind': 'user', - 'is_robot': member.user.robot, - 'teams': []} - - member_dict['teams'].append(member.team.name) - - if not member_dict: - raise NotFound() - - return {'member': member_dict} + # Remove the user from the organization. + model.remove_organization_member(org, user) + return 'Deleted', 204 raise Unauthorized() diff --git a/static/css/directives/ui/teams-manager.css b/static/css/directives/ui/teams-manager.css index 6e24ad484..5d6f74a04 100644 --- a/static/css/directives/ui/teams-manager.css +++ b/static/css/directives/ui/teams-manager.css @@ -4,7 +4,11 @@ .teams-manager .manager-header { border-bottom: 1px solid #eee; - margin-bottom: 10px; + margin-bottom: 16px; +} + +.teams-manager .manager-header i.fa { + margin-right: 6px; } .teams-manager .cor-options-menu { @@ -12,6 +16,25 @@ margin-left: 10px; } +.teams-manager td .empty { + color: #ccc; +} + +.teams-manager .cor-confirm-dialog .entity-reference .avatar { + margin-left: 4px; + margin-right: 0px; +} + +.teams-manager .cor-confirm-dialog .entity-reference .entity-name { + margin-left: 2px; +} + +@media (min-width: 768px) { + .teams-manager .manager-header { + padding-bottom: 0px; + } +} + @media (max-width: 767px) { .teams-manager .control-col { padding-left: 55px; diff --git a/static/directives/teams-manager.html b/static/directives/teams-manager.html index e18bed411..2e6034deb 100644 --- a/static/directives/teams-manager.html +++ b/static/directives/teams-manager.html @@ -1,65 +1,162 @@
-
- + + + Create New Team
- + +
+ -
-
-
-
- - - {{ team.name }} - - - {{ team.name }} - + + +
+
+
+
+ + + {{ team.name }} + + + {{ team.name }} + +
+ +
+ +
-
+
+ -
- -
- - - - - Delete Team {{ team.name }} - - -
+ + +
+
+ +
+ +
+
No organization members found matching filter.
+
+ Please change your filter to display members. +
+
+ + + + + + + + + + + + + + + + + +
Member NameTeamsDirect Repository Permissions
+
+
+ + + + + + + + (No direct permissions on any repositories) + + + + Direct permissions on {{ memberInfo.repositories.length }} + repository + repositories + under this organization + + + + + Remove From Organization + + +
+
+ + +
+
+ will be removed from all teams and repositories under this organization in which they are a + member or have permissions. +
+ + Continue with removal of ? +
diff --git a/static/js/directives/ui/teams-manager.js b/static/js/directives/ui/teams-manager.js index 61666d33a..5995b4f47 100644 --- a/static/js/directives/ui/teams-manager.js +++ b/static/js/directives/ui/teams-manager.js @@ -12,7 +12,7 @@ angular.module('quay').directive('teamsManager', function () { 'organization': '=organization', 'isEnabled': '=isEnabled' }, - controller: function($scope, $element, ApiService, CreateService, $timeout) { + controller: function($scope, $element, ApiService, CreateService, $timeout, UserService) { $scope.TEAM_PATTERN = TEAM_PATTERN; $scope.teamRoles = [ { 'id': 'member', 'title': 'Member', 'kind': 'default' }, @@ -20,8 +20,12 @@ angular.module('quay').directive('teamsManager', function () { { 'id': 'admin', 'title': 'Admin', 'kind': 'primary' } ]; + UserService.updateUserIn($scope); + $scope.members = {}; $scope.orderedTeams = []; + $scope.showingMembers = false; + $scope.fullMemberList = null; var loadTeamMembers = function() { if (!$scope.organization || !$scope.isEnabled) { return; } @@ -141,6 +145,47 @@ angular.module('quay').directive('teamsManager', function () { delete $scope.organization.teams[teamname]; }, ApiService.errorDisplay('Cannot delete team')); }; + + $scope.showMembers = function(value) { + $scope.showingMembers = value; + if (value && !$scope.fullMemberList) { + var params = { + 'orgname': $scope.organization.name + }; + + ApiService.getOrganizationMembers(null, params).then(function(resp) { + $scope.fullMemberList = resp['members']; + }, ApiService.errorDisplay('Could not load full membership list')); + } + }; + + $scope.removeMember = function(memberInfo, callback) { + var params = { + 'orgname': $scope.organization.name, + 'membername': memberInfo.name + }; + + var errorHandler = ApiService.errorDisplay('Could not remove member', function() { + callback(false); + }); + + ApiService.removeOrganizationMember(null, params).then(function(resp) { + // Reset the state of the directive. + $scope.members = {}; + $scope.orderedTeams = []; + $scope.fullMemberList = null; + + loadOrderedTeams(); + loadTeamMembers(); + $scope.showMembers(true); + + callback(true); + }, errorHandler) + }; + + $scope.askRemoveMember = function(memberInfo) { + $scope.removeMemberInfo = $.extend({}, memberInfo); + }; } }; diff --git a/static/partials/org-view.html b/static/partials/org-view.html index 3565a94e5..b824cf382 100644 --- a/static/partials/org-view.html +++ b/static/partials/org-view.html @@ -30,7 +30,7 @@ - +