permissions-policy.js (20371B)
1 // Feature test to avoid timeouts 2 function assert_permissions_policy_supported() { 3 assert_true("allow" in HTMLIFrameElement.prototype, 'permissions policy is supported'); 4 } 5 // Tests whether a feature that is enabled/disabled by permissions policy works 6 // as expected. 7 // Arguments: 8 // feature_descriptionOrObject: either and object, containing the following 9 // properties, or a string describing what feature is being tested. 10 // Examples: "usb.GetDevices()", "PaymentRequest()". 11 // test: test created by testharness. Examples: async_test, promise_test. 12 // src: URL where a feature's availability is checked. Examples: 13 // "/permissions-policy/resources/permissions-policy-payment.html", 14 // "/permissions-policy/resources/permissions-policy-usb.html". 15 // expect_feature_available: a callback(data, feature_description) to 16 // verify if a feature is available or unavailable as expected. 17 // The file under the path "src" defines what "data" is sent back via 18 // postMessage with type: 'availability-result'. 19 // Inside the callback, some tests (e.g., EXPECT_EQ, EXPECT_TRUE, etc) 20 // are run accordingly to test a feature's availability. 21 // Example: expect_feature_available_default(data, feature_description). 22 // feature_name: Optional argument, only provided when testing iframe allow 23 // attribute. "feature_name" is the feature name of a policy controlled 24 // feature (https://w3c.github.io/webappsec-permissions-policy/#features). 25 // See examples at: 26 // https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md 27 // allowfullscreen: Optional argument, only used for testing fullscreen 28 // by passing "allowfullscreen". 29 // is_promise_test: Optional argument, true if this call should return a 30 // promise. Used by test_feature_availability_with_post_message_result() 31 function test_feature_availability( 32 feature_descriptionOrObject, test, src, expect_feature_available, feature_name, 33 allowfullscreen, is_promise_test = false, needs_focus = false) { 34 35 if (feature_descriptionOrObject && feature_descriptionOrObject instanceof Object) { 36 const { 37 feature_description, 38 test, 39 src, 40 expect_feature_available, 41 feature_name, 42 allowfullscreen, 43 is_promise_test, 44 needs_focus, 45 } = feature_descriptionOrObject; 46 return test_feature_availability( 47 feature_description, 48 test, 49 src, 50 expect_feature_available, 51 feature_name, 52 allowfullscreen, 53 is_promise_test, 54 needs_focus, 55 ); 56 } 57 58 const feature_description = feature_descriptionOrObject; 59 let frame = document.createElement('iframe'); 60 frame.src = src; 61 62 if (typeof feature_name !== 'undefined') { 63 frame.allow = frame.allow.concat(";" + feature_name); 64 } 65 66 if (typeof allowfullscreen !== 'undefined') { 67 frame.setAttribute(allowfullscreen, true); 68 } 69 70 function expectFeatureAvailable(evt) { 71 if (evt.source === frame.contentWindow && 72 evt.data.type === 'availability-result') { 73 expect_feature_available(evt.data, feature_description); 74 document.body.removeChild(frame); 75 test.done(); 76 } 77 } 78 79 if (!is_promise_test) { 80 window.addEventListener('message', test.step_func(expectFeatureAvailable)); 81 document.body.appendChild(frame); 82 return; 83 } 84 85 const promise = new Promise((resolve) => { 86 window.addEventListener('message', resolve); 87 }).then(expectFeatureAvailable); 88 document.body.appendChild(frame); 89 if (needs_focus) { 90 frame.focus(); 91 } 92 return promise; 93 } 94 95 // Default helper functions to test a feature's availability: 96 function expect_feature_available_default(data, feature_description) { 97 assert_true(data.enabled, feature_description); 98 } 99 100 function expect_feature_unavailable_default(data, feature_description) { 101 assert_false(data.enabled, feature_description); 102 } 103 104 // This is the same as test_feature_availability() but instead of passing in a 105 // function to check the result of the message sent back from an iframe, instead 106 // just compares the result to an expected result passed in. 107 // Arguments: 108 // test: test created by testharness. Examples: async_test, promise_test. 109 // src: the URL to load in an iframe in which to test the feature. 110 // expected_result: the expected value to compare to the data passed back 111 // from the src page by postMessage. 112 // allow_attribute: Optional argument, only provided when an allow 113 // attribute should be specified on the iframe. 114 function test_feature_availability_with_post_message_result( 115 test, src, expected_result, allow_attribute) { 116 const test_result = ({ name, message }, feature_description) => { 117 assert_equals(name, expected_result, message + '.'); 118 }; 119 return test_feature_availability( 120 null, test, src, test_result, allow_attribute, undefined, true); 121 } 122 123 // If this page is intended to test the named feature (according to the URL), 124 // tests the feature availability and posts the result back to the parent. 125 // Otherwise, does nothing. 126 async function test_feature_in_iframe(feature_name, feature_promise_factory) { 127 if (location.hash.endsWith(`#${feature_name}`)) { 128 let message = 'Available'; 129 let name = '#OK'; 130 try { 131 await feature_promise_factory(); 132 } catch (e) { 133 ({ name, message } = e); 134 } 135 window.parent.postMessage( 136 { type: 'availability-result', name, message }, '*'); 137 } 138 } 139 140 // Returns true if the URL for this page indicates that it is embedded in an 141 // iframe. 142 function page_loaded_in_iframe() { 143 return new URLSearchParams(location.search).get('in-iframe'); 144 } 145 146 // Returns a same-origin (relative) URL suitable for embedding in an iframe for 147 // testing the availability of the feature. 148 function same_origin_url(feature_name) { 149 // Add an "in-iframe" query parameter so that we can detect the iframe'd 150 // version of the page and testharness script loading can be disabled in 151 // that version, as required for use of testdriver in non-toplevel browsing 152 // contexts. 153 return location.pathname + '?in-iframe=yes#' + feature_name; 154 } 155 156 // Returns a cross-origin (absolute) URL suitable for embedding in an iframe for 157 // testing the availability of the feature. 158 function cross_origin_url(base_url, feature_name) { 159 return base_url + same_origin_url(feature_name); 160 } 161 162 // This function runs all permissions policy tests for a particular feature that 163 // has a default policy of "self". This includes testing: 164 // 1. Feature usage succeeds by default in the top level frame. 165 // 2. Feature usage succeeds by default in a same-origin iframe. 166 // 3. Feature usage fails by default in a cross-origin iframe. 167 // 4. Feature usage succeeds when an allow attribute is specified on a 168 // cross-origin iframe. 169 // 5. Feature usage fails when an allow attribute is specified on a 170 // same-origin iframe with a value of "feature-name 'none'". 171 // 172 // The same page which called this function will be loaded in the iframe in 173 // order to test feature usage there. When this function is called in that 174 // context it will simply run the feature and return a result back via 175 // postMessage. 176 // 177 // Arguments: 178 // cross_origin: A cross-origin URL base to be used to load the page which 179 // called into this function. 180 // feature_name: The name of the feature as it should be specified in an 181 // allow attribute. 182 // error_name: If feature usage does not succeed, this is the string 183 // representation of the error that will be passed in the rejected 184 // promise. 185 // feature_promise_factory: A function which returns a promise which tests 186 // feature usage. If usage succeeds, the promise should resolve. If it 187 // fails, the promise should reject with an error that can be 188 // represented as a string. 189 function run_all_fp_tests_allow_self( 190 cross_origin, feature_name, error_name, feature_promise_factory) { 191 // This may be the version of the page loaded up in an iframe. If so, just 192 // post the result of running the feature promise back to the parent. 193 if (page_loaded_in_iframe()) { 194 test_feature_in_iframe(feature_name, feature_promise_factory); 195 return; 196 } 197 198 // Run the various tests. 199 // 1. Allowed in top-level frame. 200 promise_test( 201 () => feature_promise_factory(), 202 'Default "' + feature_name + 203 '" permissions policy ["self"] allows the top-level document.'); 204 205 // 2. Allowed in same-origin iframe. 206 const same_origin_frame_pathname = same_origin_url(feature_name); 207 promise_test( 208 t => { 209 return test_feature_availability_with_post_message_result( 210 t, same_origin_frame_pathname, '#OK'); 211 }, 212 'Default "' + feature_name + 213 '" permissions policy ["self"] allows same-origin iframes.'); 214 215 // 3. Blocked in cross-origin iframe. 216 const cross_origin_frame_url = cross_origin_url(cross_origin, feature_name); 217 promise_test( 218 t => { 219 return test_feature_availability_with_post_message_result( 220 t, cross_origin_frame_url, error_name); 221 }, 222 'Default "' + feature_name + 223 '" permissions policy ["self"] disallows cross-origin iframes.'); 224 225 // 4. Allowed in cross-origin iframe with "allow" attribute. 226 promise_test( 227 t => { 228 return test_feature_availability_with_post_message_result( 229 t, cross_origin_frame_url, '#OK', feature_name); 230 }, 231 'permissions policy "' + feature_name + 232 '" can be enabled in cross-origin iframes using "allow" attribute.'); 233 234 // 5. Blocked in same-origin iframe with "allow" attribute set to 'none'. 235 promise_test( 236 t => { 237 return test_feature_availability_with_post_message_result( 238 t, same_origin_frame_pathname, error_name, 239 feature_name + ' \'none\''); 240 }, 241 'permissions policy "' + feature_name + 242 '" can be disabled in same-origin iframes using "allow" attribute.'); 243 } 244 245 // This function runs all permissions policy tests for a particular feature that 246 // has a default policy of "*". This includes testing: 247 // 1. Feature usage succeeds by default in the top level frame. 248 // 2. Feature usage succeeds by default in a same-origin iframe. 249 // 3. Feature usage succeeds by default in a cross-origin iframe. 250 // 4. Feature usage fails when an allow attribute is specified on a 251 // cross-origin iframe with a value of "feature-name 'none'". 252 // 5. Feature usage fails when an allow attribute is specified on a 253 // same-origin iframe with a value of "feature-name 'none'". 254 // 255 // The same page which called this function will be loaded in the iframe in 256 // order to test feature usage there. When this function is called in that 257 // context it will simply run the feature and return a result back via 258 // postMessage. 259 // 260 // Arguments: 261 // cross_origin: A cross-origin URL base to be used to load the page which 262 // called into this function. 263 // feature_name: The name of the feature as it should be specified in an 264 // allow attribute. 265 // error_name: If feature usage does not succeed, this is the string 266 // representation of the error that will be passed in the rejected 267 // promise. 268 // feature_promise_factory: A function which returns a promise which tests 269 // feature usage. If usage succeeds, the promise should resolve. If it 270 // fails, the promise should reject with an error that can be 271 // represented as a string. 272 function run_all_fp_tests_allow_all( 273 cross_origin, feature_name, error_name, feature_promise_factory) { 274 // This may be the version of the page loaded up in an iframe. If so, just 275 // post the result of running the feature promise back to the parent. 276 if (page_loaded_in_iframe()) { 277 test_feature_in_iframe(feature_name, feature_promise_factory); 278 return; 279 } 280 281 // Run the various tests. 282 // 1. Allowed in top-level frame. 283 promise_test( 284 () => feature_promise_factory(), 285 'Default "' + feature_name + 286 '" permissions policy ["*"] allows the top-level document.'); 287 288 // 2. Allowed in same-origin iframe. 289 const same_origin_frame_pathname = same_origin_url(feature_name); 290 promise_test( 291 t => { 292 return test_feature_availability_with_post_message_result( 293 t, same_origin_frame_pathname, '#OK'); 294 }, 295 'Default "' + feature_name + 296 '" permissions policy ["*"] allows same-origin iframes.'); 297 298 // 3. Allowed in cross-origin iframe. 299 const cross_origin_frame_url = cross_origin_url(cross_origin, feature_name); 300 promise_test( 301 t => { 302 return test_feature_availability_with_post_message_result( 303 t, cross_origin_frame_url, '#OK'); 304 }, 305 'Default "' + feature_name + 306 '" permissions policy ["*"] allows cross-origin iframes.'); 307 308 // 4. Blocked in cross-origin iframe with "allow" attribute set to 'none'. 309 promise_test( 310 t => { 311 return test_feature_availability_with_post_message_result( 312 t, cross_origin_frame_url, error_name, feature_name + ' \'none\''); 313 }, 314 'permissions policy "' + feature_name + 315 '" can be disabled in cross-origin iframes using "allow" attribute.'); 316 317 // 5. Blocked in same-origin iframe with "allow" attribute set to 'none'. 318 promise_test( 319 t => { 320 return test_feature_availability_with_post_message_result( 321 t, same_origin_frame_pathname, error_name, 322 feature_name + ' \'none\''); 323 }, 324 'permissions policy "' + feature_name + 325 '" can be disabled in same-origin iframes using "allow" attribute.'); 326 } 327 328 // This function tests that a subframe's document policy allows a given feature. 329 // A feature is allowed in a frame either through inherited policy or specified 330 // by iframe allow attribute. 331 // Arguments: 332 // test: test created by testharness. Examples: async_test, promise_test. 333 // feature: feature name that should be allowed in the frame. 334 // src: the URL to load in the frame. 335 // allow: the allow attribute (container policy) of the iframe 336 function test_allowed_feature_for_subframe(message, feature, src, allow) { 337 let frame = document.createElement('iframe'); 338 if (typeof allow !== 'undefined') { 339 frame.allow = allow; 340 } 341 promise_test(function() { 342 assert_permissions_policy_supported(); 343 frame.src = src; 344 return new Promise(function(resolve, reject) { 345 window.addEventListener('message', function handler(evt) { 346 resolve(evt.data); 347 }, { once: true }); 348 document.body.appendChild(frame); 349 }).then(function(data) { 350 assert_true(data.includes(feature), feature); 351 }); 352 }, message); 353 } 354 355 // This function tests that a subframe's document policy disallows a given 356 // feature. A feature is allowed in a frame either through inherited policy or 357 // specified by iframe allow attribute. 358 // Arguments: 359 // test: test created by testharness. Examples: async_test, promise_test. 360 // feature: feature name that should not be allowed in the frame. 361 // src: the URL to load in the frame. 362 // allow: the allow attribute (container policy) of the iframe 363 function test_disallowed_feature_for_subframe(message, feature, src, allow) { 364 let frame = document.createElement('iframe'); 365 if (typeof allow !== 'undefined') { 366 frame.allow = allow; 367 } 368 promise_test(function() { 369 assert_permissions_policy_supported(); 370 frame.src = src; 371 return new Promise(function(resolve, reject) { 372 window.addEventListener('message', function handler(evt) { 373 resolve(evt.data); 374 }, { once: true }); 375 document.body.appendChild(frame); 376 }).then(function(data) { 377 assert_false(data.includes(feature), feature); 378 }); 379 }, message); 380 } 381 382 // This function tests that a subframe with header policy defined on a given 383 // feature allows and disallows the feature as expected. 384 // Arguments: 385 // feature: feature name. 386 // frame_header_policy: either *, self or \\(\\), defines the frame 387 // document's header policy on |feature|. 388 // '(' and ')' need to be escaped because of server end 389 // header parameter syntax limitation. 390 // src: the URL to load in the frame. 391 // test_expects: contains 6 expected results of either |feature| is allowed 392 // or not inside of a local or remote iframe nested inside 393 // the subframe given the header policy to be either *, 394 // self, or (). 395 // test_name: name of the test. 396 function test_subframe_header_policy( 397 feature, frame_header_policy, src, test_expects, test_name) { 398 let frame = document.createElement('iframe'); 399 promise_test(function() { 400 assert_permissions_policy_supported() 401 frame.src = src + '?pipe=sub|header(Permissions-Policy,' + feature + '=' 402 + frame_header_policy + ')'; 403 return new Promise(function(resolve) { 404 window.addEventListener('message', function handler(evt) { 405 resolve(evt.data); 406 }); 407 document.body.appendChild(frame); 408 }).then(function(results) { 409 for (var j = 0; j < results.length; j++) { 410 var data = results[j]; 411 412 function test_result(message, test_expect) { 413 if (test_expect) { 414 assert_true(data.allowedfeatures.includes(feature), message); 415 } else { 416 assert_false(data.allowedfeatures.includes(feature), message); 417 } 418 } 419 420 if (data.frame === 'local') { 421 if (data.policy === '*') { 422 test_result('local_all:', test_expects.local_all); 423 } 424 if (data.policy === 'self') { 425 test_result('local_self:', test_expects.local_self); 426 } 427 if (data.policy === '\\(\\)') { 428 test_result('local_none:', test_expects.local_none); 429 } 430 } 431 432 if (data.frame === 'remote') { 433 if (data.policy === '*') { 434 test_result('remote_all:', test_expects.remote_all); 435 } 436 if (data.policy === 'self') { 437 test_result('remote_self:', test_expects.remote_self); 438 } 439 if (data.policy === '\\(\\)') { 440 test_result('remote_none:', test_expects.remote_none); 441 } 442 } 443 } 444 }); 445 }, test_name); 446 } 447 448 // This function tests that frame policy allows a given feature correctly. A 449 // feature is allowed in a frame either through inherited policy or specified 450 // by iframe allow attribute. 451 // Arguments: 452 // feature: feature name. 453 // src: the URL to load in the frame. If undefined, the iframe will have a 454 // srcdoc="" attribute 455 // test_expect: boolean value of whether the feature should be allowed. 456 // allow: optional, the allow attribute (container policy) of the iframe. 457 // allowfullscreen: optional, boolean value of allowfullscreen attribute. 458 // sandbox: optional boolean. If true, the frame will be sandboxed (with 459 // allow-scripts, so that tests can run in it.) 460 function test_frame_policy( 461 feature, src, srcdoc, test_expect, allow, allowfullscreen, sandbox) { 462 let frame = document.createElement('iframe'); 463 document.body.appendChild(frame); 464 // frame_policy should be dynamically updated as allow and allowfullscreen is 465 // updated. 466 var frame_policy = frame.featurePolicy; 467 if (typeof allow !== 'undefined') { 468 frame.setAttribute('allow', allow); 469 } 470 if (!!allowfullscreen) { 471 frame.setAttribute('allowfullscreen', true); 472 } 473 if (!!sandbox) { 474 frame.setAttribute('sandbox', 'allow-scripts'); 475 } 476 if (!!src) { 477 frame.src = src; 478 } 479 if (!!srcdoc) { 480 frame.srcdoc = "<h1>Hello world!</h1>"; 481 } 482 if (test_expect) { 483 assert_true(frame_policy.allowedFeatures().includes(feature)); 484 } else { 485 assert_false(frame_policy.allowedFeatures().includes(feature)); 486 } 487 } 488 489 function expect_reports(report_count, policy_name, description) { 490 async_test(t => { 491 var num_received_reports = 0; 492 new ReportingObserver(t.step_func((reports, observer) => { 493 const relevant_reports = reports.filter(r => (r.body.featureId === policy_name)); 494 num_received_reports += relevant_reports.length; 495 if (num_received_reports >= report_count) { 496 t.done(); 497 } 498 }), {types: ['permissions-policy-violation'], buffered: true}).observe(); 499 }, description); 500 }