2015-05-14 20:47:38 +00:00
|
|
|
""" Conduct searches against all registry context. """
|
|
|
|
|
2016-03-07 15:07:41 +00:00
|
|
|
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
|
2016-10-27 19:31:32 +00:00
|
|
|
require_scope, path_param, internal_only, Unauthorized, InvalidRequest,
|
|
|
|
show_if)
|
2014-03-13 20:31:37 +00:00
|
|
|
from data import model
|
2015-07-15 21:25:41 +00:00
|
|
|
from auth.permissions import (OrganizationMemberPermission, ReadRepositoryPermission,
|
|
|
|
UserAdminPermission, AdministerOrganizationPermission,
|
|
|
|
ReadRepositoryPermission)
|
2014-03-13 20:31:37 +00:00
|
|
|
from auth.auth_context import get_authenticated_user
|
2014-03-19 18:36:56 +00:00
|
|
|
from auth import scopes
|
2016-10-27 19:31:32 +00:00
|
|
|
from app import avatar, authentication
|
2016-12-05 22:19:38 +00:00
|
|
|
from flask import abort
|
2015-04-06 23:17:18 +00:00
|
|
|
from operator import itemgetter
|
2015-04-07 16:32:23 +00:00
|
|
|
from stringscore import liquidmetal
|
2015-04-07 22:33:43 +00:00
|
|
|
from util.names import parse_robot_username
|
2014-03-13 20:31:37 +00:00
|
|
|
|
2016-10-04 09:35:04 +00:00
|
|
|
import anunidecode # Don't listen to pylint's lies. This import is required.
|
2015-04-06 23:17:18 +00:00
|
|
|
import math
|
2014-03-13 20:31:37 +00:00
|
|
|
|
2017-03-10 19:06:39 +00:00
|
|
|
|
|
|
|
ENTITY_SEARCH_SCORE = 1
|
|
|
|
TEAM_SEARCH_SCORE = 2
|
|
|
|
REPOSITORY_SEARCH_SCORE = 4
|
|
|
|
|
|
|
|
|
2016-10-27 19:31:32 +00:00
|
|
|
@resource('/v1/entities/link/<username>')
|
|
|
|
@internal_only
|
|
|
|
class LinkExternalEntity(ApiResource):
|
|
|
|
""" Resource for linking external entities to internal users. """
|
|
|
|
@nickname('linkExternalUser')
|
|
|
|
def post(self, username):
|
2016-12-05 22:19:38 +00:00
|
|
|
if not authentication.federated_service:
|
|
|
|
abort(404)
|
|
|
|
|
2016-10-27 19:31:32 +00:00
|
|
|
# Only allowed if there is a logged in user.
|
|
|
|
if not get_authenticated_user():
|
|
|
|
raise Unauthorized()
|
|
|
|
|
|
|
|
# Try to link the user with the given *external* username, to an internal record.
|
|
|
|
(user, err_msg) = authentication.link_user(username)
|
|
|
|
if user is None:
|
|
|
|
raise InvalidRequest(err_msg, payload={'username': username})
|
|
|
|
|
|
|
|
return {
|
|
|
|
'entity': {
|
|
|
|
'name': user.username,
|
|
|
|
'kind': 'user',
|
|
|
|
'is_robot': False,
|
|
|
|
'avatar': avatar.get_data_for_user(user)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-03-07 15:07:41 +00:00
|
|
|
@resource('/v1/entities/<prefix>')
|
|
|
|
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. """
|
2016-10-04 09:35:04 +00:00
|
|
|
|
|
|
|
# Ensure we don't have any unicode characters in the search, as it breaks the search. Nothing
|
|
|
|
# being searched can have unicode in it anyway, so this is a safe operation.
|
|
|
|
prefix = prefix.encode('unidecode', 'ignore').replace(' ', '').lower()
|
|
|
|
|
2016-03-07 15:07:41 +00:00
|
|
|
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
|
|
|
|
|
2016-10-27 19:31:32 +00:00
|
|
|
# Lookup users in the database for the prefix query.
|
|
|
|
users = model.user.get_matching_users(prefix, robot_namespace, organization, limit=10)
|
|
|
|
|
|
|
|
# Lookup users via the user system for the prefix query. We'll filter out any users that
|
|
|
|
# already exist in the database.
|
|
|
|
external_users, federated_id, _ = authentication.query_users(prefix, limit=10)
|
|
|
|
filtered_external_users = []
|
|
|
|
if external_users and federated_id is not None:
|
|
|
|
users = list(users)
|
|
|
|
user_ids = [user.id for user in users]
|
|
|
|
|
|
|
|
# Filter the users if any are already found via the database. We do so by looking up all
|
|
|
|
# the found users in the federated user system.
|
|
|
|
federated_query = model.user.get_federated_logins(user_ids, federated_id)
|
|
|
|
found = {result.service_ident for result in federated_query}
|
|
|
|
filtered_external_users = [user for user in external_users if not user.username in found]
|
2016-03-07 15:07:41 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2016-10-27 19:31:32 +00:00
|
|
|
def external_view(user):
|
|
|
|
result = {
|
|
|
|
'name': user.username,
|
|
|
|
'kind': 'external',
|
2016-09-08 15:23:37 +00:00
|
|
|
'title': user.email or '',
|
2016-10-27 19:31:32 +00:00
|
|
|
'avatar': avatar.get_data_for_external_user(user)
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
|
2016-03-07 15:07:41 +00:00
|
|
|
team_data = [entity_team_view(team) for team in teams]
|
|
|
|
user_data = [user_view(user) for user in users]
|
2016-10-27 19:31:32 +00:00
|
|
|
external_data = [external_view(user) for user in filtered_external_users]
|
2016-03-07 15:07:41 +00:00
|
|
|
|
|
|
|
return {
|
2016-10-27 19:31:32 +00:00
|
|
|
'results': team_data + user_data + org_data + external_data
|
2016-03-07 15:07:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-04-20 16:51:47 +00:00
|
|
|
def search_entity_view(username, entity, get_short_name=None):
|
2015-04-08 21:41:08 +00:00
|
|
|
kind = 'user'
|
|
|
|
avatar_data = avatar.get_data_for_user(entity)
|
|
|
|
href = '/user/' + entity.username
|
|
|
|
|
|
|
|
if entity.organization:
|
|
|
|
kind = 'organization'
|
|
|
|
avatar_data = avatar.get_data_for_org(entity)
|
|
|
|
href = '/organization/' + entity.username
|
|
|
|
elif entity.robot:
|
|
|
|
parts = parse_robot_username(entity.username)
|
|
|
|
if parts[0] == username:
|
|
|
|
href = '/user/' + username + '?tab=robots&showRobot=' + entity.username
|
|
|
|
else:
|
|
|
|
href = '/organization/' + parts[0] + '?tab=robots&showRobot=' + entity.username
|
|
|
|
|
|
|
|
kind = 'robot'
|
|
|
|
avatar_data = None
|
|
|
|
|
2015-04-20 16:51:47 +00:00
|
|
|
data = {
|
2015-04-08 21:41:08 +00:00
|
|
|
'kind': kind,
|
|
|
|
'avatar': avatar_data,
|
|
|
|
'name': entity.username,
|
2017-03-10 19:06:39 +00:00
|
|
|
'score': ENTITY_SEARCH_SCORE,
|
2015-04-08 21:41:08 +00:00
|
|
|
'href': href
|
|
|
|
}
|
|
|
|
|
2015-04-20 16:51:47 +00:00
|
|
|
if get_short_name:
|
|
|
|
data['short_name'] = get_short_name(entity.username)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
2015-04-08 21:41:08 +00:00
|
|
|
|
|
|
|
def conduct_team_search(username, query, encountered_teams, results):
|
|
|
|
""" Finds the matching teams where the user is a member. """
|
2015-07-15 21:25:41 +00:00
|
|
|
matching_teams = model.team.get_matching_user_teams(query, get_authenticated_user(), limit=5)
|
2015-04-08 21:41:08 +00:00
|
|
|
for team in matching_teams:
|
|
|
|
if team.id in encountered_teams:
|
|
|
|
continue
|
|
|
|
|
|
|
|
encountered_teams.add(team.id)
|
|
|
|
|
|
|
|
results.append({
|
|
|
|
'kind': 'team',
|
|
|
|
'name': team.name,
|
|
|
|
'organization': search_entity_view(username, team.organization),
|
|
|
|
'avatar': avatar.get_data_for_team(team),
|
2017-03-10 19:06:39 +00:00
|
|
|
'score': TEAM_SEARCH_SCORE,
|
2015-04-08 21:41:08 +00:00
|
|
|
'href': '/organization/' + team.organization.username + '/teams/' + team.name
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
def conduct_admined_team_search(username, query, encountered_teams, results):
|
|
|
|
""" Finds matching teams in orgs admined by the user. """
|
2015-07-15 21:25:41 +00:00
|
|
|
matching_teams = model.team.get_matching_admined_teams(query, get_authenticated_user(), limit=5)
|
2015-04-08 21:41:08 +00:00
|
|
|
for team in matching_teams:
|
|
|
|
if team.id in encountered_teams:
|
|
|
|
continue
|
|
|
|
|
|
|
|
encountered_teams.add(team.id)
|
|
|
|
|
|
|
|
results.append({
|
|
|
|
'kind': 'team',
|
|
|
|
'name': team.name,
|
|
|
|
'organization': search_entity_view(username, team.organization),
|
|
|
|
'avatar': avatar.get_data_for_team(team),
|
2017-03-10 19:06:39 +00:00
|
|
|
'score': TEAM_SEARCH_SCORE,
|
2015-04-08 21:41:08 +00:00
|
|
|
'href': '/organization/' + team.organization.username + '/teams/' + team.name
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
def conduct_repo_search(username, query, results):
|
|
|
|
""" Finds matching repositories. """
|
Optimize repository search by changing our lookup strategy
Previous to this change, repositories were looked up unfiltered in six different queries, and then filtered using the permissions model, which issued a query per repository found, making search incredibly slow. Instead, we now lookup a chunk of repositories unfiltered and then filter them via a single query to the database. By layering the filtering on top of the lookup, each as queries, we can minimize the number of queries necessary, without (at the same time) using a super expensive join.
Other changes:
- Remove the 5 page pre-lookup on V1 search and simply return that there is one more page available, until there isn't. While technically not correct, it is much more efficient, and no one should be using pagination with V1 search anyway.
- Remove the lookup for repos without entries in the RAC table. Instead, we now add a new RAC entry when the repository is created for *the day before*, with count 0, so that it is immediately searchable
- Remove lookup of results with a matching namespace; these aren't very relevant anyway, and it overly complicates sorting
2017-02-27 22:56:44 +00:00
|
|
|
matching_repos = model.repository.get_filtered_matching_repositories(query, username, limit=5)
|
2015-04-08 21:41:08 +00:00
|
|
|
|
|
|
|
for repo in matching_repos:
|
|
|
|
results.append({
|
|
|
|
'kind': 'repository',
|
|
|
|
'namespace': search_entity_view(username, repo.namespace_user),
|
|
|
|
'name': repo.name,
|
|
|
|
'description': repo.description,
|
Optimize repository search by changing our lookup strategy
Previous to this change, repositories were looked up unfiltered in six different queries, and then filtered using the permissions model, which issued a query per repository found, making search incredibly slow. Instead, we now lookup a chunk of repositories unfiltered and then filter them via a single query to the database. By layering the filtering on top of the lookup, each as queries, we can minimize the number of queries necessary, without (at the same time) using a super expensive join.
Other changes:
- Remove the 5 page pre-lookup on V1 search and simply return that there is one more page available, until there isn't. While technically not correct, it is much more efficient, and no one should be using pagination with V1 search anyway.
- Remove the lookup for repos without entries in the RAC table. Instead, we now add a new RAC entry when the repository is created for *the day before*, with count 0, so that it is immediately searchable
- Remove lookup of results with a matching namespace; these aren't very relevant anyway, and it overly complicates sorting
2017-02-27 22:56:44 +00:00
|
|
|
'is_public': model.repository.is_repository_public(repo),
|
2017-03-10 19:06:39 +00:00
|
|
|
'score': REPOSITORY_SEARCH_SCORE,
|
2015-04-08 21:41:08 +00:00
|
|
|
'href': '/repository/' + repo.namespace_user.username + '/' + repo.name
|
|
|
|
})
|
|
|
|
|
|
|
|
|
2015-04-09 16:57:20 +00:00
|
|
|
def conduct_namespace_search(username, query, results):
|
|
|
|
""" Finds matching users and organizations. """
|
2015-07-15 21:25:41 +00:00
|
|
|
matching_entities = model.user.get_matching_user_namespaces(query, username, limit=5)
|
2015-04-08 21:41:08 +00:00
|
|
|
for entity in matching_entities:
|
|
|
|
results.append(search_entity_view(username, entity))
|
2015-04-09 16:57:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
def conduct_robot_search(username, query, results):
|
|
|
|
""" Finds matching robot accounts. """
|
2015-04-20 16:51:47 +00:00
|
|
|
def get_short_name(name):
|
|
|
|
return parse_robot_username(name)[1]
|
|
|
|
|
2015-07-15 21:25:41 +00:00
|
|
|
matching_robots = model.user.get_matching_robots(query, username, limit=5)
|
2015-04-09 16:57:20 +00:00
|
|
|
for robot in matching_robots:
|
2015-04-20 16:51:47 +00:00
|
|
|
results.append(search_entity_view(username, robot, get_short_name))
|
2015-04-08 21:41:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
@resource('/v1/find/all')
|
|
|
|
class ConductSearch(ApiResource):
|
|
|
|
""" Resource for finding users, repositories, teams, etc. """
|
2016-01-26 21:27:36 +00:00
|
|
|
@parse_args()
|
2015-04-08 21:41:08 +00:00
|
|
|
@query_param('query', 'The search query.', type=str, default='')
|
|
|
|
@require_scope(scopes.READ_REPO)
|
|
|
|
@nickname('conductSearch')
|
2016-01-26 21:27:36 +00:00
|
|
|
def get(self, parsed_args):
|
2015-04-08 21:41:08 +00:00
|
|
|
""" Get a list of entities and resources that match the specified query. """
|
2016-01-26 21:27:36 +00:00
|
|
|
query = parsed_args['query']
|
2015-04-08 21:41:08 +00:00
|
|
|
if not query:
|
|
|
|
return {'results': []}
|
|
|
|
|
|
|
|
username = None
|
|
|
|
results = []
|
|
|
|
|
|
|
|
if get_authenticated_user():
|
|
|
|
username = get_authenticated_user().username
|
|
|
|
|
|
|
|
# Search for teams.
|
|
|
|
encountered_teams = set()
|
|
|
|
conduct_team_search(username, query, encountered_teams, results)
|
|
|
|
conduct_admined_team_search(username, query, encountered_teams, results)
|
|
|
|
|
2015-04-09 16:57:20 +00:00
|
|
|
# Search for robot accounts.
|
|
|
|
conduct_robot_search(username, query, results)
|
|
|
|
|
2015-04-08 21:41:08 +00:00
|
|
|
# Search for repos.
|
|
|
|
conduct_repo_search(username, query, results)
|
|
|
|
|
2015-04-09 16:57:20 +00:00
|
|
|
# Search for users and orgs.
|
|
|
|
conduct_namespace_search(username, query, results)
|
2015-04-08 21:41:08 +00:00
|
|
|
|
|
|
|
# Modify the results' scores via how close the query term is to each result's name.
|
|
|
|
for result in results:
|
2015-04-20 16:51:47 +00:00
|
|
|
name = result.get('short_name', result['name'])
|
2015-04-20 17:00:56 +00:00
|
|
|
lm_score = liquidmetal.score(name, query) or 0.5
|
2015-04-20 17:00:38 +00:00
|
|
|
result['score'] = result['score'] * lm_score
|
2015-04-08 21:41:08 +00:00
|
|
|
|
|
|
|
return {'results': sorted(results, key=itemgetter('score'), reverse=True)}
|