1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Provides the web interface for adding and editing stored configs.""" 6 7# TODO(qyearsley): If a namespaced config is set, don't show/edit 8# the non-namespaced configs. If a non-namespaced config is set, 9# don't show or edit the namespaced configs. 10 11import difflib 12import json 13 14from google.appengine.api import app_identity 15from google.appengine.api import mail 16from google.appengine.api import users 17 18from dashboard import namespaced_stored_object 19from dashboard import request_handler 20from dashboard import stored_object 21from dashboard import utils 22from dashboard import xsrf 23 24_NOTIFICATION_EMAIL_BODY = """ 25The configuration of %(hostname)s was changed by %(user)s. 26 27Key: %(key)s 28 29Non-namespaced value diff: 30%(value_diff)s 31 32Externally-visible value diff: 33%(external_value_diff)s 34 35Internal-only value diff: 36%(internal_value_diff)s 37""" 38 39# TODO(qyearsley): Make this customizable by storing the value in datastore. 40# Make sure to send a notification to both old and new address if this value 41# gets changed. 42_NOTIFICATION_ADDRESS = 'chrome-perf-dashboard-alerts@google.com' 43_SENDER_ADDRESS = 'gasper-alerts@google.com' 44 45 46class EditSiteConfigHandler(request_handler.RequestHandler): 47 """Handles editing of site config values stored with stored_entity.""" 48 49 def get(self): 50 """Renders the UI with the form.""" 51 key = self.request.get('key') 52 if not key: 53 self.RenderHtml('edit_site_config.html', {}) 54 return 55 56 value = stored_object.Get(key) 57 external_value = namespaced_stored_object.GetExternal(key) 58 internal_value = namespaced_stored_object.Get(key) 59 self.RenderHtml('edit_site_config.html', { 60 'key': key, 61 'value': _FormatJson(value), 62 'external_value': _FormatJson(external_value), 63 'internal_value': _FormatJson(internal_value), 64 }) 65 66 @xsrf.TokenRequired 67 def post(self): 68 """Accepts posted values, makes changes, and shows the form again.""" 69 key = self.request.get('key') 70 71 if not utils.IsInternalUser(): 72 self.RenderHtml('edit_site_config.html', { 73 'error': 'Only internal users can post to this end-point.' 74 }) 75 return 76 77 if not key: 78 self.RenderHtml('edit_site_config.html', {}) 79 return 80 81 new_value_json = self.request.get('value').strip() 82 new_external_value_json = self.request.get('external_value').strip() 83 new_internal_value_json = self.request.get('internal_value').strip() 84 85 template_params = { 86 'key': key, 87 'value': new_value_json, 88 'external_value': new_external_value_json, 89 'internal_value': new_internal_value_json, 90 } 91 92 try: 93 new_value = json.loads(new_value_json or 'null') 94 new_external_value = json.loads(new_external_value_json or 'null') 95 new_internal_value = json.loads(new_internal_value_json or 'null') 96 except ValueError: 97 template_params['error'] = 'Invalid JSON in at least one field.' 98 self.RenderHtml('edit_site_config.html', template_params) 99 return 100 101 old_value = stored_object.Get(key) 102 old_external_value = namespaced_stored_object.GetExternal(key) 103 old_internal_value = namespaced_stored_object.Get(key) 104 105 stored_object.Set(key, new_value) 106 namespaced_stored_object.SetExternal(key, new_external_value) 107 namespaced_stored_object.Set(key, new_internal_value) 108 109 _SendNotificationEmail( 110 key, old_value, old_external_value, old_internal_value, 111 new_value, new_external_value, new_internal_value) 112 113 self.RenderHtml('edit_site_config.html', template_params) 114 115 116def _SendNotificationEmail( 117 key, old_value, old_external_value, old_internal_value, 118 new_value, new_external_value, new_internal_value): 119 user_email = users.get_current_user().email() 120 subject = 'Config "%s" changed by %s' % (key, user_email) 121 email_body = _NOTIFICATION_EMAIL_BODY % { 122 'key': key, 123 'value_diff': _DiffJson(old_value, new_value), 124 'external_value_diff': _DiffJson(old_external_value, new_external_value), 125 'internal_value_diff': _DiffJson(old_internal_value, new_internal_value), 126 'hostname': app_identity.get_default_version_hostname(), 127 'user': users.get_current_user().email(), 128 } 129 mail.send_mail( 130 sender=_SENDER_ADDRESS, 131 to=_NOTIFICATION_ADDRESS, 132 subject=subject, 133 body=email_body) 134 135 136def _DiffJson(obj1, obj2): 137 """Returns a string diff of two JSON-serializable objects.""" 138 differ = difflib.Differ() 139 return '\n'.join(differ.compare( 140 _FormatJson(obj1).splitlines(), 141 _FormatJson(obj2).splitlines())) 142 143 144def _FormatJson(obj): 145 return json.dumps(obj, indent=2, sort_keys=True) 146