OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """API service""" |
| 7 |
| 8 import datetime |
| 9 import endpoints |
| 10 import functools |
| 11 import logging |
| 12 import re |
| 13 import time |
| 14 from google.appengine.api import oauth |
| 15 from protorpc import message_types |
| 16 from protorpc import protojson |
| 17 from protorpc import remote |
| 18 |
| 19 import settings |
| 20 from features import filterrules_helpers |
| 21 from features import notify |
| 22 from framework import actionlimit |
| 23 from framework import framework_constants |
| 24 from framework import framework_helpers |
| 25 from framework import framework_views |
| 26 from framework import monorailrequest |
| 27 from framework import permissions |
| 28 from framework import profiler |
| 29 from framework import sql |
| 30 from project import project_helpers |
| 31 from proto import api_pb2_v1 |
| 32 from proto import project_pb2 |
| 33 from search import frontendsearchpipeline |
| 34 from services import api_pb2_v1_helpers |
| 35 from services import client_config_svc |
| 36 from services import config_svc |
| 37 from services import issue_svc |
| 38 from services import project_svc |
| 39 from services import service_manager |
| 40 from services import tracker_fulltext |
| 41 from services import user_svc |
| 42 from services import usergroup_svc |
| 43 from sitewide import sitewide_helpers |
| 44 from tracker import field_helpers |
| 45 from tracker import issuedetail |
| 46 from tracker import tracker_constants |
| 47 from tracker import tracker_bizobj |
| 48 |
| 49 from infra_libs.ts_mon.common import http_metrics |
| 50 |
| 51 |
| 52 ENDPOINTS_API_NAME = 'monorail' |
| 53 DOC_URL = ('https://chromium.googlesource.com/infra/infra/+/master/' |
| 54 'appengine/monorail/doc/api.md') |
| 55 |
| 56 |
| 57 def monorail_api_method( |
| 58 request_message, response_message, **kwargs): |
| 59 """Extends endpoints.method by performing base checks.""" |
| 60 time_fn = kwargs.pop('time_fn', time.time) |
| 61 method_name = kwargs.get('name', '') |
| 62 method_path = kwargs.get('path', '') |
| 63 def new_decorator(func): |
| 64 @endpoints.method(request_message, response_message, **kwargs) |
| 65 @functools.wraps(func) |
| 66 def wrapper(self, *args, **kwargs): |
| 67 method_identifier = (ENDPOINTS_API_NAME + '.' + |
| 68 (method_name or func.__name__) |
| 69 + '/' + (method_path or func.__name__)) |
| 70 start_time = time_fn() |
| 71 approximate_http_status = 200 |
| 72 request = args[0] |
| 73 ret = None |
| 74 try: |
| 75 requester = endpoints.get_current_user() |
| 76 auth_client_ids, auth_emails = ( |
| 77 client_config_svc.GetClientConfigSvc().GetClientIDEmails()) |
| 78 auth_client_ids.append(endpoints.API_EXPLORER_CLIENT_ID) |
| 79 logging.info('Whitelist ID %r email %r', auth_client_ids, auth_emails) |
| 80 if self._services is None: |
| 81 self._set_services(service_manager.set_up_services()) |
| 82 api_base_checks( |
| 83 request, requester, |
| 84 self._services, sql.MonorailConnection(), |
| 85 auth_client_ids, auth_emails) |
| 86 self.increment_request_limit(request) |
| 87 ret = func(self, *args, **kwargs) |
| 88 except user_svc.NoSuchUserException as e: |
| 89 approximate_http_status = 404 |
| 90 raise endpoints.NotFoundException( |
| 91 'The user does not exist: %s' % str(e)) |
| 92 except (project_svc.NoSuchProjectException, |
| 93 issue_svc.NoSuchIssueException, |
| 94 config_svc.NoSuchComponentException) as e: |
| 95 approximate_http_status = 404 |
| 96 raise endpoints.NotFoundException(str(e)) |
| 97 except (permissions.BannedUserException, |
| 98 permissions.PermissionException) as e: |
| 99 approximate_http_status = 403 |
| 100 raise endpoints.ForbiddenException(str(e)) |
| 101 except endpoints.BadRequestException: |
| 102 approximate_http_status = 400 |
| 103 raise |
| 104 except endpoints.UnauthorizedException: |
| 105 approximate_http_status = 401 |
| 106 raise |
| 107 except actionlimit.ExcessiveActivityException as e: |
| 108 approximate_http_status = 403 |
| 109 raise endpoints.ForbiddenException( |
| 110 'The requester has exceeded API quotas limit') |
| 111 except (usergroup_svc.GroupExistsException, |
| 112 config_svc.InvalidComponentNameException) as e: |
| 113 approximate_http_status = 400 |
| 114 raise endpoints.BadRequestException(str(e)) |
| 115 except Exception as e: |
| 116 approximate_http_status = 500 |
| 117 logging.exception('Unexpected error in monorail API') |
| 118 raise |
| 119 finally: |
| 120 elapsed_ms = int((time_fn() - start_time) * 1000) |
| 121 |
| 122 fields = { |
| 123 # Endpoints APIs don't return the full set of http status values. |
| 124 'status': approximate_http_status, |
| 125 # Use the api name, not the request path, to prevent an |
| 126 # explosion in possible field values. |
| 127 'name': method_identifier, |
| 128 'is_robot': False, |
| 129 } |
| 130 |
| 131 http_metrics.server_durations.add(elapsed_ms, fields=fields) |
| 132 http_metrics.server_response_status.increment(fields=fields) |
| 133 http_metrics.server_request_bytes.add(len(protojson.encode_message( |
| 134 request)), fields=fields) |
| 135 response_size = 0 |
| 136 if ret: |
| 137 response_size = len(protojson.encode_message(ret)) |
| 138 http_metrics.server_response_bytes.add(response_size, fields=fields) |
| 139 |
| 140 return ret |
| 141 |
| 142 return wrapper |
| 143 return new_decorator |
| 144 |
| 145 |
| 146 def api_base_checks(request, requester, services, cnxn, |
| 147 auth_client_ids, auth_emails): |
| 148 """Base checks for API users. |
| 149 |
| 150 Args: |
| 151 request: The HTTP request from Cloud Endpoints. |
| 152 requester: The user who sends the request. |
| 153 services: Services object. |
| 154 cnxn: connection to the SQL database. |
| 155 auth_client_ids: authorized client ids. |
| 156 auth_emails: authorized emails when client is anonymous. |
| 157 |
| 158 Returns: |
| 159 Nothing |
| 160 |
| 161 Raises: |
| 162 endpoints.UnauthorizedException: If the requester is anonymous. |
| 163 user_svc.NoSuchUserException: If the requester does not exist in Monorail. |
| 164 project_svc.NoSuchProjectException: If the project does not exist in |
| 165 Monorail. |
| 166 permissions.BannedUserException: If the requester is banned. |
| 167 permissions.PermissionException: If the requester does not have |
| 168 permisssion to view. |
| 169 """ |
| 170 valid_user = False |
| 171 auth_err = '' |
| 172 client_id = None |
| 173 |
| 174 try: |
| 175 client_id = oauth.get_client_id(framework_constants.OAUTH_SCOPE) |
| 176 logging.info('Oauth client ID %s', client_id) |
| 177 except oauth.Error as ex: |
| 178 auth_err = 'oauth.Error: %s' % ex |
| 179 |
| 180 if not requester: |
| 181 try: |
| 182 requester = oauth.get_current_user(framework_constants.OAUTH_SCOPE) |
| 183 logging.info('Oauth requester %s', requester.email()) |
| 184 except oauth.Error as ex: |
| 185 auth_err = 'oauth.Error: %s' % ex |
| 186 |
| 187 if client_id and requester: |
| 188 if client_id != 'anonymous': |
| 189 if client_id in auth_client_ids: |
| 190 valid_user = True |
| 191 else: |
| 192 auth_err = 'Client ID %s is not whitelisted' % client_id |
| 193 # Some service accounts may have anonymous client ID |
| 194 else: |
| 195 if requester.email() in auth_emails: |
| 196 valid_user = True |
| 197 else: |
| 198 auth_err = 'Client email %s is not whitelisted' % requester.email() |
| 199 |
| 200 if not valid_user: |
| 201 raise endpoints.UnauthorizedException('Auth error: %s' % auth_err) |
| 202 |
| 203 project_name = None |
| 204 if hasattr(request, 'projectId'): |
| 205 project_name = request.projectId |
| 206 issue_local_id = None |
| 207 if hasattr(request, 'issueId'): |
| 208 issue_local_id = request.issueId |
| 209 # This could raise user_svc.NoSuchUserException |
| 210 requester_id = services.user.LookupUserID(cnxn, requester.email()) |
| 211 requester_pb = services.user.GetUser(cnxn, requester_id) |
| 212 requester_view = framework_views.UserView( |
| 213 requester_id, requester.email(), requester_pb.obscure_email) |
| 214 if permissions.IsBanned(requester_pb, requester_view): |
| 215 raise permissions.BannedUserException( |
| 216 'The user %s has been banned from using Monorail' % |
| 217 requester.email()) |
| 218 if project_name: |
| 219 project = services.project.GetProjectByName( |
| 220 cnxn, project_name) |
| 221 if not project: |
| 222 raise project_svc.NoSuchProjectException( |
| 223 'Project %s does not exist' % project_name) |
| 224 if project.state != project_pb2.ProjectState.LIVE: |
| 225 raise permissions.PermissionException( |
| 226 'API may not access project %s because it is not live' |
| 227 % project_name) |
| 228 requester_effective_ids = services.usergroup.LookupMemberships( |
| 229 cnxn, requester_id) |
| 230 requester_effective_ids.add(requester_id) |
| 231 if not permissions.UserCanViewProject( |
| 232 requester_pb, requester_effective_ids, project): |
| 233 raise permissions.PermissionException( |
| 234 'The user %s has no permission for project %s' % |
| 235 (requester.email(), project_name)) |
| 236 if issue_local_id: |
| 237 # This may raise a NoSuchIssueException. |
| 238 issue = services.issue.GetIssueByLocalID( |
| 239 cnxn, project.project_id, issue_local_id) |
| 240 perms = permissions.GetPermissions( |
| 241 requester_pb, requester_effective_ids, project) |
| 242 config = services.config.GetProjectConfig(cnxn, project.project_id) |
| 243 granted_perms = tracker_bizobj.GetGrantedPerms( |
| 244 issue, requester_effective_ids, config) |
| 245 if not permissions.CanViewIssue( |
| 246 requester_effective_ids, perms, project, issue, |
| 247 granted_perms=granted_perms): |
| 248 raise permissions.PermissionException( |
| 249 'User is not allowed to view this issue %s:%d' % |
| 250 (project_name, issue_local_id)) |
| 251 |
| 252 |
| 253 @endpoints.api(name=ENDPOINTS_API_NAME, version='v1', |
| 254 description='Monorail API to manage issues.', |
| 255 auth_level=endpoints.AUTH_LEVEL.NONE, |
| 256 allowed_client_ids=endpoints.SKIP_CLIENT_ID_CHECK, |
| 257 documentation=DOC_URL) |
| 258 class MonorailApi(remote.Service): |
| 259 |
| 260 # Class variables. Handy to mock. |
| 261 _services = None |
| 262 _mar = None |
| 263 |
| 264 @classmethod |
| 265 def _set_services(cls, services): |
| 266 cls._services = services |
| 267 |
| 268 def mar_factory(self, request): |
| 269 if not self._mar: |
| 270 self._mar = monorailrequest.MonorailApiRequest(request, self._services) |
| 271 return self._mar |
| 272 |
| 273 def aux_delete_comment(self, request, delete=True): |
| 274 mar = self.mar_factory(request) |
| 275 action_name = 'delete' if delete else 'undelete' |
| 276 |
| 277 issue = self._services.issue.GetIssueByLocalID( |
| 278 mar.cnxn, mar.project_id, request.issueId) |
| 279 all_comments = self._services.issue.GetCommentsForIssue( |
| 280 mar.cnxn, issue.issue_id) |
| 281 try: |
| 282 issue_comment = all_comments[request.commentId] |
| 283 except IndexError: |
| 284 raise issue_svc.NoSuchIssueException( |
| 285 'The issue %s:%d does not have comment %d.' % |
| 286 (mar.project_name, request.issueId, request.commentId)) |
| 287 |
| 288 if not permissions.CanDelete( |
| 289 mar.auth.user_id, mar.auth.effective_ids, mar.perms, |
| 290 issue_comment.deleted_by, issue_comment.user_id, mar.project, |
| 291 permissions.GetRestrictions(issue), mar.granted_perms): |
| 292 raise permissions.PermissionException( |
| 293 'User is not allowed to %s the comment %d of issue %s:%d' % |
| 294 (action_name, request.commentId, mar.project_name, |
| 295 request.issueId)) |
| 296 |
| 297 self._services.issue.SoftDeleteComment( |
| 298 mar.cnxn, mar.project_id, request.issueId, request.commentId, |
| 299 mar.auth.user_id, self._services.user, delete=delete) |
| 300 return api_pb2_v1.IssuesCommentsDeleteResponse() |
| 301 |
| 302 def increment_request_limit(self, request): |
| 303 """Check whether the requester has exceeded API quotas limit, |
| 304 and increment request count. |
| 305 """ |
| 306 mar = self.mar_factory(request) |
| 307 # soft_limit == hard_limit for api_request, so this function either |
| 308 # returns False if under limit, or raise ExcessiveActivityException |
| 309 if not actionlimit.NeedCaptcha( |
| 310 mar.auth.user_pb, actionlimit.API_REQUEST, skip_lifetime_check=True): |
| 311 actionlimit.CountAction( |
| 312 mar.auth.user_pb, actionlimit.API_REQUEST, delta=1) |
| 313 self._services.user.UpdateUser( |
| 314 mar.cnxn, mar.auth.user_id, mar.auth.user_pb) |
| 315 |
| 316 @monorail_api_method( |
| 317 api_pb2_v1.ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER, |
| 318 api_pb2_v1.IssuesCommentsDeleteResponse, |
| 319 path='projects/{projectId}/issues/{issueId}/comments/{commentId}', |
| 320 http_method='DELETE', |
| 321 name='issues.comments.delete') |
| 322 def issues_comments_delete(self, request): |
| 323 """Delete a comment.""" |
| 324 return self.aux_delete_comment(request, True) |
| 325 |
| 326 @monorail_api_method( |
| 327 api_pb2_v1.ISSUES_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER, |
| 328 api_pb2_v1.IssuesCommentsInsertResponse, |
| 329 path='projects/{projectId}/issues/{issueId}/comments', |
| 330 http_method='POST', |
| 331 name='issues.comments.insert') |
| 332 def issues_comments_insert(self, request): |
| 333 """Add a comment.""" |
| 334 mar = self.mar_factory(request) |
| 335 issue = self._services.issue.GetIssueByLocalID( |
| 336 mar.cnxn, mar.project_id, request.issueId) |
| 337 old_owner_id = tracker_bizobj.GetOwnerId(issue) |
| 338 if not permissions.CanCommentIssue( |
| 339 mar.auth.effective_ids, mar.perms, mar.project, issue, |
| 340 mar.granted_perms): |
| 341 raise permissions.PermissionException( |
| 342 'User is not allowed to comment this issue (%s, %d)' % |
| 343 (request.projectId, request.issueId)) |
| 344 |
| 345 updates_dict = {} |
| 346 if request.updates: |
| 347 if request.updates.moveToProject: |
| 348 move_to = request.updates.moveToProject.lower() |
| 349 move_to_project = issuedetail.CheckMoveIssueRequest( |
| 350 self._services, mar, issue, True, move_to, mar.errors) |
| 351 if mar.errors.AnyErrors(): |
| 352 raise endpoints.BadRequestException(mar.errors.move_to) |
| 353 updates_dict['move_to_project'] = move_to_project |
| 354 |
| 355 updates_dict['summary'] = request.updates.summary |
| 356 updates_dict['status'] = request.updates.status |
| 357 if request.updates.owner: |
| 358 if request.updates.owner == framework_constants.NO_USER_NAME: |
| 359 updates_dict['owner'] = framework_constants.NO_USER_SPECIFIED |
| 360 else: |
| 361 updates_dict['owner'] = self._services.user.LookupUserID( |
| 362 mar.cnxn, request.updates.owner) |
| 363 updates_dict['cc_add'], updates_dict['cc_remove'] = ( |
| 364 api_pb2_v1_helpers.split_remove_add(request.updates.cc)) |
| 365 updates_dict['cc_add'] = self._services.user.LookupUserIDs( |
| 366 mar.cnxn, updates_dict['cc_add']).values() |
| 367 updates_dict['cc_remove'] = self._services.user.LookupUserIDs( |
| 368 mar.cnxn, updates_dict['cc_remove']).values() |
| 369 updates_dict['labels_add'], updates_dict['labels_remove'] = ( |
| 370 api_pb2_v1_helpers.split_remove_add(request.updates.labels)) |
| 371 blocked_on_add_strs, blocked_on_remove_strs = ( |
| 372 api_pb2_v1_helpers.split_remove_add(request.updates.blockedOn)) |
| 373 updates_dict['blocked_on_add'] = api_pb2_v1_helpers.issue_global_ids( |
| 374 blocked_on_add_strs, issue.project_id, mar, |
| 375 self._services) |
| 376 updates_dict['blocked_on_remove'] = api_pb2_v1_helpers.issue_global_ids( |
| 377 blocked_on_remove_strs, issue.project_id, mar, |
| 378 self._services) |
| 379 blocking_add_strs, blocking_remove_strs = ( |
| 380 api_pb2_v1_helpers.split_remove_add(request.updates.blocking)) |
| 381 updates_dict['blocking_add'] = api_pb2_v1_helpers.issue_global_ids( |
| 382 blocking_add_strs, issue.project_id, mar, |
| 383 self._services) |
| 384 updates_dict['blocking_remove'] = api_pb2_v1_helpers.issue_global_ids( |
| 385 blocking_remove_strs, issue.project_id, mar, |
| 386 self._services) |
| 387 components_add_strs, components_remove_strs = ( |
| 388 api_pb2_v1_helpers.split_remove_add(request.updates.components)) |
| 389 updates_dict['components_add'] = ( |
| 390 api_pb2_v1_helpers.convert_component_ids( |
| 391 mar.config, components_add_strs)) |
| 392 updates_dict['components_remove'] = ( |
| 393 api_pb2_v1_helpers.convert_component_ids( |
| 394 mar.config, components_remove_strs)) |
| 395 if request.updates.mergedInto: |
| 396 updates_dict['merged_into'] = self._services.issue.LookupIssueID( |
| 397 mar.cnxn, issue.project_id, int(request.updates.mergedInto)) |
| 398 (updates_dict['field_vals_add'], updates_dict['field_vals_remove'], |
| 399 updates_dict['fields_clear'], updates_dict['fields_labels_add'], |
| 400 updates_dict['fields_labels_remove']) = ( |
| 401 api_pb2_v1_helpers.convert_field_values( |
| 402 request.updates.fieldValues, mar, self._services)) |
| 403 |
| 404 field_helpers.ValidateCustomFields( |
| 405 mar, self._services, |
| 406 (updates_dict.get('field_vals_add', []) + |
| 407 updates_dict.get('field_vals_remove', [])), |
| 408 mar.config, mar.errors) |
| 409 if mar.errors.AnyErrors(): |
| 410 raise endpoints.BadRequestException( |
| 411 'Invalid field values: %s' % mar.errors.custom_fields) |
| 412 |
| 413 _, comment = self._services.issue.DeltaUpdateIssue( |
| 414 cnxn=mar.cnxn, services=self._services, |
| 415 reporter_id=mar.auth.user_id, |
| 416 project_id=mar.project_id, config=mar.config, issue=issue, |
| 417 status=updates_dict.get('status'), owner_id=updates_dict.get('owner'), |
| 418 cc_add=updates_dict.get('cc_add', []), |
| 419 cc_remove=updates_dict.get('cc_remove', []), |
| 420 comp_ids_add=updates_dict.get('components_add', []), |
| 421 comp_ids_remove=updates_dict.get('components_remove', []), |
| 422 labels_add=(updates_dict.get('labels_add', []) + |
| 423 updates_dict.get('fields_labels_add', [])), |
| 424 labels_remove=(updates_dict.get('labels_remove', []) + |
| 425 updates_dict.get('fields_labels_remove', [])), |
| 426 field_vals_add=updates_dict.get('field_vals_add', []), |
| 427 field_vals_remove=updates_dict.get('field_vals_remove', []), |
| 428 fields_clear=updates_dict.get('fields_clear', []), |
| 429 blocked_on_add=updates_dict.get('blocked_on_add', []), |
| 430 blocked_on_remove=updates_dict.get('blocked_on_remove', []), |
| 431 blocking_add=updates_dict.get('blocking_add', []), |
| 432 blocking_remove=updates_dict.get('blocking_remove', []), |
| 433 merged_into=updates_dict.get('merged_into'), |
| 434 index_now=False, |
| 435 comment=request.content, |
| 436 summary=updates_dict.get('summary'), |
| 437 ) |
| 438 |
| 439 move_comment = None |
| 440 if 'move_to_project' in updates_dict: |
| 441 move_to_project = updates_dict['move_to_project'] |
| 442 old_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id) |
| 443 tracker_fulltext.UnindexIssues([issue.issue_id]) |
| 444 moved_back_iids = self._services.issue.MoveIssues( |
| 445 mar.cnxn, move_to_project, [issue], self._services.user) |
| 446 new_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id) |
| 447 if issue.issue_id in moved_back_iids: |
| 448 content = 'Moved %s back to %s again.' % (old_text_ref, new_text_ref) |
| 449 else: |
| 450 content = 'Moved %s to now be %s.' % (old_text_ref, new_text_ref) |
| 451 move_comment = self._services.issue.CreateIssueComment( |
| 452 mar.cnxn, move_to_project.project_id, issue.local_id, mar.auth.user_id, |
| 453 content, amendments=[ |
| 454 tracker_bizobj.MakeProjectAmendment(move_to_project.project_name)]) |
| 455 |
| 456 tracker_fulltext.IndexIssues( |
| 457 mar.cnxn, [issue], self._services.user, self._services.issue, |
| 458 self._services.config) |
| 459 |
| 460 comment = comment or move_comment |
| 461 if comment is None: |
| 462 return api_pb2_v1.IssuesCommentsInsertResponse() |
| 463 |
| 464 cmnts = self._services.issue.GetCommentsForIssue(mar.cnxn, issue.issue_id) |
| 465 seq = len(cmnts) - 1 |
| 466 |
| 467 if request.sendEmail: |
| 468 notify.PrepareAndSendIssueChangeNotification( |
| 469 issue.project_id, issue.local_id, framework_helpers.GetHostPort(), |
| 470 comment.user_id, seq, send_email=True, old_owner_id=old_owner_id) |
| 471 |
| 472 can_delete = permissions.CanDelete( |
| 473 mar.auth.user_id, mar.auth.effective_ids, mar.perms, |
| 474 comment.deleted_by, comment.user_id, mar.project, |
| 475 permissions.GetRestrictions(issue), granted_perms=mar.granted_perms) |
| 476 return api_pb2_v1.IssuesCommentsInsertResponse( |
| 477 id=seq, |
| 478 kind='monorail#issueComment', |
| 479 author=api_pb2_v1_helpers.convert_person( |
| 480 comment.user_id, mar.cnxn, self._services), |
| 481 content=comment.content, |
| 482 published=datetime.datetime.fromtimestamp(comment.timestamp), |
| 483 updates=api_pb2_v1_helpers.convert_amendments( |
| 484 issue, comment.amendments, mar, self._services), |
| 485 canDelete=can_delete) |
| 486 |
| 487 @monorail_api_method( |
| 488 api_pb2_v1.ISSUES_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER, |
| 489 api_pb2_v1.IssuesCommentsListResponse, |
| 490 path='projects/{projectId}/issues/{issueId}/comments', |
| 491 http_method='GET', |
| 492 name='issues.comments.list') |
| 493 def issues_comments_list(self, request): |
| 494 """List all comments for an issue.""" |
| 495 mar = self.mar_factory(request) |
| 496 issue = self._services.issue.GetIssueByLocalID( |
| 497 mar.cnxn, mar.project_id, request.issueId) |
| 498 comments = self._services.issue.GetCommentsForIssue( |
| 499 mar.cnxn, issue.issue_id) |
| 500 visible_comments = [] |
| 501 for comment in comments[ |
| 502 request.startIndex:(request.startIndex + request.maxResults)]: |
| 503 visible_comments.append( |
| 504 api_pb2_v1_helpers.convert_comment( |
| 505 issue, comment, mar, self._services, mar.granted_perms)) |
| 506 |
| 507 return api_pb2_v1.IssuesCommentsListResponse( |
| 508 kind='monorail#issueCommentList', |
| 509 totalResults=len(comments), |
| 510 items=visible_comments) |
| 511 |
| 512 @monorail_api_method( |
| 513 api_pb2_v1.ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER, |
| 514 api_pb2_v1.IssuesCommentsDeleteResponse, |
| 515 path='projects/{projectId}/issues/{issueId}/comments/{commentId}', |
| 516 http_method='POST', |
| 517 name='issues.comments.undelete') |
| 518 def issues_comments_undelete(self, request): |
| 519 """Restore a deleted comment.""" |
| 520 return self.aux_delete_comment(request, False) |
| 521 |
| 522 @monorail_api_method( |
| 523 api_pb2_v1.USERS_GET_REQUEST_RESOURCE_CONTAINER, |
| 524 api_pb2_v1.UsersGetResponse, |
| 525 path='users/{userId}', |
| 526 http_method='GET', |
| 527 name='users.get') |
| 528 def users_get(self, request): |
| 529 """Get a user.""" |
| 530 owner_project_only = request.ownerProjectsOnly |
| 531 mar = self.mar_factory(request) |
| 532 (visible_ownership, visible_deleted, visible_membership, |
| 533 visible_contrib) = sitewide_helpers.GetUserProjects( |
| 534 mar.cnxn, self._services, mar.auth.user_pb, mar.auth.effective_ids, |
| 535 mar.viewed_user_auth.effective_ids) |
| 536 |
| 537 project_list = [] |
| 538 for proj in (visible_ownership + visible_deleted): |
| 539 config = self._services.config.GetProjectConfig( |
| 540 mar.cnxn, proj.project_id) |
| 541 proj_result = api_pb2_v1_helpers.convert_project( |
| 542 proj, config, api_pb2_v1.Role.owner) |
| 543 project_list.append(proj_result) |
| 544 if not owner_project_only: |
| 545 for proj in visible_membership: |
| 546 config = self._services.config.GetProjectConfig( |
| 547 mar.cnxn, proj.project_id) |
| 548 proj_result = api_pb2_v1_helpers.convert_project( |
| 549 proj, config, api_pb2_v1.Role.member) |
| 550 project_list.append(proj_result) |
| 551 for proj in visible_contrib: |
| 552 config = self._services.config.GetProjectConfig( |
| 553 mar.cnxn, proj.project_id) |
| 554 proj_result = api_pb2_v1_helpers.convert_project( |
| 555 proj, config, api_pb2_v1.Role.contributor) |
| 556 project_list.append(proj_result) |
| 557 |
| 558 return api_pb2_v1.UsersGetResponse( |
| 559 id=str(mar.viewed_user_auth.user_id), |
| 560 kind='monorail#user', |
| 561 projects=project_list, |
| 562 ) |
| 563 |
| 564 @monorail_api_method( |
| 565 api_pb2_v1.ISSUES_GET_REQUEST_RESOURCE_CONTAINER, |
| 566 api_pb2_v1.IssuesGetInsertResponse, |
| 567 path='projects/{projectId}/issues/{issueId}', |
| 568 http_method='GET', |
| 569 name='issues.get') |
| 570 def issues_get(self, request): |
| 571 """Get an issue.""" |
| 572 mar = self.mar_factory(request) |
| 573 issue = self._services.issue.GetIssueByLocalID( |
| 574 mar.cnxn, mar.project_id, request.issueId) |
| 575 |
| 576 return api_pb2_v1_helpers.convert_issue( |
| 577 api_pb2_v1.IssuesGetInsertResponse, issue, mar, self._services) |
| 578 |
| 579 @monorail_api_method( |
| 580 api_pb2_v1.ISSUES_INSERT_REQUEST_RESOURCE_CONTAINER, |
| 581 api_pb2_v1.IssuesGetInsertResponse, |
| 582 path='projects/{projectId}/issues', |
| 583 http_method='POST', |
| 584 name='issues.insert') |
| 585 def issues_insert(self, request): |
| 586 """Add a new issue.""" |
| 587 mar = self.mar_factory(request) |
| 588 if not mar.perms.CanUsePerm( |
| 589 permissions.CREATE_ISSUE, mar.auth.effective_ids, mar.project, []): |
| 590 raise permissions.PermissionException( |
| 591 'The requester %s is not allowed to create issues for project %s.' % |
| 592 (mar.auth.email, mar.project_name)) |
| 593 |
| 594 owner_id = None |
| 595 if request.owner: |
| 596 try: |
| 597 owner_id = self._services.user.LookupUserID( |
| 598 mar.cnxn, request.owner.name) |
| 599 except user_svc.NoSuchUserException: |
| 600 raise endpoints.BadRequestException( |
| 601 'The specified owner %s does not exist.' % request.owner.name) |
| 602 |
| 603 cc_ids = [] |
| 604 if request.cc: |
| 605 cc_ids = self._services.user.LookupUserIDs( |
| 606 mar.cnxn, [ap.name for ap in request.cc]).values() |
| 607 comp_ids = api_pb2_v1_helpers.convert_component_ids( |
| 608 mar.config, request.components) |
| 609 fields_add, _, _, fields_labels, _ = ( |
| 610 api_pb2_v1_helpers.convert_field_values( |
| 611 request.fieldValues, mar, self._services)) |
| 612 field_helpers.ValidateCustomFields( |
| 613 mar, self._services, fields_add, mar.config, mar.errors) |
| 614 if mar.errors.AnyErrors(): |
| 615 raise endpoints.BadRequestException( |
| 616 'Invalid field values: %s' % mar.errors.custom_fields) |
| 617 |
| 618 local_id = self._services.issue.CreateIssue( |
| 619 mar.cnxn, self._services, mar.project_id, |
| 620 request.summary, request.status, owner_id, |
| 621 cc_ids, request.labels + fields_labels, fields_add, |
| 622 comp_ids, mar.auth.user_id, request.description, |
| 623 blocked_on=api_pb2_v1_helpers.convert_issueref_pbs( |
| 624 request.blockedOn, mar, self._services), |
| 625 blocking=api_pb2_v1_helpers.convert_issueref_pbs( |
| 626 request.blocking, mar, self._services)) |
| 627 new_issue = self._services.issue.GetIssueByLocalID( |
| 628 mar.cnxn, mar.project_id, local_id) |
| 629 |
| 630 if request.sendEmail: |
| 631 notify.PrepareAndSendIssueChangeNotification( |
| 632 mar.project_id, local_id, framework_helpers.GetHostPort(), |
| 633 new_issue.reporter_id, 0) |
| 634 |
| 635 return api_pb2_v1_helpers.convert_issue( |
| 636 api_pb2_v1.IssuesGetInsertResponse, new_issue, mar, self._services) |
| 637 |
| 638 @monorail_api_method( |
| 639 api_pb2_v1.ISSUES_LIST_REQUEST_RESOURCE_CONTAINER, |
| 640 api_pb2_v1.IssuesListResponse, |
| 641 path='projects/{projectId}/issues', |
| 642 http_method='GET', |
| 643 name='issues.list') |
| 644 def issues_list(self, request): |
| 645 """List issues for projects.""" |
| 646 mar = self.mar_factory(request) |
| 647 |
| 648 if request.additionalProject: |
| 649 for project_name in request.additionalProject: |
| 650 project = self._services.project.GetProjectByName( |
| 651 mar.cnxn, project_name) |
| 652 if project and not permissions.UserCanViewProject( |
| 653 mar.auth.user_pb, mar.auth.effective_ids, project): |
| 654 raise permissions.PermissionException( |
| 655 'The user %s has no permission for project %s' % |
| 656 (mar.auth.email, project_name)) |
| 657 prof = profiler.Profiler() |
| 658 pipeline = frontendsearchpipeline.FrontendSearchPipeline( |
| 659 mar, self._services, prof, mar.num) |
| 660 if not mar.errors.AnyErrors(): |
| 661 pipeline.SearchForIIDs() |
| 662 pipeline.MergeAndSortIssues() |
| 663 pipeline.Paginate() |
| 664 else: |
| 665 raise endpoints.BadRequestException(mar.errors.query) |
| 666 |
| 667 issue_list = [ |
| 668 api_pb2_v1_helpers.convert_issue( |
| 669 api_pb2_v1.IssueWrapper, r, mar, self._services) |
| 670 for r in pipeline.visible_results] |
| 671 return api_pb2_v1.IssuesListResponse( |
| 672 kind='monorail#issueList', |
| 673 totalResults=pipeline.total_count, |
| 674 items=issue_list) |
| 675 |
| 676 @monorail_api_method( |
| 677 api_pb2_v1.GROUPS_SETTINGS_LIST_REQUEST_RESOURCE_CONTAINER, |
| 678 api_pb2_v1.GroupsSettingsListResponse, |
| 679 path='groups/settings', |
| 680 http_method='GET', |
| 681 name='groups.settings.list') |
| 682 def groups_settings_list(self, request): |
| 683 """List all group settings.""" |
| 684 mar = self.mar_factory(request) |
| 685 all_groups = self._services.usergroup.GetAllUserGroupsInfo(mar.cnxn) |
| 686 group_settings = [] |
| 687 for g in all_groups: |
| 688 setting = g[2] |
| 689 wrapper = api_pb2_v1_helpers.convert_group_settings(g[0], setting) |
| 690 if not request.importedGroupsOnly or wrapper.ext_group_type: |
| 691 group_settings.append(wrapper) |
| 692 return api_pb2_v1.GroupsSettingsListResponse( |
| 693 groupSettings=group_settings) |
| 694 |
| 695 @monorail_api_method( |
| 696 api_pb2_v1.GROUPS_CREATE_REQUEST_RESOURCE_CONTAINER, |
| 697 api_pb2_v1.GroupsCreateResponse, |
| 698 path='groups', |
| 699 http_method='POST', |
| 700 name='groups.create') |
| 701 def groups_create(self, request): |
| 702 """Create a new user group.""" |
| 703 mar = self.mar_factory(request) |
| 704 if not permissions.CanCreateGroup(mar.perms): |
| 705 raise permissions.PermissionException( |
| 706 'The user is not allowed to create groups.') |
| 707 |
| 708 user_dict = self._services.user.LookupExistingUserIDs( |
| 709 mar.cnxn, [request.groupName]) |
| 710 if request.groupName.lower() in user_dict: |
| 711 raise usergroup_svc.GroupExistsException( |
| 712 'group %s already exists' % request.groupName) |
| 713 |
| 714 if request.ext_group_type: |
| 715 ext_group_type = str(request.ext_group_type).lower() |
| 716 else: |
| 717 ext_group_type = None |
| 718 group_id = self._services.usergroup.CreateGroup( |
| 719 mar.cnxn, self._services, request.groupName, |
| 720 str(request.who_can_view_members).lower(), |
| 721 ext_group_type) |
| 722 |
| 723 return api_pb2_v1.GroupsCreateResponse( |
| 724 groupID=group_id) |
| 725 |
| 726 @monorail_api_method( |
| 727 api_pb2_v1.GROUPS_GET_REQUEST_RESOURCE_CONTAINER, |
| 728 api_pb2_v1.GroupsGetResponse, |
| 729 path='groups/{groupName}', |
| 730 http_method='GET', |
| 731 name='groups.get') |
| 732 def groups_get(self, request): |
| 733 """Get a group's settings and users.""" |
| 734 mar = self.mar_factory(request) |
| 735 if not mar.viewed_user_auth: |
| 736 raise user_svc.NoSuchUserException(request.groupName) |
| 737 group_id = mar.viewed_user_auth.user_id |
| 738 group_settings = self._services.usergroup.GetGroupSettings( |
| 739 mar.cnxn, group_id) |
| 740 member_ids, owner_ids = self._services.usergroup.LookupAllMembers( |
| 741 mar.cnxn, [group_id]) |
| 742 (owned_project_ids, membered_project_ids, |
| 743 contrib_project_ids) = self._services.project.GetUserRolesInAllProjects( |
| 744 mar.cnxn, mar.auth.effective_ids) |
| 745 project_ids = owned_project_ids.union( |
| 746 membered_project_ids).union(contrib_project_ids) |
| 747 if not permissions.CanViewGroup( |
| 748 mar.perms, mar.auth.effective_ids, group_settings, member_ids[group_id], |
| 749 owner_ids[group_id], project_ids): |
| 750 raise permissions.PermissionException( |
| 751 'The user is not allowed to view this group.') |
| 752 |
| 753 member_ids, owner_ids = self._services.usergroup.LookupMembers( |
| 754 mar.cnxn, [group_id]) |
| 755 |
| 756 member_emails = self._services.user.LookupUserEmails( |
| 757 mar.cnxn, member_ids[group_id]).values() |
| 758 owner_emails = self._services.user.LookupUserEmails( |
| 759 mar.cnxn, owner_ids[group_id]).values() |
| 760 |
| 761 return api_pb2_v1.GroupsGetResponse( |
| 762 groupID=group_id, |
| 763 groupSettings=api_pb2_v1_helpers.convert_group_settings( |
| 764 request.groupName, group_settings), |
| 765 groupOwners=owner_emails, |
| 766 groupMembers=member_emails) |
| 767 |
| 768 @monorail_api_method( |
| 769 api_pb2_v1.GROUPS_UPDATE_REQUEST_RESOURCE_CONTAINER, |
| 770 api_pb2_v1.GroupsUpdateResponse, |
| 771 path='groups/{groupName}', |
| 772 http_method='POST', |
| 773 name='groups.update') |
| 774 def groups_update(self, request): |
| 775 """Update a group's settings and users.""" |
| 776 mar = self.mar_factory(request) |
| 777 group_id = mar.viewed_user_auth.user_id |
| 778 member_ids_dict, owner_ids_dict = self._services.usergroup.LookupMembers( |
| 779 mar.cnxn, [group_id]) |
| 780 owner_ids = owner_ids_dict.get(group_id, []) |
| 781 member_ids = member_ids_dict.get(group_id, []) |
| 782 if not permissions.CanEditGroup( |
| 783 mar.perms, mar.auth.effective_ids, owner_ids): |
| 784 raise permissions.PermissionException( |
| 785 'The user is not allowed to edit this group.') |
| 786 |
| 787 group_settings = self._services.usergroup.GetGroupSettings( |
| 788 mar.cnxn, group_id) |
| 789 if (request.who_can_view_members or request.ext_group_type |
| 790 or request.last_sync_time or request.friend_projects): |
| 791 group_settings.who_can_view_members = ( |
| 792 request.who_can_view_members or group_settings.who_can_view_members) |
| 793 group_settings.ext_group_type = ( |
| 794 request.ext_group_type or group_settings.ext_group_type) |
| 795 group_settings.last_sync_time = ( |
| 796 request.last_sync_time or group_settings.last_sync_time) |
| 797 if framework_constants.NO_VALUES in request.friend_projects: |
| 798 group_settings.friend_projects = [] |
| 799 else: |
| 800 id_dict = self._services.project.LookupProjectIDs( |
| 801 mar.cnxn, request.friend_projects) |
| 802 group_settings.friend_projects = ( |
| 803 id_dict.values() or group_settings.friend_projects) |
| 804 self._services.usergroup.UpdateSettings( |
| 805 mar.cnxn, group_id, group_settings) |
| 806 |
| 807 if request.groupOwners or request.groupMembers: |
| 808 self._services.usergroup.RemoveMembers( |
| 809 mar.cnxn, group_id, owner_ids + member_ids) |
| 810 owners_dict = self._services.user.LookupUserIDs( |
| 811 mar.cnxn, request.groupOwners, True) |
| 812 self._services.usergroup.UpdateMembers( |
| 813 mar.cnxn, group_id, owners_dict.values(), 'owner') |
| 814 members_dict = self._services.user.LookupUserIDs( |
| 815 mar.cnxn, request.groupMembers, True) |
| 816 self._services.usergroup.UpdateMembers( |
| 817 mar.cnxn, group_id, members_dict.values(), 'member') |
| 818 |
| 819 return api_pb2_v1.GroupsUpdateResponse() |
| 820 |
| 821 @monorail_api_method( |
| 822 api_pb2_v1.COMPONENTS_LIST_REQUEST_RESOURCE_CONTAINER, |
| 823 api_pb2_v1.ComponentsListResponse, |
| 824 path='projects/{projectId}/components', |
| 825 http_method='GET', |
| 826 name='components.list') |
| 827 def components_list(self, request): |
| 828 """List all components of a given project.""" |
| 829 mar = self.mar_factory(request) |
| 830 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id) |
| 831 components = [api_pb2_v1_helpers.convert_component_def( |
| 832 cd, mar, self._services) for cd in config.component_defs] |
| 833 return api_pb2_v1.ComponentsListResponse( |
| 834 components=components) |
| 835 |
| 836 @monorail_api_method( |
| 837 api_pb2_v1.COMPONENTS_CREATE_REQUEST_RESOURCE_CONTAINER, |
| 838 api_pb2_v1.Component, |
| 839 path='projects/{projectId}/components', |
| 840 http_method='POST', |
| 841 name='components.create') |
| 842 def components_create(self, request): |
| 843 """Create a component.""" |
| 844 mar = self.mar_factory(request) |
| 845 if not mar.perms.CanUsePerm( |
| 846 permissions.EDIT_PROJECT, mar.auth.effective_ids, mar.project, []): |
| 847 raise permissions.PermissionException( |
| 848 'User is not allowed to create components for this project') |
| 849 |
| 850 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id) |
| 851 leaf_name = request.componentName |
| 852 if not tracker_constants.COMPONENT_NAME_RE.match(leaf_name): |
| 853 raise config_svc.InvalidComponentNameException( |
| 854 'The component name %s is invalid.' % leaf_name) |
| 855 |
| 856 parent_path = request.parentPath |
| 857 if parent_path: |
| 858 parent_def = tracker_bizobj.FindComponentDef(parent_path, config) |
| 859 if not parent_def: |
| 860 raise config_svc.NoSuchComponentException( |
| 861 'Parent component %s does not exist.' % parent_path) |
| 862 if not permissions.CanEditComponentDef( |
| 863 mar.auth.effective_ids, mar.perms, mar.project, parent_def, config): |
| 864 raise permissions.PermissionException( |
| 865 'User is not allowed to add a subcomponent to component %s' % |
| 866 parent_path) |
| 867 |
| 868 path = '%s>%s' % (parent_path, leaf_name) |
| 869 else: |
| 870 path = leaf_name |
| 871 |
| 872 if tracker_bizobj.FindComponentDef(path, config): |
| 873 raise config_svc.InvalidComponentNameException( |
| 874 'The name %s is already in use.' % path) |
| 875 |
| 876 created = int(time.time()) |
| 877 user_emails = set() |
| 878 user_emails.update([mar.auth.email] + request.admin + request.cc) |
| 879 user_ids_dict = self._services.user.LookupUserIDs( |
| 880 mar.cnxn, list(user_emails), autocreate=False) |
| 881 admin_ids = [user_ids_dict[uname] for uname in request.admin] |
| 882 cc_ids = [user_ids_dict[uname] for uname in request.cc] |
| 883 |
| 884 component_id = self._services.config.CreateComponentDef( |
| 885 mar.cnxn, mar.project_id, path, request.description, request.deprecated, |
| 886 admin_ids, cc_ids, created, user_ids_dict[mar.auth.email]) |
| 887 |
| 888 return api_pb2_v1.Component( |
| 889 componentId=component_id, |
| 890 projectName=request.projectId, |
| 891 componentPath=path, |
| 892 description=request.description, |
| 893 admin=request.admin, |
| 894 cc=request.cc, |
| 895 deprecated=request.deprecated, |
| 896 created=datetime.datetime.fromtimestamp(created), |
| 897 creator=mar.auth.email) |
| 898 |
| 899 @monorail_api_method( |
| 900 api_pb2_v1.COMPONENTS_DELETE_REQUEST_RESOURCE_CONTAINER, |
| 901 message_types.VoidMessage, |
| 902 path='projects/{projectId}/components/{componentPath}', |
| 903 http_method='DELETE', |
| 904 name='components.delete') |
| 905 def components_delete(self, request): |
| 906 """Delete a component.""" |
| 907 mar = self.mar_factory(request) |
| 908 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id) |
| 909 component_path = request.componentPath |
| 910 component_def = tracker_bizobj.FindComponentDef( |
| 911 component_path, config) |
| 912 if not component_def: |
| 913 raise config_svc.NoSuchComponentException( |
| 914 'The component %s does not exist.' % component_path) |
| 915 if not permissions.CanViewComponentDef( |
| 916 mar.auth.effective_ids, mar.perms, mar.project, component_def): |
| 917 raise permissions.PermissionException( |
| 918 'User is not allowed to view this component %s' % component_path) |
| 919 if not permissions.CanEditComponentDef( |
| 920 mar.auth.effective_ids, mar.perms, mar.project, component_def, config): |
| 921 raise permissions.PermissionException( |
| 922 'User is not allowed to delete this component %s' % component_path) |
| 923 |
| 924 allow_delete = not tracker_bizobj.FindDescendantComponents( |
| 925 config, component_def) |
| 926 if not allow_delete: |
| 927 raise permissions.PermissionException( |
| 928 'User tried to delete component that had subcomponents') |
| 929 |
| 930 self._services.issue.DeleteComponentReferences( |
| 931 mar.cnxn, component_def.component_id) |
| 932 self._services.config.DeleteComponentDef( |
| 933 mar.cnxn, mar.project_id, component_def.component_id) |
| 934 return message_types.VoidMessage() |
| 935 |
| 936 @monorail_api_method( |
| 937 api_pb2_v1.COMPONENTS_UPDATE_REQUEST_RESOURCE_CONTAINER, |
| 938 message_types.VoidMessage, |
| 939 path='projects/{projectId}/components/{componentPath}', |
| 940 http_method='POST', |
| 941 name='components.update') |
| 942 def components_update(self, request): |
| 943 """Update a component.""" |
| 944 mar = self.mar_factory(request) |
| 945 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id) |
| 946 component_path = request.componentPath |
| 947 component_def = tracker_bizobj.FindComponentDef( |
| 948 component_path, config) |
| 949 if not component_def: |
| 950 raise config_svc.NoSuchComponentException( |
| 951 'The component %s does not exist.' % component_path) |
| 952 if not permissions.CanViewComponentDef( |
| 953 mar.auth.effective_ids, mar.perms, mar.project, component_def): |
| 954 raise permissions.PermissionException( |
| 955 'User is not allowed to view this component %s' % component_path) |
| 956 if not permissions.CanEditComponentDef( |
| 957 mar.auth.effective_ids, mar.perms, mar.project, component_def, config): |
| 958 raise permissions.PermissionException( |
| 959 'User is not allowed to edit this component %s' % component_path) |
| 960 |
| 961 original_path = component_def.path |
| 962 new_path = component_def.path |
| 963 new_docstring = component_def.docstring |
| 964 new_deprecated = component_def.deprecated |
| 965 new_admin_ids = component_def.admin_ids |
| 966 new_cc_ids = component_def.cc_ids |
| 967 update_filterrule = False |
| 968 for update in request.updates: |
| 969 if update.field == api_pb2_v1.ComponentUpdateFieldID.LEAF_NAME: |
| 970 leaf_name = update.leafName |
| 971 if not tracker_constants.COMPONENT_NAME_RE.match(leaf_name): |
| 972 raise config_svc.InvalidComponentNameException( |
| 973 'The component name %s is invalid.' % leaf_name) |
| 974 |
| 975 if '>' in original_path: |
| 976 parent_path = original_path[:original_path.rindex('>')] |
| 977 new_path = '%s>%s' % (parent_path, leaf_name) |
| 978 else: |
| 979 new_path = leaf_name |
| 980 |
| 981 conflict = tracker_bizobj.FindComponentDef(new_path, config) |
| 982 if conflict and conflict.component_id != component_def.component_id: |
| 983 raise config_svc.InvalidComponentNameException( |
| 984 'The name %s is already in use.' % new_path) |
| 985 update_filterrule = True |
| 986 elif update.field == api_pb2_v1.ComponentUpdateFieldID.DESCRIPTION: |
| 987 new_docstring = update.description |
| 988 elif update.field == api_pb2_v1.ComponentUpdateFieldID.ADMIN: |
| 989 user_ids_dict = self._services.user.LookupUserIDs( |
| 990 mar.cnxn, list(update.admin), autocreate=False) |
| 991 new_admin_ids = [user_ids_dict[email] for email in update.admin] |
| 992 elif update.field == api_pb2_v1.ComponentUpdateFieldID.CC: |
| 993 user_ids_dict = self._services.user.LookupUserIDs( |
| 994 mar.cnxn, list(update.cc), autocreate=False) |
| 995 new_cc_ids = [user_ids_dict[email] for email in update.cc] |
| 996 update_filterrule = True |
| 997 elif update.field == api_pb2_v1.ComponentUpdateFieldID.DEPRECATED: |
| 998 new_deprecated = update.deprecated |
| 999 else: |
| 1000 logging.error('Unknown component field %r', update.field) |
| 1001 |
| 1002 new_modified = int(time.time()) |
| 1003 new_modifier_id = self._services.user.LookupUserID( |
| 1004 mar.cnxn, mar.auth.email, autocreate=False) |
| 1005 logging.info( |
| 1006 'Updating component id %d: path-%s, docstring-%s, deprecated-%s,' |
| 1007 ' admin_ids-%s, cc_ids-%s modified by %s', component_def.component_id, |
| 1008 new_path, new_docstring, new_deprecated, new_admin_ids, new_cc_ids, |
| 1009 new_modifier_id) |
| 1010 self._services.config.UpdateComponentDef( |
| 1011 mar.cnxn, mar.project_id, component_def.component_id, |
| 1012 path=new_path, docstring=new_docstring, deprecated=new_deprecated, |
| 1013 admin_ids=new_admin_ids, cc_ids=new_cc_ids, modified=new_modified, |
| 1014 modifier_id=new_modifier_id) |
| 1015 |
| 1016 # TODO(sheyang): reuse the code in componentdetails |
| 1017 if original_path != new_path: |
| 1018 # If the name changed then update all of its subcomponents as well. |
| 1019 subcomponent_ids = tracker_bizobj.FindMatchingComponentIDs( |
| 1020 original_path, config, exact=False) |
| 1021 for subcomponent_id in subcomponent_ids: |
| 1022 if subcomponent_id == component_def.component_id: |
| 1023 continue |
| 1024 subcomponent_def = tracker_bizobj.FindComponentDefByID( |
| 1025 subcomponent_id, config) |
| 1026 subcomponent_new_path = subcomponent_def.path.replace( |
| 1027 original_path, new_path, 1) |
| 1028 self._services.config.UpdateComponentDef( |
| 1029 mar.cnxn, mar.project_id, subcomponent_def.component_id, |
| 1030 path=subcomponent_new_path) |
| 1031 |
| 1032 if update_filterrule: |
| 1033 filterrules_helpers.RecomputeAllDerivedFields( |
| 1034 mar.cnxn, self._services, mar.project, config) |
| 1035 |
| 1036 return message_types.VoidMessage() |
| 1037 |
| 1038 |
| 1039 @endpoints.api(name='monorail_client_configs', version='v1', |
| 1040 description='Monorail API client configs.') |
| 1041 class ClientConfigApi(remote.Service): |
| 1042 |
| 1043 # Class variables. Handy to mock. |
| 1044 _services = None |
| 1045 _mar = None |
| 1046 |
| 1047 @classmethod |
| 1048 def _set_services(cls, services): |
| 1049 cls._services = services |
| 1050 |
| 1051 def mar_factory(self, request): |
| 1052 if not self._mar: |
| 1053 self._mar = monorailrequest.MonorailApiRequest(request, self._services) |
| 1054 return self._mar |
| 1055 |
| 1056 @endpoints.method( |
| 1057 message_types.VoidMessage, |
| 1058 message_types.VoidMessage, |
| 1059 path='client_configs', |
| 1060 http_method='POST', |
| 1061 name='client_configs.update') |
| 1062 def client_configs_update(self, request): |
| 1063 mar = self.mar_factory(request) |
| 1064 if not mar.perms.HasPerm(permissions.ADMINISTER_SITE, None, None): |
| 1065 raise permissions.PermissionException( |
| 1066 'The requester %s is not allowed to update client configs.' % |
| 1067 mar.auth.email) |
| 1068 |
| 1069 ROLE_DICT = { |
| 1070 1: permissions.COMMITTER_ROLE, |
| 1071 2: permissions.CONTRIBUTOR_ROLE, |
| 1072 } |
| 1073 |
| 1074 client_config = client_config_svc.GetClientConfigSvc() |
| 1075 |
| 1076 cfg = client_config.GetConfigs() |
| 1077 if not cfg: |
| 1078 msg = 'Failed to fetch client configs.' |
| 1079 logging.error(msg) |
| 1080 raise endpoints.InternalServerErrorException(msg) |
| 1081 |
| 1082 for client in cfg.clients: |
| 1083 if not client.client_email: |
| 1084 continue |
| 1085 # 1: create the user if non-existent |
| 1086 user_id = self._services.user.LookupUserID( |
| 1087 mar.cnxn, client.client_email, autocreate=True) |
| 1088 user_pb = self._services.user.GetUser(mar.cnxn, user_id) |
| 1089 |
| 1090 logging.info('User ID %d for email %s', user_id, client.client_email) |
| 1091 |
| 1092 # 2: set period and lifetime limit |
| 1093 # new_soft_limit, new_hard_limit, new_lifetime_limit |
| 1094 new_limit_tuple = ( |
| 1095 client.period_limit, client.period_limit, client.lifetime_limit) |
| 1096 action_limit_updates = {'api_request': new_limit_tuple} |
| 1097 self._services.user.UpdateUserSettings( |
| 1098 mar.cnxn, user_id, user_pb, action_limit_updates=action_limit_updates) |
| 1099 |
| 1100 logging.info('Updated api request limit %r', new_limit_tuple) |
| 1101 |
| 1102 # 3: Update project role and extra perms |
| 1103 projects_dict = self._services.project.GetAllProjects(mar.cnxn) |
| 1104 project_name_to_ids = { |
| 1105 p.project_name: p.project_id for p in projects_dict.itervalues()} |
| 1106 |
| 1107 # Set project role and extra perms |
| 1108 for perm in client.project_permissions: |
| 1109 project_ids = self._GetProjectIDs(perm.project, project_name_to_ids) |
| 1110 logging.info('Matching projects %r for name %s', |
| 1111 project_ids, perm.project) |
| 1112 |
| 1113 role = ROLE_DICT[perm.role] |
| 1114 for p_id in project_ids: |
| 1115 project = projects_dict[p_id] |
| 1116 people_list = [] |
| 1117 if role == 'owner': |
| 1118 people_list = project.owner_ids |
| 1119 elif role == 'committer': |
| 1120 people_list = project.committer_ids |
| 1121 elif role == 'contributor': |
| 1122 people_list = project.contributor_ids |
| 1123 # Onlu update role/extra perms iff changed |
| 1124 if not user_id in people_list: |
| 1125 logging.info('Update project %s role %s for user %s', |
| 1126 project.project_name, role, client.client_email) |
| 1127 owner_ids, committer_ids, contributor_ids = ( |
| 1128 project_helpers.MembersWith(project, {user_id}, role)) |
| 1129 self._services.project.UpdateProjectRoles( |
| 1130 mar.cnxn, p_id, owner_ids, committer_ids, |
| 1131 contributor_ids) |
| 1132 if perm.extra_permissions: |
| 1133 member_extra_perms = permissions.FindExtraPerms(project, user_id) |
| 1134 if (member_extra_perms and |
| 1135 set(member_extra_perms.perms) == set(perm.extra_permissions)): |
| 1136 continue |
| 1137 logging.info('Update project %s extra perm %s for user %s', |
| 1138 project.project_name, perm.extra_permissions, |
| 1139 client.client_email) |
| 1140 self._services.project.UpdateExtraPerms( |
| 1141 mar.cnxn, p_id, user_id, list(perm.extra_permissions)) |
| 1142 |
| 1143 return message_types.VoidMessage() |
| 1144 |
| 1145 def _GetProjectIDs(self, project_str, project_name_to_ids): |
| 1146 result = [] |
| 1147 if any(ch in project_str for ch in ['*', '+', '?', '.']): |
| 1148 pattern = re.compile(project_str) |
| 1149 for p_name in project_name_to_ids.iterkeys(): |
| 1150 if pattern.match(p_name): |
| 1151 project_id = project_name_to_ids.get(p_name) |
| 1152 if project_id: |
| 1153 result.append(project_id) |
| 1154 else: |
| 1155 project_id = project_name_to_ids.get(project_str) |
| 1156 if project_id: |
| 1157 result.append(project_id) |
| 1158 |
| 1159 if not result: |
| 1160 logging.warning('Cannot find projects for specified name %s', |
| 1161 project_str) |
| 1162 return result |
| 1163 |
| 1164 |
OLD | NEW |