| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """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 mrproto import api_pb2_v1 |
| from mrproto import project_pb2 |
| from mrproto 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 redirect import redirect_utils |
| |
| 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) |
| # Allow to view non-live projects that were migrated. |
| if (project.state != project_pb2.ProjectState.LIVE and |
| project_name not in redirect_utils.PROJECT_REDIRECT_MAP): |
| 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.', |
| 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 (...) -> mrproto.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)) |
| |
| field_helpers.ValidateLabels( |
| mar.cnxn, |
| self._services, |
| mar.project_id, |
| updates_dict.get('labels_add', []), |
| ezt_errors=mar.errors) |
| if mar.errors.AnyErrors(): |
| raise endpoints.BadRequestException( |
| 'Invalid field values: %s' % mar.errors.labels) |
| |
| blocked_on_add_strs, blocked_on_remove_strs = ( |
| api_pb2_v1_helpers.split_remove_add(request.updates.blockedOn)) |
| blocking_add_strs, blocking_remove_strs = ( |
| api_pb2_v1_helpers.split_remove_add(request.updates.blocking)) |
| blocked_on_add_iids = api_pb2_v1_helpers.issue_global_ids( |
| blocked_on_add_strs, issue.project_id, mar, self._services) |
| blocked_on_remove_iids = api_pb2_v1_helpers.issue_global_ids( |
| blocked_on_remove_strs, issue.project_id, mar, self._services) |
| blocking_add_iids = api_pb2_v1_helpers.issue_global_ids( |
| blocking_add_strs, issue.project_id, mar, self._services) |
| blocking_remove_iids = api_pb2_v1_helpers.issue_global_ids( |
| blocking_remove_strs, issue.project_id, mar, self._services) |
| all_block = ( |
| blocked_on_add_iids + blocked_on_remove_iids + blocking_add_iids + |
| blocking_remove_iids) |
| for iid in all_block: |
| # Because we will modify issues, load from DB rather than cache. |
| issue = self._services.issue.GetIssue(mar.cnxn, iid, use_cache=False) |
| project = self._services.project.GetProjectByName( |
| mar.cnxn, issue.project_name) |
| if not tracker_helpers.CanEditProjectIssue(mar, project, issue, |
| mar.granted_perms): |
| raise permissions.PermissionException( |
| 'User is not allowed to block with issue (%s, %d)' % |
| (issue.project_name, issue.local_id)) |
| updates_dict['blocked_on_add'] = blocked_on_add_iids |
| updates_dict['blocked_on_remove'] = blocked_on_remove_iids |
| updates_dict['blocking_add'] = blocking_add_iids |
| updates_dict['blocking_remove'] = blocking_remove_iids |
| 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) |
| if not tracker_helpers.CanEditProjectIssue( |
| mar, merge_into_project, merge_into_issue, mar.granted_perms): |
| 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 (...) -> mrproto.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 |
| 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) |
| |
| with work_env.WorkEnv(mar, self._services) as we: |
| migrated_id = we.GetIssueMigratedID( |
| request.projectId, request.issueId, issue.labels) |
| |
| return api_pb2_v1_helpers.convert_issue( |
| api_pb2_v1.IssuesGetInsertResponse, issue, mar, self._services, |
| migrated_id) |
| |
| @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.ValidateLabels( |
| mar.cnxn, |
| self._services, |
| mar.project_id, |
| fields_labels, |
| ezt_errors=mar.errors) |
| if mar.errors.AnyErrors(): |
| raise endpoints.BadRequestException( |
| 'Invalid field values: %s' % mar.errors.labels) |
| |
| 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 permissions.CanEditProjectConfig(mar, self._services): |
| 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, self._services, 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, self._services, 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, self._services, 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 |