Last active
December 20, 2015 07:09
-
-
Save tbroyer/6091533 to your computer and use it in GitHub Desktop.
ServiceLayerDecorator for RequestFactory that enforces javax.annotation.security annotations on service methods.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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;