Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Feature: Hidden (aka Private) attributes #92

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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