Skip to content

Instantly share code, notes, and snippets.

@tbroyer
Last active December 20, 2015 07:09
Show Gist options
  • Save tbroyer/6091533 to your computer and use it in GitHub Desktop.
Save tbroyer/6091533 to your computer and use it in GitHub Desktop.
ServiceLayerDecorator for RequestFactory that enforces javax.annotation.security annotations on service methods.
/*
* Copyright 2013 Thomas Broyer <[email protected]>
*
* 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 net.ltgt.web.bindery.requestfactory.security;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.web.bindery.requestfactory.server.ServiceLayerDecorator;
public class SecurityServiceLayer extends ServiceLayerDecorator {
private static final Logger log = Logger.getLogger(SecurityServiceLayer.class.getName());
@Inject
Provider<HttpServletRequest> requestProvider;
@Override
public Object invoke(Method domainMethod, Object... args) {
Boolean allowed = isAllowed(domainMethod);
if (allowed == null) {
// No annotation on the method: let's look on the class.
Class<?> domainClass;
if (Modifier.isStatic(domainMethod.getModifiers())) {
domainClass = domainMethod.getDeclaringClass();
} else {
domainClass = args[0].getClass();
}
allowed = isAllowed(domainClass);
if (allowed == null) {
// No annotation on the class: autorize by default.
// FIXME: should this be denied by default, for better security?
// The code is guarded anyway by the DeobfuscatorBuilder, so authorized by
// default should be OK.
allowed = Boolean.TRUE;
}
}
if (allowed.booleanValue()) {
return doInvoke(domainMethod, args);
} else {
return doReport(domainMethod);
}
}
/**
* Returns {@link Boolean#TRUE} if explicitly autorized, {@link Boolean#FALSE} if explicitly forbidden,
* or {@code null} if unspecified.
*/
private Boolean isAllowed(AnnotatedElement obj) {
if (obj.isAnnotationPresent(DenyAll.class)) {
return Boolean.FALSE;
}
RolesAllowed rolesAllowed = obj.getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
HttpServletRequest request = requestProvider.get();
for (String roleAllowed : rolesAllowed.value()) {
if (request.isUserInRole(roleAllowed)) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
if (obj.isAnnotationPresent(PermitAll.class)) {
return Boolean.TRUE;
}
return null;
}
@VisibleForTesting
protected Object doInvoke(Method domainMethod, Object... args) {
return super.invoke(domainMethod, args);
}
@VisibleForTesting
protected Object doReport(Method domainMethod) {
log.log(Level.INFO, "Operation {0}#{1} not allowed for user {2}",
new String[] {
domainMethod.getDeclaringClass().getCanonicalName(),
domainMethod.getName(),
requestProvider.get().getRemoteUser()
});
return report("Operation not allowed: %s", domainMethod.getName());
}
}
/*
* Copyright 2013 Thomas Broyer <[email protected]>
*
* 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 net.ltgt.web.bindery.requestfactory.security;
import static org.junit.Assert.assertSame;
import static org.mockito.BDDMockito.given;
import java.lang.reflect.Method;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import com.google.inject.util.Providers;
@RunWith(MockitoJUnitRunner.class)
public class SecurityServiceLayerTest {
static final Object SUCCESS_MARKER = new Object();
static final Object FAILURE_MARKER = new Object();
public static class Unspecified {
public static void staticUnspecified() { }
@PermitAll
public static void staticAllowed() { }
@DenyAll
public static void staticDenied() { }
@RolesAllowed({ "foo", "bar" })
public static void staticAllowedToFooAndBar() { }
@RolesAllowed("foo")
public static void staticAllowedToFoo() { }
public void unspecified() { }
@PermitAll
public void allowed() { }
@DenyAll
public void denied() { }
@RolesAllowed({ "foo", "bar" })
public void allowedToFooAndBar() { }
@RolesAllowed("foo")
public void allowedToFoo() { }
}
@PermitAll
public static class Allowed {
public static void staticUnspecified() { }
@DenyAll
public static void staticDenied() { }
@RolesAllowed("foo")
public static void staticAllowedToFoo() { }
public void unspecified() { }
@DenyAll
public void denied() { }
@RolesAllowed("foo")
public void allowedToFoo() { }
}
@RolesAllowed({ "foo", "bar" })
public static class AllowedToFooAndBar {
public static void staticUnspecified() { }
@DenyAll
public static void staticDenied() { }
@RolesAllowed("foo")
public static void staticAllowedToFoo() { }
public void unspecified() { }
@DenyAll
public void denied() { }
@RolesAllowed("foo")
public void allowedToFoo() { }
}
@RolesAllowed("foo")
public static class AllowedToFoo {
public static void staticUnspecified() { }
@PermitAll
public static void staticAllowed() { }
@RolesAllowed({ "foo", "bar" })
public static void staticAllowedToFooAndBar() { }
public void unspecified() { }
@PermitAll
public void allowed() { }
@RolesAllowed({ "foo", "bar" })
public void allowedToFooAndBar() { }
}
@Mock
HttpServletRequest request;
SecurityServiceLayer securitySL;
@Before
public void setup() {
securitySL = new SecurityServiceLayer() {
@Override
protected Object doInvoke(Method domainMethod, Object... args) {
return SUCCESS_MARKER;
}
protected Object doReport(Method domainMethod) {
return FAILURE_MARKER;
}
};
securitySL.requestProvider = Providers.of(request);
given(request.isUserInRole("bar")).willReturn(true);
}
@Test
public void shouldAllowIfUnspecifiedAccessControl() throws Throwable {
assertAllowed(Unspecified.class, "staticUnspecified");
assertAllowed(Unspecified.class, "unspecified", new Unspecified());
}
@Test
public void shouldAllowIfPermitAll() throws Throwable {
assertAllowed(Unspecified.class, "staticAllowed");
assertAllowed(Unspecified.class, "allowed", new Unspecified());
assertAllowed(Allowed.class, "staticUnspecified");
assertAllowed(Allowed.class, "unspecified", new Allowed());
}
@Test
public void shouldDenyIfDenyAll() throws Throwable {
assertDenied(Unspecified.class, "staticDenied");
assertDenied(Unspecified.class, "denied", new Unspecified());
}
@Test
public void shouldAllowIfAllowedToRole() throws Throwable {
assertAllowed(Unspecified.class, "staticAllowedToFooAndBar");
assertAllowed(Unspecified.class, "allowedToFooAndBar", new Unspecified());
assertAllowed(AllowedToFooAndBar.class, "staticUnspecified");
assertAllowed(AllowedToFooAndBar.class, "unspecified", new AllowedToFooAndBar());
}
@Test
public void shouldDenyIfNotAllowedToRole() throws Throwable {
assertDenied(Unspecified.class, "staticAllowedToFoo");
assertDenied(Unspecified.class, "allowedToFoo", new Unspecified());
assertDenied(AllowedToFoo.class, "staticUnspecified");
assertDenied(AllowedToFoo.class, "unspecified", new AllowedToFoo());
}
@Test
public void shouldUseMethodLevelOverClassLevelAnnotations() throws Throwable {
assertMethodOverridesClassLevelAllowed(Allowed.class, new Allowed());
assertMethodOverridesClassLevelAllowed(AllowedToFooAndBar.class, new AllowedToFooAndBar());
assertMethodOverridesClassLevelDenied(AllowedToFoo.class, new AllowedToFoo());
}
@SuppressWarnings("unchecked")
private <T> void assertMethodOverridesClassLevelAllowed(Class<T> clazz, T instance) throws Throwable {
assertDenied(clazz, "staticDenied");
assertDenied(clazz, "staticAllowedToFoo");
assertDenied(clazz, "denied", instance);
assertDenied(clazz, "allowedToFoo", instance);
}
@SuppressWarnings("unchecked")
private <T> void assertMethodOverridesClassLevelDenied(Class<T> clazz, T instance) throws Throwable {
assertAllowed(clazz, "staticAllowed");
assertAllowed(clazz, "staticAllowedToFooAndBar");
assertAllowed(clazz, "allowed", instance);
assertAllowed(clazz, "allowedToFooAndBar", instance);
}
@Test
public void shouldNotLookupAnnotationsOnOverriddenMethods() throws Throwable {
assertAllowed(Unspecified.class, "allowed", new Unspecified() {
@Override
@DenyAll
public void allowed() {
// There's a DenyAll here, but we invoke the original method, which has a PermitAll.
// The PermitAll from the super method is the expected rule.
}
});
assertDenied(Unspecified.class, "denied", new Unspecified() {
@Override
@PermitAll
public void denied() {
// There's a PermitAll here, but we invoke the original method, which has a DenyAll.
// The DenyAll from the super method is the expected rule.
}
});
}
// The vararg is used to get an optional argument.
private <T> void assertAllowed(Class<T> clazz, String methodName, T... instance) throws Throwable {
assertResult(SUCCESS_MARKER, clazz, methodName, instance);
}
// The vararg is used to get an optional argument.
private <T> void assertDenied(Class<T> clazz, String methodName, T... instance) throws Throwable {
assertResult(FAILURE_MARKER, clazz, methodName, instance);
}
// The vararg is used to get an optional argument.
private <T> void assertResult(Object resultMarker, Class<T> clazz, String methodName, T... instance) throws Throwable {
assertSame(resultMarker, securitySL.invoke(clazz.getMethod(methodName), instance));
}
}
@PaulMazzuca
Copy link

This is quite helpful. Thanks. I have the ServiceLayerDecorator functioning properly with exception to getting access to the HttpServletRequest from within the invoke. How are you injecting the class as per line.
@Inject
Provider requestProvider;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment