killbill-uncached
Changes
util/pom.xml 8(+8 -0)
util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java 39(+39 -0)
util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInvocationAdapter.java 51(+51 -0)
Details
util/pom.xml 8(+8 -0)
diff --git a/util/pom.xml b/util/pom.xml
index b23d0db..7116c83 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -160,6 +160,14 @@
<artifactId>commons-email</artifactId>
</dependency>
<dependency>
+ <groupId>org.apache.shiro</groupId>
+ <artifactId>shiro-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.shiro</groupId>
+ <artifactId>shiro-guice</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi</artifactId>
</dependency>
diff --git a/util/src/main/java/com/ning/billing/util/glue/SecurityModule.java b/util/src/main/java/com/ning/billing/util/glue/SecurityModule.java
new file mode 100644
index 0000000..ff0d279
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/glue/SecurityModule.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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 com.ning.billing.util.glue;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.apache.shiro.aop.AnnotationMethodInterceptor;
+import org.apache.shiro.aop.AnnotationResolver;
+import org.apache.shiro.guice.aop.ShiroAopModule;
+
+import com.ning.billing.util.security.AnnotationHierarchicalResolver;
+import com.ning.billing.util.security.AopAllianceMethodInterceptorAdapter;
+import com.ning.billing.util.security.PermissionAnnotationMethodInterceptor;
+
+import com.google.inject.matcher.AbstractMatcher;
+import com.google.inject.matcher.Matchers;
+
+public class SecurityModule extends ShiroAopModule {
+
+ private final AnnotationHierarchicalResolver resolver = new AnnotationHierarchicalResolver();
+
+ @Override
+ protected AnnotationResolver createAnnotationResolver() {
+ return resolver;
+ }
+
+ @Override
+ protected void configureInterceptors(final AnnotationResolver resolver) {
+ super.configureInterceptors(resolver);
+
+ bindShiroInterceptorWithHierarchy(new PermissionAnnotationMethodInterceptor(resolver));
+ }
+
+ // Similar to bindShiroInterceptor but will look for annotations in the class hierarchy
+ protected final void bindShiroInterceptorWithHierarchy(final AnnotationMethodInterceptor methodInterceptor) {
+ bindInterceptor(Matchers.any(),
+ new AbstractMatcher<Method>() {
+ public boolean matches(final Method method) {
+ final Class<? extends Annotation> annotation = methodInterceptor.getHandler().getAnnotationClass();
+ return resolver.getAnnotationFromMethod(method, annotation) != null;
+ }
+ }, new AopAllianceMethodInterceptorAdapter(methodInterceptor));
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/security/AnnotationHierarchicalResolver.java b/util/src/main/java/com/ning/billing/util/security/AnnotationHierarchicalResolver.java
new file mode 100644
index 0000000..0f7b563
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/AnnotationHierarchicalResolver.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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 com.ning.billing.util.security;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.apache.shiro.aop.AnnotationResolver;
+import org.apache.shiro.aop.MethodInvocation;
+
+public class AnnotationHierarchicalResolver implements AnnotationResolver {
+
+ @Override
+ public Annotation getAnnotation(final MethodInvocation mi, final Class<? extends Annotation> clazz) {
+ return getAnnotationFromMethod(mi.getMethod(), clazz);
+ }
+
+ public Annotation getAnnotationFromMethod(final Method method, final Class<? extends Annotation> clazz) {
+ return findAnnotation(method, clazz);
+ }
+
+ // The following comes from spring-core (AnnotationUtils) to handle annotations on interfaces
+
+ /**
+ * Get a single {@link Annotation} of <code>annotationType</code> from the supplied {@link java.lang.reflect.Method},
+ * traversing its super methods if no annotation can be found on the given method itself.
+ * <p>Annotations on methods are not inherited by default, so we need to handle this explicitly.
+ *
+ * @param method the method to look for annotations on
+ * @param annotationType the annotation class to look for
+ * @return the annotation found, or <code>null</code> if none found
+ */
+ public static <A extends Annotation> A findAnnotation(final Method method, final Class<A> annotationType) {
+ A annotation = getAnnotation(method, annotationType);
+ Class<?> cl = method.getDeclaringClass();
+ if (annotation == null) {
+ annotation = searchOnInterfaces(method, annotationType, cl.getInterfaces());
+ }
+ while (annotation == null) {
+ cl = cl.getSuperclass();
+ if (cl == null || cl == Object.class) {
+ break;
+ }
+ try {
+ final Method equivalentMethod = cl.getDeclaredMethod(method.getName(), method.getParameterTypes());
+ annotation = getAnnotation(equivalentMethod, annotationType);
+ if (annotation == null) {
+ annotation = searchOnInterfaces(method, annotationType, cl.getInterfaces());
+ }
+ } catch (NoSuchMethodException ex) {
+ // We're done...
+ }
+ }
+ return annotation;
+ }
+
+ /**
+ * Get a single {@link Annotation} of <code>annotationType</code> from the supplied {@link Method}.
+ *
+ * @param method the method to look for annotations on
+ * @param annotationType the annotation class to look for
+ * @return the annotations found
+ */
+ public static <A extends Annotation> A getAnnotation(final Method method, final Class<A> annotationType) {
+ A ann = method.getAnnotation(annotationType);
+ if (ann == null) {
+ for (final Annotation metaAnn : method.getAnnotations()) {
+ ann = metaAnn.annotationType().getAnnotation(annotationType);
+ if (ann != null) {
+ break;
+ }
+ }
+ }
+ return ann;
+ }
+
+ private static <A extends Annotation> A searchOnInterfaces(final Method method, final Class<A> annotationType, final Class<?>[] ifcs) {
+ A annotation = null;
+ for (final Class<?> iface : ifcs) {
+ if (isInterfaceWithAnnotatedMethods(iface)) {
+ try {
+ final Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes());
+ annotation = getAnnotation(equivalentMethod, annotationType);
+ } catch (NoSuchMethodException ex) {
+ // Skip this interface - it doesn't have the method...
+ }
+ if (annotation != null) {
+ break;
+ }
+ }
+ }
+ return annotation;
+ }
+
+ private static final Map<Class<?>, Boolean> annotatedInterfaceCache = new WeakHashMap<Class<?>, Boolean>();
+
+ private static boolean isInterfaceWithAnnotatedMethods(final Class<?> iface) {
+ synchronized (annotatedInterfaceCache) {
+ final Boolean flag = annotatedInterfaceCache.get(iface);
+ if (flag != null) {
+ return flag;
+ }
+ boolean found = false;
+ for (final Method ifcMethod : iface.getMethods()) {
+ if (ifcMethod.getAnnotations().length > 0) {
+ found = true;
+ break;
+ }
+ }
+ annotatedInterfaceCache.put(iface, found);
+ return found;
+ }
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java b/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java
new file mode 100644
index 0000000..a45005c
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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 com.ning.billing.util.security;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+// Taken from Shiro - the original class is private :(
+public class AopAllianceMethodInterceptorAdapter implements MethodInterceptor {
+
+ org.apache.shiro.aop.MethodInterceptor shiroInterceptor;
+
+ public AopAllianceMethodInterceptorAdapter(org.apache.shiro.aop.MethodInterceptor shiroInterceptor) {
+ this.shiroInterceptor = shiroInterceptor;
+ }
+
+ public Object invoke(final MethodInvocation invocation) throws Throwable {
+ return shiroInterceptor.invoke(new AopAllianceMethodInvocationAdapter(invocation));
+ }
+
+ @Override
+ public String toString() {
+ return "AopAlliance Adapter for " + shiroInterceptor.toString();
+ }
+}
\ No newline at end of file
diff --git a/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInvocationAdapter.java b/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInvocationAdapter.java
new file mode 100644
index 0000000..1b93956
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInvocationAdapter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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 com.ning.billing.util.security;
+
+import java.lang.reflect.Method;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+// Taken from Shiro - the original class is private :(
+public class AopAllianceMethodInvocationAdapter implements org.apache.shiro.aop.MethodInvocation {
+
+ private final MethodInvocation mi;
+
+ public AopAllianceMethodInvocationAdapter(final MethodInvocation mi) {
+ this.mi = mi;
+ }
+
+ public Method getMethod() {
+ return mi.getMethod();
+ }
+
+ public Object[] getArguments() {
+ return mi.getArguments();
+ }
+
+ public String toString() {
+ return "Method invocation [" + mi.getMethod() + "]";
+ }
+
+ public Object proceed() throws Throwable {
+ return mi.proceed();
+ }
+
+ public Object getThis() {
+ return mi.getThis();
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationHandler.java b/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationHandler.java
new file mode 100644
index 0000000..b9b1c50
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationHandler.java
@@ -0,0 +1,68 @@
+package com.ning.billing.util.security;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import java.lang.annotation.Annotation;
+
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.aop.AuthorizingAnnotationHandler;
+import org.apache.shiro.subject.Subject;
+
+import com.ning.billing.security.Logical;
+import com.ning.billing.security.RequiresPermissions;
+
+public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {
+
+ public PermissionAnnotationHandler() {
+ super(RequiresPermissions.class);
+ }
+
+ public void assertAuthorized(final Annotation annotation) throws AuthorizationException {
+ if (!(annotation instanceof RequiresPermissions)) {
+ return;
+ }
+
+ final RequiresPermissions requiresPermissions = (RequiresPermissions) annotation;
+ final String[] permissions = new String[requiresPermissions.value().length];
+ for (int i = 0; i < permissions.length; i++) {
+ permissions[i] = requiresPermissions.value()[i].toString();
+ }
+
+ final Subject subject = getSubject();
+ if (permissions.length == 1) {
+ subject.checkPermission(permissions[0]);
+ } else if (Logical.AND.equals(requiresPermissions.logical())) {
+ subject.checkPermissions(permissions);
+ } else if (Logical.OR.equals(requiresPermissions.logical())) {
+ boolean hasAtLeastOnePermission = false;
+ for (final String permission : permissions) {
+ if (subject.isPermitted(permission)) {
+ hasAtLeastOnePermission = true;
+ break;
+ }
+ }
+
+ // Cause the exception if none match
+ if (!hasAtLeastOnePermission) {
+ getSubject().checkPermission(permissions[0]);
+ }
+ }
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java b/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java
new file mode 100644
index 0000000..cc3c837
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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 com.ning.billing.util.security;
+
+import org.apache.shiro.aop.AnnotationResolver;
+import org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor;
+
+public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {
+
+ public PermissionAnnotationMethodInterceptor(final AnnotationResolver resolver) {
+ super(new PermissionAnnotationHandler(), resolver);
+ }
+}
\ No newline at end of file
diff --git a/util/src/test/java/com/ning/billing/util/security/TestPermissionAnnotationMethodInterceptor.java b/util/src/test/java/com/ning/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
new file mode 100644
index 0000000..8978060
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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 com.ning.billing.util.security;
+
+import javax.inject.Singleton;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.UnauthenticatedException;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.config.IniSecurityManagerFactory;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.Factory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.security.Permission;
+import com.ning.billing.security.RequiresPermissions;
+import com.ning.billing.util.UtilTestSuiteNoDB;
+import com.ning.billing.util.glue.SecurityModule;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Stage;
+
+public class TestPermissionAnnotationMethodInterceptor extends UtilTestSuiteNoDB {
+
+ public static interface IAopTester {
+
+ @RequiresPermissions(Permission.PAYMENT_CAN_REFUND)
+ public void createRefund();
+ }
+
+ public static class AopTesterImpl implements IAopTester {
+
+ @Override
+ public void createRefund() {}
+ }
+
+ @Singleton
+ public static class AopTester implements IAopTester {
+
+ @RequiresPermissions(Permission.PAYMENT_CAN_REFUND)
+ public void createRefund() {}
+ }
+
+ @Test(groups = "fast")
+ public void testAOPForClass() throws Exception {
+ // Make sure it works as expected without any AOP magic
+ final IAopTester simpleTester = new AopTester();
+ try {
+ simpleTester.createRefund();
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // Now, verify the interception works
+ configureShiro();
+ final Injector injector = Guice.createInjector(Stage.PRODUCTION, new SecurityModule());
+ final AopTester aopedTester = injector.getInstance(AopTester.class);
+ verifyAopedTester(aopedTester);
+ }
+
+ @Test(groups = "fast")
+ public void testAOPForInterface() throws Exception {
+ // Make sure it works as expected without any AOP magic
+ final IAopTester simpleTester = new AopTesterImpl();
+ try {
+ simpleTester.createRefund();
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // Now, verify the interception works
+ configureShiro();
+ final Injector injector = Guice.createInjector(Stage.PRODUCTION,
+ new SecurityModule(),
+ new Module() {
+ @Override
+ public void configure(final Binder binder) {
+ binder.bind(IAopTester.class).to(AopTesterImpl.class).asEagerSingleton();
+ }
+ });
+ final IAopTester aopedTester = injector.getInstance(IAopTester.class);
+ verifyAopedTester(aopedTester);
+ }
+
+ private void verifyAopedTester(final IAopTester aopedTester) {
+ // Anonymous user
+ logout();
+ try {
+ aopedTester.createRefund();
+ Assert.fail();
+ } catch (UnauthenticatedException e) {
+ // Good!
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // pierre can credit, but not refund
+ login("pierre");
+ try {
+ aopedTester.createRefund();
+ Assert.fail();
+ } catch (AuthorizationException e) {
+ // Good!
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // stephane can refund
+ login("stephane");
+ aopedTester.createRefund();
+ }
+
+ private void login(final String username) {
+ logout();
+
+ final AuthenticationToken token = new UsernamePasswordToken(username, "password");
+ final Subject currentUser = SecurityUtils.getSubject();
+ currentUser.login(token);
+ }
+
+ private void logout() {
+ final Subject currentUser = SecurityUtils.getSubject();
+ if (currentUser.isAuthenticated()) {
+ currentUser.logout();
+ }
+ }
+
+ private void configureShiro() {
+ final Ini config = new Ini();
+ config.addSection("users");
+ config.getSection("users").put("pierre", "password, creditor");
+ config.getSection("users").put("stephane", "password, refunder");
+ config.addSection("roles");
+ config.getSection("roles").put("creditor", Permission.INVOICE_CAN_CREDIT.toString());
+ config.getSection("roles").put("refunder", Permission.PAYMENT_CAN_REFUND.toString());
+
+ final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config);
+ final SecurityManager securityManager = factory.getInstance();
+ SecurityUtils.setSecurityManager(securityManager);
+ }
+}