diff --git a/data/model/team.py b/data/model/team.py index eca222862..c7d810b80 100644 --- a/data/model/team.py +++ b/data/model/team.py @@ -181,6 +181,12 @@ def get_matching_admined_teams(team_prefix, user_obj, limit=10): return query +def get_matching_teams(team_prefix, organization): + team_prefix_search = _basequery.prefix_search(Team.name, team_prefix) + query = Team.select().where(team_prefix_search, Team.organization == organization) + return query.limit(10) + + def get_teams_within_org(organization): return Team.select().where(Team.organization == organization) diff --git a/data/model/user.py b/data/model/user.py index 66e3e8c59..38e240c9d 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -513,6 +513,41 @@ def get_matching_user_namespaces(namespace_prefix, username, limit=10): return _basequery.filter_to_repos_for_user(base_query, username).limit(limit) +def get_matching_users(username_prefix, robot_namespace=None, + organization=None): + user_search = _basequery.prefix_search(User.username, username_prefix) + direct_user_query = (user_search & (User.organization == False) & (User.robot == False)) + + if robot_namespace: + robot_prefix = format_robot_username(robot_namespace, username_prefix) + robot_search = _basequery.prefix_search(User.username, robot_prefix) + direct_user_query = (direct_user_query | (robot_search & (User.robot == True))) + + query = (User + .select(User.username, User.email, User.robot) + .group_by(User.username, User.email, User.robot) + .where(direct_user_query)) + + if organization: + query = (query + .select(User.username, User.email, User.robot, fn.Sum(Team.id)) + .join(TeamMember, JOIN_LEFT_OUTER) + .join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) & + (Team.organization == organization)))) + + class MatchingUserResult(object): + def __init__(self, *args): + self.username = args[0] + self.email = args[1] + self.robot = args[2] + + if organization: + self.is_org_member = (args[3] != None) + else: + self.is_org_member = None + + return (MatchingUserResult(*args) for args in query.tuples().limit(10)) + def verify_user(username_or_email, password): # Make sure we didn't get any unicode for the username. diff --git a/endpoints/api/search.py b/endpoints/api/search.py index 79585152f..143edc52f 100644 --- a/endpoints/api/search.py +++ b/endpoints/api/search.py @@ -1,6 +1,6 @@ """ Conduct searches against all registry context. """ -from endpoints.api import (ApiResource, parse_args, query_param, nickname, resource, +from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource, require_scope, path_param) from data import model from auth.permissions import (OrganizationMemberPermission, ReadRepositoryPermission, @@ -15,6 +15,86 @@ from util.names import parse_robot_username import math +@resource('/v1/entities/') +class EntitySearch(ApiResource): + """ Resource for searching entities. """ + @path_param('prefix', 'The prefix of the entities being looked up') + @parse_args() + @query_param('namespace', 'Namespace to use when querying for org entities.', type=str, + default='') + @query_param('includeTeams', 'Whether to include team names.', type=truthy_bool, default=False) + @query_param('includeOrgs', 'Whether to include orgs names.', type=truthy_bool, default=False) + @nickname('getMatchingEntities') + def get(self, prefix, parsed_args): + """ Get a list of entities that match the specified prefix. """ + teams = [] + org_data = [] + + namespace_name = parsed_args['namespace'] + robot_namespace = None + organization = None + + try: + organization = model.organization.get_organization(namespace_name) + + # namespace name was an org + permission = OrganizationMemberPermission(namespace_name) + if permission.can(): + robot_namespace = namespace_name + + if parsed_args['includeTeams']: + teams = model.team.get_matching_teams(prefix, organization) + + if (parsed_args['includeOrgs'] and AdministerOrganizationPermission(namespace_name) and + namespace_name.startswith(prefix)): + org_data = [{ + 'name': namespace_name, + 'kind': 'org', + 'is_org_member': True, + 'avatar': avatar.get_data_for_org(organization), + }] + + except model.organization.InvalidOrganizationException: + # namespace name was a user + user = get_authenticated_user() + if user and user.username == namespace_name: + # Check if there is admin user permissions (login only) + admin_permission = UserAdminPermission(user.username) + if admin_permission.can(): + robot_namespace = namespace_name + + users = model.user.get_matching_users(prefix, robot_namespace, organization) + + def entity_team_view(team): + result = { + 'name': team.name, + 'kind': 'team', + 'is_org_member': True, + 'avatar': avatar.get_data_for_team(team) + } + return result + + def user_view(user): + user_json = { + 'name': user.username, + 'kind': 'user', + 'is_robot': user.robot, + 'avatar': avatar.get_data_for_user(user) + } + + if organization is not None: + user_json['is_org_member'] = user.robot or user.is_org_member + + return user_json + + team_data = [entity_team_view(team) for team in teams] + user_data = [user_view(user) for user in users] + + return { + 'results': team_data + user_data + org_data + } + + def search_entity_view(username, entity, get_short_name=None): kind = 'user' avatar_data = avatar.get_data_for_user(entity) diff --git a/test/test_api_security.py b/test/test_api_security.py index 07cc323f3..c785aaee4 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -12,7 +12,7 @@ from endpoints.api import api_bp, api from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RevertTag -from endpoints.api.search import ConductSearch +from endpoints.api.search import EntitySearch from endpoints.api.image import RepositoryImage, RepositoryImageList from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs, RepositoryBuildList, RepositoryBuildResource) @@ -3672,10 +3672,10 @@ class TestRepositoryBuynlargeOrgrepo(ApiTestCase): self._run_test('DELETE', 204, 'devtable', None) -class TestConductSearchR9nz(ApiTestCase): +class TestEntitySearchR9nz(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) - self._set_url(ConductSearch, prefix="R9NZ") + self._set_url(EntitySearch, prefix="R9NZ") def test_get_anonymous(self): self._run_test('GET', 200, None, None) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 9e4103420..cd700fc1c 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -24,7 +24,7 @@ from data.database import RepositoryActionCount from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags -from endpoints.api.search import ConductSearch +from endpoints.api.search import EntitySearch, ConductSearch from endpoints.api.image import RepositoryImage, RepositoryImageList from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildList, RepositoryBuildResource from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot, @@ -668,6 +668,40 @@ class TestConductSearch(ApiTestCase): +class TestGetMatchingEntities(ApiTestCase): + def test_notinorg(self): + self.login(NO_ACCESS_USER) + + json = self.getJsonResponse(EntitySearch, + params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams='true')) + + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert not 'owners' in names + + def test_inorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse(EntitySearch, + params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams='true')) + + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert 'owners' in names + + def test_inorg_withorgs(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse(EntitySearch, + params=dict(prefix=ORGANIZATION[0], namespace=ORGANIZATION, + includeOrgs='true')) + + names = set([r['name'] for r in json['results']]) + assert ORGANIZATION in names + + class TestCreateOrganization(ApiTestCase): def test_existinguser(self): self.login(ADMIN_ACCESS_USER)