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 9 10from autotest_lib.client.bin import test 11from autotest_lib.client.bin import utils 12from autotest_lib.client.common_lib import error 13from autotest_lib.client.common_lib.cros import chrome 14from autotest_lib.client.common_lib.cros import enrollment 15from autotest_lib.client.cros import cryptohome 16from autotest_lib.client.cros import httpd 17from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver 18 19CROSQA_FLAGS = [ 20 '--gaia-url=https://gaiastaging.corp.google.com', 21 '--lso-url=https://gaiastaging.corp.google.com', 22 '--google-apis-url=https://www-googleapis-test.sandbox.google.com', 23 '--oauth2-client-id=236834563817.apps.googleusercontent.com', 24 '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI', 25 ('--cloud-print-url=' 26 'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'), 27 '--ignore-urlfetcher-cert-requests'] 28CROSALPHA_FLAGS = [ 29 ('--cloud-print-url=' 30 'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'), 31 '--ignore-urlfetcher-cert-requests'] 32TESTDMS_FLAGS = [ 33 '--ignore-urlfetcher-cert-requests', 34 '--disable-policy-key-verification'] 35FLAGS_DICT = { 36 'prod': [], 37 'crosman-qa': CROSQA_FLAGS, 38 'crosman-alpha': CROSALPHA_FLAGS, 39 'dm-test': TESTDMS_FLAGS, 40 'dm-fake': TESTDMS_FLAGS 41} 42DMS_URL_DICT = { 43 'prod': 'http://m.google.com/devicemanagement/data/api', 44 'crosman-qa': 45 'https://crosman-qa.sandbox.google.com/devicemanagement/data/api', 46 'crosman-alpha': 47 'https://crosman-alpha.sandbox.google.com/devicemanagement/data/api', 48 'dm-test': 'http://chromium-dm-test.appspot.com/d/%s', 49 'dm-fake': 'http://127.0.0.1:%d/' 50} 51DMSERVER = '--device-management-url=%s' 52# Username and password for the fake dm server can be anything, since 53# they are not used to authenticate against GAIA. 54USERNAME = 'fake-user@managedchrome.com' 55PASSWORD = 'fakepassword' 56GAIA_ID = 'fake-gaia-id' 57 58 59class EnterprisePolicyTest(test.test): 60 """Base class for Enterprise Policy Tests.""" 61 62 WEB_PORT = 8080 63 WEB_HOST = 'http://localhost:%d' % WEB_PORT 64 CHROME_POLICY_PAGE = 'chrome://policy' 65 66 def setup(self): 67 """Make the files needed for fake-dms.""" 68 os.chdir(self.srcdir) 69 utils.make() 70 71 72 def initialize(self, **kwargs): 73 """Initialize test parameters.""" 74 self._initialize_enterprise_policy_test(**kwargs) 75 76 77 def _initialize_enterprise_policy_test( 78 self, case='', env='dm-fake', dms_name=None, 79 username=USERNAME, password=PASSWORD, gaia_id=GAIA_ID): 80 """Initialize test parameters and fake DM Server. 81 82 @param case: String name of the test case to run. 83 @param env: String environment of DMS and Gaia servers. 84 @param username: String user name login credential. 85 @param password: String password login credential. 86 @param gaia_id: String gaia_id login credential. 87 @param dms_name: String name of test DM Server. 88 """ 89 self.case = case 90 self.env = env 91 self.username = username 92 self.password = password 93 self.gaia_id = gaia_id 94 self.dms_name = dms_name 95 self.dms_is_fake = (env == 'dm-fake') 96 self._enforce_variable_restrictions() 97 98 # Initialize later variables to prevent error after an early failure. 99 self._web_server = None 100 self.cr = None 101 102 # Start AutoTest DM Server if using local fake server. 103 if self.dms_is_fake: 104 self.fake_dm_server = enterprise_fake_dmserver.FakeDMServer( 105 self.srcdir) 106 self.fake_dm_server.start(self.tmpdir, self.debugdir) 107 108 # Get enterprise directory of shared resources. 109 client_dir = os.path.dirname(os.path.dirname(self.bindir)) 110 self.enterprise_dir = os.path.join(client_dir, 'cros/enterprise') 111 112 # Log the test context parameters. 113 logging.info('Test Context Parameters:') 114 logging.info(' Case: %r', self.case) 115 logging.info(' Environment: %r', self.env) 116 logging.info(' Username: %r', self.username) 117 logging.info(' Password: %r', self.password) 118 logging.info(' Test DMS Name: %r', self.dms_name) 119 120 121 def cleanup(self): 122 """Close out anything used by this test.""" 123 # Clean up AutoTest DM Server if using local fake server. 124 if self.dms_is_fake: 125 self.fake_dm_server.stop() 126 127 # Stop web server if it was started. 128 if self._web_server: 129 self._web_server.stop() 130 131 # Close Chrome instance if opened. 132 if self.cr and self._auto_logout: 133 self.cr.close() 134 135 136 def start_webserver(self): 137 """Set up HTTP Server to serve pages from enterprise directory.""" 138 self._web_server = httpd.HTTPListener( 139 self.WEB_PORT, docroot=self.enterprise_dir) 140 self._web_server.run() 141 142 143 def _enforce_variable_restrictions(self): 144 """Validate class-level test context parameters. 145 146 @raises error.TestError if context parameter has an invalid value, 147 or a combination of parameters have incompatible values. 148 """ 149 # Verify |env| is a valid environment. 150 if self.env not in FLAGS_DICT: 151 raise error.TestError('Environment is invalid: %s' % self.env) 152 153 # Verify test |dms_name| is given iff |env| is 'dm-test'. 154 if self.env == 'dm-test' and not self.dms_name: 155 raise error.TestError('dms_name must be given when using ' 156 'env=dm-test.') 157 if self.env != 'dm-test' and self.dms_name: 158 raise error.TestError('dms_name must not be given when not using ' 159 'env=dm-test.') 160 161 162 def setup_case(self, user_policies={}, suggested_user_policies={}, 163 device_policies={}, skip_policy_value_verification=False, 164 enroll=False, auto_login=True, auto_logout=True, 165 extra_chrome_flags=[], init_network_controller=False): 166 """Set up DMS, log in, and verify policy values. 167 168 If the AutoTest fake DM Server is used, make a JSON policy blob 169 and upload it to the fake DM server. 170 171 Launch Chrome and sign in to Chrome OS. Examine the user's 172 cryptohome vault, to confirm user is signed in successfully. 173 174 @param user_policies: dict of mandatory user policies in 175 name -> value format. 176 @param suggested_user_policies: optional dict of suggested policies 177 in name -> value format. 178 @param device_policies: dict of device policies in 179 name -> value format. 180 @param skip_policy_value_verification: True if setup_case should not 181 verify that the correct policy value shows on policy page. 182 @param enroll: True for enrollment instead of login. 183 @param auto_login: Sign in to chromeos. 184 @param auto_logout: Sign out of chromeos when test is complete. 185 @param extra_chrome_flags: list of flags to add to Chrome. 186 @param init_network_controller: whether to init network controller. 187 188 @raises error.TestError if cryptohome vault is not mounted for user. 189 @raises error.TestFail if |policy_name| and |policy_value| are not 190 shown on the Policies page. 191 """ 192 self._auto_logout = auto_logout 193 194 if self.dms_is_fake: 195 self.fake_dm_server.setup_policy(self._make_json_blob( 196 user_policies, suggested_user_policies, device_policies)) 197 198 self._create_chrome(enroll=enroll, auto_login=auto_login, 199 extra_chrome_flags=extra_chrome_flags, 200 init_network_controller=init_network_controller) 201 202 # Skip policy check upon request or if we enroll but don't log in. 203 skip_policy_value_verification = ( 204 skip_policy_value_verification or not auto_login) 205 if not skip_policy_value_verification: 206 self.verify_policy_stats(user_policies, suggested_user_policies, 207 device_policies) 208 209 210 def _make_json_blob(self, user_policies={}, suggested_user_policies={}, 211 device_policies={}): 212 """Create JSON policy blob from mandatory and suggested policies. 213 214 For the status of a policy to be shown as "Not set" on the 215 chrome://policy page, the policy dictionary must contain no NVP for 216 for that policy. Remove policy NVPs if value is None. 217 218 @param user_policies: mandatory user policies -> values. 219 @param suggested user_policies: suggested user policies -> values. 220 @param device_policies: mandatory device policies -> values. 221 222 @returns: JSON policy blob to send to the fake DM server. 223 """ 224 225 user_p = copy.deepcopy(user_policies) 226 s_user_p = copy.deepcopy(suggested_user_policies) 227 device_p = copy.deepcopy(device_policies) 228 229 # Remove "Not set" policies and json-ify dicts because the 230 # FakeDMServer expects "policy": "{value}" not "policy": {value} 231 # or "policy": ["{value}"] not "policy": [{value}]. 232 for policies_dict in [user_p, s_user_p, device_p]: 233 policies_to_pop = [] 234 for policy in policies_dict: 235 value = policies_dict[policy] 236 if value is None: 237 policies_to_pop.append(policy) 238 elif isinstance(value, dict): 239 policies_dict[policy] = encode_json_string(value) 240 elif isinstance(value, list): 241 if len(value) > 0 and isinstance(value[0], dict): 242 for i in xrange(len(value)): 243 value[i] = encode_json_string(value[i]) 244 policies_dict[policy] = value 245 for policy in policies_to_pop: 246 policies_dict.pop(policy) 247 248 management_dict = { 249 'managed_users': ['*'], 250 'policy_user': self.username, 251 'current_key_index': 0, 252 'invalidation_source': 16, 253 'invalidation_name': 'test_policy' 254 } 255 256 if user_p or s_user_p: 257 user_modes_dict = {} 258 if user_p: 259 user_modes_dict['mandatory'] = user_p 260 if suggested_user_policies: 261 user_modes_dict['recommended'] = s_user_p 262 management_dict['google/chromeos/user'] = user_modes_dict 263 264 if device_p: 265 management_dict['google/chromeos/device'] = device_p 266 267 268 logging.info('Created policy blob: %s', management_dict) 269 return encode_json_string(management_dict) 270 271 272 def _get_policy_stats_shown(self, policy_tab, policy_name): 273 """Get the info shown for |policy_name| from the |policy_tab| page. 274 275 Return a dict of stats for the policy given by |policy_name|, from 276 from the chrome://policy page given by |policy_tab|. 277 278 CAVEAT: the policy page does not display proper JSON. For example, lists 279 are generally shown without the [ ] and cannot be distinguished from 280 strings. This function decodes what it can and returns the string it 281 found when in doubt. 282 283 @param policy_tab: Tab displaying the Policies page. 284 @param policy_name: The name of the policy. 285 286 @returns: A dict of stats, including JSON decode 'value' (see caveat). 287 Also included are 'name', 'status', 'level', 'scope', 288 and 'source'. 289 """ 290 stats = {'name': policy_name} 291 292 row_values = policy_tab.EvaluateJavaScript(''' 293 var section = document.getElementsByClassName( 294 "policy-table-section")[0]; 295 var table = section.getElementsByTagName('table')[0]; 296 rowValues = {}; 297 for (var i = 1, row; row = table.rows[i]; i++) { 298 if (row.className !== 'expanded-value-container') { 299 var name_div = row.getElementsByClassName('name elide')[0]; 300 var name_links = name_div.getElementsByClassName( 301 'name-link'); 302 var name = (name_links.length > 0) ? 303 name_links[0].textContent : name_div.textContent; 304 rowValues['name'] = name; 305 if (name === '%s') { 306 var value_span = row.getElementsByClassName('value')[0]; 307 rowValues['value'] = value_span.textContent; 308 stat_names = ['status', 'level', 'scope', 'source']; 309 stat_names.forEach(function(entry) { 310 var entry_div = row.getElementsByClassName( 311 entry+' elide')[0]; 312 rowValues[entry] = entry_div.textContent; 313 }); 314 break; 315 } 316 } 317 } 318 rowValues; 319 ''' % policy_name) 320 321 logging.debug('Policy %s row: %s', policy_name, row_values) 322 if not row_values or len(row_values) < 6: 323 raise error.TestError( 324 'Could not get policy info for %s!' % policy_name) 325 326 entries = ['value', 'status', 'level', 'scope', 'source'] 327 for v in entries: 328 stats[v] = row_values[v].encode('ascii', 'ignore') 329 330 if stats['status'] == 'Not set.': 331 for v in entries: 332 stats[v] = None 333 else: stats['value'] = decode_json_string(stats['value']) 334 335 return stats 336 337 338 def _get_policy_value_from_new_tab(self, policy_name): 339 """Get the policy value for |policy_name| from the Policies page. 340 341 Information comes from the policy page. A single new tab is opened 342 and then closed to check this info, so device must be logged in. 343 344 @param policy_name: string of policy name. 345 346 @returns: decoded value of the policy as shown on chrome://policy. 347 """ 348 values = self._get_policy_stats_from_new_tab([policy_name]) 349 return values[policy_name]['value'] 350 351 352 def _get_policy_values_from_new_tab(self, policy_names): 353 """Get the policy values of the given policies. 354 355 Information comes from the policy page. A single new tab is opened 356 and then closed to check this info, so device must be logged in. 357 358 @param policy_names: list of strings of policy names. 359 360 @returns: dict of policy name mapped to decoded values of the policy as 361 shown on chrome://policy. 362 """ 363 values = {} 364 tab = self.navigate_to_url(self.CHROME_POLICY_PAGE) 365 for policy_name in policy_names: 366 values[policy_name] = ( 367 self._get_policy_stats_shown(tab, policy_name)['value']) 368 tab.Close() 369 370 return values 371 372 373 def _get_policy_stats_from_new_tab(self, policy_names): 374 """Get policy info about the given policy names. 375 376 Information comes from the policy page. A single new tab is opened 377 and then closed to check this info, so device must be logged in. 378 379 @param policy_name: list of policy names (strings). 380 381 @returns: dict of policy names mapped to dicts containing policy info. 382 Values are decoded JSON. 383 """ 384 stats = {} 385 tab = self.navigate_to_url(self.CHROME_POLICY_PAGE) 386 for policy_name in policy_names: 387 stats[policy_name] = self._get_policy_stats_shown(tab, policy_name) 388 tab.Close() 389 390 return stats 391 392 393 def _compare_values(self, policy_name, expected_value, value_shown): 394 """Pass if an expected value and the chrome://policy version match. 395 396 Handles some of the inconsistencies in the chrome://policy JSON format. 397 398 @raises: error.TestError if policy values do not match. 399 400 """ 401 # If we expect a list and don't have a list, modify the value_shown. 402 if isinstance(expected_value, list): 403 if isinstance(value_shown, str): 404 if '{' in value_shown: # List of dicts. 405 value_shown = decode_json_string('[%s]' % value_shown) 406 elif ',' in value_shown: # List of strs. 407 value_shown = value_shown.split(',') 408 else: # List with one str. 409 value_shown = [value_shown] 410 elif not isinstance(value_shown, list): # List with one element. 411 value_shown = [value_shown] 412 413 if not expected_value == value_shown: 414 raise error.TestError('chrome://policy shows the incorrect value ' 415 'for %s! Expected %s, got %s.' % ( 416 policy_name, expected_value, 417 value_shown)) 418 419 420 def verify_policy_value(self, policy_name, expected_value): 421 """ 422 Verify that the a single policy correctly shows in chrome://policy. 423 424 @param policy_name: the policy we are checking. 425 @param expected_value: the expected value for policy_name. 426 427 @raises error.TestError if value does not match expected. 428 429 """ 430 value_shown = self._get_policy_value_from_new_tab(policy_name) 431 self._compare_values(policy_name, expected_value, value_shown) 432 433 434 def verify_policy_stats(self, user_policies={}, suggested_user_policies={}, 435 device_policies={}): 436 """Verify that the correct policy values show in chrome://policy. 437 438 @param policy_dict: the policies we are checking. 439 440 @raises error.TestError if value does not match expected. 441 """ 442 def _compare_stat(stat, desired, name, stats): 443 """ Raise error if a stat doesn't match.""" 444 err_str = 'Incorrect '+stat+' for '+name+': expected %s, got %s!' 445 shown = stats[name][stat] 446 # If policy is not set, there are no stats to match. 447 if stats[name]['status'] == None: 448 if not shown == None: 449 raise error.TestError(err_str % (None, shown)) 450 else: 451 return 452 if not desired == shown: 453 raise error.TestError(err_str % (desired, shown)) 454 455 keys = (user_policies.keys() + suggested_user_policies.keys() + 456 device_policies.keys()) 457 458 # If no policies were modified from default, return. 459 if len(keys) == 0: 460 return 461 462 stats = self._get_policy_stats_from_new_tab(keys) 463 464 for policy in user_policies: 465 self._compare_values(policy, user_policies[policy], 466 stats[policy]['value']) 467 _compare_stat('level', 'Mandatory', policy, stats) 468 _compare_stat('scope', 'Current user', policy, stats) 469 for policy in suggested_user_policies: 470 self._compare_values(policy, suggested_user_policies[policy], 471 stats[policy]['value']) 472 _compare_stat('level', 'Recommended', policy, stats) 473 _compare_stat('scope', 'Current user', policy, stats) 474 for policy in device_policies: 475 self._compare_values(policy, device_policies[policy], 476 stats[policy]['value']) 477 _compare_stat('level', 'Mandatory', policy, stats) 478 _compare_stat('scope', 'Device', policy, stats) 479 480 481 def _initialize_chrome_extra_flags(self): 482 """ 483 Initialize flags used to create Chrome instance. 484 485 @returns: list of extra Chrome flags. 486 487 """ 488 # Construct DM Server URL flags if not using production server. 489 env_flag_list = [] 490 if self.env != 'prod': 491 if self.dms_is_fake: 492 # Use URL provided by the fake AutoTest DM server. 493 dmserver_str = (DMSERVER % self.fake_dm_server.server_url) 494 else: 495 # Use URL defined in the DMS URL dictionary. 496 dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env])) 497 if self.env == 'dm-test': 498 dmserver_str = (dmserver_str % self.dms_name) 499 500 # Merge with other flags needed by non-prod enviornment. 501 env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env]) 502 503 return env_flag_list 504 505 506 def _create_chrome(self, enroll=False, auto_login=True, 507 extra_chrome_flags=[], init_network_controller=False): 508 """ 509 Create a Chrome object. Enroll and/or sign in. 510 511 Function results in self.cr set as the Chrome object. 512 513 @param enroll: enroll the device. 514 @param auto_login: sign in to chromeos. 515 @param extra_chrome_flags: list of flags to add. 516 @param init_network_controller: whether to init network controller. 517 """ 518 extra_flags = self._initialize_chrome_extra_flags() + extra_chrome_flags 519 520 logging.info('Chrome Browser Arguments:') 521 logging.info(' extra_browser_args: %s', extra_flags) 522 logging.info(' username: %s', self.username) 523 logging.info(' password: %s', self.password) 524 logging.info(' gaia_login: %s', not self.dms_is_fake) 525 526 if enroll: 527 self.cr = chrome.Chrome(auto_login=False, 528 extra_browser_args=extra_flags, 529 expect_policy_fetch=True) 530 if self.dms_is_fake: 531 enrollment.EnterpriseFakeEnrollment( 532 self.cr.browser, self.username, self.password, self.gaia_id, 533 auto_login=auto_login) 534 else: 535 enrollment.EnterpriseEnrollment( 536 self.cr.browser, self.username, self.password, 537 auto_login=auto_login) 538 539 elif auto_login: 540 self.cr = chrome.Chrome(extra_browser_args=extra_flags, 541 username=self.username, 542 password=self.password, 543 gaia_login=not self.dms_is_fake, 544 disable_gaia_services=self.dms_is_fake, 545 autotest_ext=True, 546 init_network_controller=init_network_controller, 547 expect_policy_fetch=True) 548 else: 549 self.cr = chrome.Chrome(auto_login=False, 550 extra_browser_args=extra_flags, 551 disable_gaia_services=self.dms_is_fake, 552 autotest_ext=True, 553 expect_policy_fetch=True) 554 555 if auto_login: 556 if not cryptohome.is_vault_mounted(user=self.username, 557 allow_fail=True): 558 raise error.TestError('Expected to find a mounted vault for %s.' 559 % self.username) 560 561 562 def navigate_to_url(self, url, tab=None): 563 """Navigate tab to the specified |url|. Create new tab if none given. 564 565 @param url: URL of web page to load. 566 @param tab: browser tab to load (if any). 567 @returns: browser tab loaded with web page. 568 @raises: telemetry TimeoutException if document ready state times out. 569 """ 570 logging.info('Navigating to URL: %r', url) 571 if not tab: 572 tab = self.cr.browser.tabs.New() 573 tab.Activate() 574 tab.Navigate(url, timeout=8) 575 tab.WaitForDocumentReadyStateToBeComplete() 576 return tab 577 578 579 def get_elements_from_page(self, tab, cmd): 580 """Get collection of page elements that match the |cmd| filter. 581 582 @param tab: tab containing the page to be scraped. 583 @param cmd: JavaScript command to evaluate on the page. 584 @returns object containing elements on page that match the cmd. 585 @raises: TestFail if matching elements are not found on the page. 586 """ 587 try: 588 elements = tab.EvaluateJavaScript(cmd) 589 except Exception as err: 590 raise error.TestFail('Unable to find matching elements on ' 591 'the test page: %s\n %r' %(tab.url, err)) 592 return elements 593 594 595def encode_json_string(object_value): 596 """Convert given value to JSON format string. 597 598 @param object_value: object to be converted. 599 600 @returns: string in JSON format. 601 """ 602 return json.dumps(object_value) 603 604 605def decode_json_string(json_string): 606 """Convert given JSON format string to an object. 607 608 If no object is found, return json_string instead. This is to allow 609 us to "decode" items off the policy page that aren't real JSON. 610 611 @param json_string: the JSON string to be decoded. 612 613 @returns: Python object represented by json_string or json_string. 614 """ 615 def _decode_list(json_list): 616 result = [] 617 for value in json_list: 618 if isinstance(value, unicode): 619 value = value.encode('ascii') 620 if isinstance(value, list): 621 value = _decode_list(value) 622 if isinstance(value, dict): 623 value = _decode_dict(value) 624 result.append(value) 625 return result 626 627 def _decode_dict(json_dict): 628 result = {} 629 for key, value in json_dict.iteritems(): 630 if isinstance(key, unicode): 631 key = key.encode('ascii') 632 if isinstance(value, unicode): 633 value = value.encode('ascii') 634 elif isinstance(value, list): 635 value = _decode_list(value) 636 result[key] = value 637 return result 638 639 try: 640 # Decode JSON turning all unicode strings into ascii. 641 # object_hook will get called on all dicts, so also handle lists. 642 result = json.loads(json_string, encoding='ascii', 643 object_hook=_decode_dict) 644 if isinstance(result, list): 645 result = _decode_list(result) 646 return result 647 except ValueError as e: 648 # Input not valid, e.g. '1, 2, "c"' instead of '[1, 2, "c"]'. 649 logging.warning('Could not unload: %s (%s)', json_string, e) 650 return json_string 651