Add organization collaborators API endpoint

Adds an API endpoint, `/v1/organization/<orgname>/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.
This commit is contained in:
Brad Ison 2018-03-12 16:42:42 -04:00
parent 32a473d23c
commit e8429f9194
No known key found for this signature in database
GPG key ID: 972D14B0BE6DE287
3 changed files with 76 additions and 1 deletions

View file

@ -262,6 +262,55 @@ class OrgPrivateRepositories(ApiResource):
raise Unauthorized() raise Unauthorized()
@resource('/v1/organization/<orgname>/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/<orgname>/members') @resource('/v1/organization/<orgname>/members')
@path_param('orgname', 'The name of the organization') @path_param('orgname', 'The name of the organization')
class OrganizationMemberList(ApiResource): class OrganizationMemberList(ApiResource):

View file

@ -3,10 +3,12 @@ import pytest
from data import model from data import model
from endpoints.api import api from endpoints.api import api
from endpoints.api.test.shared import conduct_api_call 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 endpoints.test.shared import client_with_identity
from test.fixtures import * from test.fixtures import *
@pytest.mark.parametrize('expiration, expected_code', [ @pytest.mark.parametrize('expiration, expected_code', [
(0, 200), (0, 200),
(100, 400), (100, 400),
@ -17,3 +19,20 @@ def test_change_tag_expiration(expiration, expected_code, client):
conduct_api_call(cl, Organization, 'PUT', {'orgname': 'buynlarge'}, conduct_api_call(cl, Organization, 'PUT', {'orgname': 'buynlarge'},
body={'tag_expiration_s': expiration}, body={'tag_expiration_s': expiration},
expected_code=expected_code) 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']

View file

@ -4,6 +4,7 @@ import pytest
from flask_principal import AnonymousIdentity from flask_principal import AnonymousIdentity
from endpoints.api import api from endpoints.api import api
from endpoints.api.organization import OrganizationCollaboratorList
from endpoints.api.repositorynotification import RepositoryNotification from endpoints.api.repositorynotification import RepositoryNotification
from endpoints.api.permission import RepositoryUserTransitivePermission from endpoints.api.permission import RepositoryUserTransitivePermission
from endpoints.api.team import OrganizationTeamSyncing from endpoints.api.team import OrganizationTeamSyncing
@ -19,6 +20,7 @@ from endpoints.test.shared import client_with_identity, toggle_feature
from test.fixtures import * from test.fixtures import *
ORG_PARAMS = {'orgname': 'buynlarge'}
TEAM_PARAMS = {'orgname': 'buynlarge', 'teamname': 'owners'} TEAM_PARAMS = {'orgname': 'buynlarge', 'teamname': 'owners'}
BUILD_PARAMS = {'build_uuid': 'test-1234'} BUILD_PARAMS = {'build_uuid': 'test-1234'}
REPO_PARAMS = {'repository': 'devtable/someapp'} 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, {}, 'reader', 404),
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'devtable', 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, {}, None, 403),
(OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'freshuser', 403), (OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'freshuser', 403),
(OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'reader', 403), (OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'reader', 403),