diff --git a/src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSource.java b/src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSource.java index 79f3dd60971..9175551463a 100644 --- a/src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSource.java +++ b/src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSource.java @@ -15,12 +15,13 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import javax.servlet.http.HttpServletRequest; import org.geoserver.security.impl.RESTAccessRuleDAO; import org.geotools.util.logging.Logging; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; -import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.security.web.util.UrlUtils; import org.springframework.util.StringUtils; /** @author Chris Berry http://opensource.atlassian.com/projects/spring/browse/SEC-531 */ @@ -31,7 +32,7 @@ public class RESTfulDefinitionSource implements FilterInvocationSecurityMetadata private static final String[] validMethodNames = {"GET", "PUT", "DELETE", "POST"}; /** Underlying SecurityMetedataSource object */ - private RESTfulPathBasedFilterInvocationDefinitionMap delegate = null; + private RESTfulDefinitionSourceDelegateMap delegate = null; /** rest access rules dao */ private RESTAccessRuleDAO dao; @@ -41,11 +42,12 @@ public Collection getAttributes(Object object) throws IllegalArgumentException { if ((object == null) || !this.supports(object.getClass())) { - throw new IllegalArgumentException("Object must be a FilterInvocation"); + throw new IllegalArgumentException("Object must be a HTTPServletRequest"); } - String url = ((FilterInvocation) object).getRequestUrl(); - String method = ((FilterInvocation) object).getHttpRequest().getMethod(); + HttpServletRequest request = (HttpServletRequest) object; + String url = UrlUtils.buildRequestUrl(request); + String method = request.getMethod(); return delegate().lookupAttributes(cleanURL(url), method); } @@ -67,7 +69,7 @@ public Collection getAllConfigAttributes() { @Override public boolean supports(Class clazz) { - return FilterInvocation.class.isAssignableFrom(clazz); + return HttpServletRequest.class.isAssignableFrom(clazz); } public RESTfulDefinitionSource(RESTAccessRuleDAO dao) { @@ -81,10 +83,10 @@ public void reload() { delegate = null; } - RESTfulPathBasedFilterInvocationDefinitionMap delegate() { + RESTfulDefinitionSourceDelegateMap delegate() { if (delegate == null || dao.isModified()) { synchronized (this) { - delegate = new RESTfulPathBasedFilterInvocationDefinitionMap(); + delegate = new RESTfulDefinitionSourceDelegateMap(); for (String rule : dao.getRules()) { processPathList(rule); } diff --git a/src/main/src/main/java/org/geoserver/security/RESTfulPathBasedFilterInvocationDefinitionMap.java b/src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSourceDelegateMap.java similarity index 76% rename from src/main/src/main/java/org/geoserver/security/RESTfulPathBasedFilterInvocationDefinitionMap.java rename to src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSourceDelegateMap.java index 85ded19e582..2bbf65bcbe2 100644 --- a/src/main/src/main/java/org/geoserver/security/RESTfulPathBasedFilterInvocationDefinitionMap.java +++ b/src/main/src/main/java/org/geoserver/security/RESTfulDefinitionSourceDelegateMap.java @@ -15,18 +15,14 @@ import java.util.logging.Logger; import org.geotools.util.logging.Logging; import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.web.FilterInvocation; -import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; /** @author Chris Berry http://opensource.atlassian.com/projects/spring/browse/SEC-531 */ -public class RESTfulPathBasedFilterInvocationDefinitionMap - implements FilterInvocationSecurityMetadataSource { +public class RESTfulDefinitionSourceDelegateMap { - private static Logger log = - Logging.getLogger(RESTfulPathBasedFilterInvocationDefinitionMap.class); + private static Logger log = Logging.getLogger(RESTfulDefinitionSourceDelegateMap.class); // ~ Instance fields // ================================================================================================ @@ -35,13 +31,6 @@ public class RESTfulPathBasedFilterInvocationDefinitionMap private PathMatcher pathMatcher = new AntPathMatcher(); private boolean convertUrlToLowercaseBeforeComparison = false; - // ~ Methods - // ======================================================================================================== - @Override - public boolean supports(Class clazz) { - return FilterInvocation.class.isAssignableFrom(clazz); - } - public void addSecureUrl( String antPath, String[] httpMethods, Collection attrs) { requestMap.add(new EntryHolder(antPath, httpMethods, attrs)); @@ -57,12 +46,6 @@ public void addSecureUrl( } } - public void addSecureUrl(String antPath, Collection attrs) { - throw new IllegalArgumentException( - "addSecureUrl(String, Collection ) is INVALID for RESTfulDefinitionSource"); - } - - @Override public Collection getAllConfigAttributes() { Set set = new HashSet<>(); @@ -74,10 +57,6 @@ public Collection getAllConfigAttributes() { // return set.iterator(); } - public int getMapSize() { - return this.requestMap.size(); - } - public boolean isConvertUrlToLowercaseBeforeComparison() { return convertUrlToLowercaseBeforeComparison; } @@ -87,24 +66,6 @@ public void setConvertUrlToLowercaseBeforeComparison( this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison; } - @Override - public Collection getAttributes(Object object) - throws IllegalArgumentException { - if ((object == null) || !this.supports(object.getClass())) { - throw new IllegalArgumentException("Object must be a FilterInvocation"); - } - - String url = ((FilterInvocation) object).getRequestUrl(); - String method = ((FilterInvocation) object).getHttpRequest().getMethod(); - - return this.lookupAttributes(url, method); - } - - public Collection lookupAttributes(String url) { - throw new IllegalArgumentException( - "lookupAttributes(String url) is INVALID for RESTfulDefinitionSource"); - } - public Collection lookupAttributes(String url, String httpMethod) { // Strip anything after a question mark symbol, as per SEC-161. See also SEC-321 int firstQuestionMarkIndex = url.indexOf("?"); @@ -127,9 +88,9 @@ public Collection lookupAttributes(String url, String httpMetho } } - Iterator iter = requestMap.iterator(); + Iterator iter = requestMap.iterator(); while (iter.hasNext()) { - EntryHolder entryHolder = (EntryHolder) iter.next(); + EntryHolder entryHolder = iter.next(); String antPath = entryHolder.getAntPath(); String[] methodList = entryHolder.getHttpMethodList(); diff --git a/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityContextPersistenceFilter.java b/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityContextPersistenceFilter.java index 3042373d2ec..6c4b55b0d5f 100644 --- a/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityContextPersistenceFilter.java +++ b/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityContextPersistenceFilter.java @@ -7,6 +7,7 @@ package org.geoserver.security.filter; import java.io.IOException; +import java.util.function.Supplier; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -67,8 +68,9 @@ public void doFilterInternal( } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); try { - SecurityContext securityContext = repo.loadContext(request).get(); - SecurityContextHolder.setContext(securityContext); + Supplier securityContext = + repo.loadDeferredContext(request); + SecurityContextHolder.setDeferredContext(securityContext); chain.doFilter(request, response); } finally { SecurityContext contextAfterChainExecution = diff --git a/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityInterceptorFilter.java b/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityInterceptorFilter.java index 08c50348c56..6c7a06639af 100644 --- a/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityInterceptorFilter.java +++ b/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityInterceptorFilter.java @@ -6,54 +6,225 @@ package org.geoserver.security.filter; +import static org.geoserver.platform.GeoServerExtensions.bean; + import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.geoserver.platform.GeoServerExtensions; +import java.util.Collection; +import java.util.function.Supplier; +import javax.servlet.http.HttpServletRequest; import org.geoserver.security.config.SecurityInterceptorFilterConfig; import org.geoserver.security.config.SecurityNamedServiceConfig; -import org.springframework.security.access.AccessDecisionVoter; -import org.springframework.security.access.vote.AffirmativeBased; -import org.springframework.security.access.vote.AuthenticatedVoter; -import org.springframework.security.access.vote.RoleVoter; -import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; -import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityMetadataSource; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.access.intercept.AuthorizationFilter; /** * Security interceptor filter * * @author mcr + * @author awaterme */ public class GeoServerSecurityInterceptorFilter extends GeoServerCompositeFilter { + + private static final AuthorizationDecision ACCESS_GRANTED = new AuthorizationDecision(true); + private static final AuthorizationDecision ACCESS_DENIED = new AuthorizationDecision(false); + private static final AuthorizationDecision ACCESS_ABSTAIN = null; + + /** + * AuthorizationManager implementation considers the authentication requirements of the + * respective request, in contrast to Spring's AuthenticatedAuthorizationManager, which is setup + * for one authentication requirement explicitly.
+ * Based on org.springframework.security.access.vote.AuthenticatedVoter, which is deprecated + * with Spring Security 5.8. + */ + private static final class AuthenticatedAuthorizationManager + implements AuthorizationManager { + + public static final String IS_AUTHENTICATED_FULLY = "IS_AUTHENTICATED_FULLY"; + + public static final String IS_AUTHENTICATED_REMEMBERED = "IS_AUTHENTICATED_REMEMBERED"; + + public static final String IS_AUTHENTICATED_ANONYMOUSLY = "IS_AUTHENTICATED_ANONYMOUSLY"; + + private SecurityMetadataSource metadata; + + private AuthenticationTrustResolver authenticationTrustResolver = + new AuthenticationTrustResolverImpl(); + + /** @param metadata */ + public AuthenticatedAuthorizationManager(SecurityMetadataSource metadata) { + super(); + this.metadata = metadata; + } + + private boolean isFullyAuthenticated(Authentication authentication) { + return (!this.authenticationTrustResolver.isAnonymous(authentication) + && !this.authenticationTrustResolver.isRememberMe(authentication)); + } + + private boolean supports(ConfigAttribute attribute) { + return (attribute.getAttribute() != null) + && (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute()) + || IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute()) + || IS_AUTHENTICATED_ANONYMOUSLY.equals(attribute.getAttribute())); + } + + private AuthorizationDecision vote( + Authentication authentication, + Object object, + Collection attributes) { + AuthorizationDecision result = ACCESS_ABSTAIN; + for (ConfigAttribute attribute : attributes) { + if (this.supports(attribute)) { + result = ACCESS_DENIED; + if (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())) { + if (isFullyAuthenticated(authentication)) { + return ACCESS_GRANTED; + } + } + if (IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute())) { + if (this.authenticationTrustResolver.isRememberMe(authentication) + || isFullyAuthenticated(authentication)) { + return ACCESS_GRANTED; + } + } + if (IS_AUTHENTICATED_ANONYMOUSLY.equals(attribute.getAttribute())) { + if (this.authenticationTrustResolver.isAnonymous(authentication) + || isFullyAuthenticated(authentication) + || this.authenticationTrustResolver.isRememberMe(authentication)) { + return ACCESS_GRANTED; + } + } + } + } + return result; + } + + @Override + public AuthorizationDecision check( + Supplier authentication, HttpServletRequest request) { + Collection attributes = metadata.getAttributes(request); + AuthorizationDecision vote = vote(authentication.get(), request, attributes); + return vote; + } + } + + /** + * {@link AuthorizationManager} implementation considers the role requirements of the respective + * request, in contrast to Spring's AuthorityAuthorizationManager which is setup for certain + * roles explicitly.
+ * Based on org.springframework.security.access.vote.RoleVoter, which is deprecated with Spring + * Security 5.8. + */ + private static final class RoleAuthorizationManager + implements AuthorizationManager { + + private SecurityMetadataSource metadata; + + /** @param metadata */ + public RoleAuthorizationManager(SecurityMetadataSource metadata) { + super(); + this.metadata = metadata; + } + + private AuthorizationDecision vote( + Authentication authentication, + Object object, + Collection attributes) { + if (authentication == null) { + return ACCESS_DENIED; + } + AuthorizationDecision result = ACCESS_ABSTAIN; + Collection authorities = authentication.getAuthorities(); + for (ConfigAttribute attribute : attributes) { + if (attribute.getAttribute() != null) { + result = ACCESS_DENIED; + // Attempt to find a matching granted authority + for (GrantedAuthority authority : authorities) { + if (attribute.getAttribute().equals(authority.getAuthority())) { + return ACCESS_GRANTED; + } + } + } + } + return result; + } + + @Override + public AuthorizationDecision check( + Supplier authentication, HttpServletRequest request) { + Collection attributes = metadata.getAttributes(request); + AuthorizationDecision vote = vote(authentication.get(), request, attributes); + return vote; + } + } + + /** + * Compound {@link AuthorizationManager} implementation forwards the check to delegates.
+ * Based on org.springframework.security.access.vote.AffirmativeBased, which is deprecated with + * Spring Security 5.8. + */ + private static final class AffirmativeAuthorizationManager + implements AuthorizationManager { + + private AuthorizationManager delegate1; + private AuthorizationManager delegate2; + private boolean allowIfAllAbstainDecisions; + + /** + * @param delegate1 + * @param delegate2 + * @param allowIfAllAbstainDecisions + */ + public AffirmativeAuthorizationManager( + AuthorizationManager delegate1, + AuthorizationManager delegate2, + boolean allowIfAllAbstainDecisions) { + super(); + this.delegate1 = delegate1; + this.delegate2 = delegate2; + this.allowIfAllAbstainDecisions = allowIfAllAbstainDecisions; + } + + @Override + public AuthorizationDecision check( + Supplier authentication, HttpServletRequest object) { + AuthorizationDecision d1 = delegate1.check(authentication, object); + AuthorizationDecision d2 = delegate2.check(authentication, object); + if (d1 == null && d2 == null) { + return allowIfAllAbstainDecisions ? ACCESS_GRANTED : ACCESS_DENIED; + } + if (d1 != null && d1.isGranted()) { + return ACCESS_GRANTED; + } + if (d2 != null && d2.isGranted()) { + return ACCESS_GRANTED; + } + return ACCESS_DENIED; + } + } + @Override public void initializeFromConfig(SecurityNamedServiceConfig config) throws IOException { super.initializeFromConfig(config); SecurityInterceptorFilterConfig siConfig = (SecurityInterceptorFilterConfig) config; + boolean allowIfAllAbstainDecisions = siConfig.isAllowIfAllAbstainDecisions(); + String sourceName = siConfig.getSecurityMetadataSource(); + SecurityMetadataSource source = (SecurityMetadataSource) bean(sourceName); + + AuthenticatedAuthorizationManager aam = new AuthenticatedAuthorizationManager(source); + RoleAuthorizationManager ram = new RoleAuthorizationManager(source); + AffirmativeAuthorizationManager am = + new AffirmativeAuthorizationManager(aam, ram, allowIfAllAbstainDecisions); + AuthorizationFilter filter = new AuthorizationFilter(am); - FilterSecurityInterceptor filter = new FilterSecurityInterceptor(); - - filter.setAuthenticationManager(getSecurityManager().authenticationManager()); - - List> voters = new ArrayList<>(); - RoleVoter roleVoter = new RoleVoter(); - roleVoter.setRolePrefix(""); - voters.add(roleVoter); - voters.add(new AuthenticatedVoter()); - AffirmativeBased accessDecisionManager = new AffirmativeBased(voters); - accessDecisionManager.setAllowIfAllAbstainDecisions( - siConfig.isAllowIfAllAbstainDecisions()); - filter.setAccessDecisionManager(accessDecisionManager); - - // TODO, Justin, is this correct - filter.setSecurityMetadataSource( - (FilterInvocationSecurityMetadataSource) - GeoServerExtensions.bean(siConfig.getSecurityMetadataSource())); - try { - filter.afterPropertiesSet(); - } catch (Exception e) { - throw new RuntimeException(e); - } getNestedFilters().add(filter); } } diff --git a/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityMetadataSource.java b/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityMetadataSource.java index ef63fdd8fc1..318c4856722 100644 --- a/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityMetadataSource.java +++ b/src/main/src/main/java/org/geoserver/security/filter/GeoServerSecurityMetadataSource.java @@ -8,25 +8,33 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import org.geoserver.security.GeoServerSecurityFilterChain; import org.geoserver.security.impl.GeoServerRole; +import org.geotools.util.logging.Logging; +import org.springframework.core.log.LogMessage; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; -import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; +import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; /** - * Justin, nasty hack to get rid of the spring bean "filterSecurityInterceptor"; I think, there is a - * better was to solve this. + * {@link SecurityMetadataSource} implementation for GeoServer web UI, provides {@link + * ConfigAttribute}s with authentication and authorization constraints for evaluation through {@link + * GeoServerSecurityInterceptorFilter}. * * @author mcr */ -public class GeoServerSecurityMetadataSource extends DefaultFilterInvocationSecurityMetadataSource { +public class GeoServerSecurityMetadataSource implements SecurityMetadataSource { /** * Should match @@ -51,48 +59,83 @@ public boolean matches(HttpServletRequest request) { webChainMatcher1.matches(request) || webChainMatcher2.matches(request); if (isOnWebChain == false) return false; - Map params = request.getParameterMap(); + Map params = request.getParameterMap(); if (params.size() != 2) return false; - String[] pageClass = (String[]) params.get("wicket:bookmarkablePage"); + String[] pageClass = params.get("wicket:bookmarkablePage"); if (pageClass == null || pageClass.length != 1) return false; if (":org.geoserver.web.GeoServerLoginPage".equals(pageClass[0]) == false) return false; - String[] error = (String[]) params.get("error"); + String[] error = params.get("error"); if (error == null || error.length != 1) return false; return true; } }; - static LinkedHashMap> requestMap; + static final Map> requestMap; static { - requestMap = new LinkedHashMap<>(); + LinkedHashMap> map = new LinkedHashMap<>(); // the login page is a public resource - requestMap.put(new LoginPageRequestMatcher(), new ArrayList<>()); + map.put(new LoginPageRequestMatcher(), new ArrayList<>()); // images,java script,... are public resources - requestMap.put(new AntPathRequestMatcher("/web/resources/**"), new ArrayList<>()); + map.put(new AntPathRequestMatcher("/web/resources/**"), new ArrayList<>()); RequestMatcher matcher = new AntPathRequestMatcher("/config/**"); List list = new ArrayList<>(); list.add(new SecurityConfig(GeoServerRole.ADMIN_ROLE.getAuthority())); - requestMap.put(matcher, list); + map.put(matcher, list); matcher = new AntPathRequestMatcher("/**"); list = new ArrayList<>(); list.add(new SecurityConfig("IS_AUTHENTICATED_ANONYMOUSLY")); - requestMap.put(matcher, list); + map.put(matcher, list); + + requestMap = Collections.unmodifiableMap(map); }; + private Logger logger = Logging.getLogger(GeoServerSecurityMetadataSource.class); + public GeoServerSecurityMetadataSource() { - super(requestMap); - /* - - - */ + super(); + } + + @Override + public Collection getAllConfigAttributes() { + Set allAttributes = new HashSet<>(); + requestMap.values().forEach(allAttributes::addAll); + return allAttributes; + } + + @Override + public Collection getAttributes(Object object) { + final HttpServletRequest request = (HttpServletRequest) object; + int count = 0; + for (Map.Entry> entry : requestMap.entrySet()) { + if (entry.getKey().matches(request)) { + return entry.getValue(); + } else { + if (this.logger.isLoggable(Level.FINEST)) { + String msg = + LogMessage.format( + "Did not match request to %s - %s (%d/%d)", + entry.getKey(), + entry.getValue(), + ++count, + requestMap.size()) + .toString(); + this.logger.finest(msg); + } + } + } + return null; + } + @Override + public boolean supports(Class clazz) { + return HttpServletRequest.class.isAssignableFrom(clazz); } } diff --git a/src/pom.xml b/src/pom.xml index a87274e8106..ee79e5479f8 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -92,8 +92,8 @@ 32-SNAPSHOT 1.26-SNAPSHOT 1.19.0 - 5.3.34 - 5.7.12 + 5.3.37 + 5.8.13 5.5.18 2.5.2.RELEASE 3.1.0