Add permissionsPolicy http header

This commit is contained in:
Christophe Gilles 2020-12-04 23:00:09 +01:00 committed by Eleftheria Stein-Kousathana
parent 48ef27b80a
commit 54d3839f63
21 changed files with 1015 additions and 3 deletions

View File

@ -34,6 +34,7 @@ import org.springframework.security.web.header.writers.ContentSecurityPolicyHead
import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter;
import org.springframework.security.web.header.writers.HpkpHeaderWriter;
import org.springframework.security.web.header.writers.HstsHeaderWriter;
import org.springframework.security.web.header.writers.PermissionsPolicyHeaderWriter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter;
@ -93,6 +94,8 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
private final FeaturePolicyConfig featurePolicy = new FeaturePolicyConfig();
private final PermissionsPolicyConfig permissionsPolicy = new PermissionsPolicyConfig();
/**
* Creates a new instance
*
@ -387,6 +390,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
addIfNotNull(writers, this.contentSecurityPolicy.writer);
addIfNotNull(writers, this.referrerPolicy.writer);
addIfNotNull(writers, this.featurePolicy.writer);
addIfNotNull(writers, this.permissionsPolicy.writer);
writers.addAll(this.headerWriters);
return writers;
}
@ -487,12 +491,58 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
* @throws IllegalArgumentException if policyDirectives is {@code null} or empty
* @since 5.1
* @see FeaturePolicyHeaderWriter
* @deprecated Use {@link #permissionsPolicy(Customizer)} instead.
*/
@Deprecated
public FeaturePolicyConfig featurePolicy(String policyDirectives) {
this.featurePolicy.writer = new FeaturePolicyHeaderWriter(policyDirectives);
return this.featurePolicy;
}
/**
* <p>
* Allows configuration for
* <a href="https://w3c.github.io/webappsec-permissions-policy/">Permissions
* Policy</a>.
* </p>
*
* <p>
* Configuration is provided to the {@link PermissionsPolicyHeaderWriter} which
* support the writing of the header as detailed in the W3C Technical Report:
* </p>
* <ul>
* <li>Permissions-Policy</li>
* </ul>
* @return the {@link PermissionsPolicyConfig} for additional configuration
* @since 5.5
* @see PermissionsPolicyHeaderWriter
*/
public PermissionsPolicyConfig permissionsPolicy() {
this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter();
return this.permissionsPolicy;
}
/**
* Allows configuration for
* <a href="https://w3c.github.io/webappsec-permissions-policy/"> Permissions
* Policy</a>.
* <p>
* Calling this method automatically enables (includes) the {@code Permissions-Policy}
* header in the response using the supplied policy directive(s).
* <p>
* Configuration is provided to the {@link PermissionsPolicyHeaderWriter} which is
* responsible for writing the header.
* @return the {@link PermissionsPolicyConfig} for additional configuration
* @throws IllegalArgumentException if policyDirectives is {@code null} or empty
* @since 5.5
* @see PermissionsPolicyHeaderWriter
*/
public PermissionsPolicyConfig permissionsPolicy(Customizer<PermissionsPolicyConfig> permissionsPolicyCustomizer) {
this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter();
permissionsPolicyCustomizer.customize(this.permissionsPolicy);
return this.permissionsPolicy;
}
public final class ContentTypeOptionsConfig {
private XContentTypeOptionsHeaderWriter writer;
@ -1063,4 +1113,33 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
}
public final class PermissionsPolicyConfig {
private PermissionsPolicyHeaderWriter writer;
private PermissionsPolicyConfig() {
}
/**
* Sets the policy to be used in the response header.
* @param policy a permissions policy
* @return the {@link PermissionsPolicyConfig} for additional configuration
* @throws IllegalArgumentException if policy is null
*/
public PermissionsPolicyConfig policy(String policy) {
this.writer.setPolicy(policy);
return this;
}
/**
* Allows completing configuration of Permissions Policy and continuing
* configuration of headers.
* @return the {@link HeadersConfigurer} for additional configuration
*/
public HeadersConfigurer<H> and() {
return HeadersConfigurer.this;
}
}
}

View File

@ -39,6 +39,7 @@ import org.springframework.security.web.header.writers.ContentSecurityPolicyHead
import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter;
import org.springframework.security.web.header.writers.HpkpHeaderWriter;
import org.springframework.security.web.header.writers.HstsHeaderWriter;
import org.springframework.security.web.header.writers.PermissionsPolicyHeaderWriter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
@ -119,6 +120,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
private static final String FEATURE_POLICY_ELEMENT = "feature-policy";
private static final String PERMISSIONS_POLICY_ELEMENT = "permissions-policy";
private static final String ALLOW_FROM = "ALLOW-FROM";
private ManagedList<BeanMetadataElement> headerWriters;
@ -140,6 +143,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
parseContentSecurityPolicyElement(disabled, element, parserContext);
parseReferrerPolicyElement(element, parserContext);
parseFeaturePolicyElement(element, parserContext);
parsePermissionsPolicyElement(element, parserContext);
parseHeaderElements(element);
boolean noWriters = this.headerWriters.isEmpty();
if (disabled && !noWriters) {
@ -351,6 +355,27 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
this.headerWriters.add(headersWriter.getBeanDefinition());
}
private void parsePermissionsPolicyElement(Element element, ParserContext context) {
Element permissionsPolicyElement = (element != null)
? DomUtils.getChildElementByTagName(element, PERMISSIONS_POLICY_ELEMENT) : null;
if (permissionsPolicyElement != null) {
addPermissionsPolicy(permissionsPolicyElement, context);
}
}
private void addPermissionsPolicy(Element permissionsPolicyElement, ParserContext context) {
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder
.genericBeanDefinition(PermissionsPolicyHeaderWriter.class);
String policyDirectives = permissionsPolicyElement.getAttribute(ATT_POLICY);
if (!StringUtils.hasText(policyDirectives)) {
context.getReaderContext().error(ATT_POLICY + " requires a 'value' to be set.", permissionsPolicyElement);
}
else {
headersWriter.addConstructorArgValue(policyDirectives);
}
this.headerWriters.add(headersWriter.getBeanDefinition());
}
private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) {
context.getReaderContext().error("Only one of '" + attrName + "' or '" + otherAttrName + "' can be set.",
element);

View File

@ -149,6 +149,7 @@ import org.springframework.security.web.server.header.ContentSecurityPolicyServe
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter;
import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter;
import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;
import org.springframework.security.web.server.header.ServerHttpHeadersWriter;
@ -2232,13 +2233,16 @@ public class ServerHttpSecurity {
private FeaturePolicyServerHttpHeadersWriter featurePolicy = new FeaturePolicyServerHttpHeadersWriter();
private PermissionsPolicyServerHttpHeadersWriter permissionsPolicy = new PermissionsPolicyServerHttpHeadersWriter();
private ContentSecurityPolicyServerHttpHeadersWriter contentSecurityPolicy = new ContentSecurityPolicyServerHttpHeadersWriter();
private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter();
private HeaderSpec() {
this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts,
this.frameOptions, this.xss, this.featurePolicy, this.contentSecurityPolicy, this.referrerPolicy));
this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy,
this.referrerPolicy));
}
/**
@ -2395,13 +2399,32 @@ public class ServerHttpSecurity {
/**
* Configures {@code Feature-Policy} response header.
* @param policyDirectives the policy directive(s)
* @param policyDirectives the policy
* @return the {@link FeaturePolicySpec} to configure
*/
public FeaturePolicySpec featurePolicy(String policyDirectives) {
return new FeaturePolicySpec(policyDirectives);
}
/**
* Configures {@code Permissions-Policy} response header.
* @return the {@link PermissionsPolicySpec} to configure
*/
public PermissionsPolicySpec permissionsPolicy() {
return new PermissionsPolicySpec();
}
/**
* Configures {@code Permissions-Policy} response header.
* @param permissionsPolicyCustomizer the {@link Customizer} to provide more
* options for the {@link PermissionsPolicySpec}
* @return the {@link HeaderSpec} to customize
*/
public HeaderSpec permissionsPolicy(Customizer<PermissionsPolicySpec> permissionsPolicyCustomizer) {
permissionsPolicyCustomizer.customize(new PermissionsPolicySpec());
return this;
}
/**
* Configures {@code Referrer-Policy} response header.
* @param referrerPolicy the policy to use
@ -2677,6 +2700,38 @@ public class ServerHttpSecurity {
}
/**
* Configures {@code Permissions-Policy} response header.
*
* @since 5.5
* @see #permissionsPolicy()
*/
public final class PermissionsPolicySpec {
private PermissionsPolicySpec() {
}
/**
* Sets the policy to be used in the response header.
* @param policy a permissions policy
* @return the {@link PermissionsPolicySpec} to continue configuring
*/
public PermissionsPolicySpec policy(String policy) {
HeaderSpec.this.permissionsPolicy.setPolicy(policy);
return this;
}
/**
* Allows method chaining to continue configuring the
* {@link ServerHttpSecurity}.
* @return the {@link HeaderSpec} to continue configuring
*/
public HeaderSpec and() {
return HeaderSpec.this;
}
}
/**
* Configures {@code Referrer-Policy} response header.
*

View File

@ -34,6 +34,7 @@ class ServerHeadersDsl {
private var contentSecurityPolicy: ((ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit)? = null
private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null
private var featurePolicyDirectives: String? = null
private var permissionsPolicy: ((ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit)? = null
private var disabled = false
@ -140,6 +141,21 @@ class ServerHeadersDsl {
this.featurePolicyDirectives = policyDirectives
}
/**
* Allows configuration for <a href="https://w3c.github.io/webappsec-permissions-policy/">Permissions
* Policy</a>.
*
* <p>
* Calling this method automatically enables (includes) the Permissions-Policy
* header in the response using the supplied policy directive(s).
* <p>
*
* @param permissionsPolicyConfig the customization to apply to the header
*/
fun permissionsPolicy(permissionsPolicyConfig: ServerPermissionsPolicyDsl.() -> Unit) {
this.permissionsPolicy = ServerPermissionsPolicyDsl().apply(permissionsPolicyConfig).get()
}
/**
* Disables HTTP response headers.
*/
@ -170,6 +186,9 @@ class ServerHeadersDsl {
featurePolicyDirectives?.also {
headers.featurePolicy(featurePolicyDirectives)
}
permissionsPolicy?.also {
headers.permissionsPolicy(permissionsPolicy)
}
referrerPolicy?.also {
headers.referrerPolicy(referrerPolicy)
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter
/**
* A Kotlin DSL to configure the [ServerHttpSecurity] permissions policy header using
* idiomatic Kotlin code.
*
* @author Christophe Gilles
* @since 5.5
* @property policy the policy to be used in the response header.
*/
@ServerSecurityMarker
class ServerPermissionsPolicyDsl {
var policy: String? = null
internal fun get(): (ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit {
return { permissionsPolicy ->
policy?.also {
permissionsPolicy.policy(policy)
}
}
}
}

View File

@ -41,6 +41,7 @@ class HeadersDsl {
private var contentSecurityPolicy: ((HeadersConfigurer<HttpSecurity>.ContentSecurityPolicyConfig) -> Unit)? = null
private var referrerPolicy: ((HeadersConfigurer<HttpSecurity>.ReferrerPolicyConfig) -> Unit)? = null
private var featurePolicyDirectives: String? = null
private var permissionsPolicy: ((HeadersConfigurer<HttpSecurity>.PermissionsPolicyConfig) -> Unit)? = null
private var disabled = false
private var headerWriters = mutableListOf<HeaderWriter>()
@ -164,6 +165,21 @@ class HeadersDsl {
this.featurePolicyDirectives = policyDirectives
}
/**
* Allows configuration for <a href="https://w3c.github.io/webappsec-permissions-policy/">Permissions
* Policy</a>.
*
* <p>
* Calling this method automatically enables (includes) the Permissions-Policy
* header in the response using the supplied policy directive(s).
* <p>
*
* @param policyDirectives policyDirectives the security policy directive(s)
*/
fun permissionsPolicy(permissionsPolicyConfig: PermissionsPolicyDsl.() -> Unit) {
this.permissionsPolicy = PermissionsPolicyDsl().apply(permissionsPolicyConfig).get()
}
/**
* Adds a [HeaderWriter] instance.
*
@ -217,6 +233,9 @@ class HeadersDsl {
featurePolicyDirectives?.also {
headers.featurePolicy(featurePolicyDirectives)
}
permissionsPolicy?.also {
headers.permissionsPolicy(permissionsPolicy)
}
headerWriters.forEach { headerWriter ->
headers.addHeaderWriter(headerWriter)
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.servlet.headers
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
/**
* A Kotlin DSL to configure the [HttpSecurity] permissions policy header using
* idiomatic Kotlin code.
*
* @author Christophe Gilles
* @since 5.5
* @property policy the policy to be used in the response header.
*/
@HeadersSecurityMarker
class PermissionsPolicyDsl {
var policy: String? = null
internal fun get(): (HeadersConfigurer<HttpSecurity>.PermissionsPolicyConfig) -> Unit {
return { permissionsPolicy ->
policy?.also {
permissionsPolicy.policy(policy)
}
}
}
}

View File

@ -919,7 +919,7 @@ csrf-options.attlist &=
headers =
## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.
element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & header*)}
element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & header*)}
headers-options.attlist &=
## Specifies if the default headers should be disabled. Default false.
attribute defaults-disabled {xsd:token}?
@ -1007,6 +1007,13 @@ feature-options.attlist &=
## The security policy directive(s) for the Feature-Policy header.
attribute policy-directives {xsd:token}?
permissions-policy =
## Adds support for Permissions Policy
element permissions-policy {permissions-options.attlist}
permissions-options.attlist &=
## The policies for the Permissions-Policy header.
attribute policy {xsd:token}?
cache-control =
## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request
element cache-control {cache-control.attlist}

View File

@ -2692,6 +2692,7 @@
<xs:element ref="security:content-security-policy"/>
<xs:element ref="security:referrer-policy"/>
<xs:element ref="security:feature-policy"/>
<xs:element ref="security:permissions-policy"/>
<xs:element ref="security:header"/>
</xs:choice>
<xs:attributeGroup ref="security:headers-options.attlist"/>
@ -2926,6 +2927,23 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="permissions-policy">
<xs:annotation>
<xs:documentation>Adds support for Permissions Policy
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:permissions-options.attlist"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="permissions-options.attlist">
<xs:attribute name="policy" type="xs:token">
<xs:annotation>
<xs:documentation>The policies for the Permissions-Policy header.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="cache-control">
<xs:annotation>
<xs:documentation>Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for

View File

@ -447,6 +447,44 @@ public class HeadersConfigurerTests {
.withRootCauseInstanceOf(IllegalArgumentException.class);
}
@Test
public void getWhenPermissionsPolicyConfiguredThenPermissionsPolicyHeaderInResponse() throws Exception {
this.spring.register(PermissionsPolicyConfig.class).autowire();
ResultMatcher permissionsPolicy = header().string("Permissions-Policy", "geolocation=(self)");
// @formatter:off
MvcResult mvcResult = this.mvc.perform(get("/").secure(true))
.andExpect(permissionsPolicy)
.andReturn();
// @formatter:on
assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Permissions-Policy");
}
@Test
public void getWhenPermissionsPolicyConfiguredWithStringThenPermissionsPolicyHeaderInResponse() throws Exception {
this.spring.register(PermissionsPolicyStringConfig.class).autowire();
ResultMatcher permissionsPolicy = header().string("Permissions-Policy", "geolocation=(self)");
// @formatter:off
MvcResult mvcResult = this.mvc.perform(get("/").secure(true))
.andExpect(permissionsPolicy)
.andReturn();
// @formatter:on
assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Permissions-Policy");
}
@Test
public void configureWhenPermissionsPolicyEmptyThenException() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(PermissionsPolicyInvalidConfig.class).autowire())
.withRootCauseInstanceOf(IllegalArgumentException.class);
}
@Test
public void configureWhenPermissionsPolicyStringEmptyThenException() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(PermissionsPolicyInvalidStringConfig.class).autowire())
.withRootCauseInstanceOf(IllegalArgumentException.class);
}
@Test
public void getWhenHstsConfiguredWithPreloadThenStrictTransportSecurityHeaderWithPreloadInResponse()
throws Exception {
@ -1012,6 +1050,68 @@ public class HeadersConfigurerTests {
}
@EnableWebSecurity
static class PermissionsPolicyConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.headers()
.defaultsDisabled()
.permissionsPolicy((permissionsPolicy) -> permissionsPolicy.policy("geolocation=(self)"));
// @formatter:on
}
}
@EnableWebSecurity
static class PermissionsPolicyStringConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.headers()
.defaultsDisabled()
.permissionsPolicy()
.policy("geolocation=(self)");
// @formatter:on
}
}
@EnableWebSecurity
static class PermissionsPolicyInvalidConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.headers()
.defaultsDisabled()
.permissionsPolicy((permissionsPolicy) -> permissionsPolicy.policy(null));
// @formatter:on
}
}
@EnableWebSecurity
static class PermissionsPolicyInvalidStringConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.headers()
.defaultsDisabled()
.permissionsPolicy()
.policy("");
// @formatter:on
}
}
@EnableWebSecurity
static class HstsWithPreloadConfig extends WebSecurityConfigurerAdapter {

View File

@ -332,6 +332,17 @@ public class HttpHeadersConfigTests {
() -> this.spring.configLocations(this.xml("DefaultsDisabledWithOnlyHeaderValue")).autowire());
}
@Test
public void requestWhenPermissionsPolicyConfiguredWithGeolocationSelfThenGeolocationSelf() throws Exception {
this.spring.configLocations(this.xml("DefaultsDisabledWithPermissionsPolicy")).autowire();
// @formatter:off
this.mvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(excludesDefaults())
.andExpect(header().string("Permissions-Policy", "geolocation=(self)"));
// @formatter:on
}
@Test
public void requestWhenUsingXssProtectionThenDefaultsToModeBlock() throws Exception {
Set<String> excludedHeaders = new HashSet<>(defaultHeaders.keySet());

View File

@ -0,0 +1,97 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerPermissionsPolicyDsl]
*
* @author Christophe Gilles
*/
class ServerPermissionsPolicyDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when permissions policy configured then permissions policy header in response`() {
this.spring.register(PermissionsPolicyConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().doesNotExist("Permissions-Policy")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PermissionsPolicyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
permissionsPolicy { }
}
}
}
}
@Test
fun `request when custom policy configured then custom policy in response header`() {
this.spring.register(CustomPolicyConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals("Permissions-Policy", "geolocation=(self)")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomPolicyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
permissionsPolicy {
policy = "geolocation=(self)"
}
}
}
}
}
}

View File

@ -24,6 +24,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.servlet.headers.PermissionsPolicyDsl
import org.springframework.security.web.header.writers.StaticHeadersWriter
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
@ -93,6 +94,29 @@ class HeadersDslTests {
}
}
@Test
fun `headers when permissions policy configured then header in response`() {
this.spring.register(PermissionsPolicyConfig::class.java).autowire()
this.mockMvc.get("/")
.andExpect {
header { string("Permissions-Policy", "geolocation=(self)") }
}
}
@EnableWebSecurity
open class PermissionsPolicyConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
headers {
permissionsPolicy {
policy = "geolocation=(self)"
}
}
}
}
}
@Test
fun `request when headers disabled then no security headers are in the response`() {
this.spring.register(HeadersDisabledConfig::class.java).autowire()

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2020 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<http auto-config="true">
<headers defaults-disabled="true">
<permissions-policy policy="geolocation=(self)"/>
</headers>
</http>
<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
<b:import resource="userservice.xml"/>
</b:beans>

View File

@ -339,6 +339,28 @@ With Feature Policy, developers can opt-in to a set of "policies" for the browse
These policies restrict what APIs the site can access or modify the browser's default behavior for certain features.
[[headers-permissions]]
== Permissions Policy
[NOTE]
====
Refer to the relevant sections to see how to configure both <<servlet-headers-permissions,servlet>> and <<webflux-headers-permissions,webflux>> based applications.
====
https://w3c.github.io/webappsec-permissions-policy/[Permissions Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser.
.Permissions Policy Example
====
[source]
----
Permissions-Policy: geolocation=(self)
----
====
With Permissions Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site.
These policies restrict what APIs the site can access or modify the browser's default behavior for certain features.
[[headers-clear-site-data]]
== Clear Site Data

View File

@ -472,6 +472,58 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
====
[[webflux-headers-permissions]]
== Permissions Policy
Spring Security does not add <<headers-permissions,Permissions Policy>> headers by default.
The following `Permissions-Policy` header:
.Permissions-Policy Example
====
[source]
----
Permissions-Policy: geolocation=(self)
----
====
You can enable the Permissions Policy header as shown below:
.Permissions-Policy Configuration
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers -> headers
.permissionsPolicy(permissions -> permissions
.policy("geolocation=(self)")
)
);
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
headers {
permissionsPolicy {
policy = "geolocation=(self)"
}
}
}
}
----
====
[[webflux-headers-clear-site-data]]
== Clear Site Data

View File

@ -816,6 +816,76 @@ class SecurityConfig : WebSecurityConfigurerAdapter() {
----
====
[[servlet-headers-permissions]]
== Permissions Policy
Spring Security does not add <<headers-permissions,Permissions Policy>> headers by default.
The following `Permissions-Policy` header:
.Permissions-Policy Example
====
[source]
----
Permissions-Policy: geolocation=(self)
----
====
can enable the Permissions Policy header using the configuration shown below:
.Permissions-Policy
====
.Java
[source,java,role="primary"]
----
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.permissionsPolicy(permissions -> permissions
.policy("geolocation=(self)")
)
);
}
}
----
.XML
[source,xml,role="secondary"]
----
<http>
<!-- ... -->
<headers>
<permissions-policy policy="geolocation=(self)" />
</headers>
</http>
----
.Kotlin
[source,kotlin,role="secondary"]
----
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
// ...
headers {
permissionPolicy {
policy = "geolocation=(self)"
}
}
}
}
}
----
====
[[servlet-headers-clear-site-data]]
== Clear Site Data

View File

@ -0,0 +1,82 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.header.writers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.util.Assert;
/**
* Provides support for
* <a href="https://w3c.github.io/webappsec-permissions-policy//">Permisisons Policy</a>.
* <p>
* Permissions Policy allows web developers to selectively enable, disable, and modify the
* behavior of certain APIs and web features in the browser.
* <p>
* A declaration of a permissions policy contains a set of security policies, each
* responsible for declaring the restrictions for a particular feature type.
*
* @author Christophe Gilles
* @since 5.5
*/
public final class PermissionsPolicyHeaderWriter implements HeaderWriter {
private static final String PERMISSIONS_POLICY_HEADER = "Permissions-Policy";
private String policy;
/**
* Create a new instance of {@link PermissionsPolicyHeaderWriter}.
*/
public PermissionsPolicyHeaderWriter() {
}
/**
* Create a new instance of {@link PermissionsPolicyHeaderWriter} with supplied
* security policy.
* @param policy the security policy
* @throws IllegalArgumentException if policy is {@code null} or empty
*/
public PermissionsPolicyHeaderWriter(String policy) {
setPolicy(policy);
}
/**
* Sets the policy to be used in the response header.
* @param policy a permissions policy
* @throws IllegalArgumentException if policy is null
*/
public void setPolicy(String policy) {
Assert.hasLength(policy, "policy can not be null or empty");
this.policy = policy;
}
@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (!response.containsHeader(PERMISSIONS_POLICY_HEADER)) {
response.setHeader(PERMISSIONS_POLICY_HEADER, this.policy);
}
}
@Override
public String toString() {
return getClass().getName() + " [policy=" + this.policy + "]";
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.server.header;
import reactor.core.publisher.Mono;
import org.springframework.security.web.server.header.StaticServerHttpHeadersWriter.Builder;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* Writes the {@code Permissions-Policy} response header with configured policy
* directives.
*
* @author Christophe Gilles
* @since 5.5
*/
public final class PermissionsPolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter {
public static final String PERMISSIONS_POLICY = "Permissions-Policy";
private ServerHttpHeadersWriter delegate;
@Override
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty();
}
private static ServerHttpHeadersWriter createDelegate(String policyDirectives) {
Builder builder = StaticServerHttpHeadersWriter.builder();
builder.header(PERMISSIONS_POLICY, policyDirectives);
return builder.build();
}
/**
* Set the policy to be used in the response header.
* @param policy the policy
* @throws IllegalArgumentException if policy is {@code null}
*/
public void setPolicy(String policy) {
Assert.notNull(policy, "policy must not be null");
this.delegate = createDelegate(policy);
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.header.writers;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link PermissionsPolicyHeaderWriter}.
*
* @author Christophe Gilles
*/
public class PermissionsPolicyHeaderWriterTests {
private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation=(self)";
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private PermissionsPolicyHeaderWriter writer;
private static final String PERMISSIONS_POLICY_HEADER = "Permissions-Policy";
@Before
public void setUp() {
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.writer = new PermissionsPolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES);
}
@Test
public void writeHeadersPermissionsPolicyDefault() {
this.writer.writeHeaders(this.request, this.response);
assertThat(this.response.getHeaderNames()).hasSize(1);
assertThat(this.response.getHeader("Permissions-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES);
}
@Test
public void createWriterWithNullPolicyShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new PermissionsPolicyHeaderWriter(null))
.withMessage("policy can not be null or empty");
}
@Test
public void createWriterWithEmptyPolicyShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new PermissionsPolicyHeaderWriter(""))
.withMessage("policy can not be null or empty");
}
@Test
public void writeHeaderOnlyIfNotPresent() {
String value = new String("value");
this.response.setHeader(PERMISSIONS_POLICY_HEADER, value);
this.writer.writeHeaders(this.request, this.response);
assertThat(this.response.getHeader(PERMISSIONS_POLICY_HEADER)).isSameAs(value);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.server.header;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PermissionsPolicyServerHttpHeadersWriter}.
*
* @author Christophe Gilles
*/
public class PermissionsPolicyServerHttpHeadersWriterTests {
private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation=(self)";
private ServerWebExchange exchange;
private PermissionsPolicyServerHttpHeadersWriter writer;
@Before
public void setup() {
this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
this.writer = new PermissionsPolicyServerHttpHeadersWriter();
}
@Test
public void writeHeadersWhenUsingDefaultsThenDoesNotWrite() {
this.writer.writeHttpHeaders(this.exchange);
HttpHeaders headers = this.exchange.getResponse().getHeaders();
assertThat(headers).isEmpty();
}
@Test
public void writeHeadersWhenUsingPolicyThenWritesPolicy() {
this.writer.setPolicy(DEFAULT_POLICY_DIRECTIVES);
this.writer.writeHttpHeaders(this.exchange);
HttpHeaders headers = this.exchange.getResponse().getHeaders();
assertThat(headers).hasSize(1);
assertThat(headers.get(PermissionsPolicyServerHttpHeadersWriter.PERMISSIONS_POLICY))
.containsOnly(DEFAULT_POLICY_DIRECTIVES);
}
@Test
public void writeHeadersWhenAlreadyWrittenThenWritesHeader() {
this.writer.setPolicy(DEFAULT_POLICY_DIRECTIVES);
String headerValue = "camera=(self)";
this.exchange.getResponse().getHeaders().set(PermissionsPolicyServerHttpHeadersWriter.PERMISSIONS_POLICY,
headerValue);
this.writer.writeHttpHeaders(this.exchange);
HttpHeaders headers = this.exchange.getResponse().getHeaders();
assertThat(headers).hasSize(1);
assertThat(headers.get(PermissionsPolicyServerHttpHeadersWriter.PERMISSIONS_POLICY)).containsOnly(headerValue);
}
}