/*
* Copyright 2002-2011 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.oauth2.provider.vote;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
/**
* <p>
* Votes if any {@link ConfigAttribute#getAttribute()} starts with a prefix indicating that it is an OAuth2 scope. The
* default prefix string is <code>SCOPE_</code>, but this may be overridden to any value. Can also be used to deny
* access to an OAuth2 client by explicitly specifying an attribute value <code>DENY_OAUTH</code>. Typically you would
* want to explicitly deny access to all non-public resources that are not part of any scope.
* </p>
*
* <p>
* Abstains from voting if no configuration attribute commences with the scope prefix, or if the current
* <code>Authentication</code> is not a {@link OAuth2Authentication} or the current client authentication is not a
* {@link AuthorizationRequest} (which contains the scope data). Votes to grant access if there is an exact matching
* {@link AuthorizationRequest#getScope() authorized scope} to a <code>ConfigAttribute</code> starting with the scope
* prefix. Votes to deny access if there is no exact matching authorized scope to a <code>ConfigAttribute</code>
* starting with the scope prefix.
* </p>
*
* <p>
* All comparisons and prefixes are case insensitive so you can use (e.g.) <code>SCOPE_READ</code> for simple
* Facebook-like scope names that might be lower case in the resource definition, or
* <code>scope=http://my.company.com/scopes/read/</code> (<code>scopePrefix="scope="</code>) for Google-like URI scope
* names.
* </p>
*
* @author Dave Syer
*
*/
public class ScopeVoter implements AccessDecisionVoter<Object> {
private String scopePrefix = "SCOPE_";
private String denyAccess = "DENY_OAUTH";
private boolean throwException = true;
/**
* Flag to determine the behaviour on access denied. If set then we throw an {@link InsufficientScopeException}
* instead of returning {@link AccessDecisionVoter#ACCESS_DENIED}. This is unconventional for an access decision
* voter because it vetos the other voters in the chain, but it enables us to pass a message to the caller with
* information about the required scope.
*
* @param throwException the flag to set (default true)
*/
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
/**
* Allows the default role prefix of <code>SCOPE_</code> to be overridden. May be set to an empty value, although
* this is usually not desirable.
*
* @param scopePrefix the new prefix
*/
public void setScopePrefix(String scopePrefix) {
this.scopePrefix = scopePrefix;
}
/**
* The name of the config attribute that can be used to deny access to OAuth2 client. Defaults to
* <code>DENY_OAUTH</code>.
*
* @param denyAccess the deny access attribute value to set
*/
public void setDenyAccess(String denyAccess) {
this.denyAccess = denyAccess;
}
public boolean supports(ConfigAttribute attribute) {
if (denyAccess.equals(attribute.getAttribute()) || (attribute.getAttribute() != null)
&& attribute.getAttribute().startsWith(scopePrefix)) {
return true;
}
else {
return false;
}
}
/**
* This implementation supports any type of class, because it does not query the presented secure object.
*
* @param clazz the secure object
*
* @return always <code>true</code>
*/
public boolean supports(Class<?> clazz) {
return true;
}
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
if (!(authentication instanceof OAuth2Authentication)) {
return result;
}
for (ConfigAttribute attribute : attributes) {
if (denyAccess.equals(attribute.getAttribute())) {
return ACCESS_DENIED;
}
}
OAuth2Request clientAuthentication = ((OAuth2Authentication) authentication).getOAuth2Request();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
Set<String> scopes = clientAuthentication.getScope();
for (String scope : scopes) {
if (attribute.getAttribute().toUpperCase().equals((scopePrefix + scope).toUpperCase())) {
return ACCESS_GRANTED;
}
}
if (result == ACCESS_DENIED && throwException) {
InsufficientScopeException failure = new InsufficientScopeException(
"Insufficient scope for this resource", Collections.singleton(attribute.getAttribute()
.substring(scopePrefix.length())));
throw new AccessDeniedException(failure.getMessage(), failure);
}
}
}
return result;
}
}