Skip to content

Commit

Permalink
implement parameter-level permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
John Roesler committed Feb 16, 2017
1 parent ebdc895 commit 16a71b4
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 161 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.bazaarvoice.emodb.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamRequiresPermissions {
/**
* The permission string which will be passed to {org.apache.shiro.subject.Subject#isPermitted(String)}
* to determine if the user is allowed to invoke the code protected by this annotation.
*/
String[] value();

public static enum CustomLogical {
AND,OR;
}

/**
* The logical operation for the permission checks in case multiple roles are specified. AND is the default
* @since 1.1.0
*/
CustomLogical logical() default CustomLogical.AND;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bazaarvoice.emodb.auth.jersey;

import com.bazaarvoice.emodb.auth.ParamRequiresPermissions;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
Expand All @@ -8,12 +9,18 @@
import com.sun.jersey.api.model.AbstractMethod;
import com.sun.jersey.spi.container.ResourceFilter;
import com.sun.jersey.spi.container.ResourceFilterFactory;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.mgt.SecurityManager;

import javax.annotation.Nullable;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MultivaluedMap;
import java.lang.reflect.Parameter;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -66,15 +73,43 @@ public List<ResourceFilter> create(AbstractMethod am) {
LinkedList<ResourceFilter> filters = Lists.newLinkedList();

// Check the resource
RequiresPermissions permAnnotation = am.getResource().getAnnotation(RequiresPermissions.class);
if (permAnnotation != null) {
filters.add(new AuthorizationResourceFilter(ImmutableList.copyOf(permAnnotation.value()), permAnnotation.logical(), createSubstitutionMap(permAnnotation, am)));
{
final RequiresPermissions permAnnotation = am.getResource().getAnnotation(RequiresPermissions.class);
if (permAnnotation != null) {
filters.add(new AuthorizationResourceFilter(
ImmutableList.copyOf(permAnnotation.value()),
permAnnotation.logical(),
createSubstitutionMap(permAnnotation.value(), am)
));
}
}

// Check the method
permAnnotation = am.getAnnotation(RequiresPermissions.class);
if (permAnnotation != null) {
filters.add(new AuthorizationResourceFilter(ImmutableList.copyOf(permAnnotation.value()), permAnnotation.logical(), createSubstitutionMap(permAnnotation, am)));
{
final RequiresPermissions permAnnotation = am.getAnnotation(RequiresPermissions.class);
if (permAnnotation != null) {
filters.add(new AuthorizationResourceFilter(
ImmutableList.copyOf(permAnnotation.value()),
permAnnotation.logical(),
createSubstitutionMap(permAnnotation.value(), am)
));
}
}

// Check the parameters
{
for (Parameter parameter: am.getMethod().getParameters()) {
final ParamRequiresPermissions permAnnotation = parameter.getAnnotation(ParamRequiresPermissions.class);
if (permAnnotation != null) {
final QueryParam queryParamAnnotation = parameter.getAnnotation(QueryParam.class);
filters.add(new AuthorizationParameterFilter(
queryParamAnnotation.value(),
ImmutableList.copyOf(permAnnotation.value()),
adapt(permAnnotation.logical()),
createSubstitutionMap(permAnnotation.value(), am)
));
}
}
}

// If we're doing authorization or if authentication is explicitly requested then add it as the first filter
Expand All @@ -87,8 +122,15 @@ public List<ResourceFilter> create(AbstractMethod am) {
return filters;
}

private Map<String,Function<HttpRequestContext, String>> createSubstitutionMap(RequiresPermissions permAnnotation, AbstractMethod am) {
return createSubstitutionMap(permAnnotation.value(), am);
private Logical adapt(final ParamRequiresPermissions.CustomLogical customLogical) {
switch (customLogical) {
case AND:
return Logical.AND;
case OR:
return Logical.OR;
default:
throw new IllegalArgumentException();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.bazaarvoice.emodb.auth.jersey;

import com.bazaarvoice.emodb.auth.permissions.MatchingPermission;
import com.google.common.base.Function;
import com.sun.jersey.api.core.HttpRequestContext;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
import com.sun.jersey.spi.container.ContainerResponseFilter;
import com.sun.jersey.spi.container.ResourceFilter;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;

import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Resource filter for parameters which require authorization. The subject should already be authenticated prior
* to this filter executing.
*/
@SuppressWarnings("Duplicates")
public class AuthorizationParameterFilter implements ResourceFilter, ContainerRequestFilter {

private final String parameter;
private final String[] _permissions;
private final Logical _logical;
private final Map<String, Function<HttpRequestContext, String>> _substitutions;

public AuthorizationParameterFilter(String parameter,
List<String> permissions,
Logical logical,
Map<String, Function<HttpRequestContext, String>> substitutions) {
this.parameter = parameter;
_permissions = permissions.toArray(new String[permissions.size()]);
_logical = logical;
_substitutions = substitutions;
}

@Override
public ContainerRequestFilter getRequestFilter() {
return this;
}

@Override
public ContainerResponseFilter getResponseFilter() {
return null;
}

/**
* Authorizes the client for the annotated permissions. If any authorizations fail an {@link AuthorizationException}
* will be thrown, otherwise the original request is returned.
*/
@Override
public ContainerRequest filter(ContainerRequest request) {
if (request.getQueryParameters().containsKey(parameter)) {
Subject subject = ThreadContext.getSubject();

String[] permissions = resolvePermissions(request);

if (permissions.length == 1 || _logical == Logical.AND) {
// Shortcut call to check all permissions at once
subject.checkPermissions(permissions);
} else {
// Check each permission until any passes
boolean anyPermitted = false;
int p = 0;
while (!anyPermitted) {
try {
subject.checkPermission(permissions[p]);
anyPermitted = true;
} catch (AuthorizationException e) {
// If this is the last permission then pass the exception along
if (++p == permissions.length) {
throw e;
}
}
}
}
}

return request;
}

/**
* Resolves permissions based on the request. For example, if the annotation's permission is
* "get|{thing}" and the method's @Path annotation is "/resources/{thing}" then a request to
* "/resources/table" will resolve to the permission "get|table".
*/
private String[] resolvePermissions(ContainerRequest request) {
String[] values = _permissions;

if (_substitutions.isEmpty()) {
return values;
}

String[] permissions = new String[values.length];
System.arraycopy(values, 0, permissions, 0, values.length);

for (Map.Entry<String, Function<HttpRequestContext, String>> entry : _substitutions.entrySet()) {
String key = Pattern.quote(entry.getKey());
String substitution = Matcher.quoteReplacement(MatchingPermission.escape(entry.getValue().apply(request)));

for (int i=0; i < values.length; i++) {
permissions[i] = permissions[i].replaceAll(key, substitution);
}
}

return permissions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.sun.jersey.spi.container.ContainerRequestFilter;
import com.sun.jersey.spi.container.ContainerResponseFilter;
import com.sun.jersey.spi.container.ResourceFilter;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.subject.Subject;
Expand All @@ -21,6 +22,7 @@
* Resource filter for methods which require authorization. The subject should already be authenticated prior
* to this filter executing.
*/
@SuppressWarnings("Duplicates")
public class AuthorizationResourceFilter implements ResourceFilter, ContainerRequestFilter {

private final String[] _permissions;
Expand Down
Loading

0 comments on commit 16a71b4

Please sign in to comment.