blob: 2f228fab5364c9d4f8624da76cad8644a79ad508 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is govered by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""API service"""
import datetime
import endpoints
import functools
import logging
import re
import time
from google.appengine.api import oauth
from protorpc import message_types
from protorpc import protojson
from protorpc import remote
import settings
from features import filterrules_helpers
from features import notify
from framework import actionlimit
from framework import framework_constants
from framework import framework_helpers
from framework import framework_views
from framework import monorailrequest
from framework import permissions
from framework import profiler
from framework import sql
from project import project_helpers
from proto import api_pb2_v1
from proto import project_pb2
from search import frontendsearchpipeline
from services import api_pb2_v1_helpers
from services import client_config_svc
from services import config_svc
from services import issue_svc
from services import project_svc
from services import service_manager
from services import tracker_fulltext
from services import user_svc
from services import usergroup_svc
from sitewide import sitewide_helpers
from tracker import field_helpers
from tracker import issuedetail
from tracker import tracker_constants
from tracker import tracker_bizobj
from infra_libs.ts_mon.common import http_metrics
ENDPOINTS_API_NAME = 'monorail'
DOC_URL = ('https://chromium.googlesource.com/infra/infra/+/master/'
'appengine/monorail/doc/api.md')
def monorail_api_method(
request_message, response_message, **kwargs):
"""Extends endpoints.method by performing base checks."""
time_fn = kwargs.pop('time_fn', time.time)
method_name = kwargs.get('name', '')
method_path = kwargs.get('path', '')
def new_decorator(func):
@endpoints.method(request_message, response_message, **kwargs)
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
method_identifier = (ENDPOINTS_API_NAME + '.' +
(method_name or func.__name__)
+ '/' + (method_path or func.__name__))
start_time = time_fn()
approximate_http_status = 200
request = args[0]
ret = None
try:
requester = endpoints.get_current_user()
auth_client_ids, auth_emails = (
client_config_svc.GetClientConfigSvc().GetClientIDEmails())
auth_client_ids.append(endpoints.API_EXPLORER_CLIENT_ID)
logging.info('Whitelist ID %r email %r', auth_client_ids, auth_emails)
if self._services is None:
self._set_services(service_manager.set_up_services())
api_base_checks(
request, requester,
self._services, sql.MonorailConnection(),
auth_client_ids, auth_emails)
self.increment_request_limit(request)
ret = func(self, *args, **kwargs)
except user_svc.NoSuchUserException as e:
approximate_http_status = 404
raise endpoints.NotFoundException(
'The user does not exist: %s' % str(e))
except (project_svc.NoSuchProjectException,
issue_svc.NoSuchIssueException,
config_svc.NoSuchComponentException) as e:
approximate_http_status = 404
raise endpoints.NotFoundException(str(e))
except (permissions.BannedUserException,
permissions.PermissionException) as e:
approximate_http_status = 403
raise endpoints.ForbiddenException(str(e))
except endpoints.BadRequestException:
approximate_http_status = 400
raise
except endpoints.UnauthorizedException:
approximate_http_status = 401
raise
except actionlimit.ExcessiveActivityException as e:
approximate_http_status = 403
raise endpoints.ForbiddenException(
'The requester has exceeded API quotas limit')
except (usergroup_svc.GroupExistsException,
config_svc.InvalidComponentNameException) as e:
approximate_http_status = 400
raise endpoints.BadRequestException(str(e))
except Exception as e:
approximate_http_status = 500
logging.exception('Unexpected error in monorail API')
raise
finally:
elapsed_ms = int((time_fn() - start_time) * 1000)
fields = {
# Endpoints APIs don't return the full set of http status values.
'status': approximate_http_status,
# Use the api name, not the request path, to prevent an
# explosion in possible field values.
'name': method_identifier,
'is_robot': False,
}
http_metrics.server_durations.add(elapsed_ms, fields=fields)
http_metrics.server_response_status.increment(fields=fields)
http_metrics.server_request_bytes.add(len(protojson.encode_message(
request)), fields=fields)
response_size = 0
if ret:
response_size = len(protojson.encode_message(ret))
http_metrics.server_response_bytes.add(response_size, fields=fields)
return ret
return wrapper
return new_decorator
def api_base_checks(request, requester, services, cnxn,
auth_client_ids, auth_emails):
"""Base checks for API users.
Args:
request: The HTTP request from Cloud Endpoints.
requester: The user who sends the request.
services: Services object.
cnxn: connection to the SQL database.
auth_client_ids: authorized client ids.
auth_emails: authorized emails when client is anonymous.
Returns:
Nothing
Raises:
endpoints.UnauthorizedException: If the requester is anonymous.
user_svc.NoSuchUserException: If the requester does not exist in Monorail.
project_svc.NoSuchProjectException: If the project does not exist in
Monorail.
permissions.BannedUserException: If the requester is banned.
permissions.PermissionException: If the requester does not have
permisssion to view.
"""
valid_user = False
auth_err = ''
client_id = None
try:
client_id = oauth.get_client_id(framework_constants.OAUTH_SCOPE)
logging.info('Oauth client ID %s', client_id)
except oauth.Error as ex:
auth_err = 'oauth.Error: %s' % ex
if not requester:
try:
requester = oauth.get_current_user(framework_constants.OAUTH_SCOPE)
logging.info('Oauth requester %s', requester.email())
except oauth.Error as ex:
auth_err = 'oauth.Error: %s' % ex
if client_id and requester:
if client_id != 'anonymous':
if client_id in auth_client_ids:
valid_user = True
else:
auth_err = 'Client ID %s is not whitelisted' % client_id
# Some service accounts may have anonymous client ID
else:
if requester.email() in auth_emails:
valid_user = True
else:
auth_err = 'Client email %s is not whitelisted' % requester.email()
if not valid_user:
raise endpoints.UnauthorizedException('Auth error: %s' % auth_err)
project_name = None
if hasattr(request, 'projectId'):
project_name = request.projectId
issue_local_id = None
if hasattr(request, 'issueId'):
issue_local_id = request.issueId
# This could raise user_svc.NoSuchUserException
requester_id = services.user.LookupUserID(cnxn, requester.email())
requester_pb = services.user.GetUser(cnxn, requester_id)
requester_view = framework_views.UserView(
requester_id, requester.email(), requester_pb.obscure_email)
if permissions.IsBanned(requester_pb, requester_view):
raise permissions.BannedUserException(
'The user %s has been banned from using Monorail' %
requester.email())
if project_name:
project = services.project.GetProjectByName(
cnxn, project_name)
if not project:
raise project_svc.NoSuchProjectException(
'Project %s does not exist' % project_name)
if project.state != project_pb2.ProjectState.LIVE:
raise permissions.PermissionException(
'API may not access project %s because it is not live'
% project_name)
requester_effective_ids = services.usergroup.LookupMemberships(
cnxn, requester_id)
requester_effective_ids.add(requester_id)
if not permissions.UserCanViewProject(
requester_pb, requester_effective_ids, project):
raise permissions.PermissionException(
'The user %s has no permission for project %s' %
(requester.email(), project_name))
if issue_local_id:
# This may raise a NoSuchIssueException.
issue = services.issue.GetIssueByLocalID(
cnxn, project.project_id, issue_local_id)
perms = permissions.GetPermissions(
requester_pb, requester_effective_ids, project)
config = services.config.GetProjectConfig(cnxn, project.project_id)
granted_perms = tracker_bizobj.GetGrantedPerms(
issue, requester_effective_ids, config)
if not permissions.CanViewIssue(
requester_effective_ids, perms, project, issue,
granted_perms=granted_perms):
raise permissions.PermissionException(
'User is not allowed to view this issue %s:%d' %
(project_name, issue_local_id))
@endpoints.api(name=ENDPOINTS_API_NAME, version='v1',
description='Monorail API to manage issues.',
auth_level=endpoints.AUTH_LEVEL.NONE,
allowed_client_ids=endpoints.SKIP_CLIENT_ID_CHECK,
documentation=DOC_URL)
class MonorailApi(remote.Service):
# Class variables. Handy to mock.
_services = None
_mar = None
@classmethod
def _set_services(cls, services):
cls._services = services
def mar_factory(self, request):
if not self._mar:
self._mar = monorailrequest.MonorailApiRequest(request, self._services)
return self._mar
def aux_delete_comment(self, request, delete=True):
mar = self.mar_factory(request)
action_name = 'delete' if delete else 'undelete'
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
all_comments = self._services.issue.GetCommentsForIssue(
mar.cnxn, issue.issue_id)
try:
issue_comment = all_comments[request.commentId]
except IndexError:
raise issue_svc.NoSuchIssueException(
'The issue %s:%d does not have comment %d.' %
(mar.project_name, request.issueId, request.commentId))
if not permissions.CanDelete(
mar.auth.user_id, mar.auth.effective_ids, mar.perms,
issue_comment.deleted_by, issue_comment.user_id, mar.project,
permissions.GetRestrictions(issue), mar.granted_perms):
raise permissions.PermissionException(
'User is not allowed to %s the comment %d of issue %s:%d' %
(action_name, request.commentId, mar.project_name,
request.issueId))
self._services.issue.SoftDeleteComment(
mar.cnxn, mar.project_id, request.issueId, request.commentId,
mar.auth.user_id, self._services.user, delete=delete)
return api_pb2_v1.IssuesCommentsDeleteResponse()
def increment_request_limit(self, request):
"""Check whether the requester has exceeded API quotas limit,
and increment request count.
"""
mar = self.mar_factory(request)
# soft_limit == hard_limit for api_request, so this function either
# returns False if under limit, or raise ExcessiveActivityException
if not actionlimit.NeedCaptcha(
mar.auth.user_pb, actionlimit.API_REQUEST, skip_lifetime_check=True):
actionlimit.CountAction(
mar.auth.user_pb, actionlimit.API_REQUEST, delta=1)
self._services.user.UpdateUser(
mar.cnxn, mar.auth.user_id, mar.auth.user_pb)
@monorail_api_method(
api_pb2_v1.ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesCommentsDeleteResponse,
path='projects/{projectId}/issues/{issueId}/comments/{commentId}',
http_method='DELETE',
name='issues.comments.delete')
def issues_comments_delete(self, request):
"""Delete a comment."""
return self.aux_delete_comment(request, True)
@monorail_api_method(
api_pb2_v1.ISSUES_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesCommentsInsertResponse,
path='projects/{projectId}/issues/{issueId}/comments',
http_method='POST',
name='issues.comments.insert')
def issues_comments_insert(self, request):
"""Add a comment."""
mar = self.mar_factory(request)
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
old_owner_id = tracker_bizobj.GetOwnerId(issue)
if not permissions.CanCommentIssue(
mar.auth.effective_ids, mar.perms, mar.project, issue,
mar.granted_perms):
raise permissions.PermissionException(
'User is not allowed to comment this issue (%s, %d)' %
(request.projectId, request.issueId))
updates_dict = {}
if request.updates:
if request.updates.moveToProject:
move_to = request.updates.moveToProject.lower()
move_to_project = issuedetail.CheckMoveIssueRequest(
self._services, mar, issue, True, move_to, mar.errors)
if mar.errors.AnyErrors():
raise endpoints.BadRequestException(mar.errors.move_to)
updates_dict['move_to_project'] = move_to_project
updates_dict['summary'] = request.updates.summary
updates_dict['status'] = request.updates.status
if request.updates.owner:
if request.updates.owner == framework_constants.NO_USER_NAME:
updates_dict['owner'] = framework_constants.NO_USER_SPECIFIED
else:
updates_dict['owner'] = self._services.user.LookupUserID(
mar.cnxn, request.updates.owner)
updates_dict['cc_add'], updates_dict['cc_remove'] = (
api_pb2_v1_helpers.split_remove_add(request.updates.cc))
updates_dict['cc_add'] = self._services.user.LookupUserIDs(
mar.cnxn, updates_dict['cc_add']).values()
updates_dict['cc_remove'] = self._services.user.LookupUserIDs(
mar.cnxn, updates_dict['cc_remove']).values()
updates_dict['labels_add'], updates_dict['labels_remove'] = (
api_pb2_v1_helpers.split_remove_add(request.updates.labels))
blocked_on_add_strs, blocked_on_remove_strs = (
api_pb2_v1_helpers.split_remove_add(request.updates.blockedOn))
updates_dict['blocked_on_add'] = api_pb2_v1_helpers.issue_global_ids(
blocked_on_add_strs, issue.project_id, mar,
self._services)
updates_dict['blocked_on_remove'] = api_pb2_v1_helpers.issue_global_ids(
blocked_on_remove_strs, issue.project_id, mar,
self._services)
blocking_add_strs, blocking_remove_strs = (
api_pb2_v1_helpers.split_remove_add(request.updates.blocking))
updates_dict['blocking_add'] = api_pb2_v1_helpers.issue_global_ids(
blocking_add_strs, issue.project_id, mar,
self._services)
updates_dict['blocking_remove'] = api_pb2_v1_helpers.issue_global_ids(
blocking_remove_strs, issue.project_id, mar,
self._services)
components_add_strs, components_remove_strs = (
api_pb2_v1_helpers.split_remove_add(request.updates.components))
updates_dict['components_add'] = (
api_pb2_v1_helpers.convert_component_ids(
mar.config, components_add_strs))
updates_dict['components_remove'] = (
api_pb2_v1_helpers.convert_component_ids(
mar.config, components_remove_strs))
if request.updates.mergedInto:
updates_dict['merged_into'] = self._services.issue.LookupIssueID(
mar.cnxn, issue.project_id, int(request.updates.mergedInto))
(updates_dict['field_vals_add'], updates_dict['field_vals_remove'],
updates_dict['fields_clear'], updates_dict['fields_labels_add'],
updates_dict['fields_labels_remove']) = (
api_pb2_v1_helpers.convert_field_values(
request.updates.fieldValues, mar, self._services))
field_helpers.ValidateCustomFields(
mar, self._services,
(updates_dict.get('field_vals_add', []) +
updates_dict.get('field_vals_remove', [])),
mar.config, mar.errors)
if mar.errors.AnyErrors():
raise endpoints.BadRequestException(
'Invalid field values: %s' % mar.errors.custom_fields)
_, comment = self._services.issue.DeltaUpdateIssue(
cnxn=mar.cnxn, services=self._services,
reporter_id=mar.auth.user_id,
project_id=mar.project_id, config=mar.config, issue=issue,
status=updates_dict.get('status'), owner_id=updates_dict.get('owner'),
cc_add=updates_dict.get('cc_add', []),
cc_remove=updates_dict.get('cc_remove', []),
comp_ids_add=updates_dict.get('components_add', []),
comp_ids_remove=updates_dict.get('components_remove', []),
labels_add=(updates_dict.get('labels_add', []) +
updates_dict.get('fields_labels_add', [])),
labels_remove=(updates_dict.get('labels_remove', []) +
updates_dict.get('fields_labels_remove', [])),
field_vals_add=updates_dict.get('field_vals_add', []),
field_vals_remove=updates_dict.get('field_vals_remove', []),
fields_clear=updates_dict.get('fields_clear', []),
blocked_on_add=updates_dict.get('blocked_on_add', []),
blocked_on_remove=updates_dict.get('blocked_on_remove', []),
blocking_add=updates_dict.get('blocking_add', []),
blocking_remove=updates_dict.get('blocking_remove', []),
merged_into=updates_dict.get('merged_into'),
index_now=False,
comment=request.content,
summary=updates_dict.get('summary'),
)
move_comment = None
if 'move_to_project' in updates_dict:
move_to_project = updates_dict['move_to_project']
old_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id)
tracker_fulltext.UnindexIssues([issue.issue_id])
moved_back_iids = self._services.issue.MoveIssues(
mar.cnxn, move_to_project, [issue], self._services.user)
new_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id)
if issue.issue_id in moved_back_iids:
content = 'Moved %s back to %s again.' % (old_text_ref, new_text_ref)
else:
content = 'Moved %s to now be %s.' % (old_text_ref, new_text_ref)
move_comment = self._services.issue.CreateIssueComment(
mar.cnxn, move_to_project.project_id, issue.local_id, mar.auth.user_id,
content, amendments=[
tracker_bizobj.MakeProjectAmendment(move_to_project.project_name)])
tracker_fulltext.IndexIssues(
mar.cnxn, [issue], self._services.user, self._services.issue,
self._services.config)
comment = comment or move_comment
if comment is None:
return api_pb2_v1.IssuesCommentsInsertResponse()
cmnts = self._services.issue.GetCommentsForIssue(mar.cnxn, issue.issue_id)
seq = len(cmnts) - 1
if request.sendEmail:
notify.PrepareAndSendIssueChangeNotification(
issue.project_id, issue.local_id, framework_helpers.GetHostPort(),
comment.user_id, seq, send_email=True, old_owner_id=old_owner_id)
can_delete = permissions.CanDelete(
mar.auth.user_id, mar.auth.effective_ids, mar.perms,
comment.deleted_by, comment.user_id, mar.project,
permissions.GetRestrictions(issue), granted_perms=mar.granted_perms)
return api_pb2_v1.IssuesCommentsInsertResponse(
id=seq,
kind='monorail#issueComment',
author=api_pb2_v1_helpers.convert_person(
comment.user_id, mar.cnxn, self._services),
content=comment.content,
published=datetime.datetime.fromtimestamp(comment.timestamp),
updates=api_pb2_v1_helpers.convert_amendments(
issue, comment.amendments, mar, self._services),
canDelete=can_delete)
@monorail_api_method(
api_pb2_v1.ISSUES_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesCommentsListResponse,
path='projects/{projectId}/issues/{issueId}/comments',
http_method='GET',
name='issues.comments.list')
def issues_comments_list(self, request):
"""List all comments for an issue."""
mar = self.mar_factory(request)
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
comments = self._services.issue.GetCommentsForIssue(
mar.cnxn, issue.issue_id)
visible_comments = []
for comment in comments[
request.startIndex:(request.startIndex + request.maxResults)]:
visible_comments.append(
api_pb2_v1_helpers.convert_comment(
issue, comment, mar, self._services, mar.granted_perms))
return api_pb2_v1.IssuesCommentsListResponse(
kind='monorail#issueCommentList',
totalResults=len(comments),
items=visible_comments)
@monorail_api_method(
api_pb2_v1.ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesCommentsDeleteResponse,
path='projects/{projectId}/issues/{issueId}/comments/{commentId}',
http_method='POST',
name='issues.comments.undelete')
def issues_comments_undelete(self, request):
"""Restore a deleted comment."""
return self.aux_delete_comment(request, False)
@monorail_api_method(
api_pb2_v1.USERS_GET_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.UsersGetResponse,
path='users/{userId}',
http_method='GET',
name='users.get')
def users_get(self, request):
"""Get a user."""
owner_project_only = request.ownerProjectsOnly
mar = self.mar_factory(request)
(visible_ownership, visible_deleted, visible_membership,
visible_contrib) = sitewide_helpers.GetUserProjects(
mar.cnxn, self._services, mar.auth.user_pb, mar.auth.effective_ids,
mar.viewed_user_auth.effective_ids)
project_list = []
for proj in (visible_ownership + visible_deleted):
config = self._services.config.GetProjectConfig(
mar.cnxn, proj.project_id)
proj_result = api_pb2_v1_helpers.convert_project(
proj, config, api_pb2_v1.Role.owner)
project_list.append(proj_result)
if not owner_project_only:
for proj in visible_membership:
config = self._services.config.GetProjectConfig(
mar.cnxn, proj.project_id)
proj_result = api_pb2_v1_helpers.convert_project(
proj, config, api_pb2_v1.Role.member)
project_list.append(proj_result)
for proj in visible_contrib:
config = self._services.config.GetProjectConfig(
mar.cnxn, proj.project_id)
proj_result = api_pb2_v1_helpers.convert_project(
proj, config, api_pb2_v1.Role.contributor)
project_list.append(proj_result)
return api_pb2_v1.UsersGetResponse(
id=str(mar.viewed_user_auth.user_id),
kind='monorail#user',
projects=project_list,
)
@monorail_api_method(
api_pb2_v1.ISSUES_GET_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesGetInsertResponse,
path='projects/{projectId}/issues/{issueId}',
http_method='GET',
name='issues.get')
def issues_get(self, request):
"""Get an issue."""
mar = self.mar_factory(request)
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
return api_pb2_v1_helpers.convert_issue(
api_pb2_v1.IssuesGetInsertResponse, issue, mar, self._services)
@monorail_api_method(
api_pb2_v1.ISSUES_INSERT_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesGetInsertResponse,
path='projects/{projectId}/issues',
http_method='POST',
name='issues.insert')
def issues_insert(self, request):
"""Add a new issue."""
mar = self.mar_factory(request)
if not mar.perms.CanUsePerm(
permissions.CREATE_ISSUE, mar.auth.effective_ids, mar.project, []):
raise permissions.PermissionException(
'The requester %s is not allowed to create issues for project %s.' %
(mar.auth.email, mar.project_name))
owner_id = None
if request.owner:
try:
owner_id = self._services.user.LookupUserID(
mar.cnxn, request.owner.name)
except user_svc.NoSuchUserException:
raise endpoints.BadRequestException(
'The specified owner %s does not exist.' % request.owner.name)
cc_ids = []
if request.cc:
cc_ids = self._services.user.LookupUserIDs(
mar.cnxn, [ap.name for ap in request.cc]).values()
comp_ids = api_pb2_v1_helpers.convert_component_ids(
mar.config, request.components)
fields_add, _, _, fields_labels, _ = (
api_pb2_v1_helpers.convert_field_values(
request.fieldValues, mar, self._services))
field_helpers.ValidateCustomFields(
mar, self._services, fields_add, mar.config, mar.errors)
if mar.errors.AnyErrors():
raise endpoints.BadRequestException(
'Invalid field values: %s' % mar.errors.custom_fields)
local_id = self._services.issue.CreateIssue(
mar.cnxn, self._services, mar.project_id,
request.summary, request.status, owner_id,
cc_ids, request.labels + fields_labels, fields_add,
comp_ids, mar.auth.user_id, request.description,
blocked_on=api_pb2_v1_helpers.convert_issueref_pbs(
request.blockedOn, mar, self._services),
blocking=api_pb2_v1_helpers.convert_issueref_pbs(
request.blocking, mar, self._services))
new_issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, local_id)
if request.sendEmail:
notify.PrepareAndSendIssueChangeNotification(
mar.project_id, local_id, framework_helpers.GetHostPort(),
new_issue.reporter_id, 0)
return api_pb2_v1_helpers.convert_issue(
api_pb2_v1.IssuesGetInsertResponse, new_issue, mar, self._services)
@monorail_api_method(
api_pb2_v1.ISSUES_LIST_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.IssuesListResponse,
path='projects/{projectId}/issues',
http_method='GET',
name='issues.list')
def issues_list(self, request):
"""List issues for projects."""
mar = self.mar_factory(request)
if request.additionalProject:
for project_name in request.additionalProject:
project = self._services.project.GetProjectByName(
mar.cnxn, project_name)
if project and not permissions.UserCanViewProject(
mar.auth.user_pb, mar.auth.effective_ids, project):
raise permissions.PermissionException(
'The user %s has no permission for project %s' %
(mar.auth.email, project_name))
prof = profiler.Profiler()
pipeline = frontendsearchpipeline.FrontendSearchPipeline(
mar, self._services, prof, mar.num)
if not mar.errors.AnyErrors():
pipeline.SearchForIIDs()
pipeline.MergeAndSortIssues()
pipeline.Paginate()
else:
raise endpoints.BadRequestException(mar.errors.query)
issue_list = [
api_pb2_v1_helpers.convert_issue(
api_pb2_v1.IssueWrapper, r, mar, self._services)
for r in pipeline.visible_results]
return api_pb2_v1.IssuesListResponse(
kind='monorail#issueList',
totalResults=pipeline.total_count,
items=issue_list)
@monorail_api_method(
api_pb2_v1.GROUPS_SETTINGS_LIST_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.GroupsSettingsListResponse,
path='groups/settings',
http_method='GET',
name='groups.settings.list')
def groups_settings_list(self, request):
"""List all group settings."""
mar = self.mar_factory(request)
all_groups = self._services.usergroup.GetAllUserGroupsInfo(mar.cnxn)
group_settings = []
for g in all_groups:
setting = g[2]
wrapper = api_pb2_v1_helpers.convert_group_settings(g[0], setting)
if not request.importedGroupsOnly or wrapper.ext_group_type:
group_settings.append(wrapper)
return api_pb2_v1.GroupsSettingsListResponse(
groupSettings=group_settings)
@monorail_api_method(
api_pb2_v1.GROUPS_CREATE_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.GroupsCreateResponse,
path='groups',
http_method='POST',
name='groups.create')
def groups_create(self, request):
"""Create a new user group."""
mar = self.mar_factory(request)
if not permissions.CanCreateGroup(mar.perms):
raise permissions.PermissionException(
'The user is not allowed to create groups.')
user_dict = self._services.user.LookupExistingUserIDs(
mar.cnxn, [request.groupName])
if request.groupName.lower() in user_dict:
raise usergroup_svc.GroupExistsException(
'group %s already exists' % request.groupName)
if request.ext_group_type:
ext_group_type = str(request.ext_group_type).lower()
else:
ext_group_type = None
group_id = self._services.usergroup.CreateGroup(
mar.cnxn, self._services, request.groupName,
str(request.who_can_view_members).lower(),
ext_group_type)
return api_pb2_v1.GroupsCreateResponse(
groupID=group_id)
@monorail_api_method(
api_pb2_v1.GROUPS_GET_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.GroupsGetResponse,
path='groups/{groupName}',
http_method='GET',
name='groups.get')
def groups_get(self, request):
"""Get a group's settings and users."""
mar = self.mar_factory(request)
if not mar.viewed_user_auth:
raise user_svc.NoSuchUserException(request.groupName)
group_id = mar.viewed_user_auth.user_id
group_settings = self._services.usergroup.GetGroupSettings(
mar.cnxn, group_id)
member_ids, owner_ids = self._services.usergroup.LookupAllMembers(
mar.cnxn, [group_id])
(owned_project_ids, membered_project_ids,
contrib_project_ids) = self._services.project.GetUserRolesInAllProjects(
mar.cnxn, mar.auth.effective_ids)
project_ids = owned_project_ids.union(
membered_project_ids).union(contrib_project_ids)
if not permissions.CanViewGroup(
mar.perms, mar.auth.effective_ids, group_settings, member_ids[group_id],
owner_ids[group_id], project_ids):
raise permissions.PermissionException(
'The user is not allowed to view this group.')
member_ids, owner_ids = self._services.usergroup.LookupMembers(
mar.cnxn, [group_id])
member_emails = self._services.user.LookupUserEmails(
mar.cnxn, member_ids[group_id]).values()
owner_emails = self._services.user.LookupUserEmails(
mar.cnxn, owner_ids[group_id]).values()
return api_pb2_v1.GroupsGetResponse(
groupID=group_id,
groupSettings=api_pb2_v1_helpers.convert_group_settings(
request.groupName, group_settings),
groupOwners=owner_emails,
groupMembers=member_emails)
@monorail_api_method(
api_pb2_v1.GROUPS_UPDATE_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.GroupsUpdateResponse,
path='groups/{groupName}',
http_method='POST',
name='groups.update')
def groups_update(self, request):
"""Update a group's settings and users."""
mar = self.mar_factory(request)
group_id = mar.viewed_user_auth.user_id
member_ids_dict, owner_ids_dict = self._services.usergroup.LookupMembers(
mar.cnxn, [group_id])
owner_ids = owner_ids_dict.get(group_id, [])
member_ids = member_ids_dict.get(group_id, [])
if not permissions.CanEditGroup(
mar.perms, mar.auth.effective_ids, owner_ids):
raise permissions.PermissionException(
'The user is not allowed to edit this group.')
group_settings = self._services.usergroup.GetGroupSettings(
mar.cnxn, group_id)
if (request.who_can_view_members or request.ext_group_type
or request.last_sync_time or request.friend_projects):
group_settings.who_can_view_members = (
request.who_can_view_members or group_settings.who_can_view_members)
group_settings.ext_group_type = (
request.ext_group_type or group_settings.ext_group_type)
group_settings.last_sync_time = (
request.last_sync_time or group_settings.last_sync_time)
if framework_constants.NO_VALUES in request.friend_projects:
group_settings.friend_projects = []
else:
id_dict = self._services.project.LookupProjectIDs(
mar.cnxn, request.friend_projects)
group_settings.friend_projects = (
id_dict.values() or group_settings.friend_projects)
self._services.usergroup.UpdateSettings(
mar.cnxn, group_id, group_settings)
if request.groupOwners or request.groupMembers:
self._services.usergroup.RemoveMembers(
mar.cnxn, group_id, owner_ids + member_ids)
owners_dict = self._services.user.LookupUserIDs(
mar.cnxn, request.groupOwners, True)
self._services.usergroup.UpdateMembers(
mar.cnxn, group_id, owners_dict.values(), 'owner')
members_dict = self._services.user.LookupUserIDs(
mar.cnxn, request.groupMembers, True)
self._services.usergroup.UpdateMembers(
mar.cnxn, group_id, members_dict.values(), 'member')
return api_pb2_v1.GroupsUpdateResponse()
@monorail_api_method(
api_pb2_v1.COMPONENTS_LIST_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.ComponentsListResponse,
path='projects/{projectId}/components',
http_method='GET',
name='components.list')
def components_list(self, request):
"""List all components of a given project."""
mar = self.mar_factory(request)
config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
components = [api_pb2_v1_helpers.convert_component_def(
cd, mar, self._services) for cd in config.component_defs]
return api_pb2_v1.ComponentsListResponse(
components=components)
@monorail_api_method(
api_pb2_v1.COMPONENTS_CREATE_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.Component,
path='projects/{projectId}/components',
http_method='POST',
name='components.create')
def components_create(self, request):
"""Create a component."""
mar = self.mar_factory(request)
if not mar.perms.CanUsePerm(
permissions.EDIT_PROJECT, mar.auth.effective_ids, mar.project, []):
raise permissions.PermissionException(
'User is not allowed to create components for this project')
config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
leaf_name = request.componentName
if not tracker_constants.COMPONENT_NAME_RE.match(leaf_name):
raise config_svc.InvalidComponentNameException(
'The component name %s is invalid.' % leaf_name)
parent_path = request.parentPath
if parent_path:
parent_def = tracker_bizobj.FindComponentDef(parent_path, config)
if not parent_def:
raise config_svc.NoSuchComponentException(
'Parent component %s does not exist.' % parent_path)
if not permissions.CanEditComponentDef(
mar.auth.effective_ids, mar.perms, mar.project, parent_def, config):
raise permissions.PermissionException(
'User is not allowed to add a subcomponent to component %s' %
parent_path)
path = '%s>%s' % (parent_path, leaf_name)
else:
path = leaf_name
if tracker_bizobj.FindComponentDef(path, config):
raise config_svc.InvalidComponentNameException(
'The name %s is already in use.' % path)
created = int(time.time())
user_emails = set()
user_emails.update([mar.auth.email] + request.admin + request.cc)
user_ids_dict = self._services.user.LookupUserIDs(
mar.cnxn, list(user_emails), autocreate=False)
admin_ids = [user_ids_dict[uname] for uname in request.admin]
cc_ids = [user_ids_dict[uname] for uname in request.cc]
component_id = self._services.config.CreateComponentDef(
mar.cnxn, mar.project_id, path, request.description, request.deprecated,
admin_ids, cc_ids, created, user_ids_dict[mar.auth.email])
return api_pb2_v1.Component(
componentId=component_id,
projectName=request.projectId,
componentPath=path,
description=request.description,
admin=request.admin,
cc=request.cc,
deprecated=request.deprecated,
created=datetime.datetime.fromtimestamp(created),
creator=mar.auth.email)
@monorail_api_method(
api_pb2_v1.COMPONENTS_DELETE_REQUEST_RESOURCE_CONTAINER,
message_types.VoidMessage,
path='projects/{projectId}/components/{componentPath}',
http_method='DELETE',
name='components.delete')
def components_delete(self, request):
"""Delete a component."""
mar = self.mar_factory(request)
config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
component_path = request.componentPath
component_def = tracker_bizobj.FindComponentDef(
component_path, config)
if not component_def:
raise config_svc.NoSuchComponentException(
'The component %s does not exist.' % component_path)
if not permissions.CanViewComponentDef(
mar.auth.effective_ids, mar.perms, mar.project, component_def):
raise permissions.PermissionException(
'User is not allowed to view this component %s' % component_path)
if not permissions.CanEditComponentDef(
mar.auth.effective_ids, mar.perms, mar.project, component_def, config):
raise permissions.PermissionException(
'User is not allowed to delete this component %s' % component_path)
allow_delete = not tracker_bizobj.FindDescendantComponents(
config, component_def)
if not allow_delete:
raise permissions.PermissionException(
'User tried to delete component that had subcomponents')
self._services.issue.DeleteComponentReferences(
mar.cnxn, component_def.component_id)
self._services.config.DeleteComponentDef(
mar.cnxn, mar.project_id, component_def.component_id)
return message_types.VoidMessage()
@monorail_api_method(
api_pb2_v1.COMPONENTS_UPDATE_REQUEST_RESOURCE_CONTAINER,
message_types.VoidMessage,
path='projects/{projectId}/components/{componentPath}',
http_method='POST',
name='components.update')
def components_update(self, request):
"""Update a component."""
mar = self.mar_factory(request)
config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
component_path = request.componentPath
component_def = tracker_bizobj.FindComponentDef(
component_path, config)
if not component_def:
raise config_svc.NoSuchComponentException(
'The component %s does not exist.' % component_path)
if not permissions.CanViewComponentDef(
mar.auth.effective_ids, mar.perms, mar.project, component_def):
raise permissions.PermissionException(
'User is not allowed to view this component %s' % component_path)
if not permissions.CanEditComponentDef(
mar.auth.effective_ids, mar.perms, mar.project, component_def, config):
raise permissions.PermissionException(
'User is not allowed to edit this component %s' % component_path)
original_path = component_def.path
new_path = component_def.path
new_docstring = component_def.docstring
new_deprecated = component_def.deprecated
new_admin_ids = component_def.admin_ids
new_cc_ids = component_def.cc_ids
update_filterrule = False
for update in request.updates:
if update.field == api_pb2_v1.ComponentUpdateFieldID.LEAF_NAME:
leaf_name = update.leafName
if not tracker_constants.COMPONENT_NAME_RE.match(leaf_name):
raise config_svc.InvalidComponentNameException(
'The component name %s is invalid.' % leaf_name)
if '>' in original_path:
parent_path = original_path[:original_path.rindex('>')]
new_path = '%s>%s' % (parent_path, leaf_name)
else:
new_path = leaf_name
conflict = tracker_bizobj.FindComponentDef(new_path, config)
if conflict and conflict.component_id != component_def.component_id:
raise config_svc.InvalidComponentNameException(
'The name %s is already in use.' % new_path)
update_filterrule = True
elif update.field == api_pb2_v1.ComponentUpdateFieldID.DESCRIPTION:
new_docstring = update.description
elif update.field == api_pb2_v1.ComponentUpdateFieldID.ADMIN:
user_ids_dict = self._services.user.LookupUserIDs(
mar.cnxn, list(update.admin), autocreate=False)
new_admin_ids = [user_ids_dict[email] for email in update.admin]
elif update.field == api_pb2_v1.ComponentUpdateFieldID.CC:
user_ids_dict = self._services.user.LookupUserIDs(
mar.cnxn, list(update.cc), autocreate=False)
new_cc_ids = [user_ids_dict[email] for email in update.cc]
update_filterrule = True
elif update.field == api_pb2_v1.ComponentUpdateFieldID.DEPRECATED:
new_deprecated = update.deprecated
else:
logging.error('Unknown component field %r', update.field)
new_modified = int(time.time())
new_modifier_id = self._services.user.LookupUserID(
mar.cnxn, mar.auth.email, autocreate=False)
logging.info(
'Updating component id %d: path-%s, docstring-%s, deprecated-%s,'
' admin_ids-%s, cc_ids-%s modified by %s', component_def.component_id,
new_path, new_docstring, new_deprecated, new_admin_ids, new_cc_ids,
new_modifier_id)
self._services.config.UpdateComponentDef(
mar.cnxn, mar.project_id, component_def.component_id,
path=new_path, docstring=new_docstring, deprecated=new_deprecated,
admin_ids=new_admin_ids, cc_ids=new_cc_ids, modified=new_modified,
modifier_id=new_modifier_id)
# TODO(sheyang): reuse the code in componentdetails
if original_path != new_path:
# If the name changed then update all of its subcomponents as well.
subcomponent_ids = tracker_bizobj.FindMatchingComponentIDs(
original_path, config, exact=False)
for subcomponent_id in subcomponent_ids:
if subcomponent_id == component_def.component_id:
continue
subcomponent_def = tracker_bizobj.FindComponentDefByID(
subcomponent_id, config)
subcomponent_new_path = subcomponent_def.path.replace(
original_path, new_path, 1)
self._services.config.UpdateComponentDef(
mar.cnxn, mar.project_id, subcomponent_def.component_id,
path=subcomponent_new_path)
if update_filterrule:
filterrules_helpers.RecomputeAllDerivedFields(
mar.cnxn, self._services, mar.project, config)
return message_types.VoidMessage()
@endpoints.api(name='monorail_client_configs', version='v1',
description='Monorail API client configs.')
class ClientConfigApi(remote.Service):
# Class variables. Handy to mock.
_services = None
_mar = None
@classmethod
def _set_services(cls, services):
cls._services = services
def mar_factory(self, request):
if not self._mar:
self._mar = monorailrequest.MonorailApiRequest(request, self._services)
return self._mar
@endpoints.method(
message_types.VoidMessage,
message_types.VoidMessage,
path='client_configs',
http_method='POST',
name='client_configs.update')
def client_configs_update(self, request):
mar = self.mar_factory(request)
if not mar.perms.HasPerm(permissions.ADMINISTER_SITE, None, None):
raise permissions.PermissionException(
'The requester %s is not allowed to update client configs.' %
mar.auth.email)
ROLE_DICT = {
1: permissions.COMMITTER_ROLE,
2: permissions.CONTRIBUTOR_ROLE,
}
client_config = client_config_svc.GetClientConfigSvc()
cfg = client_config.GetConfigs()
if not cfg:
msg = 'Failed to fetch client configs.'
logging.error(msg)
raise endpoints.InternalServerErrorException(msg)
for client in cfg.clients:
if not client.client_email:
continue
# 1: create the user if non-existent
user_id = self._services.user.LookupUserID(
mar.cnxn, client.client_email, autocreate=True)
user_pb = self._services.user.GetUser(mar.cnxn, user_id)
logging.info('User ID %d for email %s', user_id, client.client_email)
# 2: set period and lifetime limit
# new_soft_limit, new_hard_limit, new_lifetime_limit
new_limit_tuple = (
client.period_limit, client.period_limit, client.lifetime_limit)
action_limit_updates = {'api_request': new_limit_tuple}
self._services.user.UpdateUserSettings(
mar.cnxn, user_id, user_pb, action_limit_updates=action_limit_updates)
logging.info('Updated api request limit %r', new_limit_tuple)
# 3: Update project role and extra perms
projects_dict = self._services.project.GetAllProjects(mar.cnxn)
project_name_to_ids = {
p.project_name: p.project_id for p in projects_dict.itervalues()}
# Set project role and extra perms
for perm in client.project_permissions:
project_ids = self._GetProjectIDs(perm.project, project_name_to_ids)
logging.info('Matching projects %r for name %s',
project_ids, perm.project)
role = ROLE_DICT[perm.role]
for p_id in project_ids:
project = projects_dict[p_id]
people_list = []
if role == 'owner':
people_list = project.owner_ids
elif role == 'committer':
people_list = project.committer_ids
elif role == 'contributor':
people_list = project.contributor_ids
# Onlu update role/extra perms iff changed
if not user_id in people_list:
logging.info('Update project %s role %s for user %s',
project.project_name, role, client.client_email)
owner_ids, committer_ids, contributor_ids = (
project_helpers.MembersWith(project, {user_id}, role))
self._services.project.UpdateProjectRoles(
mar.cnxn, p_id, owner_ids, committer_ids,
contributor_ids)
if perm.extra_permissions:
member_extra_perms = permissions.FindExtraPerms(project, user_id)
if (member_extra_perms and
set(member_extra_perms.perms) == set(perm.extra_permissions)):
continue
logging.info('Update project %s extra perm %s for user %s',
project.project_name, perm.extra_permissions,
client.client_email)
self._services.project.UpdateExtraPerms(
mar.cnxn, p_id, user_id, list(perm.extra_permissions))
return message_types.VoidMessage()
def _GetProjectIDs(self, project_str, project_name_to_ids):
result = []
if any(ch in project_str for ch in ['*', '+', '?', '.']):
pattern = re.compile(project_str)
for p_name in project_name_to_ids.iterkeys():
if pattern.match(p_name):
project_id = project_name_to_ids.get(p_name)
if project_id:
result.append(project_id)
else:
project_id = project_name_to_ids.get(project_str)
if project_id:
result.append(project_id)
if not result:
logging.warning('Cannot find projects for specified name %s',
project_str)
return result