From e8429f919483889ad206bb4b53fccaf3032dca34 Mon Sep 17 00:00:00 2001 From: Brad Ison Date: Mon, 12 Mar 2018 16:42:42 -0400 Subject: [PATCH] Add organization collaborators API endpoint Adds an API endpoint, `/v1/organization//collaborators`, that lists an organization's "outside collaborators", i.e. users that have direct permissions on one or more repositories belonging to the organization, but who aren't members of any teams in the organization. --- endpoints/api/organization.py | 49 +++++++++++++++++++++++++ endpoints/api/test/test_organization.py | 21 ++++++++++- endpoints/api/test/test_security.py | 7 ++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index c449bd843..ae43a86fb 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -262,6 +262,55 @@ class OrgPrivateRepositories(ApiResource): raise Unauthorized() +@resource('/v1/organization//collaborators') +@path_param('orgname', 'The name of the organization') +class OrganizationCollaboratorList(ApiResource): + """ Resource for listing outside collaborators of an organization. + + Collaborators are users that do not belong to any team in the + organiztion, but who have direct permissions on one or more + repositories belonging to the organization. + """ + + @require_scope(scopes.ORG_ADMIN) + @nickname('getOrganizationCollaborators') + def get(self, orgname): + """ List outside collaborators of the specified organization. """ + permission = AdministerOrganizationPermission(orgname) + if not permission.can(): + raise Unauthorized() + + try: + org = model.organization.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + all_perms = model.permission.list_organization_member_permissions(org) + membership = model.team.list_organization_members_by_teams(org) + + org_members = set(m.user.username for m in membership) + + collaborators = {} + for perm in all_perms: + username = perm.user.username + + # Only interested in non-member permissions. + if username in org_members: + continue + + if username not in collaborators: + collaborators[username] = { + 'kind': 'user', + 'name': username, + 'avatar': avatar.get_data_for_user(perm.user), + 'repositories': [], + } + + collaborators[username]['repositories'].append(perm.repository.name) + + return {'collaborators': collaborators.values()} + + @resource('/v1/organization//members') @path_param('orgname', 'The name of the organization') class OrganizationMemberList(ApiResource): diff --git a/endpoints/api/test/test_organization.py b/endpoints/api/test/test_organization.py index 9a6525113..4341e1125 100644 --- a/endpoints/api/test/test_organization.py +++ b/endpoints/api/test/test_organization.py @@ -3,10 +3,12 @@ import pytest from data import model from endpoints.api import api from endpoints.api.test.shared import conduct_api_call -from endpoints.api.organization import Organization +from endpoints.api.organization import (Organization, + OrganizationCollaboratorList) from endpoints.test.shared import client_with_identity from test.fixtures import * + @pytest.mark.parametrize('expiration, expected_code', [ (0, 200), (100, 400), @@ -17,3 +19,20 @@ def test_change_tag_expiration(expiration, expected_code, client): conduct_api_call(cl, Organization, 'PUT', {'orgname': 'buynlarge'}, body={'tag_expiration_s': expiration}, expected_code=expected_code) + + +def test_get_organization_collaborators(client): + params = {'orgname': 'buynlarge'} + + with client_with_identity('devtable', client) as cl: + resp = conduct_api_call(cl, OrganizationCollaboratorList, 'GET', params) + + collaborator_names = [c['name'] for c in resp.json['collaborators']] + assert 'outsideorg' in collaborator_names + assert 'devtable' not in collaborator_names + assert 'reader' not in collaborator_names + + for collaborator in resp.json['collaborators']: + if collaborator['name'] == 'outsideorg': + assert 'orgrepo' in collaborator['repositories'] + assert 'anotherorgrepo' not in collaborator['repositories'] diff --git a/endpoints/api/test/test_security.py b/endpoints/api/test/test_security.py index 863a78f2e..95d9b3acd 100644 --- a/endpoints/api/test/test_security.py +++ b/endpoints/api/test/test_security.py @@ -4,6 +4,7 @@ import pytest from flask_principal import AnonymousIdentity from endpoints.api import api +from endpoints.api.organization import OrganizationCollaboratorList from endpoints.api.repositorynotification import RepositoryNotification from endpoints.api.permission import RepositoryUserTransitivePermission from endpoints.api.team import OrganizationTeamSyncing @@ -19,6 +20,7 @@ from endpoints.test.shared import client_with_identity, toggle_feature from test.fixtures import * +ORG_PARAMS = {'orgname': 'buynlarge'} TEAM_PARAMS = {'orgname': 'buynlarge', 'teamname': 'owners'} BUILD_PARAMS = {'build_uuid': 'test-1234'} REPO_PARAMS = {'repository': 'devtable/someapp'} @@ -48,6 +50,11 @@ TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'} (AppToken, 'DELETE', TOKEN_PARAMS, {}, 'reader', 404), (AppToken, 'DELETE', TOKEN_PARAMS, {}, 'devtable', 404), + (OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, None, 401), + (OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'freshuser', 403), + (OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'reader', 403), + (OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'devtable', 200), + (OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, None, 403), (OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'freshuser', 403), (OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'reader', 403),