/*
* Copyright 2002-2013 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
*
* http://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.annotation.web.configurers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.AuthorizedUrl;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Adds URL based authorization using
* {@link DefaultFilterInvocationSecurityMetadataSource}. At least one
* {@link org.springframework.web.bind.annotation.RequestMapping} needs to be mapped to
* {@link ConfigAttribute}'s for this {@link SecurityContextConfigurer} to have meaning.
* <h2>Security Filters</h2>
*
* <p>
* Usage includes applying the {@link UrlAuthorizationConfigurer} and then modifying the
* StandardInterceptUrlRegistry. For example:
* </p>
*
* <pre>
* protected void configure(HttpSecurity http) throws Exception {
* http.apply(new UrlAuthorizationConfigurer<HttpSecurity>()).getRegistry()
* .antMatchers("/users**", "/sessions/**").hasRole("USER")
* .antMatchers("/signup").hasRole("ANONYMOUS").anyRequest().hasRole("USER");
* }
* </pre>
*
* The following Filters are populated
*
* <ul>
* <li>
* {@link org.springframework.security.web.access.intercept.FilterSecurityInterceptor}</li>
* </ul>
*
* <h2>Shared Objects Created</h2>
*
* The following shared objects are populated to allow other
* {@link org.springframework.security.config.annotation.SecurityConfigurer}'s to
* customize:
* <ul>
* <li>
* {@link org.springframework.security.web.access.intercept.FilterSecurityInterceptor}</li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>
* AuthenticationManager</li>
* </ul>
*
* @param <H> the type of {@link HttpSecurityBuilder} that is being configured
*
* @author Rob Winch
* @since 3.2
* @see ExpressionUrlAuthorizationConfigurer
*/
public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractInterceptUrlConfigurer<UrlAuthorizationConfigurer<H>, H> {
private final StandardInterceptUrlRegistry REGISTRY;
public UrlAuthorizationConfigurer(ApplicationContext context) {
this.REGISTRY = new StandardInterceptUrlRegistry(context);
}
/**
* The StandardInterceptUrlRegistry is what users will interact with after applying
* the {@link UrlAuthorizationConfigurer}.
*
* @return
*/
public StandardInterceptUrlRegistry getRegistry() {
return REGISTRY;
}
/**
* Adds an {@link ObjectPostProcessor} for this class.
*
* @param objectPostProcessor
* @return the {@link UrlAuthorizationConfigurer} for further customizations
*/
public UrlAuthorizationConfigurer<H> withObjectPostProcessor(
ObjectPostProcessor<?> objectPostProcessor) {
addObjectPostProcessor(objectPostProcessor);
return this;
}
public class StandardInterceptUrlRegistry
extends
ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<StandardInterceptUrlRegistry, AuthorizedUrl> {
/**
* @param context
*/
private StandardInterceptUrlRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override
public MvcMatchersAuthorizedUrl mvcMatchers(HttpMethod method,
String... mvcPatterns) {
return new MvcMatchersAuthorizedUrl(createMvcMatchers(method, mvcPatterns));
}
@Override
public MvcMatchersAuthorizedUrl mvcMatchers(String... patterns) {
return mvcMatchers(null, patterns);
}
@Override
protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {
return new AuthorizedUrl(requestMatchers);
}
/**
* Adds an {@link ObjectPostProcessor} for this class.
*
* @param objectPostProcessor
* @return the {@link ExpressionUrlAuthorizationConfigurer} for further
* customizations
*/
public StandardInterceptUrlRegistry withObjectPostProcessor(
ObjectPostProcessor<?> objectPostProcessor) {
addObjectPostProcessor(objectPostProcessor);
return this;
}
public H and() {
return UrlAuthorizationConfigurer.this.and();
}
}
/**
* Creates the default {@link AccessDecisionVoter} instances used if an
* {@link AccessDecisionManager} was not specified.
*
* @param http the builder to use
*/
@Override
@SuppressWarnings("rawtypes")
final List<AccessDecisionVoter<? extends Object>> getDecisionVoters(H http) {
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
return decisionVoters;
}
/**
* Creates the {@link FilterInvocationSecurityMetadataSource} to use. The
* implementation is a {@link DefaultFilterInvocationSecurityMetadataSource}.
*
* @param http the builder to use
*/
@Override
FilterInvocationSecurityMetadataSource createMetadataSource(H http) {
return new DefaultFilterInvocationSecurityMetadataSource(
REGISTRY.createRequestMap());
}
/**
* Adds a mapping of the {@link RequestMatcher} instances to the
* {@link ConfigAttribute} instances.
* @param requestMatchers the {@link RequestMatcher} instances that should map to the
* provided {@link ConfigAttribute} instances
* @param configAttributes the {@link ConfigAttribute} instances that should be mapped
* by the {@link RequestMatcher} instances
* @return the {@link UrlAuthorizationConfigurer} for further customizations
*/
private StandardInterceptUrlRegistry addMapping(
Iterable<? extends RequestMatcher> requestMatchers,
Collection<ConfigAttribute> configAttributes) {
for (RequestMatcher requestMatcher : requestMatchers) {
REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
requestMatcher, configAttributes));
}
return REGISTRY;
}
/**
* Creates a String for specifying a user requires a role.
*
* @param role the role that should be required which is prepended with ROLE_
* automatically (i.e. USER, ADMIN, etc). It should not start with ROLE_
* @return the {@link ConfigAttribute} expressed as a String
*/
private static String hasRole(String role) {
Assert.isTrue(
!role.startsWith("ROLE_"),
role
+ " should not start with ROLE_ since ROLE_ is automatically prepended when using hasRole. Consider using hasAuthority or access instead.");
return "ROLE_" + role;
}
/**
* Creates a String for specifying that a user requires one of many roles.
*
* @param roles the roles that the user should have at least one of (i.e. ADMIN, USER,
* etc). Each role should not start with ROLE_ since it is automatically prepended
* already.
* @return the {@link ConfigAttribute} expressed as a String
*/
private static String[] hasAnyRole(String... roles) {
for (int i = 0; i < roles.length; i++) {
roles[i] = "ROLE_" + roles[i];
}
return roles;
}
/**
* Creates a String for specifying that a user requires one of many authorities
* @param authorities the authorities that the user should have at least one of (i.e.
* ROLE_USER, ROLE_ADMIN, etc).
* @return the {@link ConfigAttribute} expressed as a String.
*/
private static String[] hasAnyAuthority(String... authorities) {
return authorities;
}
/**
* An {@link AuthorizedUrl} that allows optionally configuring the
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
*
* @author Rob Winch
*/
public final class MvcMatchersAuthorizedUrl extends AuthorizedUrl {
/**
* Creates a new instance
*
* @param requestMatchers the {@link RequestMatcher} instances to map
*/
private MvcMatchersAuthorizedUrl(List<MvcRequestMatcher> requestMatchers) {
super(requestMatchers);
}
@SuppressWarnings("unchecked")
public AuthorizedUrl servletPath(String servletPath) {
for (MvcRequestMatcher matcher : (List<MvcRequestMatcher>) getMatchers()) {
matcher.setServletPath(servletPath);
}
return this;
}
}
/**
* Maps the specified {@link RequestMatcher} instances to {@link ConfigAttribute}
* instances.
*
* @author Rob Winch
* @since 3.2
*/
public class AuthorizedUrl {
private final List<? extends RequestMatcher> requestMatchers;
/**
* Creates a new instance
* @param requestMatchers the {@link RequestMatcher} instances to map to some
* {@link ConfigAttribute} instances.
*/
private AuthorizedUrl(List<? extends RequestMatcher> requestMatchers) {
Assert.notEmpty(requestMatchers,
"requestMatchers must contain at least one value");
this.requestMatchers = requestMatchers;
}
/**
* Specifies a user requires a role.
*
* @param role the role that should be required which is prepended with ROLE_
* automatically (i.e. USER, ADMIN, etc). It should not start with ROLE_ the
* {@link UrlAuthorizationConfigurer} for further customization
*/
public StandardInterceptUrlRegistry hasRole(String role) {
return access(UrlAuthorizationConfigurer.hasRole(role));
}
/**
* Specifies that a user requires one of many roles.
*
* @param roles the roles that the user should have at least one of (i.e. ADMIN,
* USER, etc). Each role should not start with ROLE_ since it is automatically
* prepended already.
* @return the {@link UrlAuthorizationConfigurer} for further customization
*/
public StandardInterceptUrlRegistry hasAnyRole(String... roles) {
return access(UrlAuthorizationConfigurer.hasAnyRole(roles));
}
/**
* Specifies a user requires an authority.
*
* @param authority the authority that should be required
* @return the {@link UrlAuthorizationConfigurer} for further customization
*/
public StandardInterceptUrlRegistry hasAuthority(String authority) {
return access(authority);
}
/**
* Specifies that a user requires one of many authorities
* @param authorities the authorities that the user should have at least one of
* (i.e. ROLE_USER, ROLE_ADMIN, etc).
* @return the {@link UrlAuthorizationConfigurer} for further customization
*/
public StandardInterceptUrlRegistry hasAnyAuthority(String... authorities) {
return access(UrlAuthorizationConfigurer.hasAnyAuthority(authorities));
}
/**
* Specifies that an anonymous user is allowed access
* @return the {@link UrlAuthorizationConfigurer} for further customization
*/
public StandardInterceptUrlRegistry anonymous() {
return hasRole("ROLE_ANONYMOUS");
}
/**
* Specifies that the user must have the specified {@link ConfigAttribute}'s
* @param attributes the {@link ConfigAttribute}'s that restrict access to a URL
* @return the {@link UrlAuthorizationConfigurer} for further customization
*/
public StandardInterceptUrlRegistry access(String... attributes) {
addMapping(requestMatchers, SecurityConfig.createList(attributes));
return UrlAuthorizationConfigurer.this.REGISTRY;
}
protected List<? extends RequestMatcher> getMatchers() {
return this.requestMatchers;
}
}
}