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 import time
     10 
     11 from autotest_lib.client.common_lib.cros import arc
     12 from autotest_lib.client.common_lib.cros.arc import is_android_container_alive
     13 
     14 from autotest_lib.client.bin import test
     15 from autotest_lib.client.common_lib import error
     16 from autotest_lib.client.common_lib.cros import chrome
     17 from autotest_lib.client.common_lib.cros import enrollment
     18 from autotest_lib.client.common_lib.cros import policy
     19 from autotest_lib.client.cros import cryptohome
     20 from autotest_lib.client.cros import httpd
     21 from autotest_lib.client.cros.input_playback import keyboard
     22 from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver
     23 from py_utils import TimeoutException
     24 
     25 from telemetry.core import exceptions
     26 
     27 CROSQA_FLAGS = [
     28     '--gaia-url=https://gaiastaging.corp.google.com',
     29     '--lso-url=https://gaiastaging.corp.google.com',
     30     '--google-apis-url=https://www-googleapis-test.sandbox.google.com',
     31     '--oauth2-client-id=236834563817.apps.googleusercontent.com',
     32     '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI',
     33     ('--cloud-print-url='
     34      'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
     35     '--ignore-urlfetcher-cert-requests']
     36 CROSALPHA_FLAGS = [
     37     ('--cloud-print-url='
     38      'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
     39     '--ignore-urlfetcher-cert-requests']
     40 TESTDMS_FLAGS = [
     41     '--ignore-urlfetcher-cert-requests',
     42     '--disable-policy-key-verification']
     43 FLAGS_DICT = {
     44     'prod': [],
     45     'crosman-qa': CROSQA_FLAGS,
     46     'crosman-alpha': CROSALPHA_FLAGS,
     47     'dm-test': TESTDMS_FLAGS,
     48     'dm-fake': TESTDMS_FLAGS
     49 }
     50 DMS_URL_DICT = {
     51     'prod': 'http://m.google.com/devicemanagement/data/api',
     52     'crosman-qa':
     53         'https://crosman-qa.sandbox.google.com/devicemanagement/data/api',
     54     'crosman-alpha':
     55         'https://crosman-alpha.sandbox.google.com/devicemanagement/data/api',
     56     'dm-test': 'http://chromium-dm-test.appspot.com/d/%s',
     57     'dm-fake': 'http://127.0.0.1:%d/'
     58 }
     59 DMSERVER = '--device-management-url=%s'
     60 # Username and password for the fake dm server can be anything, since
     61 # they are not used to authenticate against GAIA.
     62 USERNAME = 'fake-user (at] managedchrome.com'
     63 PASSWORD = 'fakepassword'
     64 GAIA_ID = 'fake-gaia-id'
     65 
     66 # Convert from chrome://policy name to what fake dms expects.
     67 DEVICE_POLICY_DICT = {
     68     'DeviceAutoUpdateDisabled': 'update_disabled',
     69     'DeviceEphemeralUsersEnabled': 'ephemeral_users_enabled',
     70     'DeviceOpenNetworkConfiguration': 'open_network_configuration',
     71     'DeviceRollbackToTargetVersion': 'rollback_to_target_version',
     72     'DeviceTargetVersionPrefix': 'target_version_prefix',
     73     'SystemTimezone': 'timezone',
     74     'ReportUploadFrequency': 'device_status_frequency',
     75     'DeviceLocalAccounts': 'account',
     76     'DeviceLocalAccountAutoLoginId': 'auto_login_id'
     77 }
     78 
     79 # Default settings for managed user policies
     80 DEFAULT_POLICY = {
     81     'AllowDinosaurEasterEgg': False,
     82     'ArcBackupRestoreServiceEnabled': 0,
     83     'ArcEnabled': False,
     84     'ArcGoogleLocationServicesEnabled': 0,
     85     'CaptivePortalAuthenticationIgnoresProxy': False,
     86     'CastReceiverEnabled': False,
     87     'ChromeOsMultiProfileUserBehavior': 'primary-only',
     88     'EasyUnlockAllowed': False,
     89     'InstantTetheringAllowed': False,
     90     'NTLMShareAuthenticationEnabled': False,
     91     'NTPContentSuggestionsEnabled': False,
     92     'NetBiosShareDiscoveryEnabled': False,
     93     'QuickUnlockModeWhitelist': [],
     94     'SmartLockSigninAllowed': False,
     95     'SmsMessagesAllowed': False
     96 }
     97 
     98 
     99 class EnterprisePolicyTest(arc.ArcTest, test.test):
    100     """Base class for Enterprise Policy Tests."""
    101 
    102     WEB_PORT = 8080
    103     WEB_HOST = 'http://localhost:%d' % WEB_PORT
    104     CHROME_POLICY_PAGE = 'chrome://policy'
    105     CHROME_VERSION_PAGE = 'chrome://version'
    106 
    107 
    108     def initialize(self, **kwargs):
    109         """
    110         Initialize test parameters.
    111 
    112         Consume the check_client_result parameter if this test was started
    113         from a server test.
    114 
    115         """
    116         self._initialize_enterprise_policy_test(**kwargs)
    117 
    118 
    119     def _initialize_enterprise_policy_test(
    120             self, case='', env='dm-fake', dms_name=None,
    121             username=USERNAME, password=PASSWORD, gaia_id=GAIA_ID,
    122             set_auto_logout=None, **kwargs):
    123         """
    124         Initialize test parameters and fake DM Server.
    125 
    126         This function exists so that ARC++ tests (which inherit from the
    127         ArcTest class) can also initialize a policy setup.
    128 
    129         @param case: String name of the test case to run.
    130         @param env: String environment of DMS and Gaia servers.
    131         @param username: String user name login credential.
    132         @param password: String password login credential.
    133         @param gaia_id: String gaia_id login credential.
    134         @param dms_name: String name of test DM Server.
    135         @param kwargs: Not used.
    136 
    137         """
    138         self.case = case
    139         self.env = env
    140         self.username = username
    141         self.password = password
    142         self.gaia_id = gaia_id
    143         self.set_auto_logout = set_auto_logout
    144         self.dms_name = dms_name
    145         self.dms_is_fake = (env == 'dm-fake')
    146         self.arc_enabled = False
    147         self.version = None
    148         self._enforce_variable_restrictions()
    149 
    150         # Install protobufs and add import path.
    151         policy.install_protobufs(self.autodir, self.job)
    152 
    153         # Initialize later variables to prevent error after an early failure.
    154         self._web_server = None
    155         self.cr = None
    156 
    157         # Start AutoTest DM Server if using local fake server.
    158         if self.dms_is_fake:
    159             self.fake_dm_server = enterprise_fake_dmserver.FakeDMServer()
    160             self.fake_dm_server.start(self.tmpdir, self.debugdir)
    161 
    162         # Get enterprise directory of shared resources.
    163         client_dir = os.path.dirname(os.path.dirname(self.bindir))
    164         self.enterprise_dir = os.path.join(client_dir, 'cros/enterprise')
    165 
    166         if self.set_auto_logout is not None:
    167             self._auto_logout = self.set_auto_logout
    168 
    169         # Log the test context parameters.
    170         logging.info('Test Context Parameters:')
    171         logging.info('  Case: %r', self.case)
    172         logging.info('  Environment: %r', self.env)
    173         logging.info('  Username: %r', self.username)
    174         logging.info('  Password: %r', self.password)
    175         logging.info('  Test DMS Name: %r', self.dms_name)
    176 
    177 
    178     def check_page_readiness(self, tab, command):
    179         """
    180         Checks to see if page has fully loaded.
    181 
    182         @param tab: chrome tab loading the page.
    183         @param command: JS command to be checked in the tab.
    184 
    185         @returns True if loaded and False if not.
    186 
    187         """
    188         try:
    189             tab.EvaluateJavaScript(command)
    190             return True
    191         except exceptions.EvaluateException:
    192             return False
    193 
    194 
    195     def cleanup(self):
    196         """Close out anything used by this test."""
    197         # Clean up AutoTest DM Server if using local fake server.
    198         if self.dms_is_fake:
    199             self.fake_dm_server.stop()
    200 
    201         # Stop web server if it was started.
    202         if self._web_server:
    203             self._web_server.stop()
    204 
    205         # Close Chrome instance if opened.
    206         if self.cr and self._auto_logout:
    207             self.cr.close()
    208 
    209         # Cleanup the ARC test if needed.
    210         if self.arc_enabled:
    211             super(EnterprisePolicyTest, self).cleanup()
    212 
    213 
    214     def start_webserver(self):
    215         """Set up HTTP Server to serve pages from enterprise directory."""
    216         self._web_server = httpd.HTTPListener(
    217                 self.WEB_PORT, docroot=self.enterprise_dir)
    218         self._web_server.run()
    219 
    220 
    221     def _enforce_variable_restrictions(self):
    222         """Validate class-level test context parameters.
    223 
    224         @raises error.TestError if context parameter has an invalid value,
    225                 or a combination of parameters have incompatible values.
    226         """
    227         # Verify |env| is a valid environment.
    228         if self.env not in FLAGS_DICT:
    229             raise error.TestError('Environment is invalid: %s' % self.env)
    230 
    231         # Verify test |dms_name| is given iff |env| is 'dm-test'.
    232         if self.env == 'dm-test' and not self.dms_name:
    233             raise error.TestError('dms_name must be given when using '
    234                                   'env=dm-test.')
    235         if self.env != 'dm-test' and self.dms_name:
    236             raise error.TestError('dms_name must not be given when not using '
    237                                   'env=dm-test.')
    238 
    239 
    240     def setup_case(self,
    241                    user_policies={},
    242                    suggested_user_policies={},
    243                    device_policies={},
    244                    extension_policies={},
    245                    skip_policy_value_verification=False,
    246                    kiosk_mode=False,
    247                    enroll=False,
    248                    auto_login=True,
    249                    auto_logout=True,
    250                    init_network_controller=False,
    251                    arc_mode=False,
    252                    setup_arc=True,
    253                    use_clouddpc_test=None,
    254                    disable_default_apps=True,
    255                    extension_paths=[],
    256                    extra_chrome_flags=[]):
    257         """Set up DMS, log in, and verify policy values.
    258 
    259         If the AutoTest fake DM Server is used, make a JSON policy blob
    260         and upload it to the fake DM server.
    261 
    262         Launch Chrome and sign in to Chrome OS. Examine the user's
    263         cryptohome vault, to confirm user is signed in successfully.
    264 
    265         @param user_policies: dict of mandatory user policies in
    266                 name -> value format.
    267         @param suggested_user_policies: optional dict of suggested policies
    268                 in name -> value format.
    269         @param device_policies: dict of device policies in
    270                 name -> value format.
    271         @param extension_policies: dict of extension policies.
    272         @param skip_policy_value_verification: True if setup_case should not
    273                 verify that the correct policy value shows on policy page.
    274         @param enroll: True for enrollment instead of login.
    275         @param auto_login: Sign in to chromeos.
    276         @param auto_logout: Sign out of chromeos when test is complete.
    277         @param init_network_controller: whether to init network controller.
    278         @param arc_mode: whether to enable arc_mode on chrome.chrome().
    279         @param setup_arc: whether to run setup_arc in arc.Arctest.
    280         @param use_clouddpc_test: whether to run the cloud dpc test.
    281         @param extension_paths: list of extensions to install.
    282         @param extra_chrome_flags: list of flags to add to Chrome.
    283 
    284         @raises error.TestError if cryptohome vault is not mounted for user.
    285         @raises error.TestFail if |policy_name| and |policy_value| are not
    286                 shown on the Policies page.
    287         """
    288 
    289         # Need a real account, for now. Note: Even though the account is 'real'
    290         # you can still use a fake DM server.
    291         if arc_mode and self.username == USERNAME:
    292             self.username = 'tester50 (at] managedchrome.com'
    293             self.password = 'Test0000'
    294 
    295         self._auto_logout = auto_logout
    296         self._kiosk_mode = kiosk_mode
    297 
    298         if self.dms_is_fake:
    299             self.fake_dm_server.setup_policy(self._make_json_blob(
    300                 user_policies, suggested_user_policies, device_policies,
    301                 extension_policies))
    302 
    303         self._create_chrome(enroll=enroll,
    304                             auto_login=auto_login,
    305                             init_network_controller=init_network_controller,
    306                             extension_paths=extension_paths,
    307                             arc_mode=arc_mode,
    308                             disable_default_apps=disable_default_apps,
    309                             extra_chrome_flags=extra_chrome_flags)
    310 
    311         # Skip policy check upon request or if we enroll but don't log in.
    312         skip_policy_value_verification = (
    313                 skip_policy_value_verification or not auto_login)
    314 
    315         if not skip_policy_value_verification:
    316             self.verify_policy_stats(user_policies, suggested_user_policies,
    317                                      device_policies)
    318             self.verify_extension_stats(extension_policies)
    319 
    320         if arc_mode:
    321             self.start_arc(use_clouddpc_test, setup_arc)
    322 
    323     def start_arc(self, use_clouddpc_test, setup_arc):
    324         '''
    325         Starts ARC when creating the chrome object. Specifically will create
    326         the ADB shell container for testing use.
    327 
    328         We are NOT going to use the arc class initialize, it overwrites the
    329         creation of chrome.chrome() in a way which cannot support the DM sever.
    330 
    331         Instead we check for the android container, and run arc_setup if
    332         needed. Note: To use the cloud dpc test, you MUST also run setup_arc
    333 
    334         @param setup_arc: whether to run setup_arc in arc.Arctest.
    335         @param use_clouddpc_test: bool, run_clouddpc_test() or not.
    336         '''
    337         _APP_FILENAME = 'autotest-deps-cloudpctest-0.4.apk'
    338         _DEP_PACKAGE = 'CloudDPCTest-apks'
    339         _PKG_NAME = 'com.google.android.apps.work.clouddpc.e2etests'
    340 
    341         # By default on boot the container is alive, and will not close until
    342         # a user with ARC disabled logs in. This wait accounts for that.
    343         time.sleep(3)
    344 
    345         if use_clouddpc_test and not setup_arc:
    346             raise error.TestFail('For cloud DPC setup_arc cannot be disabled')
    347 
    348         if is_android_container_alive():
    349             logging.info('Android Container is alive!')
    350         else:
    351             logging.error('Android Container is not alive!')
    352 
    353         # Install the clouddpc test.
    354         if use_clouddpc_test:
    355             self.arc_setup(dep_packages=_DEP_PACKAGE,
    356                            apks=[_APP_FILENAME],
    357                            full_pkg_names=[_PKG_NAME])
    358             self.run_clouddpc_test()
    359         else:
    360             if setup_arc:
    361                 self.arc_setup()
    362 
    363         self.arc_enabled = True
    364 
    365     def run_clouddpc_test(self):
    366         """
    367         Run clouddpc end-to-end test and fail this test if it fails.
    368 
    369         Assumes start_arc() was run with use_clouddpc_test.
    370 
    371         Determines the policy values to pass to the test from those set in
    372         Chrome OS.
    373 
    374         @raises error.TestFail if the test does not pass.
    375 
    376         """
    377         policy_blob = self._get_clouddpc_policies()
    378         policy_blob_str = json.dumps(policy_blob, separators=(',',':'))
    379         cmd = ('am instrument -w -e policy "%s" '
    380                'com.google.android.apps.work.clouddpc.e2etests/'
    381                '.ArcInstrumentationTestRunner') % policy_blob_str
    382 
    383         # Run the command as a shell script so that its length is not capped.
    384         temp_shell_script_path = '/sdcard/tmp.sh'
    385         arc.write_android_file(temp_shell_script_path, cmd)
    386 
    387         logging.info('Running clouddpc test with policy: %s', policy_blob_str)
    388         results = arc.adb_shell('sh ' + temp_shell_script_path).strip()
    389         arc.remove_android_file(temp_shell_script_path)
    390         if results.find('FAILURES!!!') >= 0:
    391             logging.info('CloudDPC E2E Results:\n%s', results)
    392             err_msg = results.splitlines()[-1]
    393             raise error.TestFail('CloudDPC E2E failure: %s' % err_msg)
    394 
    395         logging.debug(results)
    396         logging.info('CloudDPC E2E test passed!')
    397 
    398     def _get_clouddpc_policies(self):
    399         """
    400         Return the relevant CloudDPC policies and their values for e2e testing.
    401 
    402         The CloudDPC end-to-end test takes in a string of the policies which
    403         are set.  These policies don't have the same names in Chrome OS, or
    404         they don't map 1:1 with Chrome OS's policies.
    405 
    406         Figuring out the values from chrome://policy (rather than creating a
    407         dict of the policies under test) will prevent the test from failing on
    408         accounts which happen to have unrelated policies set.
    409 
    410         Constructs a json object of the CloudDPC policy names and values.
    411         Finds the values which map 1:1 to Chrome OS and handles the exceptions.
    412 
    413         @returns a dict where the keys are CloudDPC policy names and the values
    414                  are as shown on chrome://policy.
    415 
    416         """
    417 
    418         # CloudDPC policy -> value
    419         policy_map = {}
    420 
    421         # The policies which are a 1:1 rename between Chrome OS and CloudDPC.
    422         chromeos_to_clouddpc = {
    423             'AudioCaptureAllowed': 'unmuteMicrophoneDisabled',
    424             'DefaultGeolocationSetting': 'shareLocationDisabled',
    425             'DeviceBlockDevmode': 'debuggingFeaturesDisabled',
    426             'DisableScreenshots': 'screenCaptureDisabled',
    427             'ExternalStorageDisabled': 'usbFileTransferDisabled',
    428             'VideoCaptureAllowed': 'cameraDisabled',
    429         }
    430 
    431         # Only check caCerts if the value is allowed to be passed to Android.
    432         caCerts_passed = self._get_policy_value_from_new_tab(
    433             'ArcCertificatesSyncMode')
    434         if caCerts_passed:
    435             chromeos_to_clouddpc['OpenNetworkConfiguration'] = 'caCerts'
    436 
    437         # The policies which take some special handling to convert.
    438         exception_policies = ['URLBlacklist', 'URLWhitelist', 'ArcPolicy']
    439 
    440         values = self._get_policy_values_from_new_tab(
    441             chromeos_to_clouddpc.keys() + exception_policies)
    442 
    443         # Map the 1:1 policies: Android policy name to ChromeOS policy value.
    444         for chromeos_policy in chromeos_to_clouddpc:
    445             clouddpc_policy = chromeos_to_clouddpc[chromeos_policy]
    446             value = values[chromeos_policy]
    447             if value is not None:
    448                 policy_map[clouddpc_policy] = value
    449 
    450         # ArcPolicy value contains some stand-alone CloudDPC policies.
    451         arc_policy_value = values['ArcPolicy']
    452 
    453         if arc_policy_value:
    454             for key in ['applications', 'accountTypesWithManagementDisabled']:
    455                 if key in arc_policy_value:
    456                     policy_map[key] = arc_policy_value[key]
    457 
    458         return policy_map
    459 
    460     def _make_json_blob(self, user_policies={}, suggested_user_policies={},
    461                         device_policies={}, extension_policies={}):
    462         """Create JSON policy blob from mandatory and suggested policies.
    463 
    464         For the status of a policy to be shown as "Not set" on the
    465         chrome://policy page, the policy dictionary must contain no NVP for
    466         for that policy. Remove policy NVPs if value is None.
    467 
    468         @param user_policies: mandatory user policies -> values.
    469         @param suggested user_policies: suggested user policies -> values.
    470         @param device_policies: mandatory device policies -> values.
    471         @param extension_policies: extension policies.
    472 
    473         @returns: JSON policy blob to send to the fake DM server.
    474         """
    475 
    476         user_p = copy.deepcopy(user_policies)
    477         s_user_p = copy.deepcopy(suggested_user_policies)
    478         device_p = copy.deepcopy(device_policies)
    479         extension_p = copy.deepcopy(extension_policies)
    480 
    481         # Replace all device policies with their FakeDMS-friendly names.
    482         fixed_device_p = {}
    483         for policy in device_p:
    484             if policy not in DEVICE_POLICY_DICT:
    485                 raise error.TestError('Cannot convert %s!' % policy)
    486             fixed_device_p[DEVICE_POLICY_DICT[policy]] = device_p[policy]
    487 
    488         # Remove "Not set" policies and json-ify dicts because the
    489         # FakeDMServer expects "policy": "{value}" not "policy": {value}
    490         # and "policy": "[{value}]" not "policy": [{value}].
    491         for policies_dict in [user_p, s_user_p, fixed_device_p]:
    492             policies_to_pop = []
    493             for policy in policies_dict:
    494                 value = policies_dict[policy]
    495                 if value is None:
    496                     policies_to_pop.append(policy)
    497                 elif isinstance(value, dict):
    498                     policies_dict[policy] = encode_json_string(value)
    499                 elif isinstance(value, list) and not (
    500                     policies_dict in [fixed_device_p]):
    501                     if value and isinstance(value[0], dict):
    502                         policies_dict[policy] = encode_json_string(value)
    503             for policy in policies_to_pop:
    504                 policies_dict.pop(policy)
    505 
    506         management_dict = {
    507             'managed_users': ['*'],
    508             'policy_user': self.username,
    509             'current_key_index': 0,
    510             'invalidation_source': 16,
    511             'invalidation_name': 'test_policy'
    512         }
    513 
    514         if user_p or s_user_p:
    515             user_modes_dict = {}
    516             if user_p:
    517                 user_modes_dict['mandatory'] = user_p
    518             if suggested_user_policies:
    519                 user_modes_dict['recommended'] = s_user_p
    520             management_dict['google/chromeos/user'] = user_modes_dict
    521 
    522         if fixed_device_p:
    523             management_dict['google/chromeos/device'] = fixed_device_p
    524 
    525         if extension_p:
    526             management_dict['google/chrome/extension'] = extension_p
    527         logging.info('Created policy blob: %s', management_dict)
    528         return encode_json_string(management_dict)
    529 
    530 
    531     def _get_extension_policy_table(self, policy_tab, ext_id):
    532         """
    533         Find the policy table that matches the given extension ID.
    534 
    535         The user and device policies are in table[0]. Extension policies are
    536         in their own tables.
    537 
    538         @param policy_tab: Tab displaying the policy page.
    539         @param ext_id: Extension ID.
    540 
    541         @returns: Index of the table in the DOM.
    542         @raises error.TestError: if the table for the extension ID does
    543             not exist.
    544 
    545         """
    546         table_index = policy_tab.EvaluateJavaScript("""
    547         var table_id = -1;
    548         var section = document.getElementsByClassName('policy-table');
    549         for (var i = 0; i < section.length; i++) {
    550             var temp_name = section[i]
    551                 .getElementsByClassName('id')[0].innerText;
    552             if (temp_name === "%s")
    553                 { var table_id = i;
    554                   break ;
    555                 }
    556            };
    557         table_id;
    558             """ % ext_id)
    559         if table_index == -1:
    560             raise error.TestError(
    561                     'Policy table for extension %s does not exist. '
    562                     'Make sure the extension is installed.' % ext_id)
    563 
    564         return table_index
    565 
    566 
    567     def reload_policies(self):
    568         """Force a policy fetch."""
    569         policy_tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    570         reload_button = "document.querySelector('button#reload-policies')"
    571         policy_tab.ExecuteJavaScript("%s.click()" % reload_button)
    572         policy_tab.WaitForJavaScriptCondition("!%s.disabled" % reload_button,
    573                                               timeout=1)
    574         policy_tab.Close()
    575 
    576 
    577     def verify_extension_stats(self, extension_policies, sensitive_fields=[]):
    578         """
    579         Verify the extension policies match what is on chrome://policy.
    580 
    581         @param extension_policies: the dictionary of extension IDs mapping
    582             to download_url and secure_hash.
    583         @param sensitive_fields: list of fields that should have their value
    584             censored.
    585         @raises error.TestError: if the shown values do not match what we are
    586             expecting.
    587 
    588         """
    589         policy_tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    590         for id in extension_policies.keys():
    591             table = self._get_extension_policy_table(policy_tab, id)
    592             download_url = extension_policies[id]['download_url']
    593             policy_file = os.path.join(self.enterprise_dir,
    594                                        download_url.split('/')[-1])
    595 
    596             with open(policy_file) as f:
    597                 policies = json.load(f)
    598 
    599             for policy_name, settings in policies.items():
    600                 expected_value = settings['Value']
    601                 value_shown = self._get_policy_stats_shown(
    602                         policy_tab, policy_name, table)['value']
    603 
    604                 if policy_name in sensitive_fields:
    605                     expected_value = '********'
    606 
    607                 self._compare_values(policy_name, expected_value, value_shown)
    608 
    609         policy_tab.Close()
    610 
    611     def _get_policy_stats_shown(self, policy_tab, policy_name,
    612                                 table_index=0):
    613         """Get the info shown for |policy_name| from the |policy_tab| page.
    614 
    615         Return a dict of stats for the policy given by |policy_name|, from
    616         from the chrome://policy page given by |policy_tab|.
    617 
    618         CAVEAT: the policy page does not display proper JSON. For example, lists
    619         are generally shown without the [ ] and cannot be distinguished from
    620         strings.  This function decodes what it can and returns the string it
    621         found when in doubt.
    622 
    623         @param policy_tab: Tab displaying the Policies page.
    624         @param policy_name: The name of the policy.
    625         @param table_index: Index of table in DOM to check.
    626 
    627         @returns: A dict of stats, including JSON decode 'value' (see caveat).
    628                   Also included are 'name', 'status', 'level', 'scope',
    629                   and 'source'.
    630         """
    631         stats = {'name': policy_name}
    632         row_values = policy_tab.EvaluateJavaScript('''
    633         var rowValues = {};
    634         var section = document.getElementsByClassName('policy-table')[%s];
    635         table = section.getElementsByClassName('main')[0];
    636         var pol_rows = table.getElementsByClassName('policy-data');
    637         for (i = 0; i < pol_rows.length; i++) {
    638             if (window.getComputedStyle(pol_rows[i]).display === "none")
    639                 { break ;}
    640             var pol_name = pol_rows[i]
    641                 .getElementsByClassName('policy row')[0]
    642                 .getElementsByClassName('name')[0].innerText;
    643             if (pol_name === '%s'){
    644                 var pol_data = pol_rows[i]
    645                     .getElementsByClassName('policy row')[0];
    646                 var value_data = pol_rows[i]
    647                     .getElementsByClassName('value row')[0];
    648                 rowValues["value"] = value_data
    649                     .getElementsByClassName('value')[0].innerText;
    650                 var column_titles = ["name", "source",
    651                                      "scope", "level", "messages"];
    652                 column_titles.forEach(function(entry) {
    653                     var entry_div = pol_data.getElementsByClassName(entry)[0];
    654                     rowValues[entry] = entry_div.innerText});
    655            };
    656         };
    657         rowValues;
    658         ''' % (table_index, policy_name))
    659 
    660         entries = ["name", "value", "source", "scope", "level", "messages"]
    661 
    662         # New Policy Parser returns empty, rather than 'Not Set.'. This is
    663         # a fix to make it compatible with the rest of the parsing code rather
    664         # than a larger re-write.
    665         if not row_values:
    666             for entry in entries:
    667                 row_values[entry] = ''
    668             row_values['messages'] = 'Not set.'
    669 
    670         logging.debug('Policy %s row: %s', policy_name, row_values)
    671         key_diff = set(entries) - set(row_values.keys())
    672         if key_diff:
    673             raise error.TestError(
    674                 'Could not get policy info for %s. '
    675                 'Missing columns: %s.' % (policy_name, key_diff))
    676 
    677         for v in entries:
    678             stats[v] = row_values[v].encode('ascii', 'ignore')
    679 
    680         if stats['messages'] == 'Not set.':
    681             for v in entries:
    682                 stats[v] = None
    683         else:
    684             stats['value'] = decode_json_string(stats['value'])
    685 
    686         return stats
    687 
    688     def _get_policy_value_from_new_tab(self, policy_name):
    689         """Get the policy value for |policy_name| from the Policies page.
    690 
    691         Information comes from the policy page.  A single new tab is opened
    692         and then closed to check this info, so device must be logged in.
    693 
    694         @param policy_name: string of policy name.
    695 
    696         @returns: decoded value of the policy as shown on chrome://policy.
    697         """
    698         values = self._get_policy_stats_from_new_tab([policy_name])
    699         return values[policy_name]['value']
    700 
    701     def _get_chrome_version_from_browser(self):
    702         """Get the Chrome Version from the ://version page.
    703 
    704         @returns: Version as shown on ://version page.
    705         """
    706         tab = self.navigate_to_url(self.CHROME_VERSION_PAGE)
    707         table_name = 'inner'
    708         version_box = 'version'
    709         version_row = 0
    710         return tab.EvaluateJavaScript(
    711             "document.getElementById('{}').rows[{}]\
    712             .getElementsByClassName('{}')[0].innerText"
    713             .format(table_name, version_row, version_box))
    714 
    715     def _get_policy_values_from_new_tab(self, policy_names):
    716         """Get the policy values of the given policies.
    717 
    718         Information comes from the policy page.  A single new tab is opened
    719         and then closed to check this info, so device must be logged in.
    720 
    721         @param policy_names: list of strings of policy names.
    722 
    723         @returns: dict of policy name mapped to decoded values of the policy as
    724                   shown on chrome://policy.
    725         """
    726         values = {}
    727         tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    728         for policy_name in policy_names:
    729             values[policy_name] = (
    730                 self._get_policy_stats_shown(tab, policy_name)['value'])
    731         tab.Close()
    732 
    733         return values
    734 
    735 
    736     def _get_policy_stats_from_new_tab(self, policy_names):
    737         """Get policy info about the given policy names.
    738 
    739         Information comes from the policy page.  A single new tab is opened
    740         and then closed to check this info, so device must be logged in.
    741 
    742         @param policy_name: list of policy names (strings).
    743 
    744         @returns: dict of policy names mapped to dicts containing policy info.
    745                   Values are decoded JSON.
    746         """
    747         stats = {}
    748         tab = self.navigate_to_url(self.CHROME_POLICY_PAGE)
    749         for policy_name in policy_names:
    750             stats[policy_name] = self._get_policy_stats_shown(tab, policy_name)
    751         tab.Close()
    752 
    753         return stats
    754 
    755 
    756     def _compare_values(self, policy_name, input_value, value_shown):
    757         """Pass if an expected value and the chrome://policy version match.
    758 
    759         Handles some of the inconsistencies in the chrome://policy JSON format.
    760 
    761         @param policy_name: The policy name to be verified.
    762         @param input_value: The setting given for the policy.
    763         @param value_shown: The setting of the policy from the DUT.
    764 
    765         @raises: error.TestError if policy values do not match.
    766 
    767         """
    768         expected_value = copy.deepcopy(input_value)
    769 
    770         # If we expect a list and don't have a list, modify the value_shown.
    771         if isinstance(expected_value, list):
    772             if isinstance(value_shown, str):
    773                 if '{' in value_shown:  # List of dicts.
    774                     value_shown = decode_json_string('[%s]' % value_shown)
    775                 elif ',' in value_shown:  # List of strs.
    776                     value_shown = value_shown.split(',')
    777                 else:  # List with one str.
    778                     value_shown = [value_shown]
    779             elif not isinstance(value_shown, list):  # List with one element.
    780                 value_shown = [value_shown]
    781 
    782         # Special case for user and device network configurations.
    783         # Passphrases are hidden on the policy page, so the passphrase
    784         # field needs to be converted to asterisks to be compared.
    785         SANITIZED_PASSWORD = '*' * 8
    786         if policy_name.endswith('OpenNetworkConfiguration'):
    787             for network in expected_value.get('NetworkConfigurations', []):
    788                 wifi = network.get('WiFi', {})
    789                 if 'Passphrase' in wifi:
    790                     wifi['Passphrase'] = SANITIZED_PASSWORD
    791                 if 'EAP' in wifi and 'Password' in wifi['EAP']:
    792                     wifi['EAP']['Password'] = SANITIZED_PASSWORD
    793             for cert in expected_value.get('Certificates', []):
    794                 if 'PKCS12' in cert:
    795                     cert['PKCS12'] = SANITIZED_PASSWORD
    796 
    797         # Some managed policies have a default value when they are not set.
    798         # Replace these unset policies with their default value.
    799         elif policy_name in DEFAULT_POLICY and expected_value is None:
    800             expected_value = DEFAULT_POLICY[policy_name]
    801 
    802         if expected_value != value_shown:
    803             raise error.TestError('chrome://policy shows the incorrect value '
    804                                   'for %s!  Expected %s, got %s.' % (
    805                                       policy_name, expected_value,
    806                                       value_shown))
    807 
    808 
    809     def verify_policy_value(self, policy_name, expected_value):
    810         """
    811         Verify that the a single policy correctly shows in chrome://policy.
    812 
    813         @param policy_name: the policy we are checking.
    814         @param expected_value: the expected value for policy_name.
    815 
    816         @raises error.TestError if value does not match expected.
    817 
    818         """
    819         value_shown = self._get_policy_value_from_new_tab(policy_name)
    820         self._compare_values(policy_name, expected_value, value_shown)
    821 
    822 
    823     def verify_policy_stats(self, user_policies={}, suggested_user_policies={},
    824                             device_policies={}):
    825         """Verify that the correct policy values show in chrome://policy.
    826 
    827         @param policy_dict: the policies we are checking.
    828 
    829         @raises error.TestError if value does not match expected.
    830         """
    831         def _compare_stat(stat, desired, name, stats):
    832             """ Raise error if a stat doesn't match."""
    833             err_str = 'Incorrect '+stat+' for '+name+': expected %s, got %s!'
    834             shown = stats[name][stat]
    835             # If policy is not set, there are no stats to match.
    836             if stats[name]['messages'] is None:
    837                 if not shown == None:
    838                     raise error.TestError(err_str % (None, shown))
    839                 else:
    840                     return
    841             if not desired == shown:
    842                 raise error.TestError(err_str % (desired, shown))
    843 
    844         keys = (user_policies.keys() + suggested_user_policies.keys() +
    845                 device_policies.keys())
    846 
    847         # If no policies were modified from default, return.
    848         if len(keys) == 0:
    849             return
    850 
    851         stats = self._get_policy_stats_from_new_tab(keys)
    852 
    853         for policy in user_policies:
    854             self._compare_values(policy, user_policies[policy],
    855                                  stats[policy]['value'])
    856             _compare_stat('level', 'Mandatory', policy, stats)
    857             _compare_stat('scope', 'Current user', policy, stats)
    858         for policy in suggested_user_policies:
    859             self._compare_values(policy, suggested_user_policies[policy],
    860                                  stats[policy]['value'])
    861             _compare_stat('level', 'Recommended', policy, stats)
    862             _compare_stat('scope', 'Current user', policy, stats)
    863         for policy in device_policies:
    864             self._compare_values(policy, device_policies[policy],
    865                                  stats[policy]['value'])
    866             _compare_stat('level', 'Mandatory', policy, stats)
    867             _compare_stat('scope', 'Device', policy, stats)
    868 
    869 
    870     def _initialize_chrome_extra_flags(self):
    871         """
    872         Initialize flags used to create Chrome instance.
    873 
    874         @returns: list of extra Chrome flags.
    875 
    876         """
    877         # Construct DM Server URL flags if not using production server.
    878         env_flag_list = []
    879         if self.env != 'prod':
    880             if self.dms_is_fake:
    881                 # Use URL provided by the fake AutoTest DM server.
    882                 dmserver_str = (DMSERVER % self.fake_dm_server.server_url)
    883             else:
    884                 # Use URL defined in the DMS URL dictionary.
    885                 dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env]))
    886                 if self.env == 'dm-test':
    887                     dmserver_str = (dmserver_str % self.dms_name)
    888 
    889             # Merge with other flags needed by non-prod enviornment.
    890             env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env])
    891 
    892         return env_flag_list
    893 
    894 
    895     def _create_chrome(self,
    896                        enroll=False,
    897                        auto_login=True,
    898                        arc_mode=False,
    899                        init_network_controller=False,
    900                        disable_default_apps=True,
    901                        extension_paths=[],
    902                        extra_chrome_flags=[]):
    903         """
    904         Create a Chrome object. Enroll and/or sign in.
    905 
    906         Function results in self.cr set as the Chrome object.
    907 
    908         @param enroll: enroll the device.
    909         @param auto_login: sign in to chromeos.
    910         @param arc_mode: enable arc mode.
    911         @param extension_paths: list of extensions to install.
    912         @param init_network_controller: whether to init network controller.
    913         @param extra_chrome_flags: list of flags to add.
    914         """
    915         extra_flags = self._initialize_chrome_extra_flags() + extra_chrome_flags
    916 
    917         logging.info('Chrome Browser Arguments:')
    918         logging.info('  extra_browser_args: %s', extra_flags)
    919         logging.info('  username: %s', self.username)
    920         logging.info('  password: %s', self.password)
    921         logging.info('  gaia_login: %s', not self.dms_is_fake)
    922 
    923         if enroll:
    924             self.cr = chrome.Chrome(
    925                     auto_login=False,
    926                     extra_browser_args=extra_flags,
    927                     extension_paths=extension_paths,
    928                     expect_policy_fetch=True)
    929             if self.dms_is_fake:
    930                 if self._kiosk_mode:
    931                     # This try is needed for kiosk; without it the test fails
    932                     # in telemtry code in _WaitForEnterpriseWebview. Webview
    933                     # never loads since it's kiosk.
    934                     # TODO(rzakarian): Try to modify telemetry code to not
    935                     # wait for Webview when in kiosk mode.
    936                     # http://crbug.com/934876.
    937                     try:
    938                         enrollment.EnterpriseFakeEnrollment(
    939                             self.cr.browser, self.username, self.password,
    940                             self.gaia_id, auto_login=auto_login)
    941                     except TimeoutException:
    942                         pass
    943                 else:
    944                     enrollment.EnterpriseFakeEnrollment(
    945                         self.cr.browser, self.username, self.password,
    946                         self.gaia_id, auto_login=auto_login)
    947             else:
    948                 enrollment.EnterpriseEnrollment(
    949                         self.cr.browser, self.username, self.password,
    950                         auto_login=auto_login)
    951 
    952         elif auto_login:
    953             if arc_mode:
    954                 self.cr = chrome.Chrome(extension_paths=extension_paths,
    955                                         username=self.username,
    956                                         password=self.password,
    957                                         arc_mode=arc_mode,
    958                                         disable_gaia_services=False,
    959                                         disable_arc_opt_in=False,
    960                                         enterprise_arc_test=True,
    961                                         extra_browser_args=extra_flags)
    962 
    963             else:
    964                 self.cr = chrome.Chrome(
    965                         extra_browser_args=extra_flags,
    966                         username=self.username,
    967                         password=self.password,
    968                         gaia_login=not self.dms_is_fake,
    969                         disable_gaia_services=self.dms_is_fake,
    970                         autotest_ext=True,
    971                         init_network_controller=init_network_controller,
    972                         expect_policy_fetch=True,
    973                         extension_paths=extension_paths,
    974                         disable_default_apps=disable_default_apps)
    975         else:
    976             self.cr = chrome.Chrome(
    977                     auto_login=False,
    978                     extra_browser_args=extra_flags,
    979                     disable_gaia_services=self.dms_is_fake,
    980                     autotest_ext=True,
    981                     expect_policy_fetch=True)
    982 
    983         # Used by arc.py to determine the state of the chrome obj
    984         self.initialized = True
    985         if auto_login:
    986             if not cryptohome.is_vault_mounted(user=self.username,
    987                                                allow_fail=True):
    988                 raise error.TestError('Expected to find a mounted vault for %s.'
    989                                       % self.username)
    990 
    991 
    992     def navigate_to_url(self, url, tab=None):
    993         """Navigate tab to the specified |url|. Create new tab if none given.
    994 
    995         @param url: URL of web page to load.
    996         @param tab: browser tab to load (if any).
    997         @returns: browser tab loaded with web page.
    998         @raises: telemetry TimeoutException if document ready state times out.
    999         """
   1000         logging.info('Navigating to URL: %r', url)
   1001         if not tab:
   1002             tab = self.cr.browser.tabs.New()
   1003             tab.Activate()
   1004         tab.Navigate(url, timeout=8)
   1005         tab.WaitForDocumentReadyStateToBeComplete()
   1006         return tab
   1007 
   1008 
   1009     def get_elements_from_page(self, tab, cmd):
   1010         """Get collection of page elements that match the |cmd| filter.
   1011 
   1012         @param tab: tab containing the page to be scraped.
   1013         @param cmd: JavaScript command to evaluate on the page.
   1014         @returns object containing elements on page that match the cmd.
   1015         @raises: TestFail if matching elements are not found on the page.
   1016         """
   1017         try:
   1018             elements = tab.EvaluateJavaScript(cmd)
   1019         except Exception as err:
   1020             raise error.TestFail('Unable to find matching elements on '
   1021                                  'the test page: %s\n %r' %(tab.url, err))
   1022         return elements
   1023 
   1024     def log_out_via_keyboard(self):
   1025         """
   1026         Logs out of the device using the keyboard shortcut
   1027 
   1028         """
   1029         _keyboard = keyboard.Keyboard()
   1030         _keyboard.press_key('ctrl+shift+q')
   1031         _keyboard.press_key('ctrl+shift+q')
   1032         _keyboard.close()
   1033 
   1034 def encode_json_string(object_value):
   1035     """Convert given value to JSON format string.
   1036 
   1037     @param object_value: object to be converted.
   1038 
   1039     @returns: string in JSON format.
   1040     """
   1041     return json.dumps(object_value)
   1042 
   1043 
   1044 def decode_json_string(json_string):
   1045     """Convert given JSON format string to an object.
   1046 
   1047     If no object is found, return json_string instead.  This is to allow
   1048     us to "decode" items off the policy page that aren't real JSON.
   1049 
   1050     @param json_string: the JSON string to be decoded.
   1051 
   1052     @returns: Python object represented by json_string or json_string.
   1053     """
   1054     def _decode_list(json_list):
   1055         result = []
   1056         for value in json_list:
   1057             if isinstance(value, unicode):
   1058                 value = value.encode('ascii')
   1059             if isinstance(value, list):
   1060                 value = _decode_list(value)
   1061             if isinstance(value, dict):
   1062                 value = _decode_dict(value)
   1063             result.append(value)
   1064         return result
   1065 
   1066     def _decode_dict(json_dict):
   1067         result = {}
   1068         for key, value in json_dict.iteritems():
   1069             if isinstance(key, unicode):
   1070                 key = key.encode('ascii')
   1071             if isinstance(value, unicode):
   1072                 value = value.encode('ascii')
   1073             elif isinstance(value, list):
   1074                 value = _decode_list(value)
   1075             result[key] = value
   1076         return result
   1077 
   1078     try:
   1079         # Decode JSON turning all unicode strings into ascii.
   1080         # object_hook will get called on all dicts, so also handle lists.
   1081         result = json.loads(json_string, encoding='ascii',
   1082                             object_hook=_decode_dict)
   1083         if isinstance(result, list):
   1084             result = _decode_list(result)
   1085         return result
   1086     except ValueError as e:
   1087         # Input not valid, e.g. '1, 2, "c"' instead of '[1, 2, "c"]'.
   1088         logging.warning('Could not unload: %s (%s)', json_string, e)
   1089         return json_string
   1090