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