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