blob: 8d8f23828d5cb4313f44c98f6800ae621f690549 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed 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.
To manually test this API locally, use the following steps:
1. Start the development server via 'make serve'.
2. Start a new Chrome session via the command-line:
PATH_TO_CHROME --user-data-dir=/tmp/test \
--unsafely-treat-insecure-origin-as-secure=http://localhost:8080
3. Visit http://localhost:8080/_ah/api/explorer
4. Click shield icon in the omnibar and allow unsafe scripts.
5. Click on the "Services" menu item in the API Explorer.
"""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import calendar
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 businesslogic import work_env
from features import filterrules_helpers
from features import send_notifications
from framework import authdata
from framework import exceptions
from framework import framework_constants
from framework import framework_helpers
from framework import framework_views
from framework import monitoring
from framework import monorailrequest
from framework import permissions
from framework import ratelimiter
from framework import sql
from project import project_helpers
from proto import api_pb2_v1
from proto import project_pb2
from proto import tracker_pb2
from search import frontendsearchpipeline
from services import api_pb2_v1_helpers
from services import client_config_svc
from services import service_manager
from services import tracker_fulltext
from sitewide import sitewide_helpers
from tracker import field_helpers
from tracker import issuedetailezt
from tracker import tracker_bizobj
from tracker import tracker_constants
from tracker import tracker_helpers
from infra_libs import ts_mon
ENDPOINTS_API_NAME = 'monorail'
DOC_URL = (
'https://chromium.googlesource.com/infra/infra/+/main/'
'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', '')
http_method = kwargs.get('http_method', '')
def new_decorator(func):
@endpoints.method(request_message, response_message, **kwargs)
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
start_time = time_fn()
approximate_http_status = 200
request = args[0]
ret = None
c_id = None
c_email = None
mar = None
try:
if settings.read_only and http_method.lower() != 'get':
raise permissions.PermissionException(
'This request is not allowed in read-only mode')
requester = endpoints.get_current_user()
logging.info('requester is %r', requester)
logging.info('args is %r', args)
logging.info('kwargs is %r', kwargs)
auth_client_ids, auth_emails = (
client_config_svc.GetClientConfigSvc().GetClientIDEmails())
if settings.local_mode:
auth_client_ids.append(endpoints.API_EXPLORER_CLIENT_ID)
if self._services is None:
self._set_services(service_manager.set_up_services())
cnxn = sql.MonorailConnection()
c_id, c_email = api_base_checks(
request, requester, self._services, cnxn,
auth_client_ids, auth_emails)
mar = self.mar_factory(request, cnxn)
self.ratelimiter.CheckStart(c_id, c_email, start_time)
monitoring.IncrementAPIRequestsCount(
'endpoints', c_id, client_email=c_email, handler=method_name)
ret = func(self, mar, *args, **kwargs)
except exceptions.NoSuchUserException as e:
approximate_http_status = 404
raise endpoints.NotFoundException(
'The user does not exist: %s' % str(e))
except (exceptions.NoSuchProjectException,
exceptions.NoSuchIssueException,
exceptions.NoSuchComponentException) as e:
approximate_http_status = 404
raise endpoints.NotFoundException(str(e))
except (permissions.BannedUserException,
permissions.PermissionException) as e:
approximate_http_status = 403
logging.info('Allowlist ID %r email %r', auth_client_ids, auth_emails)
raise endpoints.ForbiddenException(str(e))
except endpoints.BadRequestException:
approximate_http_status = 400
raise
except endpoints.UnauthorizedException:
approximate_http_status = 401
# Client will refresh token and retry.
raise
except oauth.InvalidOAuthTokenError:
approximate_http_status = 401
# Client will refresh token and retry.
raise endpoints.UnauthorizedException(
'Auth error: InvalidOAuthTokenError')
except (exceptions.GroupExistsException,
exceptions.InvalidComponentNameException,
ratelimiter.ApiRateLimitExceeded) 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:
if mar:
mar.CleanUp()
now = time_fn()
if c_id and c_email:
self.ratelimiter.CheckEnd(c_id, c_email, now, start_time)
_RecordMonitoringStats(
start_time, request, ret, (method_name or func.__name__),
(method_path or func.__name__), approximate_http_status, now)
return ret
return wrapper
return new_decorator
def _RecordMonitoringStats(
start_time,
request,
response,
method_name,
method_path,
approximate_http_status,
now=None):
now = now or time.time()
elapsed_ms = int((now - start_time) * 1000)
# Use the api name, not the request path, to prevent an explosion in
# possible field values.
method_identifier = (
ENDPOINTS_API_NAME + '.endpoints.' + method_name + '/' + method_path)
# Endpoints APIs don't return the full set of http status values.
fields = monitoring.GetCommonFields(
approximate_http_status, method_identifier)
monitoring.AddServerDurations(elapsed_ms, fields)
monitoring.IncrementServerResponseStatusCount(fields)
request_length = len(protojson.encode_message(request))
monitoring.AddServerRequesteBytes(request_length, fields)
response_length = 0
if response:
response_length = len(protojson.encode_message(response))
monitoring.AddServerResponseBytes(response_length, fields)
def _is_requester_in_allowed_domains(requester):
if requester.email().endswith(settings.api_allowed_email_domains):
if framework_constants.MONORAIL_SCOPE in oauth.get_authorized_scopes(
framework_constants.MONORAIL_SCOPE):
return True
else:
logging.info("User is not authenticated with monorail scope")
return False
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:
Client ID and client email.
Raises:
endpoints.UnauthorizedException: If the requester is anonymous.
exceptions.NoSuchUserException: If the requester does not exist in Monorail.
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:
logging.info('Got oauth error: %r', ex)
auth_err = 'oauth.Error: %s' % ex
if client_id and requester:
if client_id in auth_client_ids:
# A allowlisted client app can make requests for any user or anon.
logging.info('Client ID %r is allowlisted', client_id)
valid_user = True
elif requester.email() in auth_emails:
# A allowlisted user account can make requests via any client app.
logging.info('Client email %r is allowlisted', requester.email())
valid_user = True
elif _is_requester_in_allowed_domains(requester):
# A user with an allowed-domain email and authenticated with the
# monorail scope is allowed to make requests via any client app.
logging.info(
'User email %r is within the allowed domains', requester.email())
valid_user = True
else:
auth_err = (
'Neither client ID %r nor email %r is allowlisted' %
(client_id, requester.email()))
if not valid_user:
raise endpoints.UnauthorizedException('Auth error: %s' % auth_err)
else:
logging.info('API request from user %s:%s', client_id, requester.email())
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 exceptions.NoSuchUserException
requester_id = services.user.LookupUserID(cnxn, requester.email())
auth = authdata.AuthData.FromUserID(cnxn, requester_id, services)
if permissions.IsBanned(auth.user_pb, auth.user_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 exceptions.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)
if not permissions.UserCanViewProject(
auth.user_pb, auth.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(
auth.user_pb, auth.effective_ids, project)
config = services.config.GetProjectConfig(cnxn, project.project_id)
granted_perms = tracker_bizobj.GetGrantedPerms(
issue, auth.effective_ids, config)
if not permissions.CanViewIssue(
auth.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))
return client_id, requester.email()
@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
ratelimiter = ratelimiter.ApiRateLimiter()
@classmethod
def _set_services(cls, services):
cls._services = services
def mar_factory(self, request, cnxn):
if not self._mar:
self._mar = monorailrequest.MonorailApiRequest(
request, self._services, cnxn=cnxn)
return self._mar
def aux_delete_comment(self, mar, request, delete=True):
action_name = 'delete' if delete else 'undelete'
with work_env.WorkEnv(mar, self._services) as we:
issue = we.GetIssueByLocalID(
mar.project_id, request.issueId, use_cache=False)
all_comments = we.ListIssueComments(issue)
try:
issue_comment = all_comments[request.commentId]
except IndexError:
raise exceptions.NoSuchIssueException(
'The issue %s:%d does not have comment %d.' %
(mar.project_name, request.issueId, request.commentId))
issue_perms = permissions.UpdateIssuePermissions(
mar.perms, mar.project, issue, mar.auth.effective_ids,
granted_perms=mar.granted_perms)
commenter = we.GetUser(issue_comment.user_id)
if not permissions.CanDeleteComment(
issue_comment, commenter, mar.auth.user_id, issue_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))
we.DeleteComment(issue, issue_comment, delete=delete)
return api_pb2_v1.IssuesCommentsDeleteResponse()
@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, mar, request):
"""Delete a comment."""
return self.aux_delete_comment(mar, request, True)
def parse_imported_reporter(self, mar, request):
"""Handle the case where an API client is importing issues for users.
Args:
mar: monorail API request object including auth and perms.
request: A request PB that defines author and published fields.
Returns:
A pair (reporter_id, timestamp) with the user ID of the user to
attribute the comment to and timestamp of the original comment.
If the author field is not set, this is not an import request
and the comment is attributed to the API client as per normal.
An API client that is attempting to post on behalf of other
users must have the ImportComment permission in the current
project.
"""
reporter_id = mar.auth.user_id
timestamp = None
if (request.author and request.author.name and
request.author.name != mar.auth.email):
if not mar.perms.HasPerm(
permissions.IMPORT_COMMENT, mar.auth.user_id, mar.project):
logging.info('name is %r', request.author.name)
raise permissions.PermissionException(
'User is not allowed to attribue comments to others')
reporter_id = self._services.user.LookupUserID(
mar.cnxn, request.author.name, autocreate=True)
logging.info('Importing issue or comment.')
if request.published:
timestamp = calendar.timegm(request.published.utctimetuple())
return reporter_id, timestamp
@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, mar, request):
# type (...) -> proto.api_pb2_v1.IssuesCommentsInsertResponse
"""Add a comment."""
# Because we will modify issues, load from DB rather than cache.
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId, use_cache=False)
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))
# Temporary block on updating approval subfields.
if request.updates and request.updates.fieldValues:
fds_by_name = {fd.field_name.lower():fd for fd in mar.config.field_defs}
for fv in request.updates.fieldValues:
# Checking for fv.approvalName is unreliable since it can be removed.
fd = fds_by_name.get(fv.fieldName.lower())
if fd and fd.approval_id:
raise exceptions.ActionNotSupported(
'No API support for approval field changes: (approval %s owns %s)'
% (fd.approval_id, fd.field_name))
# if fd was None, that gets dealt with later.
if request.content and len(
request.content) > tracker_constants.MAX_COMMENT_CHARS:
raise endpoints.BadRequestException(
'Comment is too long on this issue (%s, %d' %
(request.projectId, request.issueId))
updates_dict = {}
move_to_project = None
if request.updates:
if not permissions.CanEditIssue(
mar.auth.effective_ids, mar.perms, mar.project, issue,
mar.granted_perms):
raise permissions.PermissionException(
'User is not allowed to edit this issue (%s, %d)' %
(request.projectId, request.issueId))
if request.updates.moveToProject:
move_to = request.updates.moveToProject.lower()
move_to_project = issuedetailezt.CheckMoveIssueRequest(
self._services, mar, issue, True, move_to, mar.errors)
if mar.errors.AnyErrors():
raise endpoints.BadRequestException(mar.errors.move_to)
updates_dict['summary'] = request.updates.summary
updates_dict['status'] = request.updates.status
updates_dict['is_description'] = request.updates.is_description
if request.updates.owner:
# A current issue owner can be removed via the API with a
# NO_USER_NAME('----') input.
if request.updates.owner == framework_constants.NO_USER_NAME:
updates_dict['owner'] = framework_constants.NO_USER_SPECIFIED
else:
new_owner_id = self._services.user.LookupUserID(
mar.cnxn, request.updates.owner)
valid, msg = tracker_helpers.IsValidIssueOwner(
mar.cnxn, mar.project, new_owner_id, self._services)
if not valid:
raise endpoints.BadRequestException(msg)
updates_dict['owner'] = new_owner_id
updates_dict['cc_add'], updates_dict['cc_remove'] = (
api_pb2_v1_helpers.split_remove_add(request.updates.cc))
updates_dict['cc_add'] = list(self._services.user.LookupUserIDs(
mar.cnxn, updates_dict['cc_add'], autocreate=True).values())
updates_dict['cc_remove'] = list(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:
merge_project_name, merge_local_id = tracker_bizobj.ParseIssueRef(
request.updates.mergedInto)
merge_into_project = self._services.project.GetProjectByName(
mar.cnxn, merge_project_name or issue.project_name)
# Because we will modify issues, load from DB rather than cache.
merge_into_issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, merge_into_project.project_id, merge_local_id,
use_cache=False)
merge_allowed = tracker_helpers.IsMergeAllowed(
merge_into_issue, mar, self._services)
if not merge_allowed:
raise permissions.PermissionException(
'User is not allowed to merge into issue %s:%s' %
(merge_into_issue.project_name, merge_into_issue.local_id))
updates_dict['merged_into'] = merge_into_issue.issue_id
(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.cnxn, self._services,
(updates_dict.get('field_vals_add', []) +
updates_dict.get('field_vals_remove', [])),
mar.config, mar.project, ezt_errors=mar.errors)
if mar.errors.AnyErrors():
raise endpoints.BadRequestException(
'Invalid field values: %s' % mar.errors.custom_fields)
updates_dict['labels_add'] = (
updates_dict.get('labels_add', []) +
updates_dict.get('fields_labels_add', []))
updates_dict['labels_remove'] = (
updates_dict.get('labels_remove', []) +
updates_dict.get('fields_labels_remove', []))
# TODO(jrobbins): Stop using updates_dict in the first place.
delta = tracker_bizobj.MakeIssueDelta(
updates_dict.get('status'),
updates_dict.get('owner'),
updates_dict.get('cc_add', []),
updates_dict.get('cc_remove', []),
updates_dict.get('components_add', []),
updates_dict.get('components_remove', []),
(updates_dict.get('labels_add', []) +
updates_dict.get('fields_labels_add', [])),
(updates_dict.get('labels_remove', []) +
updates_dict.get('fields_labels_remove', [])),
updates_dict.get('field_vals_add', []),
updates_dict.get('field_vals_remove', []),
updates_dict.get('fields_clear', []),
updates_dict.get('blocked_on_add', []),
updates_dict.get('blocked_on_remove', []),
updates_dict.get('blocking_add', []),
updates_dict.get('blocking_remove', []),
updates_dict.get('merged_into'),
updates_dict.get('summary'))
importer_id = None
reporter_id, timestamp = self.parse_imported_reporter(mar, request)
if reporter_id != mar.auth.user_id:
importer_id = mar.auth.user_id
# TODO(jrobbins): Finish refactoring to make everything go through work_env.
_, comment = self._services.issue.DeltaUpdateIssue(
cnxn=mar.cnxn, services=self._services,
reporter_id=reporter_id, project_id=mar.project_id, config=mar.config,
issue=issue, delta=delta, index_now=False, comment=request.content,
is_description=updates_dict.get('is_description'),
timestamp=timestamp, importer_id=importer_id)
move_comment = None
if 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, issue, mar.auth.user_id, content, amendments=[
tracker_bizobj.MakeProjectAmendment(move_to_project.project_name)])
if 'merged_into' in updates_dict:
new_starrers = tracker_helpers.GetNewIssueStarrers(
mar.cnxn, self._services, [issue.issue_id], merge_into_issue.issue_id)
tracker_helpers.AddIssueStarrers(
mar.cnxn, self._services, mar,
merge_into_issue.issue_id, merge_into_project, new_starrers)
# Load target issue again to get the updated star count.
merge_into_issue = self._services.issue.GetIssue(
mar.cnxn, merge_into_issue.issue_id, use_cache=False)
merge_comment_pb = tracker_helpers.MergeCCsAndAddComment(
self._services, mar, issue, merge_into_issue)
hostport = framework_helpers.GetHostPort(
project_name=merge_into_issue.project_name)
send_notifications.PrepareAndSendIssueChangeNotification(
merge_into_issue.issue_id, hostport,
mar.auth.user_id, send_email=True, comment_id=merge_comment_pb.id)
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:
hostport = framework_helpers.GetHostPort(project_name=issue.project_name)
send_notifications.PrepareAndSendIssueChangeNotification(
issue.issue_id, hostport, comment.user_id, send_email=True,
old_owner_id=old_owner_id, comment_id=comment.id)
issue_perms = permissions.UpdateIssuePermissions(
mar.perms, mar.project, issue, mar.auth.effective_ids,
granted_perms=mar.granted_perms)
commenter = self._services.user.GetUser(mar.cnxn, comment.user_id)
can_delete = permissions.CanDeleteComment(
comment, commenter, mar.auth.user_id, issue_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, mar, request):
"""List all comments for an issue."""
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
comments = self._services.issue.GetCommentsForIssue(
mar.cnxn, issue.issue_id)
comments = [comment for comment in comments if not comment.approval_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, mar, request):
"""Restore a deleted comment."""
return self.aux_delete_comment(mar, request, False)
@monorail_api_method(
api_pb2_v1.APPROVALS_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.ApprovalsCommentsListResponse,
path='projects/{projectId}/issues/{issueId}/'
'approvals/{approvalName}/comments',
http_method='GET',
name='approvals.comments.list')
def approvals_comments_list(self, mar, request):
"""List all comments for an issue approval."""
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
if not permissions.CanViewIssue(
mar.auth.effective_ids, mar.perms, mar.project, issue,
mar.granted_perms):
raise permissions.PermissionException(
'User is not allowed to view this issue (%s, %d)' %
(request.projectId, request.issueId))
config = self._services.config.GetProjectConfig(mar.cnxn, issue.project_id)
approval_fd = tracker_bizobj.FindFieldDef(request.approvalName, config)
if not approval_fd:
raise endpoints.BadRequestException(
'Field definition for %s not found in project config' %
request.approvalName)
comments = self._services.issue.GetCommentsForIssue(
mar.cnxn, issue.issue_id)
comments = [comment for comment in comments
if comment.approval_id == approval_fd.field_id]
visible_comments = []
for comment in comments[
request.startIndex:(request.startIndex + request.maxResults)]:
visible_comments.append(
api_pb2_v1_helpers.convert_approval_comment(
issue, comment, mar, self._services, mar.granted_perms))
return api_pb2_v1.ApprovalsCommentsListResponse(
kind='monorail#approvalCommentList',
totalResults=len(comments),
items=visible_comments)
@monorail_api_method(
api_pb2_v1.APPROVALS_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER,
api_pb2_v1.ApprovalsCommentsInsertResponse,
path=("projects/{projectId}/issues/{issueId}/"
"approvals/{approvalName}/comments"),
http_method='POST',
name='approvals.comments.insert')
def approvals_comments_insert(self, mar, request):
# type (...) -> proto.api_pb2_v1.ApprovalsCommentsInsertResponse
"""Add an approval comment."""
approval_fd = tracker_bizobj.FindFieldDef(
request.approvalName, mar.config)
if not approval_fd or (
approval_fd.field_type != tracker_pb2.FieldTypes.APPROVAL_TYPE):
raise endpoints.BadRequestException(
'Field definition for %s not found in project config' %
request.approvalName)
try:
issue = self._services.issue.GetIssueByLocalID(
mar.cnxn, mar.project_id, request.issueId)
except exceptions.NoSuchIssueException:
raise endpoints.BadRequestException(
'Issue %s:%s not found' % (request.projectId, request.issueId))
approval = tracker_bizobj.FindApprovalValueByID(
approval_fd.field_id, issue.approval_values)
if not approval:
raise endpoints.BadRequestException(
'Approval %s not found in issue.' % request.approvalName)
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 on this issue (%s, %d)' %
(request.projectId, request.issueId))
if request.content and len(
request.content) > tracker_constants.MAX_COMMENT_CHARS:
raise endpoints.BadRequestException(
'Comment is too long on this issue (%s, %d' %
(request.projectId, request.issueId))
updates_dict = {}
if request.approvalUpdates:
if request.approvalUpdates.fieldValues:
# Block updating field values that don't belong to the approval.
approvals_fds_by_name = {
fd.field_name.lower():fd for fd in mar.config.field_defs
if fd.approval_id == approval_fd.field_id}
for fv in request.approvalUpdates.fieldValues:
if approvals_fds_by_name.get(fv.fieldName.lower()) is None:
raise endpoints.BadRequestException(
'Field defition for %s not found in %s subfields.' %
(fv.fieldName, request.approvalName))
(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.approvalUpdates.fieldValues, mar, self._services))
if request.approvalUpdates.approvers:
if not permissions.CanUpdateApprovers(
mar.auth.effective_ids, mar.perms, mar.project,
approval.approver_ids):
raise permissions.PermissionException(
'User is not allowed to update approvers')
approvers_add, approvers_remove = api_pb2_v1_helpers.split_remove_add(
request.approvalUpdates.approvers)
updates_dict['approver_ids_add'] = list(
self._services.user.LookupUserIDs(mar.cnxn, approvers_add,
autocreate=True).values())
updates_dict['approver_ids_remove'] = list(
self._services.user.LookupUserIDs(mar.cnxn, approvers_remove,
autocreate=True).values())
if request.approvalUpdates.status:
status = tracker_pb2.ApprovalStatus(
api_pb2_v1.ApprovalStatus(request.approvalUpdates.status).number)
if not permissions.CanUpdateApprovalStatus(
mar.auth.effective_ids, mar.perms, mar.project,
approval.approver_ids, status):
raise permissions.PermissionException(
'User is not allowed to make this status change')
updates_dict['status'] = status
logging.info(time.time)
approval_delta = tracker_bizobj.MakeApprovalDelta(
updates_dict.get('status'), mar.auth.user_id,
updates_dict.get('approver_ids_add', []),
updates_dict.get('approver_ids_remove', []),
updates_dict.get('field_vals_add', []),
updates_dict.get('field_vals_remove', []),
updates_dict.get('fields_clear', []),
updates_dict.get('fields_labels_add', []),
updates_dict.get('fields_labels_remove', []))
comment = self._services.issue.DeltaUpdateIssueApproval(
mar.cnxn, mar.auth.user_id, mar.config, issue, approval, approval_delta,
comment_content=request.content,
is_description=request.is_description)
cmnts = self._services.issue.GetCommentsForIssue(mar.cnxn, issue.issue_id)
seq = len(cmnts) - 1
if request.sendEmail:
hostport = framework_helpers.GetHostPort(project_name=issue.project_name)
send_notifications.PrepareAndSendApprovalChangeNotification(
issue.issue_id, approval.approval_id,
hostport, comment.id, send_email=True)
issue_perms = permissions.UpdateIssuePermissions(
mar.perms, mar.project, issue, mar.auth.effective_ids,
granted_perms=mar.granted_perms)
commenter = self._services.user.GetUser(mar.cnxn, comment.user_id)
can_delete = permissions.CanDeleteComment(
comment, commenter, mar.auth.user_id, issue_perms)
return api_pb2_v1.ApprovalsCommentsInsertResponse(
id=seq,
kind='monorail#approvalComment',
author=api_pb2_v1_helpers.convert_person(
comment.user_id, mar.cnxn, self._services),
content=comment.content,
published=datetime.datetime.fromtimestamp(comment.timestamp),
approvalUpdates=api_pb2_v1_helpers.convert_approval_amendments(
comment.amendments, mar, self._services),
canDelete=can_delete)
@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, mar, request):
"""Get a user."""
owner_project_only = request.ownerProjectsOnly
with work_env.WorkEnv(mar, self._services) as we:
(visible_ownership, visible_deleted, visible_membership,
visible_contrib) = we.GetUserProjects(
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)
templates = self._services.template.GetProjectTemplates(
mar.cnxn, config.project_id)
proj_result = api_pb2_v1_helpers.convert_project(
proj, config, api_pb2_v1.Role.owner, templates)
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)
templates = self._services.template.GetProjectTemplates(
mar.cnxn, config.project_id)
proj_result = api_pb2_v1_helpers.convert_project(
proj, config, api_pb2_v1.Role.member, templates)
project_list.append(proj_result)
for proj in visible_contrib:
config = self._services.config.GetProjectConfig(
mar.cnxn, proj.project_id)
templates = self._services.template.GetProjectTemplates(
mar.cnxn, config.project_id)
proj_result = api_pb2_v1_helpers.convert_project(
proj, config, api_pb2_v1.Role.contributor, templates)
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, mar, request):
"""Get an issue."""
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, mar, request):
"""Add a new issue."""
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))
with work_env.WorkEnv(mar, self._services) as we:
owner_id = framework_constants.NO_USER_SPECIFIED
if request.owner and request.owner.name:
try:
owner_id = self._services.user.LookupUserID(
mar.cnxn, request.owner.name)
except exceptions.NoSuchUserException:
raise endpoints.BadRequestException(
'The specified owner %s does not exist.' % request.owner.name)
cc_ids = []
request.cc = [cc for cc in request.cc if cc]
if request.cc:
cc_ids = list(self._services.user.LookupUserIDs(
mar.cnxn, [ap.name for ap in request.cc],
autocreate=True).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.cnxn, self._services, fields_add, mar.config, mar.project,
ezt_errors=mar.errors)
if mar.errors.AnyErrors():
raise endpoints.BadRequestException(
'Invalid field values: %s' % mar.errors.custom_fields)
logging.info('request.author is %r', request.author)
reporter_id, timestamp = self.parse_imported_reporter(mar, request)
# To preserve previous behavior, do not raise filter rule errors.
try:
new_issue, _ = we.CreateIssue(
mar.project_id,
request.summary,
request.status,
owner_id,
cc_ids,
request.labels + fields_labels,
fields_add,
comp_ids,
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),
reporter_id=reporter_id,
timestamp=timestamp,
send_email=request.sendEmail,
raise_filter_errors=False)
we.StarIssue(new_issue, True)
except exceptions.InputException as e:
raise endpoints.BadRequestException(str(e))
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, mar, request):
"""List issues for projects."""
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))
# TODO(jrobbins): This should go through work_env.
pipeline = frontendsearchpipeline.FrontendSearchPipeline(
mar.cnxn,
self._services,
mar.auth, [mar.me_user_id],
mar.query,
mar.query_project_names,
mar.num,
mar.start,
mar.can,
mar.group_by_spec,
mar.sort_spec,
mar.warnings,
mar.errors,
mar.use_cached_searches,
mar.profiler,
project=mar.project)
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='groupsettings',
http_method='GET',
name='groups.settings.list')
def groups_settings_list(self, mar, request):
"""List all group settings."""
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, mar, request):
"""Create a new user group."""
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 exceptions.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, mar, request):
"""Get a group's settings and users."""
if not mar.viewed_user_auth:
raise exceptions.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.CanViewGroupMembers(
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 = list(self._services.user.LookupUserEmails(
mar.cnxn, member_ids[group_id]).values())
owner_emails = list(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, mar, request):
"""Update a group's settings and users."""
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 = (
list(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, autocreate=True)
self._services.usergroup.UpdateMembers(
mar.cnxn, group_id, list(owners_dict.values()), 'owner')
members_dict = self._services.user.LookupUserIDs(
mar.cnxn, request.groupMembers, autocreate=True)
self._services.usergroup.UpdateMembers(
mar.cnxn, group_id, list(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, mar, _request):
"""List all components of a given project."""
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, mar, request):
"""Create a component."""
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 exceptions.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 exceptions.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 exceptions.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)
request.admin = [admin for admin in request.admin if admin]
admin_ids = [user_ids_dict[uname] for uname in request.admin]
request.cc = [cc for cc in request.cc if cc]
cc_ids = [user_ids_dict[uname] for uname in request.cc]
label_ids = [] # TODO(jrobbins): allow API clients to specify this too.
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], label_ids)
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, mar, request):
"""Delete a component."""
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 exceptions.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, mar, request):
"""Update a component."""
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 exceptions.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 exceptions.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 exceptions.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=True)
new_admin_ids = list(set(user_ids_dict.values()))
elif update.field == api_pb2_v1.ComponentUpdateFieldID.CC:
user_ids_dict = self._services.user.LookupUserIDs(
mar.cnxn, list(update.cc), autocreate=True)
new_cc_ids = list(set(user_ids_dict.values()))
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, cnxn):
if not self._mar:
self._mar = monorailrequest.MonorailApiRequest(
request, self._services, cnxn=cnxn)
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):
if self._services is None:
self._set_services(service_manager.set_up_services())
mar = self.mar_factory(request, sql.MonorailConnection())
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.values()}
# 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.MembersWithGivenIDs(project, {user_id}, role))
self._services.project.UpdateProjectRoles(
mar.cnxn, p_id, owner_ids, committer_ids,
contributor_ids)
if perm.extra_permissions:
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))
mar.CleanUp()
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.keys():
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