Home | History | Annotate | Download | only in enterprise
      1 # Copyright 2015 The Chromium OS 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 import copy
      6 import json
      7 import logging
      8 import os
      9 
     10 from autotest_lib.client.bin import test
     11 from autotest_lib.client.bin import utils
     12 from autotest_lib.client.common_lib import error
     13 from autotest_lib.client.common_lib.cros import chrome
     14 from autotest_lib.client.common_lib.cros import enrollment
     15 from autotest_lib.client.cros import cryptohome
     16 from autotest_lib.client.cros import httpd
     17 from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver
     18 
     19 CROSQA_FLAGS = [
     20     '--gaia-url=https://gaiastaging.corp.google.com',
     21     '--lso-url=https://gaiastaging.corp.google.com',
     22     '--google-apis-url=https://www-googleapis-test.sandbox.google.com',
     23     '--oauth2-client-id=236834563817.apps.googleusercontent.com',
     24     '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI',
     25     ('--cloud-print-url='
     26      'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
     27     '--ignore-urlfetcher-cert-requests']
     28 CROSALPHA_FLAGS = [
     29     ('--cloud-print-url='
     30      'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
     31     '--ignore-urlfetcher-cert-requests']
     32 TESTDMS_FLAGS = [
     33     '--ignore-urlfetcher-cert-requests',
     34     '--disable-policy-key-verification']
     35 FLAGS_DICT = {
     36     'prod': [],
     37     'crosman-qa': CROSQA_FLAGS,
     38     'crosman-alpha': CROSALPHA_FLAGS,
     39     'dm-test': TESTDMS_FLAGS,
     40     'dm-fake': TESTDMS_FLAGS
     41 }
     42 DMS_URL_DICT = {
     43     'prod': 'http://m.google.com/devicemanagement/data/api',
     44     'crosman-qa':
     45         'https://crosman-qa.sandbox.google.com/devicemanagement/data/api',
     46     'crosman-alpha':
     47         'https://crosman-alpha.sandbox.google.com/devicemanagement/data/api',
     48     'dm-test': 'http://chromium-dm-test.appspot.com/d/%s',
     49     'dm-fake': 'http://127.0.0.1:%d/'
     50 }
     51 DMSERVER = '--device-management-url=%s'
     52 # Username and password for the fake dm server can be anything, since
     53 # they are not used to authenticate against GAIA.
     54 USERNAME = 'fake-user (at] managedchrome.com'
     55 PASSWORD = 'fakepassword'
     56 GAIA_ID = 'fake-gaia-id'
     57 
     58 
     59 class EnterprisePolicyTest(test.test):
     60     """Base class for Enterprise Policy Tests."""
     61 
     62     WEB_PORT = 8080
     63     WEB_HOST = 'http://localhost:%d' % WEB_PORT
     64     CHROME_POLICY_PAGE = 'chrome://policy'
     65 
     66     def setup(self):
     67         """Make the files needed for fake-dms."""
     68         os.chdir(self.srcdir)
     69         utils.make()
     70 
     71 
     72     def initialize(self, **kwargs):
     73         """Initialize test parameters."""
     74         self._initialize_enterprise_policy_test(**kwargs)
     75 
     76 
     77     def _initialize_enterprise_policy_test(
     78             self, case='', env='dm-fake', dms_name=None,
     79             username=USERNAME, password=PASSWORD, gaia_id=GAIA_ID):
     80         """Initialize test parameters and fake DM Server.
     81 
     82         @param case: String name of the test case to run.
     83         @param env: String environment of DMS and Gaia servers.
     84         @param username: String user name login credential.
     85         @param password: String password login credential.
     86         @param gaia_id: String gaia_id login credential.
     87         @param dms_name: String name of test DM Server.
     88         """
     89         self.case = case
     90         self.env = env
     91         self.username = username
     92         self.password = password
     93         self.gaia_id = gaia_id
     94         self.dms_name = dms_name
     95         self.dms_is_fake = (env == 'dm-fake')
     96         self._enforce_variable_restrictions()
     97 
     98         # Initialize later variables to prevent error after an early failure.
     99         self._web_server = None
    100         self.cr = None
    101 
    102         # Start AutoTest DM Server if using local fake server.
    103         if self.dms_is_fake:
    104             self.fake_dm_server = enterprise_fake_dmserver.FakeDMServer(
    105                 self.srcdir)
    106             self.fake_dm_server.start(self.tmpdir, self.debugdir)
    107 
    108         # Get enterprise directory of shared resources.
    109         client_dir = os.path.dirname(os.path.dirname(self.bindir))
    110         self.enterprise_dir = os.path.join(client_dir, 'cros/enterprise')
    111 
    112         # Log the test context parameters.
    113         logging.info('Test Context Parameters:')
    114         logging.info('  Case: %r', self.case)
    115         logging.info('  Environment: %r', self.env)
    116         logging.info('  Username: %r', self.username)
    117         logging.info('  Password: %r', self.password)
    118         logging.info('  Test DMS Name: %r', self.dms_name)
    119 
    120 
    121     def cleanup(self):
    122         """Close out anything used by this test."""
    123         # Clean up AutoTest DM Server if using local fake server.
    124         if self.dms_is_fake:
    125             self.fake_dm_server.stop()
    126 
    127         # Stop web server if it was started.
    128         if self._web_server:
    129             self._web_server.stop()
    130 
    131         # Close Chrome instance if opened.
    132         if self.cr and self._auto_logout:
    133             self.cr.close()
    134 
    135 
    136     def start_webserver(self):
    137         """Set up HTTP Server to serve pages from enterprise directory."""
    138         self._web_server = httpd.HTTPListener(
    139                 self.WEB_PORT, docroot=self.enterprise_dir)
    140         self._web_server.run()
    141 
    142 
    143     def _enforce_variable_restrictions(self):
    144         """Validate class-level test context parameters.
    145 
    146         @raises error.TestError if context parameter has an invalid value,
    147                 or a combination of parameters have incompatible values.
    148         """
    149         # Verify |env| is a valid environment.
    150         if self.env not in FLAGS_DICT:
    151             raise error.TestError('Environment is invalid: %s' % self.env)
    152 
    153         # Verify test |dms_name| is given iff |env| is 'dm-test'.
    154         if self.env == 'dm-test' and not self.dms_name:
    155             raise error.TestError('dms_name must be given when using '
    156                                   'env=dm-test.')
    157         if self.env != 'dm-test' and self.dms_name:
    158             raise error.TestError('dms_name must not be given when not using '
    159                                   'env=dm-test.')
    160 
    161 
    162     def setup_case(self, user_policies={}, suggested_user_policies={},
    163                    device_policies={}, skip_policy_value_verification=False,
    164                    enroll=False, auto_login=True, auto_logout=True,
    165                    extra_chrome_flags=[], init_network_controller=False):
    166         """Set up DMS, log in, and verify policy values.
    167 
    168         If the AutoTest fake DM Server is used, make a JSON policy blob
    169         and upload it to the fake DM server.
    170 
    171         Launch Chrome and sign in to Chrome OS. Examine the user's
    172         cryptohome vault, to confirm user is signed in successfully.
    173 
    174         @param user_policies: dict of mandatory user policies in
    175                 name -> value format.
    176         @param suggested_user_policies: optional dict of suggested policies
    177                 in name -> value format.
    178         @param device_policies: dict of device policies in
    179                 name -> value format.
    180         @param skip_policy_value_verification: True if setup_case should not
    181                 verify that the correct policy value shows on policy page.
    182         @param enroll: True for enrollment instead of login.
    183         @param auto_login: Sign in to chromeos.
    184         @param auto_logout: Sign out of chromeos when test is complete.
    185         @param extra_chrome_flags: list of flags to add to Chrome.
    186         @param init_network_controller: whether to init network controller.
    187 
    188         @raises error.TestError if cryptohome vault is not mounted for user.
    189         @raises error.TestFail if |policy_name| and |policy_value| are not
    190                 shown on the Policies page.
    191         """
    192         self._auto_logout = auto_logout
    193 
    194         if self.dms_is_fake:
    195             self.fake_dm_server.setup_policy(self._make_json_blob(
    196                 user_policies, suggested_user_policies, device_policies))
    197 
    198         self._create_chrome(enroll=enroll, auto_login=auto_login,
    199                             extra_chrome_flags=extra_chrome_flags,
    200                             init_network_controller=init_network_controller)
    201 
    202         # Skip policy check upon request or if we enroll but don't log in.
    203         skip_policy_value_verification = (
    204                 skip_policy_value_verification or not auto_login)
    205         if not skip_policy_value_verification:
    206             self.verify_policy_stats(user_policies, suggested_user_policies,
    207                                      device_policies)
    208 
    209 
    210     def _make_json_blob(self, user_policies={}, suggested_user_policies={},
    211                         device_policies={}):
    212         """Create JSON policy blob from mandatory and suggested policies.
    213 
    214         For the status of a policy to be shown as "Not set" on the
    215         chrome://policy page, the policy dictionary must contain no NVP for
    216         for that policy. Remove policy NVPs if value is None.
    217 
    218         @param user_policies: mandatory user policies -> values.
    219         @param suggested user_policies: suggested user policies -> values.
    220         @param device_policies: mandatory device policies -> values.
    221 
    222         @returns: JSON policy blob to send to the fake DM server.
    223         """
    224 
    225         user_p = copy.deepcopy(user_policies)
    226         s_user_p = copy.deepcopy(suggested_user_policies)
    227         device_p = copy.deepcopy(device_policies)
    228 
    229         # Remove "Not set" policies and json-ify dicts because the
    230         # FakeDMServer expects "policy": "{value}" not "policy": {value}
    231         # or "policy": ["{value}"] not "policy": [{value}].
    232         for policies_dict in [user_p, s_user_p, device_p]:
    233             policies_to_pop = []
    234             for policy in policies_dict:
    235                 value = policies_dict[policy]
    236                 if value is None:
    237                     policies_to_pop.append(policy)
    238                 elif isinstance(value, dict):
    239                     policies_dict[policy] = encode_json_string(value)
    240                 elif isinstance(value, list):
    241                     if len(value) > 0 and isinstance(value[0], dict):
    242                         for i in xrange(len(value)):
    243                             value[i] = encode_json_string(value[i])
    244                         policies_dict[policy] = value
    245             for policy in policies_to_pop:
    246                 policies_dict.pop(policy)
    247 
    248         management_dict = {
    249             'managed_users': ['*'],
    250             'policy_user': self.username,
    251             'current_key_index': 0,
    252             'invalidation_source': 16,
    253             'invalidation_name': 'test_policy'
    254         }
    255 
    256         if user_p or s_user_p:
    257             user_modes_dict = {}
    258             if user_p:
    259                 user_modes_dict['mandatory'] = user_p
    260             if suggested_user_policies:
    261                 user_modes_dict['recommended'] = s_user_p
    262             management_dict['google/chromeos/user'] = user_modes_dict
    263 
    264         if device_p:
    265             management_dict['google/chromeos/device'] = device_p
    266 
    267 
    268         logging.info('Created policy blob: %s', management_dict)
    269         return encode_json_string(management_dict)
    270 
    271 
    272     def _get_policy_stats_shown(self, policy_tab, policy_name):
    273         """Get the info shown for |policy_name| from the |policy_tab| page.
    274 
    275         Return a dict of stats for the policy given by |policy_name|, from
    276         from the chrome://policy page given by |policy_tab|.
    277 
    278         CAVEAT: the policy page does not display proper JSON. For example, lists
    279         are generally shown without the [ ] and cannot be distinguished from
    280         strings.  This function decodes what it can and returns the string it
    281         found when in doubt.
    282 
    283         @param policy_tab: Tab displaying the Policies page.
    284         @param policy_name: The name of the policy.
    285 
    286         @returns: A dict of stats, including JSON decode 'value' (see caveat).
    287                   Also included are 'name', 'status', 'level', 'scope',
    288                   and 'source'.
    289         """
    290         stats = {'name': policy_name}
    291 
    292         row_values = policy_tab.EvaluateJavaScript('''
    293             var section = document.getElementsByClassName(
    294                     "policy-table-section")[0];
    295             var table = section.getElementsByTagName('table')[0];
    296             rowValues = {};
    297             for (var i = 1, row; row = table.rows[i]; i++) {
    298                 if (row.className !== 'expanded-value-container') {
    299                     var name_div = row.getElementsByClassName('name elide')[0];
    300                     var name_links = name_div.getElementsByClassName(
    301                             'name-link');
    302                     var name = (name_links.length > 0) ?
    303                             name_links[0].textContent : name_div.textContent;
    304                     rowValues['name'] = name;
    305                     if (name === '%s') {
    306                         var value_span = row.getElementsByClassName('value')[0];
    307                         rowValues['value'] = value_span.textContent;
    308                         stat_names = ['status', 'level', 'scope', 'source'];
    309                         stat_names.forEach(function(entry) {
    310                             var entry_div = row.getElementsByClassName(
    311                                     entry+' elide')[0];
    312                             rowValues[entry] = entry_div.textContent;
    313                         });
    314                         break;
    315                     }
    316                }
    317             }
    318             rowValues;
    319         ''' % policy_name)
    320 
    321         logging.debug('Policy %s row: %s', policy_name, row_values)
    322         if not row_values or len(row_values) < 6:
    323             raise error.TestError(
    324                     'Could not get policy info for %s!' % policy_name)
    325 
    326         entries = ['value', 'status', 'level', 'scope', 'source']
    327         for v in entries:
    328             stats[v] = row_values[v].encode('ascii', 'ignore')
    329 
    330         if stats['status'] == 'Not set.':
    331             for v in entries:
    332                 stats[v] = None
    333         else: stats['value'] = decode_json_string(stats['value'])
    334 
    335         return stats
    336 
    337 
    338     def _get_policy_value_from_new_tab(self, policy_name):
    339         """Get the policy value for |policy_name| from the Policies page.
    340 
    341         Information comes from the policy page.  A single new tab is opened
    342         and then closed to check this info, so device must be logged in.
    343 
    344         @param policy_name: string of policy name.
    345 
    346         @returns: decoded value of the policy as shown on chrome://policy.
    347         """
    348         values = self._get_policy_stats_from_new_tab([policy_name])
    349         return values[policy_name]['value']
    350 
    351 
    352     def _get_policy_values_from_new_tab(self, policy_names):
    353         """Get the policy values of the given policies.
    354 
    355         Information comes from the policy page.  A single new tab is opened
    356         and then closed to check this info, so device must be logged in.
    357 
    358         @param policy_names: list of strings of policy names.
    359 
    360         @returns: dict of policy name mapped to decoded values of the policy as
    361                   shown on chrome://policy.
    362         """
    363         values = {}
    364         tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    365         for policy_name in policy_names:
    366           values[policy_name] = (
    367                   self._get_policy_stats_shown(tab, policy_name)['value'])
    368         tab.Close()
    369 
    370         return values
    371 
    372 
    373     def _get_policy_stats_from_new_tab(self, policy_names):
    374         """Get policy info about the given policy names.
    375 
    376         Information comes from the policy page.  A single new tab is opened
    377         and then closed to check this info, so device must be logged in.
    378 
    379         @param policy_name: list of policy names (strings).
    380 
    381         @returns: dict of policy names mapped to dicts containing policy info.
    382                   Values are decoded JSON.
    383         """
    384         stats = {}
    385         tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    386         for policy_name in policy_names:
    387             stats[policy_name] = self._get_policy_stats_shown(tab, policy_name)
    388         tab.Close()
    389 
    390         return stats
    391 
    392 
    393     def _compare_values(self, policy_name, expected_value, value_shown):
    394         """Pass if an expected value and the chrome://policy version match.
    395 
    396         Handles some of the inconsistencies in the chrome://policy JSON format.
    397 
    398         @raises: error.TestError if policy values do not match.
    399 
    400         """
    401         # If we expect a list and don't have a list, modify the value_shown.
    402         if isinstance(expected_value, list):
    403             if isinstance(value_shown, str):
    404                 if '{' in value_shown: # List of dicts.
    405                     value_shown = decode_json_string('[%s]' % value_shown)
    406                 elif ',' in value_shown: # List of strs.
    407                     value_shown = value_shown.split(',')
    408                 else: # List with one str.
    409                     value_shown = [value_shown]
    410             elif not isinstance(value_shown, list): # List with one element.
    411                 value_shown = [value_shown]
    412 
    413         if not expected_value == value_shown:
    414             raise error.TestError('chrome://policy shows the incorrect value '
    415                                   'for %s!  Expected %s, got %s.' % (
    416                                           policy_name, expected_value,
    417                                           value_shown))
    418 
    419 
    420     def verify_policy_value(self, policy_name, expected_value):
    421         """
    422         Verify that the a single policy correctly shows in chrome://policy.
    423 
    424         @param policy_name: the policy we are checking.
    425         @param expected_value: the expected value for policy_name.
    426 
    427         @raises error.TestError if value does not match expected.
    428 
    429         """
    430         value_shown = self._get_policy_value_from_new_tab(policy_name)
    431         self._compare_values(policy_name, expected_value, value_shown)
    432 
    433 
    434     def verify_policy_stats(self, user_policies={}, suggested_user_policies={},
    435                             device_policies={}):
    436         """Verify that the correct policy values show in chrome://policy.
    437 
    438         @param policy_dict: the policies we are checking.
    439 
    440         @raises error.TestError if value does not match expected.
    441         """
    442         def _compare_stat(stat, desired, name, stats):
    443             """ Raise error if a stat doesn't match."""
    444             err_str = 'Incorrect '+stat+' for '+name+': expected %s, got %s!'
    445             shown = stats[name][stat]
    446             # If policy is not set, there are no stats to match.
    447             if stats[name]['status'] == None:
    448                 if not shown == None:
    449                     raise error.TestError(err_str % (None, shown))
    450                 else:
    451                     return
    452             if not desired == shown:
    453                 raise error.TestError(err_str % (desired, shown))
    454 
    455         keys = (user_policies.keys() + suggested_user_policies.keys() +
    456                 device_policies.keys())
    457 
    458         # If no policies were modified from default, return.
    459         if len(keys) == 0:
    460             return
    461 
    462         stats = self._get_policy_stats_from_new_tab(keys)
    463 
    464         for policy in user_policies:
    465             self._compare_values(policy, user_policies[policy],
    466                                  stats[policy]['value'])
    467             _compare_stat('level', 'Mandatory', policy, stats)
    468             _compare_stat('scope', 'Current user', policy, stats)
    469         for policy in suggested_user_policies:
    470             self._compare_values(policy, suggested_user_policies[policy],
    471                                  stats[policy]['value'])
    472             _compare_stat('level', 'Recommended', policy, stats)
    473             _compare_stat('scope', 'Current user', policy, stats)
    474         for policy in device_policies:
    475             self._compare_values(policy, device_policies[policy],
    476                                  stats[policy]['value'])
    477             _compare_stat('level', 'Mandatory', policy, stats)
    478             _compare_stat('scope', 'Device', policy, stats)
    479 
    480 
    481     def _initialize_chrome_extra_flags(self):
    482         """
    483         Initialize flags used to create Chrome instance.
    484 
    485         @returns: list of extra Chrome flags.
    486 
    487         """
    488         # Construct DM Server URL flags if not using production server.
    489         env_flag_list = []
    490         if self.env != 'prod':
    491             if self.dms_is_fake:
    492                 # Use URL provided by the fake AutoTest DM server.
    493                 dmserver_str = (DMSERVER % self.fake_dm_server.server_url)
    494             else:
    495                 # Use URL defined in the DMS URL dictionary.
    496                 dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env]))
    497                 if self.env == 'dm-test':
    498                     dmserver_str = (dmserver_str % self.dms_name)
    499 
    500             # Merge with other flags needed by non-prod enviornment.
    501             env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env])
    502 
    503         return env_flag_list
    504 
    505 
    506     def _create_chrome(self, enroll=False, auto_login=True,
    507                        extra_chrome_flags=[], init_network_controller=False):
    508         """
    509         Create a Chrome object. Enroll and/or sign in.
    510 
    511         Function results in self.cr set as the Chrome object.
    512 
    513         @param enroll: enroll the device.
    514         @param auto_login: sign in to chromeos.
    515         @param extra_chrome_flags: list of flags to add.
    516         @param init_network_controller: whether to init network controller.
    517         """
    518         extra_flags = self._initialize_chrome_extra_flags() + extra_chrome_flags
    519 
    520         logging.info('Chrome Browser Arguments:')
    521         logging.info('  extra_browser_args: %s', extra_flags)
    522         logging.info('  username: %s', self.username)
    523         logging.info('  password: %s', self.password)
    524         logging.info('  gaia_login: %s', not self.dms_is_fake)
    525 
    526         if enroll:
    527             self.cr = chrome.Chrome(auto_login=False,
    528                                     extra_browser_args=extra_flags,
    529                                     expect_policy_fetch=True)
    530             if self.dms_is_fake:
    531                 enrollment.EnterpriseFakeEnrollment(
    532                     self.cr.browser, self.username, self.password, self.gaia_id,
    533                     auto_login=auto_login)
    534             else:
    535                 enrollment.EnterpriseEnrollment(
    536                     self.cr.browser, self.username, self.password,
    537                     auto_login=auto_login)
    538 
    539         elif auto_login:
    540             self.cr = chrome.Chrome(extra_browser_args=extra_flags,
    541                                     username=self.username,
    542                                     password=self.password,
    543                                     gaia_login=not self.dms_is_fake,
    544                                     disable_gaia_services=self.dms_is_fake,
    545                                     autotest_ext=True,
    546                                     init_network_controller=init_network_controller,
    547                                     expect_policy_fetch=True)
    548         else:
    549             self.cr = chrome.Chrome(auto_login=False,
    550                                     extra_browser_args=extra_flags,
    551                                     disable_gaia_services=self.dms_is_fake,
    552                                     autotest_ext=True,
    553                                     expect_policy_fetch=True)
    554 
    555         if auto_login:
    556             if not cryptohome.is_vault_mounted(user=self.username,
    557                                                allow_fail=True):
    558                 raise error.TestError('Expected to find a mounted vault for %s.'
    559                                       % self.username)
    560 
    561 
    562     def navigate_to_url(self, url, tab=None):
    563         """Navigate tab to the specified |url|. Create new tab if none given.
    564 
    565         @param url: URL of web page to load.
    566         @param tab: browser tab to load (if any).
    567         @returns: browser tab loaded with web page.
    568         @raises: telemetry TimeoutException if document ready state times out.
    569         """
    570         logging.info('Navigating to URL: %r', url)
    571         if not tab:
    572             tab = self.cr.browser.tabs.New()
    573             tab.Activate()
    574         tab.Navigate(url, timeout=8)
    575         tab.WaitForDocumentReadyStateToBeComplete()
    576         return tab
    577 
    578 
    579     def get_elements_from_page(self, tab, cmd):
    580         """Get collection of page elements that match the |cmd| filter.
    581 
    582         @param tab: tab containing the page to be scraped.
    583         @param cmd: JavaScript command to evaluate on the page.
    584         @returns object containing elements on page that match the cmd.
    585         @raises: TestFail if matching elements are not found on the page.
    586         """
    587         try:
    588             elements = tab.EvaluateJavaScript(cmd)
    589         except Exception as err:
    590             raise error.TestFail('Unable to find matching elements on '
    591                                  'the test page: %s\n %r' %(tab.url, err))
    592         return elements
    593 
    594 
    595 def encode_json_string(object_value):
    596     """Convert given value to JSON format string.
    597 
    598     @param object_value: object to be converted.
    599 
    600     @returns: string in JSON format.
    601     """
    602     return json.dumps(object_value)
    603 
    604 
    605 def decode_json_string(json_string):
    606     """Convert given JSON format string to an object.
    607 
    608     If no object is found, return json_string instead.  This is to allow
    609     us to "decode" items off the policy page that aren't real JSON.
    610 
    611     @param json_string: the JSON string to be decoded.
    612 
    613     @returns: Python object represented by json_string or json_string.
    614     """
    615     def _decode_list(json_list):
    616         result = []
    617         for value in json_list:
    618             if isinstance(value, unicode):
    619                 value = value.encode('ascii')
    620             if isinstance(value, list):
    621                 value = _decode_list(value)
    622             if isinstance(value, dict):
    623                 value = _decode_dict(value)
    624             result.append(value)
    625         return result
    626 
    627     def _decode_dict(json_dict):
    628         result = {}
    629         for key, value in json_dict.iteritems():
    630             if isinstance(key, unicode):
    631                 key = key.encode('ascii')
    632             if isinstance(value, unicode):
    633                 value = value.encode('ascii')
    634             elif isinstance(value, list):
    635                 value = _decode_list(value)
    636             result[key] = value
    637         return result
    638 
    639     try:
    640         # Decode JSON turning all unicode strings into ascii.
    641         # object_hook will get called on all dicts, so also handle lists.
    642         result = json.loads(json_string, encoding='ascii',
    643                             object_hook=_decode_dict)
    644         if isinstance(result, list):
    645             result = _decode_list(result)
    646         return result
    647     except ValueError as e:
    648         # Input not valid, e.g. '1, 2, "c"' instead of '[1, 2, "c"]'.
    649         logging.warning('Could not unload: %s (%s)', json_string, e)
    650         return json_string
    651