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 utils 10from autotest_lib.client.common_lib import error 11from autotest_lib.client.common_lib.cros import chrome 12from autotest_lib.client.cros import cryptohome 13from autotest_lib.client.cros import enterprise_base 14from autotest_lib.client.cros import httpd 15 16CROSDEV_FLAGS = [ 17 '--gaia-url=https://gaiastaging.corp.google.com', 18 '--lso-url=https://gaiastaging.corp.google.com', 19 '--google-apis-url=https://www-googleapis-test.sandbox.google.com', 20 '--oauth2-client-id=236834563817.apps.googleusercontent.com', 21 '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI', 22 ('--cloud-print-url=' 23 'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'), 24 '--ignore-urlfetcher-cert-requests'] 25CROSAUTO_FLAGS = [ 26 ('--cloud-print-url=' 27 'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'), 28 '--ignore-urlfetcher-cert-requests'] 29TESTDMS_FLAGS = [ 30 '--ignore-urlfetcher-cert-requests', 31 '--enterprise-enrollment-skip-robot-auth', 32 '--disable-policy-key-verification'] 33FLAGS_DICT = { 34 'prod': [], 35 'cr-dev': CROSDEV_FLAGS, 36 'cr-auto': CROSAUTO_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 'cr-dev': 'https://cros-dev.sandbox.google.com/devicemanagement/data/api', 43 'cr-auto': 'https://cros-auto.sandbox.google.com/devicemanagement/data/api', 44 'dm-test': 'http://chromium-dm-test.appspot.com/d/%s', 45 'dm-fake': 'http://127.0.0.1:%d/' 46} 47DMSERVER = '--device-management-url=%s' 48 49 50class EnterprisePolicyTest(enterprise_base.EnterpriseTest): 51 """Base class for Enterprise Policy Tests.""" 52 53 def setup(self): 54 os.chdir(self.srcdir) 55 utils.make() 56 57 def initialize(self, args=()): 58 self._initialize_test_context(args) 59 60 # Start AutoTest DM Server if using local fake server. 61 if self.env == 'dm-fake': 62 self.import_dmserver(self.srcdir) 63 self.start_dmserver() 64 self._initialize_chrome_extra_flags() 65 self._web_server = None 66 67 def cleanup(self): 68 # Clean up AutoTest DM Server if using local fake server. 69 if self.env == 'dm-fake': 70 super(EnterprisePolicyTest, self).cleanup() 71 72 # Stop web server if it was started. 73 if self._web_server: 74 self._web_server.stop() 75 76 # Close Chrome instance if opened. 77 if self.cr: 78 self.cr.close() 79 80 def start_webserver(self, port): 81 """Set up an HTTP Server to serve pages from localhost. 82 83 @param port: Port used by HTTP server. 84 85 """ 86 if self.mode != 'list': 87 self._web_server = httpd.HTTPListener(port, docroot=self.bindir) 88 self._web_server.run() 89 90 def _initialize_test_context(self, args=()): 91 """Initialize class-level test context parameters. 92 93 @raises error.TestError if an arg is given an invalid value or some 94 combination of args is given incompatible values. 95 96 """ 97 # Extract local parameters from command line args. 98 args_dict = utils.args_to_dict(args) 99 self.mode = args_dict.get('mode', 'all') 100 self.case = args_dict.get('case') 101 self.value = args_dict.get('value') 102 self.env = args_dict.get('env', 'dm-fake') 103 self.username = args_dict.get('username') 104 self.password = args_dict.get('password') 105 self.dms_name = args_dict.get('dms_name') 106 107 # If |mode| is 'list', set |env| to generic 'prod', and blank out 108 # the other key parameters: case, value. 109 if self.mode == 'list': 110 self.env = 'prod' 111 self.case = None 112 self.value = None 113 114 # If |case| is given then set |mode| to 'single'. 115 if self.case: 116 self.mode = 'single' 117 118 # If |mode| is 'all', then |env| must be 'dm-fake', and 119 # the |case| and |value| args must not be given. 120 if self.mode == 'all': 121 if self.env != 'dm-fake': 122 raise error.TestError('env must be "dm-fake" ' 123 'when mode=all.') 124 if self.case: 125 raise error.TestError('case must not be given ' 126 'when mode=all.') 127 if self.value: 128 raise error.TestError('value must not be given ' 129 'when mode=all.') 130 131 # If |value| is given, set |is_value_given| flag to True. If it 132 # was given as 'none', 'null', or '', then set |value| to 'null'. 133 if self.value is not None: 134 self.is_value_given = True 135 if (self.value.lower() == 'none' or 136 self.value.lower() == 'null' or 137 self.value == ''): 138 self.value = 'null' 139 else: 140 self.is_value_given = False 141 142 # Verify |env| is a valid environment. 143 if self.env is not None and self.env not in FLAGS_DICT: 144 raise error.TestError('env=%s is invalid.' % self.env) 145 146 # If |env| is 'dm-fake', ensure value and credentials are not given. 147 if self.env == 'dm-fake': 148 if self.is_value_given: 149 raise error.TestError('value must not be given when using ' 150 'the fake DM Server.') 151 if self.username or self.password: 152 raise error.TestError('user credentials must not be given ' 153 'when using the fake DM Server.') 154 155 # If either credential is not given, set both to default. 156 if self.username is None or self.password is None: 157 self.username = self.USERNAME 158 self.password = self.PASSWORD 159 160 # Verify |case| is given if |mode|==single. 161 if self.mode == 'single' and not self.case: 162 raise error.TestError('case must be given when mode is single.') 163 164 # Verify |case| is given if a |value| is given. 165 if self.is_value_given and self.case is None: 166 raise error.TestError('value must not be given without also ' 167 'giving a test case.') 168 169 # Verify |dms_name| is given iff |env|==dm-test. 170 if self.env == 'dm-test' and not self.dms_name: 171 raise error.TestError('dms_name must be given when using ' 172 'env=dm-test.') 173 if self.env != 'dm-test' and self.dms_name: 174 raise error.TestError('dms_name must not be given when not using ' 175 'env=dm-test.') 176 177 # Log the test context parameters. 178 logging.info('Test Context Parameters:') 179 logging.info(' Run Mode: %r', self.mode) 180 logging.info(' Test Case: %r', self.case) 181 logging.info(' Expected Value: %r', self.value) 182 logging.info(' Environment: %r', self.env) 183 logging.info(' Username: %r', self.username) 184 logging.info(' Password: %r', self.password) 185 logging.info(' Test DMS Name: %r', self.dms_name) 186 187 def _initialize_chrome_extra_flags(self): 188 """Initialize flags used to create Chrome instance.""" 189 # Construct DM Server URL flags. 190 env_flag_list = [] 191 if self.env != 'prod': 192 if self.env == 'dm-fake': 193 # Use URL provided by AutoTest DM server. 194 dmserver_str = (DMSERVER % self.dm_server_url) 195 else: 196 # Use URL defined in DMS URL dictionary. 197 dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env])) 198 if self.env == 'dm-test': 199 dmserver_str = (dmserver_str % self.dms_name) 200 201 # Merge with other flags needed by non-prod enviornment. 202 env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env]) 203 204 self.extra_flags = env_flag_list 205 self.cr = None 206 207 def setup_case(self, policy_name, policy_value, policies_json): 208 """Set up and confirm the preconditions of a test case. 209 210 If the AutoTest fake DM Server is initialized, make a policy blob 211 from |policies_json|, and upload it to the fake server. 212 213 Launch a chrome browser, and sign in to Chrome OS. Examine the user's 214 cryptohome vault, to confirm it signed in successfully. 215 216 Open the Policies page, and confirm that it shows the specified 217 |policy_name| and has the correct |policy_value|. 218 219 @param policy_name: Name of the policy under test. 220 @param policy_value: Expected value to appear on chrome://policy page. 221 @param policies_json: JSON string to set up the fake DMS policy value. 222 223 @raises error.TestError if cryptohome vault is not mounted for user. 224 @raises error.TestFail if |policy_name| and |policy_value| are not 225 shown on the Policies page. 226 227 """ 228 # Set up policy on AutoTest DM Server only if initialized. 229 if self.env == 'dm-fake': 230 self.setup_policy(self._make_json_blob(policies_json)) 231 232 self._launch_chrome_browser() 233 tab = self.navigate_to_url('chrome://policy') 234 if not cryptohome.is_vault_mounted(user=self.username, 235 allow_fail=True): 236 raise error.TestError('Expected to find a mounted vault for %s.' 237 % self.username) 238 value_shown = self._get_policy_value_shown(tab, policy_name) 239 if not self._policy_value_matches_shown(policy_value, value_shown): 240 raise error.TestFail('Policy value shown is not correct: %s ' 241 '(expected: %s)' % 242 (value_shown, policy_value)) 243 tab.Close() 244 245 def _launch_chrome_browser(self): 246 """Launch Chrome browser and sign in.""" 247 logging.info('Chrome Browser Arguments:') 248 logging.info(' extra_browser_args: %s', self.extra_flags) 249 logging.info(' username: %s', self.username) 250 logging.info(' password: %s', self.password) 251 252 self.cr = chrome.Chrome(extra_browser_args=self.extra_flags, 253 username=self.username, 254 password=self.password, 255 gaia_login=True, 256 disable_gaia_services=False, 257 autotest_ext=True) 258 259 def navigate_to_url(self, url, tab=None): 260 """Navigate tab to the specified |url|. Create new tab if none given. 261 262 @param url: URL of web page to load. 263 @param tab: browser tab to load (if any). 264 @returns: browser tab loaded with web page. 265 266 """ 267 logging.info('Navigating to URL: %r', url) 268 if not tab: 269 tab = self.cr.browser.tabs.New() 270 tab.Activate() 271 tab.Navigate(url, timeout=5) 272 tab.WaitForDocumentReadyStateToBeComplete() 273 return tab 274 275 def _policy_value_matches_shown(self, policy_value, value_shown): 276 """Compare |policy_value| to |value_shown| with whitespace removed. 277 278 Compare the expected policy value with the value actually shown on the 279 chrome://policies page. Before comparing, convert both values to JSON 280 formatted strings, and remove all whitespace. Whitespace is removed 281 because Chrome processes some policy values to show them in a more 282 human readable format. 283 284 @param policy_value: Expected value to appear on chrome://policy page. 285 @param value_shown: Value as it appears on chrome://policy page. 286 @param policies_json: JSON string to set up the fake DMS policy value. 287 288 @returns: True if the strings match after removing all whitespace. 289 290 """ 291 # Convert Python None or '' to JSON formatted 'null' string. 292 if value_shown is None or value_shown == '': 293 value_shown = 'null' 294 if policy_value is None or policy_value == '': 295 policy_value = 'null' 296 297 # Remove whitespace. 298 trimmed_value = ''.join(policy_value.split()) 299 trimmed_shown = ''.join(value_shown.split()) 300 logging.info('Trimmed policy value shown: %r (expected: %r)', 301 trimmed_shown, trimmed_value) 302 return trimmed_value == trimmed_shown 303 304 def _make_json_blob(self, policies_json): 305 """Create policy blob from policies JSON object. 306 307 @param policies_json: Policies JSON object (name-value pairs). 308 @returns: Policy blob to be used to setup the policy server. 309 310 """ 311 policies_json = self._move_modeless_to_mandatory(policies_json) 312 policies_json = self._remove_null_policies(policies_json) 313 314 policy_blob = """{ 315 "google/chromeos/user": %s, 316 "managed_users": ["*"], 317 "policy_user": "%s", 318 "current_key_index": 0, 319 "invalidation_source": 16, 320 "invalidation_name": "test_policy" 321 }""" % (json.dumps(policies_json), self.USERNAME) 322 return policy_blob 323 324 def _move_modeless_to_mandatory(self, policies_json): 325 """Add the 'mandatory' mode if a policy's mode was omitted. 326 327 The AutoTest fake DM Server requires that every policy be contained 328 within either a 'mandatory' or 'recommended' dictionary, to indicate 329 the mode of the policy. This function moves modeless policies into 330 the 'mandatory' dictionary. 331 332 @param policies_json: The policy JSON data (name-value pairs). 333 @returns: dict of policies grouped by mode keys. 334 335 """ 336 mandatory_policies = {} 337 recommended_policies = {} 338 collated_json = {} 339 340 # Extract mandatory and recommended mode dicts. 341 if 'mandatory' in policies_json: 342 mandatory_policies = policies_json['mandatory'] 343 del policies_json['mandatory'] 344 if 'recommended' in policies_json: 345 recommended_policies = policies_json['recommended'] 346 del policies_json['recommended'] 347 348 # Move any remaining modeless policies into mandatory dict. 349 if policies_json: 350 mandatory_policies.update(policies_json) 351 352 # Collate all policies into mandatory & recommended dicts. 353 if recommended_policies: 354 collated_json.update({'recommended': recommended_policies}) 355 if mandatory_policies: 356 collated_json.update({'mandatory': mandatory_policies}) 357 358 return collated_json 359 360 def _remove_null_policies(self, policies_json): 361 """Remove policy dict data that is set to None or ''. 362 363 For the status of a policy to be shown as "Not set" on the 364 chrome://policy page, the policy blob must contain no dictionary entry 365 for that policy. This function removes policy NVPs from a copy of the 366 |policies_json| dictionary that the test case had set to None or ''. 367 368 @param policies_json: setup policy JSON data (name-value pairs). 369 @returns: setup policy JSON data with all 'Not set' policies removed. 370 371 """ 372 policies_json_copy = policies_json.copy() 373 for policies in policies_json_copy.values(): 374 for policy_data in policies.items(): 375 if policy_data[1] is None or policy_data[1] == '': 376 policies.pop(policy_data[0]) 377 return policies_json_copy 378 379 def _get_policy_value_shown(self, policy_tab, policy_name): 380 """Get the value shown for the named policy on the Policies page. 381 382 Takes |policy_name| as a parameter and returns the corresponding 383 policy value shown on the chrome://policy page. 384 385 @param policy_tab: Tab displaying the chrome://policy page. 386 @param policy_name: The name of the policy. 387 @returns: The value shown for the policy on the Policies page. 388 389 """ 390 row_values = policy_tab.EvaluateJavaScript(''' 391 var section = document.getElementsByClassName("policy-table-section")[0]; 392 var table = section.getElementsByTagName('table')[0]; 393 rowValues = ''; 394 for (var i = 1, row; row = table.rows[i]; i++) { 395 if (row.className !== 'expanded-value-container') { 396 var name_div = row.getElementsByClassName('name elide')[0]; 397 var name = name_div.textContent; 398 if (name === '%s') { 399 var value_span = row.getElementsByClassName('value')[0]; 400 var value = value_span.textContent; 401 var status_div = row.getElementsByClassName('status elide')[0]; 402 var status = status_div.textContent; 403 rowValues = [name, value, status]; 404 break; 405 } 406 } 407 } 408 rowValues; 409 ''' % policy_name) 410 411 value_shown = row_values[1].encode('ascii', 'ignore') 412 status_shown = row_values[2].encode('ascii', 'ignore') 413 if status_shown == 'Not set.': 414 return None 415 return value_shown 416 417 def get_elements_from_page(self, tab, cmd): 418 """Get collection of page elements that match the |cmd| filter. 419 420 @param tab: tab containing the page to be scraped. 421 @param cmd: JavaScript command to evaluate on the page. 422 @returns object containing elements on page that match the cmd. 423 @raises: TestFail if matching elements are not found on the page. 424 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 def json_string(self, policy_value): 434 """Convert policy value to a JSON formatted string. 435 436 @param policy_value: object containing a policy value. 437 @returns: string in JSON format. 438 """ 439 return json.dumps(policy_value) 440 441 def _validate_and_run_test_case(self, test_case, run_test): 442 """Validate test case and call the test runner in the test class. 443 444 @param test_case: name of the test case to run. 445 @param run_test: method in test class that runs a test case. 446 447 """ 448 if test_case not in self.TEST_CASES: 449 raise error.TestError('Test case is not valid: %s' % test_case) 450 logging.info('Running test case: %s', test_case) 451 run_test(test_case) 452 453 def run_once_impl(self, run_test): 454 """Dispatch the common run modes for all child test classes. 455 456 @param run_test: method in test class that runs a test case. 457 458 """ 459 if self.mode == 'all': 460 for test_case in sorted(self.TEST_CASES): 461 self._validate_and_run_test_case(test_case, run_test) 462 elif self.mode == 'single': 463 self._validate_and_run_test_case(self.case, run_test) 464 elif self.mode == 'list': 465 logging.info('List Test Cases:') 466 for test_case, value in sorted(self.TEST_CASES.items()): 467 logging.info(' case=%s, value="%s"', test_case, value) 468 else: 469 raise error.TestError('Run mode is not valid: %s' % self.mode) 470 471 def run_once(self): 472 # The run_once() method is core to all autotest tests. We define it 473 # herein to support tests that do not define their own override. 474 self.run_once_impl(self.run_test_case) 475