• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import copy
6import json
7import logging
8import os
9import time
10
11from autotest_lib.client.common_lib.cros import arc
12from autotest_lib.client.common_lib.cros.arc import is_android_container_alive
13
14from autotest_lib.client.bin import test
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.common_lib.cros import chrome
17from autotest_lib.client.common_lib.cros import enrollment
18from autotest_lib.client.common_lib.cros import policy
19from autotest_lib.client.cros import cryptohome
20from autotest_lib.client.cros import httpd
21from autotest_lib.client.cros.input_playback import keyboard
22from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver
23from py_utils import TimeoutException
24
25from telemetry.core import exceptions
26
27CROSQA_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']
36CROSALPHA_FLAGS = [
37    ('--cloud-print-url='
38     'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
39    '--ignore-urlfetcher-cert-requests']
40TESTDMS_FLAGS = [
41    '--ignore-urlfetcher-cert-requests',
42    '--disable-policy-key-verification']
43FLAGS_DICT = {
44    'prod': [],
45    'crosman-qa': CROSQA_FLAGS,
46    'crosman-alpha': CROSALPHA_FLAGS,
47    'dm-test': TESTDMS_FLAGS,
48    'dm-fake': TESTDMS_FLAGS
49}
50DMS_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}
59DMSERVER = '--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.
62USERNAME = 'fake-user@managedchrome.com'
63PASSWORD = 'fakepassword'
64GAIA_ID = 'fake-gaia-id'
65
66# Convert from chrome://policy name to what fake dms expects.
67DEVICE_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
80DEFAULT_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
99class 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@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
1034def 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
1044def 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