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 json
      6 import logging
      7 import os
      8 
      9 from autotest_lib.client.bin import test
     10 from autotest_lib.client.bin import utils
     11 from autotest_lib.client.common_lib import error
     12 from autotest_lib.client.common_lib.cros import chrome
     13 from autotest_lib.client.cros import cryptohome
     14 from autotest_lib.client.cros import httpd
     15 from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver
     16 
     17 CROSQA_FLAGS = [
     18     '--gaia-url=https://gaiastaging.corp.google.com',
     19     '--lso-url=https://gaiastaging.corp.google.com',
     20     '--google-apis-url=https://www-googleapis-test.sandbox.google.com',
     21     '--oauth2-client-id=236834563817.apps.googleusercontent.com',
     22     '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI',
     23     ('--cloud-print-url='
     24      'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
     25     '--ignore-urlfetcher-cert-requests']
     26 CROSALPHA_FLAGS = [
     27     ('--cloud-print-url='
     28      'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
     29     '--ignore-urlfetcher-cert-requests']
     30 TESTDMS_FLAGS = [
     31     '--ignore-urlfetcher-cert-requests',
     32     '--disable-policy-key-verification']
     33 FLAGS_DICT = {
     34     'prod': [],
     35     'crosman-qa': CROSQA_FLAGS,
     36     'crosman-alpha': CROSALPHA_FLAGS,
     37     'dm-test': TESTDMS_FLAGS,
     38     'dm-fake': TESTDMS_FLAGS
     39 }
     40 DMS_URL_DICT = {
     41     'prod': 'http://m.google.com/devicemanagement/data/api',
     42     'crosman-qa':
     43         'https://crosman-qa.sandbox.google.com/devicemanagement/data/api',
     44     'crosman-alpha':
     45         'https://crosman-alpha.sandbox.google.com/devicemanagement/data/api',
     46     'dm-test': 'http://chromium-dm-test.appspot.com/d/%s',
     47     'dm-fake': 'http://127.0.0.1:%d/'
     48 }
     49 DMSERVER = '--device-management-url=%s'
     50 # Username and password for the fake dm server can be anything, since
     51 # they are not used to authenticate against GAIA.
     52 USERNAME = 'fake-user (at] managedchrome.com'
     53 PASSWORD = 'fakepassword'
     54 
     55 
     56 class EnterprisePolicyTest(test.test):
     57     """Base class for Enterprise Policy Tests."""
     58 
     59     WEB_PORT = 8080
     60     WEB_HOST = 'http://localhost:%d' % WEB_PORT
     61     CHROME_POLICY_PAGE = 'chrome://policy'
     62 
     63     def setup(self):
     64         os.chdir(self.srcdir)
     65         utils.make()
     66 
     67 
     68     def initialize(self, **kwargs):
     69         self._initialize_enterprise_policy_test(**kwargs)
     70 
     71 
     72     def _initialize_enterprise_policy_test(
     73             self, case, env='dm-fake', dms_name=None,
     74             username=USERNAME, password=PASSWORD):
     75         """Initialize test parameters, fake DM Server, and Chrome flags.
     76 
     77         @param case: String name of the test case to run.
     78         @param env: String environment of DMS and Gaia servers.
     79         @param username: String user name login credential.
     80         @param password: String password login credential.
     81         @param dms_name: String name of test DM Server.
     82         """
     83         self.case = case
     84         self.env = env
     85         self.username = username
     86         self.password = password
     87         self.dms_name = dms_name
     88         self.dms_is_fake = (env == 'dm-fake')
     89         self._enforce_variable_restrictions()
     90 
     91         # Initialize later variables to prevent error after an early failure.
     92         self._web_server = None
     93         self.cr = None
     94 
     95         # Start AutoTest DM Server if using local fake server.
     96         if self.dms_is_fake:
     97             self.fake_dm_server = enterprise_fake_dmserver.FakeDMServer(
     98                 self.srcdir)
     99             self.fake_dm_server.start(self.tmpdir, self.debugdir)
    100 
    101         # Get enterprise directory of shared resources.
    102         client_dir = os.path.dirname(os.path.dirname(self.bindir))
    103         self.enterprise_dir = os.path.join(client_dir, 'cros/enterprise')
    104 
    105         # Log the test context parameters.
    106         logging.info('Test Context Parameters:')
    107         logging.info('  Case: %r', self.case)
    108         logging.info('  Environment: %r', self.env)
    109         logging.info('  Username: %r', self.username)
    110         logging.info('  Password: %r', self.password)
    111         logging.info('  Test DMS Name: %r', self.dms_name)
    112 
    113 
    114     def cleanup(self):
    115         # Clean up AutoTest DM Server if using local fake server.
    116         if self.dms_is_fake:
    117             self.fake_dm_server.stop()
    118 
    119         # Stop web server if it was started.
    120         if self._web_server:
    121             self._web_server.stop()
    122 
    123         # Close Chrome instance if opened.
    124         if self.cr:
    125             self.cr.close()
    126 
    127 
    128     def start_webserver(self):
    129         """Set up HTTP Server to serve pages from enterprise directory."""
    130         self._web_server = httpd.HTTPListener(
    131                 self.WEB_PORT, docroot=self.enterprise_dir)
    132         self._web_server.run()
    133 
    134 
    135     def _enforce_variable_restrictions(self):
    136         """Validate class-level test context parameters.
    137 
    138         @raises error.TestError if context parameter has an invalid value,
    139                 or a combination of parameters have incompatible values.
    140         """
    141         # Verify |env| is a valid environment.
    142         if self.env not in FLAGS_DICT:
    143             raise error.TestError('Environment is invalid: %s' % self.env)
    144 
    145         # Verify test |dms_name| is given iff |env| is 'dm-test'.
    146         if self.env == 'dm-test' and not self.dms_name:
    147             raise error.TestError('dms_name must be given when using '
    148                                   'env=dm-test.')
    149         if self.env != 'dm-test' and self.dms_name:
    150             raise error.TestError('dms_name must not be given when not using '
    151                                   'env=dm-test.')
    152 
    153 
    154     def setup_case(self, policy_name, policy_value, mandatory_policies={},
    155                    suggested_policies={}, policy_name_is_suggested=False,
    156                    skip_policy_value_verification=False):
    157         """Set up and confirm the preconditions of a test case.
    158 
    159         If the AutoTest fake DM Server is used, make a JSON policy blob
    160         and upload it to the fake DM server.
    161 
    162         Launch Chrome and sign in to Chrome OS. Examine the user's
    163         cryptohome vault, to confirm user is signed in successfully.
    164 
    165         Open the Policies page, and confirm that it shows the specified
    166         |policy_name| and has the correct |policy_value|.
    167 
    168         @param policy_name: Name of the policy under test.
    169         @param policy_value: Expected value for the policy under test.
    170         @param mandatory_policies: optional dict of mandatory policies
    171                 (not policy_name) in name -> value format.
    172         @param suggested_policies: optional dict of suggested policies
    173                 (not policy_name) in name -> value format.
    174         @param policy_name_is_suggested: True if policy_name a suggested policy.
    175         @param skip_policy_value_verification: True if setup_case should not
    176                 verify that the correct policy value shows on policy page.
    177 
    178         @raises error.TestError if cryptohome vault is not mounted for user.
    179         @raises error.TestFail if |policy_name| and |policy_value| are not
    180                 shown on the Policies page.
    181         """
    182         logging.info('Setting up case: (%s: %s)', policy_name, policy_value)
    183         logging.info('Mandatory policies: %s', mandatory_policies)
    184         logging.info('Suggested policies: %s', suggested_policies)
    185 
    186         if self.dms_is_fake:
    187             if policy_name_is_suggested:
    188                 suggested_policies[policy_name] = policy_value
    189             else:
    190                 mandatory_policies[policy_name] = policy_value
    191             self.fake_dm_server.setup_policy(self._make_json_blob(
    192                 mandatory_policies, suggested_policies))
    193 
    194         self._launch_chrome_browser()
    195         if not cryptohome.is_vault_mounted(user=self.username,
    196                                            allow_fail=True):
    197             raise error.TestError('Expected to find a mounted vault for %s.'
    198                                   % self.username)
    199         if not skip_policy_value_verification:
    200             self.verify_policy_value(policy_name, policy_value)
    201 
    202 
    203     def _make_json_blob(self, mandatory_policies, suggested_policies):
    204         """Create JSON policy blob from mandatory and suggested policies.
    205 
    206         For the status of a policy to be shown as "Not set" on the
    207         chrome://policy page, the policy dictionary must contain no NVP for
    208         for that policy. Remove policy NVPs if value is None.
    209 
    210         @param mandatory_policies: dict of mandatory policies -> values.
    211         @param suggested_policies: dict of suggested policies -> values.
    212 
    213         @returns: JSON policy blob to send to the fake DM server.
    214         """
    215         # Remove "Not set" policies and json-ify dicts because the
    216         # FakeDMServer expects "policy": "{value}" not "policy": {value}.
    217         for policies_dict in [mandatory_policies, suggested_policies]:
    218             policies_to_pop = []
    219             for policy in policies_dict:
    220                 value = policies_dict[policy]
    221                 if value is None:
    222                     policies_to_pop.append(policy)
    223                 elif isinstance(value, dict):
    224                     policies_dict[policy] = encode_json_string(value)
    225                 elif isinstance(value, list):
    226                     if len(value) > 0 and isinstance(value[0], dict):
    227                         policies_dict[policy] = encode_json_string(value)
    228             for policy in policies_to_pop:
    229                 policies_dict.pop(policy)
    230 
    231         modes_dict = {}
    232         if mandatory_policies:
    233             modes_dict['mandatory'] = mandatory_policies
    234         if suggested_policies:
    235             modes_dict['suggested'] = suggested_policies
    236 
    237         device_management_dict = {
    238             'google/chromeos/user': modes_dict,
    239             'managed_users': ['*'],
    240             'policy_user': self.username,
    241             'current_key_index': 0,
    242             'invalidation_source': 16,
    243             'invalidation_name': 'test_policy'
    244         }
    245 
    246         logging.info('Created policy blob: %s', device_management_dict)
    247         return encode_json_string(device_management_dict)
    248 
    249 
    250     def _get_policy_value_shown(self, policy_tab, policy_name):
    251         """Get the value shown for |policy_name| from the |policy_tab| page.
    252 
    253         Return the policy value for the policy given by |policy_name|, from
    254         from the chrome://policy page given by |policy_tab|.
    255 
    256         CAVEAT: the policy page does not display proper JSON. For example, lists
    257         are generally shown without the [ ] and cannot be distinguished from
    258         strings.  This function decodes what it can and returns the string it
    259         found when in doubt.
    260 
    261         @param policy_tab: Tab displaying the Policies page.
    262         @param policy_name: The name of the policy.
    263 
    264         @returns: The decoded value shown for the policy on the Policies page,
    265                 with the aforementioned caveat.
    266         """
    267         row_values = policy_tab.EvaluateJavaScript('''
    268             var section = document.getElementsByClassName(
    269                     "policy-table-section")[0];
    270             var table = section.getElementsByTagName('table')[0];
    271             rowValues = '';
    272             for (var i = 1, row; row = table.rows[i]; i++) {
    273                if (row.className !== 'expanded-value-container') {
    274                   var name_div = row.getElementsByClassName('name elide')[0];
    275                   var name = name_div.textContent;
    276                   if (name === '%s') {
    277                      var value_span = row.getElementsByClassName('value')[0];
    278                      var value = value_span.textContent;
    279                      var status_div = row.getElementsByClassName(
    280                             'status elide')[0];
    281                      var status = status_div.textContent;
    282                      rowValues = [name, value, status];
    283                      break;
    284                   }
    285                }
    286             }
    287             rowValues;
    288         ''' % policy_name)
    289 
    290         value_shown = row_values[1].encode('ascii', 'ignore')
    291         status_shown = row_values[2].encode('ascii', 'ignore')
    292         logging.debug('Policy %s row: %s', policy_name, row_values)
    293 
    294         if status_shown == 'Not set.':
    295             return None
    296         return decode_json_string(value_shown)
    297 
    298 
    299     def _get_policy_value_from_new_tab(self, policy_name):
    300         """Get the policy value for |policy_name| from the Policies page.
    301 
    302         @param policy_name: string of policy name.
    303 
    304         @returns: decoded value of the policy as shown on chrome://policy.
    305         """
    306         values = self._get_policy_values_from_new_tab([policy_name])
    307         return values[policy_name]
    308 
    309 
    310     def _get_policy_values_from_new_tab(self, policy_names):
    311         """Get a given policy value by opening a new tab then closing it.
    312 
    313         @param policy_names: list of strings of policy names.
    314 
    315         @returns: dict of policy name mapped to decoded values of the policy as
    316                   shown on chrome://policy.
    317         """
    318         values = {}
    319         tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    320         for policy_name in policy_names:
    321           values[policy_name] = self._get_policy_value_shown(tab, policy_name)
    322         tab.Close()
    323 
    324         return values
    325 
    326 
    327     def verify_policy_value(self, policy_name, expected_value):
    328         """
    329         Verify that the correct policy values shows in chrome://policy.
    330 
    331         @param policy_name: the policy we are checking.
    332         @param expected_value: the expected value for policy_name.
    333 
    334         @raises error.TestError if value does not match expected.
    335 
    336         """
    337         value_shown = self._get_policy_value_from_new_tab(policy_name)
    338         logging.info('Value decoded from chrome://policy: %s', value_shown)
    339 
    340         # If we expect a list and don't have a list, modify the value_shown.
    341         if isinstance(expected_value, list):
    342             if isinstance(value_shown, str):
    343                 if '{' in value_shown: # List of dicts.
    344                     value_shown = decode_json_string('[%s]' % value_shown)
    345                 elif ',' in value_shown: # List of strs.
    346                     value_shown = value_shown.split(',')
    347                 else: # List with one str.
    348                     value_shown = [value_shown]
    349             elif not isinstance(value_shown, list): # List with one element.
    350                 value_shown = [value_shown]
    351 
    352         if not expected_value == value_shown:
    353             raise error.TestError('chrome://policy shows the incorrect value '
    354                                   'for %s!  Expected %s, got %s.' % (
    355                                           policy_name, expected_value,
    356                                           value_shown))
    357 
    358 
    359     def _initialize_chrome_extra_flags(self):
    360         """
    361         Initialize flags used to create Chrome instance.
    362 
    363         @returns: list of extra Chrome flags.
    364 
    365         """
    366         # Construct DM Server URL flags if not using production server.
    367         env_flag_list = []
    368         if self.env != 'prod':
    369             if self.dms_is_fake:
    370                 # Use URL provided by the fake AutoTest DM server.
    371                 dmserver_str = (DMSERVER % self.fake_dm_server.server_url)
    372             else:
    373                 # Use URL defined in the DMS URL dictionary.
    374                 dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env]))
    375                 if self.env == 'dm-test':
    376                     dmserver_str = (dmserver_str % self.dms_name)
    377 
    378             # Merge with other flags needed by non-prod enviornment.
    379             env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env])
    380 
    381         return env_flag_list
    382 
    383 
    384     def _launch_chrome_browser(self):
    385         """Launch Chrome browser and sign in."""
    386         extra_flags = self._initialize_chrome_extra_flags()
    387 
    388         logging.info('Chrome Browser Arguments:')
    389         logging.info('  extra_browser_args: %s', extra_flags)
    390         logging.info('  username: %s', self.username)
    391         logging.info('  password: %s', self.password)
    392         logging.info('  gaia_login: %s', not self.dms_is_fake)
    393 
    394         self.cr = chrome.Chrome(extra_browser_args=extra_flags,
    395                                 username=self.username,
    396                                 password=self.password,
    397                                 gaia_login=not self.dms_is_fake,
    398                                 disable_gaia_services=self.dms_is_fake,
    399                                 autotest_ext=True)
    400 
    401 
    402     def navigate_to_url(self, url, tab=None):
    403         """Navigate tab to the specified |url|. Create new tab if none given.
    404 
    405         @param url: URL of web page to load.
    406         @param tab: browser tab to load (if any).
    407         @returns: browser tab loaded with web page.
    408         """
    409         logging.info('Navigating to URL: %r', url)
    410         if not tab:
    411             tab = self.cr.browser.tabs.New()
    412             tab.Activate()
    413         tab.Navigate(url, timeout=8)
    414         tab.WaitForDocumentReadyStateToBeComplete()
    415         return tab
    416 
    417 
    418     def get_elements_from_page(self, tab, cmd):
    419         """Get collection of page elements that match the |cmd| filter.
    420 
    421         @param tab: tab containing the page to be scraped.
    422         @param cmd: JavaScript command to evaluate on the page.
    423         @returns object containing elements on page that match the cmd.
    424         @raises: TestFail if matching elements are not found on the page.
    425         """
    426         try:
    427             elements = tab.EvaluateJavaScript(cmd)
    428         except Exception as err:
    429             raise error.TestFail('Unable to find matching elements on '
    430                                  'the test page: %s\n %r' %(tab.url, err))
    431         return elements
    432 
    433 
    434     def run_once(self):
    435         """The run_once() method is required by all AutoTest tests.
    436 
    437         run_once() is defined herein to automatically determine which test
    438         case in the test class to run. The test class must have a public
    439         run_test_case() method defined. Note: The test class may override
    440         run_once() if it determines which test case to run.
    441         """
    442         logging.info('Running test case: %s', self.case)
    443         self.run_test_case(self.case)
    444 
    445 
    446 def encode_json_string(object_value):
    447     """Convert given value to JSON format string.
    448 
    449     @param object_value: object to be converted.
    450 
    451     @returns: string in JSON format.
    452     """
    453     return json.dumps(object_value)
    454 
    455 
    456 def decode_json_string(json_string):
    457     """Convert given JSON format string to an object.
    458 
    459     If no object is found, return json_string instead.  This is to allow
    460     us to "decode" items off the policy page that aren't real JSON.
    461 
    462     @param json_string: the JSON string to be decoded.
    463 
    464     @returns: Python object represented by json_string or json_string.
    465     """
    466     def _decode_list(json_list):
    467         result = []
    468         for value in json_list:
    469             if isinstance(value, unicode):
    470                 value = value.encode('ascii')
    471             if isinstance(value, list):
    472                 value = _decode_list(value)
    473             if isinstance(value, dict):
    474                 value = _decode_dict(value)
    475             result.append(value)
    476         return result
    477 
    478     def _decode_dict(json_dict):
    479         result = {}
    480         for key, value in json_dict.iteritems():
    481             if isinstance(key, unicode):
    482                 key = key.encode('ascii')
    483             if isinstance(value, unicode):
    484                 value = value.encode('ascii')
    485             elif isinstance(value, list):
    486                 value = _decode_list(value)
    487             result[key] = value
    488         return result
    489 
    490     try:
    491         # Decode JSON turning all unicode strings into ascii.
    492         # object_hook will get called on all dicts, so also handle lists.
    493         result = json.loads(json_string, encoding='ascii',
    494                             object_hook=_decode_dict)
    495         if isinstance(result, list):
    496             result = _decode_list(result)
    497         return result
    498     except ValueError as e:
    499         # Input not valid, e.g. '1, 2, "c"' instead of '[1, 2, "c"]'.
    500         logging.warning('Could not unload: %s (%s)', json_string, e)
    501         return json_string
    502