| Index: appengine/monorail/services/api_svc_v1.py
|
| diff --git a/appengine/monorail/services/api_svc_v1.py b/appengine/monorail/services/api_svc_v1.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..2f228fab5364c9d4f8624da76cad8644a79ad508
|
| --- /dev/null
|
| +++ b/appengine/monorail/services/api_svc_v1.py
|
| @@ -0,0 +1,1164 @@
|
| +# 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
|
| +
|
| +
|
|
|