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