implement token scopes, scopes can be used to issue a token which is only suitable for a single or set explicit actions

This commit is contained in:
Sebastian Sdorra
2017-01-16 15:04:44 +01:00
parent df6d9dacf8
commit e7d6f50fd9
13 changed files with 788 additions and 152 deletions

View File

@@ -39,6 +39,7 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.List;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
@@ -83,6 +84,7 @@ import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import sonia.scm.security.Scope;
/**
*
@@ -144,6 +146,7 @@ public class AuthenticationResource
* @param username the username for the authentication
* @param password the password for the authentication
* @param cookie create authentication token
* @param scope scope of created token
*
* @return
*/
@@ -153,8 +156,9 @@ public class AuthenticationResource
public Response authenticate(@Context HttpServletRequest request,
@Context HttpServletResponse response,
@FormParam("username") String username,
@FormParam("password") String password, @FormParam("rememberMe")
@QueryParam("cookie") boolean cookie)
@FormParam("password") String password,
@QueryParam("cookie") boolean cookie,
@QueryParam("scope") List<String> scope)
{
Preconditions.checkArgument(!Strings.isNullOrEmpty(username),
"username parameter is required");
@@ -171,7 +175,7 @@ public class AuthenticationResource
User user = subject.getPrincipals().oneByType(User.class);
String token = tokenGenerator.createBearerToken(user);
String token = tokenGenerator.createBearerToken(user, scope != null ? Scope.valueOf(scope) : Scope.empty());
ScmState state;

View File

@@ -50,6 +50,8 @@ import sonia.scm.plugin.Extension;
import sonia.scm.user.UserDAO;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.List;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -113,8 +115,7 @@ public class BearerRealm extends AuthenticatingRealm
* @return authentication data from user and group dao
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token)
protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token)
{
checkArgument(token instanceof BearerAuthenticationToken, "%s is required",
BearerAuthenticationToken.class);
@@ -122,7 +123,7 @@ public class BearerRealm extends AuthenticatingRealm
BearerAuthenticationToken bt = (BearerAuthenticationToken) token;
Claims c = checkToken(bt);
return helper.getAuthenticationInfo(c.getSubject(), bt.getCredentials());
return helper.getAuthenticationInfo(c.getSubject(), bt.getCredentials(), Scopes.fromClaims(c));
}
/**

View File

@@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.user.User;
import static com.google.common.base.Preconditions.*;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
@@ -95,10 +96,11 @@ public final class BearerTokenGenerator
*
*
* @param user user
* @param scope scope of token
*
* @return bearer token
*/
public String createBearerToken(User user) {
public String createBearerToken(User user, Scope scope) {
checkNotNull(user, "user is required");
String username = user.getName();
@@ -114,16 +116,19 @@ public final class BearerTokenGenerator
// TODO: should be configurable
long expiration = TimeUnit.MILLISECONDS.convert(10, TimeUnit.HOURS);
Map<String,Object> claim = Maps.newHashMap();
Map<String,Object> claims = Maps.newHashMap();
// add scope to claims
Scopes.toClaims(claims, scope);
// enrich claims with registered enrichers
enrichers.forEach((enricher) -> {
enricher.enrich(claim);
enricher.enrich(claims);
});
//J-
return Jwts.builder()
.setClaims(claim)
.setClaims(claims)
.setSubject(username)
.setId(id)
.signWith(SignatureAlgorithm.HS256, key.getBytes())

View File

@@ -39,7 +39,6 @@ import com.github.legman.Subscribe;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.inject.Inject;
@@ -444,38 +443,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
info.addStringPermissions(permissions);
if (logger.isTraceEnabled()){
logger.trace(createAuthorizationSummary(user, groups, info));
}
return info;
}
private String createAuthorizationSummary(User user, GroupNames groups, AuthorizationInfo authzInfo)
{
StringBuilder buffer = new StringBuilder("authorization summary: ");
buffer.append(SEPARATOR).append("username : ").append(user.getName());
buffer.append(SEPARATOR).append("groups : ");
append(buffer, groups);
buffer.append(SEPARATOR).append("roles : ");
append(buffer, authzInfo.getRoles());
buffer.append(SEPARATOR).append("permissions:");
append(buffer, authzInfo.getStringPermissions());
append(buffer, authzInfo.getObjectPermissions());
return buffer.toString();
}
private void append(StringBuilder buffer, Iterable<?> iterable){
if (iterable != null){
for ( Object item : iterable )
{
buffer.append(SEPARATOR).append(" - ").append(item);
}
}
}
//~--- get methods ----------------------------------------------------------

View File

@@ -45,15 +45,17 @@ import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
import sonia.scm.plugin.Extension;
import sonia.scm.user.UserDAO;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default authorizing realm.
*
@@ -64,6 +66,13 @@ import javax.inject.Singleton;
@Singleton
public class DefaultRealm extends AuthorizingRealm
{
private static final String SEPARATOR = System.getProperty("line.separator", "\n");
/**
* the logger for DefaultRealm
*/
private static final Logger LOG = LoggerFactory.getLogger(DefaultRealm.class);
/** Field description */
@VisibleForTesting
@@ -122,10 +131,61 @@ public class DefaultRealm extends AuthorizingRealm
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals)
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
{
return collector.collect(principals);
AuthorizationInfo info = collector.collect(principals);
Scope scope = principals.oneByType(Scope.class);
if (scope != null && ! scope.isEmpty()) {
LOG.trace("filter permissions by scope {}", scope);
AuthorizationInfo filtered = Scopes.filter(getPermissionResolver(), info, scope);
if (LOG.isTraceEnabled()) {
log(principals, info, filtered);
}
return filtered;
} else if (LOG.isTraceEnabled()) {
LOG.trace("principal does not contain scope informations, returning all permissions");
log(principals, info, null);
}
return info;
}
private void log( PrincipalCollection collection, AuthorizationInfo original, AuthorizationInfo filtered ) {
StringBuilder buffer = new StringBuilder("authorization summary: ");
buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal());
buffer.append(SEPARATOR).append("groups : ");
append(buffer, collection.oneByType(GroupNames.class));
buffer.append(SEPARATOR).append("roles : ");
append(buffer, original.getRoles());
buffer.append(SEPARATOR).append("scope : ");
append(buffer, collection.oneByType(Scope.class));
if ( filtered != null ) {
buffer.append(SEPARATOR).append("permissions (filtered by scope): ");
append(buffer, filtered);
buffer.append(SEPARATOR).append("permissions (unfiltered): ");
} else {
buffer.append(SEPARATOR).append("permissions: ");
}
append(buffer, original);
LOG.trace(buffer.toString());
}
private void append(StringBuilder buffer, AuthorizationInfo authz) {
append(buffer, authz.getStringPermissions());
append(buffer, authz.getObjectPermissions());
}
private void append(StringBuilder buffer, Iterable<?> iterable){
if (iterable != null){
for ( Object item : iterable )
{
buffer.append(SEPARATOR).append(" - ").append(item);
}
}
}
//~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,143 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.permission.PermissionResolver;
/**
* Utile methods for {@link Scope}.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public final class Scopes {
/** Key of scope in the claims of a token **/
public final static String CLAIMS_KEY = "scope";
private Scopes() {
}
/**
* Returns scope from a token claims. If the claims does not contain a scope object, the method will return an empty
* scope.
*
* @param claims token claims
*
* @return scope of claims
*/
@SuppressWarnings("unchecked")
public static Scope fromClaims(Map<String,Object> claims) {
Scope scope = Scope.empty();
if (claims.containsKey(Scopes.CLAIMS_KEY)) {
scope = Scope.valueOf((List<String>)claims.get(Scopes.CLAIMS_KEY));
}
return scope;
}
/**
* Adds a scope to a token claims. The method will add the scope to the claims, if the scope is non null and not
* empty.
*
* @param claims token claims
* @param scope scope
*/
public static void toClaims(Map<String,Object> claims, Scope scope) {
if (scope != null && ! scope.isEmpty()) {
claims.put(CLAIMS_KEY, ImmutableSet.copyOf(scope));
}
}
/**
* Filter permissions from {@link AuthorizationInfo} by scope values. Only permission definitions from the scope will
* be returned and only if a permission from the {@link AuthorizationInfo} implies the requested scope permission.
*
* @param resolver permission resolver
* @param authz authorization info
* @param scope scope
*
* @return filtered {@link AuthorizationInfo}
*/
public static AuthorizationInfo filter(PermissionResolver resolver, AuthorizationInfo authz, Scope scope) {
List<Permission> authzPermissions = authzPermissions(resolver, authz);
Predicate<Permission> predicate = implies(authzPermissions);
Set<Permission> filteredPermissions = resolve(resolver, ImmutableList.copyOf(scope))
.stream()
.filter(predicate)
.collect(Collectors.toSet());
Set<String> roles = ImmutableSet.copyOf(nullToEmpty(authz.getRoles()));
SimpleAuthorizationInfo authzFiltered = new SimpleAuthorizationInfo(roles);
authzFiltered.setObjectPermissions(filteredPermissions);
return authzFiltered;
}
private static <T> Collection<T> nullToEmpty(Collection<T> collection) {
return collection != null ? collection : Collections.emptySet();
}
private static Collection<Permission> resolve(PermissionResolver resolver, Collection<String> permissions) {
return Collections2.transform(nullToEmpty(permissions), resolver::resolvePermission);
}
private static Predicate<Permission> implies(Iterable<Permission> authzPermissions){
return (scopePermission) -> {
for ( Permission authzPermission : authzPermissions ) {
if (authzPermission.implies(scopePermission)) {
return true;
}
}
return false;
};
}
private static List<Permission> authzPermissions(PermissionResolver resolver, AuthorizationInfo authz){
List<Permission> authzPermissions = Lists.newArrayList();
authzPermissions.addAll(nullToEmpty(authz.getObjectPermissions()));
authzPermissions.addAll(resolve(resolver, authz.getStringPermissions()));
return authzPermissions;
}
}