/* * 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; } } }