killbill-memoizeit

jaxrs: add endpoint to retrieve RBAC permissions This commit

8/13/2013 8:01:20 PM

Changes

Details

diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
index fd2abb3..2c82fa6 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -73,6 +73,19 @@ public abstract class ExceptionMapperBase {
                        .build();
     }
 
+    protected Response buildAuthorizationErrorResponse(final Exception e, final UriInfo uriInfo) {
+        // Log the full stacktrace
+        log.warn("Authorization error", e);
+        return buildAuthorizationErrorResponse(exceptionToString(e), uriInfo);
+    }
+
+    private Response buildAuthorizationErrorResponse(final String error, final UriInfo uriInfo) {
+        return Response.status(Status.UNAUTHORIZED) // TODO Forbidden?
+                       .entity(error)
+                       .type(MediaType.TEXT_PLAIN_TYPE)
+                       .build();
+    }
+
     protected Response buildInternalErrorResponse(final Exception e, final UriInfo uriInfo) {
         // Log the full stacktrace
         log.warn("Internal error", e);
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ShiroExceptionMapper.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ShiroExceptionMapper.java
new file mode 100644
index 0000000..4558b1c
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ShiroExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * 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.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.shiro.ShiroException;
+
+@Singleton
+@Provider
+public class ShiroExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<ShiroException> {
+
+    private final UriInfo uriInfo;
+
+    public ShiroExceptionMapper(@Context final UriInfo uriInfo) {
+        this.uriInfo = uriInfo;
+    }
+
+    @Override
+    public Response toResponse(final ShiroException exception) {
+        return buildAuthorizationErrorResponse(exception, uriInfo);
+    }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index c1c2817..015fdc0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -96,6 +96,9 @@ public interface JaxrsResource {
     public static final String BUNDLES = "bundles";
     public static final String BUNDLES_PATH = PREFIX + "/" + BUNDLES;
 
+    public static final String SECURITY = "security";
+    public static final String SECURITY_PATH = PREFIX + "/" + SECURITY;
+
     public static final String SUBSCRIPTIONS = "subscriptions";
     public static final String SUBSCRIPTIONS_PATH = PREFIX + "/" + SUBSCRIPTIONS;
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SecurityResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SecurityResource.java
new file mode 100644
index 0000000..428e401
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SecurityResource.java
@@ -0,0 +1,72 @@
+/*
+ * 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.jaxrs.resources;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Singleton;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import com.ning.billing.jaxrs.util.Context;
+import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
+import com.ning.billing.security.Permission;
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.subscription.api.user.SubscriptionUserApiException;
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.api.CustomFieldUserApi;
+import com.ning.billing.util.api.TagUserApi;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.SECURITY_PATH)
+public class SecurityResource extends JaxRsResourceBase {
+
+    private final SecurityApi securityApi;
+
+    @Inject
+    public SecurityResource(final SecurityApi securityApi,
+                            final JaxrsUriBuilder uriBuilder,
+                            final TagUserApi tagUserApi,
+                            final CustomFieldUserApi customFieldUserApi,
+                            final AuditUserApi auditUserApi,
+                            final Context context) {
+        super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, context);
+        this.securityApi = securityApi;
+    }
+
+    @GET
+    @Path("/permissions")
+    @Produces(APPLICATION_JSON)
+    public Response getCurrentUserPermissions(@javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionUserApiException {
+        final Set<Permission> permissions = securityApi.getCurrentUserPermissions(context.createContext(request));
+        final List<String> json = ImmutableList.<String>copyOf(Iterables.<Permission, String>transform(permissions, Functions.toStringFunction()));
+        return Response.status(Status.OK).entity(json).build();
+    }
+
+}
diff --git a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
index cabcf5a..85b9f47 100644
--- a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
+++ b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
@@ -65,6 +65,7 @@ import com.ning.billing.util.glue.GlobalLockerModule;
 import com.ning.billing.util.glue.NonEntityDaoModule;
 import com.ning.billing.util.glue.NotificationQueueModule;
 import com.ning.billing.util.glue.RecordIdModule;
+import com.ning.billing.util.glue.SecurityModule;
 import com.ning.billing.util.glue.TagStoreModule;
 
 import com.google.inject.AbstractModule;
@@ -146,6 +147,7 @@ public class KillbillServerModule extends AbstractModule {
         install(new DefaultOSGIModule(configSource));
         install(new UsageModule(configSource));
         install(new RecordIdModule());
+        install(new SecurityModule());
 
         installClock();
     }
diff --git a/server/src/main/webapp/WEB-INF/shiro.ini b/server/src/main/webapp/WEB-INF/shiro.ini
index dd1d115..4eb4e39 100644
--- a/server/src/main/webapp/WEB-INF/shiro.ini
+++ b/server/src/main/webapp/WEB-INF/shiro.ini
@@ -24,6 +24,21 @@ sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
 # Use the configured native session manager
 securityManager.sessionManager = $sessionManager
 
+# Example on how to define an admin user
+#
+# [users]
+# admin = password, root
+#
+# [roles]
+# root = *:*
+
 [urls]
+# All urls omitted will be available by anonymous users (RBAC disabled).
+# You need to enable auth at least for the security endpoint though, otherwise
+# Shiro won't try to look up the username/password (so, it won't be able
+# to return the correct permissions).
+/1.0/kb/security/** = authcBasic
 # RBAC disabled by default
 /** = anon
+# To enable RBAC
+# /1.0/kb/** = authcBasic
diff --git a/server/src/test/java/com/ning/billing/jaxrs/KillbillClient.java b/server/src/test/java/com/ning/billing/jaxrs/KillbillClient.java
index 4534858..97d1d74 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/KillbillClient.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/KillbillClient.java
@@ -67,6 +67,8 @@ import com.ning.http.client.AsyncCompletionHandler;
 import com.ning.http.client.AsyncHttpClient;
 import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder;
 import com.ning.http.client.ListenableFuture;
+import com.ning.http.client.Realm;
+import com.ning.http.client.Realm.AuthScheme;
 import com.ning.http.client.Response;
 import com.ning.jetty.core.CoreConfig;
 
@@ -75,6 +77,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableMap;
+import com.google.inject.TypeLiteral;
 
 import static com.ning.billing.jaxrs.resources.JaxrsResource.ACCOUNTS;
 import static com.ning.billing.jaxrs.resources.JaxrsResource.BUNDLES;
@@ -110,6 +113,10 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
     protected String apiKey = DEFAULT_API_KEY;
     protected String apiSecret = DEFAULT_API_SECRET;
 
+    // RBAC information, if enabled
+    protected String username = null;
+    protected String password = null;
+
     // Context information to be passed around
     protected static final String createdBy = "Toto";
     protected static final String reason = "i am god";
@@ -165,6 +172,40 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
     }
 
     //
+    // SECURITY UTILITIES
+    //
+
+    protected void loginAsAdmin() {
+        this.username = "tester";
+        this.password = "tester";
+    }
+
+    protected void logout() {
+        this.username = null;
+        this.password = null;
+    }
+
+    protected List<String> getPermissions(@Nullable final String username, @Nullable final String password) throws Exception {
+        final String oldUsername = this.username;
+        final String oldPassword = this.password;
+
+        this.username = username;
+        this.password = password;
+
+        final Response response = doGet(JaxrsResource.SECURITY_PATH + "/permissions", DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+
+        this.username = oldUsername;
+        this.password = oldPassword;
+
+        final String baseJson = response.getResponseBody();
+        final List<String> objFromJson = mapper.readValue(baseJson, new TypeReference<List<String>>() {});
+        Assert.assertNotNull(objFromJson);
+
+        return objFromJson;
+    }
+
+    //
     // ACCOUNT UTILITIES
     //
 
@@ -999,6 +1040,16 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
             builder.addHeader(JaxrsResource.HDR_COMMENT, comment);
         }
 
+        if (username != null && password != null) {
+            final Realm realm = new Realm.RealmBuilder()
+                    .setPrincipal(username)
+                    .setPassword(password)
+                    .setUsePreemptiveAuth(true)
+                    .setScheme(AuthScheme.BASIC)
+                    .build();
+            builder.setRealm(realm);
+        }
+
         Response response = null;
         try {
             final ListenableFuture<Response> futureStatus =
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index 34ea64e..7aaf3c6 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -29,6 +29,9 @@ import javax.servlet.Servlet;
 import com.ning.billing.DBTestingHelper;
 import com.ning.billing.commons.embeddeddb.EmbeddedDB;
 import com.ning.billing.entitlement.glue.DefaultEntitlementModule;
+
+import org.apache.shiro.web.env.EnvironmentLoaderListener;
+import org.apache.shiro.web.servlet.ShiroFilter;
 import org.eclipse.jetty.servlet.FilterHolder;
 import org.joda.time.LocalDate;
 import org.skife.config.ConfigSource;
@@ -75,6 +78,7 @@ import com.ning.billing.util.glue.ExportModule;
 import com.ning.billing.util.glue.NonEntityDaoModule;
 import com.ning.billing.util.glue.NotificationQueueModule;
 import com.ning.billing.util.glue.RecordIdModule;
+import com.ning.billing.util.glue.SecurityModule;
 import com.ning.billing.util.glue.TagStoreModule;
 import com.ning.http.client.AsyncHttpClient;
 import com.ning.http.client.AsyncHttpClientConfig;
@@ -85,6 +89,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.datatype.joda.JodaModule;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Module;
 
 import static org.testng.Assert.assertNotNull;
@@ -222,6 +227,7 @@ public class TestJaxrsBase extends KillbillClient {
             install(new UsageModule(configSource));
             install(new RecordIdModule());
             installClock();
+            install(new SecurityModule());
         }
     }
 
@@ -235,6 +241,8 @@ public class TestJaxrsBase extends KillbillClient {
         clock.resetDeltaFromReality();
         clock.setDay(new LocalDate(2012, 8, 25));
 
+        loginAsAdmin();
+
         // Recreate the tenant (tables have been cleaned-up)
         createTenant(DEFAULT_API_KEY, DEFAULT_API_SECRET);
     }
@@ -288,13 +296,16 @@ public class TestJaxrsBase extends KillbillClient {
         return new Iterable<EventListener>() {
             @Override
             public Iterator<EventListener> iterator() {
-                return ImmutableList.<EventListener>of(listener).iterator();
+                // Note! This needs to be in sync with web.xml
+                return ImmutableList.<EventListener>of(listener,
+                                                       new EnvironmentLoaderListener()).iterator();
             }
         };
     }
 
     protected Map<FilterHolder, String> getFilters() {
-        return new HashMap<FilterHolder, String>();
+        // Note! This needs to be in sync with web.xml
+        return ImmutableMap.<FilterHolder, String>of(new FilterHolder(new ShiroFilter()), "/*");
     }
 
     @AfterSuite(groups = "slow")
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestSecurity.java b/server/src/test/java/com/ning/billing/jaxrs/TestSecurity.java
new file mode 100644
index 0000000..1c5e705
--- /dev/null
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestSecurity.java
@@ -0,0 +1,52 @@
+/*
+ * 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.jaxrs;
+
+import java.util.HashSet;
+import java.util.List;
+
+import javax.ws.rs.core.Response.Status;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.jaxrs.resources.JaxrsResource;
+import com.ning.billing.security.Permission;
+import com.ning.http.client.Response;
+
+import com.google.common.collect.ImmutableSet;
+
+public class TestSecurity extends TestJaxrsBase {
+
+    @Test(groups = "slow")
+    public void testPermissions() throws Exception {
+        logout();
+
+        final Response anonResponse = doGet(JaxrsResource.SECURITY_PATH + "/permissions", DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(anonResponse.getStatusCode(), Status.UNAUTHORIZED.getStatusCode());
+
+        // See src/test/resources/shiro.ini
+
+        final List<String> pierresPermissions = getPermissions("pierre", "password");
+        Assert.assertEquals(pierresPermissions.size(), 2);
+        Assert.assertEquals(new HashSet<String>(pierresPermissions), ImmutableSet.<String>of(Permission.INVOICE_CAN_CREDIT.toString(), Permission.INVOICE_CAN_ITEM_ADJUST.toString()));
+
+        final List<String> stephanesPermissions = getPermissions("stephane", "password");
+        Assert.assertEquals(stephanesPermissions.size(), 1);
+        Assert.assertEquals(new HashSet<String>(stephanesPermissions), ImmutableSet.<String>of(Permission.PAYMENT_CAN_REFUND.toString()));
+    }
+}
diff --git a/server/src/test/resources/shiro.ini b/server/src/test/resources/shiro.ini
index 664ee13..ac4aa4c 100644
--- a/server/src/test/resources/shiro.ini
+++ b/server/src/test/resources/shiro.ini
@@ -24,14 +24,15 @@ sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
 # Use the configured native session manager
 securityManager.sessionManager = $sessionManager
 
-jdbcRealm=com.ning.billing.server.security.KillbillJdbcRealm
+[users]
+tester = tester, admin
+pierre = password, creditor
+stephane = password, refunder
+
+[roles]
+admin = *:*
+creditor = invoice:credit, invoice:item_adjust
+refunder = payment:refund
 
 [urls]
-# Special endpoints: healthcheck, tenant API.
-# TODO: don't secure them for now - eventually require admin privileges
-/1.0/healthcheck = anon
-/1.0/kb/tenants/** = anon
-# For all other resources, require basic auth
-# TODO: ssl, authcBasic
-# Commented out because that seems to break the server tests that don't require authentification
-#/1.0/kb/** = authcBasic
+/1.0/kb/** = authcBasic
diff --git a/util/src/main/java/com/ning/billing/util/config/SecurityConfig.java b/util/src/main/java/com/ning/billing/util/config/SecurityConfig.java
new file mode 100644
index 0000000..124299a
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/config/SecurityConfig.java
@@ -0,0 +1,29 @@
+/*
+ * 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.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface SecurityConfig extends KillbillConfig {
+
+    @Config("killbill.security.shiroResourcePath")
+    @Default("classpath:shiro.ini")
+    @Description("Path to the shiro.ini file (classpath, url or file resource)")
+    public String getShiroResourcePath();
+}
diff --git a/util/src/main/java/com/ning/billing/util/glue/SecurityApiProvider.java b/util/src/main/java/com/ning/billing/util/glue/SecurityApiProvider.java
new file mode 100644
index 0000000..7d88782
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/glue/SecurityApiProvider.java
@@ -0,0 +1,75 @@
+/*
+ * 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 javax.inject.Provider;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.UnavailableSecurityManagerException;
+import org.apache.shiro.config.ConfigurationException;
+import org.apache.shiro.config.IniSecurityManagerFactory;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.util.Factory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.util.config.SecurityConfig;
+import com.ning.billing.util.security.api.DefaultSecurityApi;
+import com.ning.billing.util.security.api.NoOpSecurityApi;
+
+public class SecurityApiProvider implements Provider<SecurityApi> {
+
+    private final Logger log = LoggerFactory.getLogger(SecurityApiProvider.class);
+
+    private final SecurityConfig securityConfig;
+
+    private boolean shiroConfigured;
+
+    public SecurityApiProvider(final SecurityConfig securityConfig) {
+        this.securityConfig = securityConfig;
+    }
+
+    @Override
+    public SecurityApi get() {
+        installShiro();
+
+        if (shiroConfigured) {
+            return new DefaultSecurityApi();
+        } else {
+            return new NoOpSecurityApi();
+        }
+    }
+
+    private void installShiro() {
+        try {
+            // Hook, mainly for testing
+            SecurityUtils.getSecurityManager();
+            shiroConfigured = true;
+        } catch (UnavailableSecurityManagerException ignored) {
+            try {
+                final Factory<SecurityManager> factory = new IniSecurityManagerFactory(securityConfig.getShiroResourcePath());
+                final SecurityManager securityManager = factory.getInstance();
+                SecurityUtils.setSecurityManager(securityManager);
+                shiroConfigured = true;
+            } catch (ConfigurationException e) {
+                log.warn(String.format("Unable to configure Shiro [%s] - RBAC WILL BE DISABLED!", e.getLocalizedMessage()));
+                shiroConfigured = false;
+            }
+        }
+    }
+}
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
index ff0d279..0c60e27 100644
--- a/util/src/main/java/com/ning/billing/util/glue/SecurityModule.java
+++ b/util/src/main/java/com/ning/billing/util/glue/SecurityModule.java
@@ -22,7 +22,12 @@ 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 org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
 
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.util.config.SecurityConfig;
 import com.ning.billing.util.security.AnnotationHierarchicalResolver;
 import com.ning.billing.util.security.AopAllianceMethodInterceptorAdapter;
 import com.ning.billing.util.security.PermissionAnnotationMethodInterceptor;
@@ -34,6 +39,36 @@ public class SecurityModule extends ShiroAopModule {
 
     private final AnnotationHierarchicalResolver resolver = new AnnotationHierarchicalResolver();
 
+    private final ConfigSource configSource;
+
+    private SecurityConfig securityConfig;
+    private SecurityApi securityApi;
+
+    public SecurityModule() {
+        this(new SimplePropertyConfigSource(System.getProperties()));
+    }
+
+    public SecurityModule(final ConfigSource configSource) {
+        super();
+        this.configSource = configSource;
+    }
+
+    // LAME - the configure method is final in ShiroAopModule so we piggy back configureInterceptors
+    private void doConfigure() {
+        installConfig();
+        installSecurityApi();
+    }
+
+    private void installConfig() {
+        securityConfig = new ConfigurationObjectFactory(configSource).build(SecurityConfig.class);
+        bind(SecurityConfig.class).toInstance(securityConfig);
+    }
+
+    private void installSecurityApi() {
+        securityApi = new SecurityApiProvider(securityConfig).get();
+        bind(SecurityApi.class).toInstance(securityApi);
+    }
+
     @Override
     protected AnnotationResolver createAnnotationResolver() {
         return resolver;
@@ -41,9 +76,11 @@ public class SecurityModule extends ShiroAopModule {
 
     @Override
     protected void configureInterceptors(final AnnotationResolver resolver) {
-        super.configureInterceptors(resolver);
+        // HACK
+        doConfigure();
 
-        bindShiroInterceptorWithHierarchy(new PermissionAnnotationMethodInterceptor(resolver));
+        super.configureInterceptors(resolver);
+        bindShiroInterceptorWithHierarchy(new PermissionAnnotationMethodInterceptor(securityApi, resolver));
     }
 
     // Similar to bindShiroInterceptor but will look for annotations in the class hierarchy
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
index a45005c..034ab28 100644
--- a/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java
+++ b/util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java
@@ -24,7 +24,7 @@ public class AopAllianceMethodInterceptorAdapter implements MethodInterceptor {
 
     org.apache.shiro.aop.MethodInterceptor shiroInterceptor;
 
-    public AopAllianceMethodInterceptorAdapter(org.apache.shiro.aop.MethodInterceptor shiroInterceptor) {
+    public AopAllianceMethodInterceptorAdapter(final org.apache.shiro.aop.MethodInterceptor shiroInterceptor) {
         this.shiroInterceptor = shiroInterceptor;
     }
 
diff --git a/util/src/main/java/com/ning/billing/util/security/api/DefaultSecurityApi.java b/util/src/main/java/com/ning/billing/util/security/api/DefaultSecurityApi.java
new file mode 100644
index 0000000..efddf40
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/api/DefaultSecurityApi.java
@@ -0,0 +1,103 @@
+/*
+ * 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.api;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.subject.Subject;
+
+import com.ning.billing.ErrorCode;
+import com.ning.billing.security.Logical;
+import com.ning.billing.security.Permission;
+import com.ning.billing.security.SecurityApiException;
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.util.callcontext.TenantContext;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.Lists;
+
+public class DefaultSecurityApi implements SecurityApi {
+
+    private static final String[] allPermissions = new String[Permission.values().length];
+
+    @Override
+    public Set<Permission> getCurrentUserPermissions(final TenantContext context) {
+        final Permission[] killbillPermissions = Permission.values();
+        final String[] killbillPermissionsString = getAllPermissionsAsStrings();
+
+        final Subject subject = SecurityUtils.getSubject();
+        // Bulk (optimized) call
+        final boolean[] permissions = subject.isPermitted(killbillPermissionsString);
+
+        final Set<Permission> userPermissions = new HashSet<Permission>();
+        for (int i = 0; i < permissions.length; i++) {
+            if (permissions[i]) {
+                userPermissions.add(killbillPermissions[i]);
+            }
+        }
+
+        return userPermissions;
+    }
+
+    @Override
+    public void checkCurrentUserPermissions(final List<Permission> permissions, final Logical logical, final TenantContext context) throws SecurityApiException {
+        final String[] permissionsString = Lists.<Permission, String>transform(permissions, Functions.toStringFunction()).toArray(new String[permissions.size()]);
+
+        try {
+            final Subject subject = SecurityUtils.getSubject();
+            if (permissionsString.length == 1) {
+                subject.checkPermission(permissionsString[0]);
+            } else if (Logical.AND.equals(logical)) {
+                subject.checkPermissions(permissionsString);
+            } else if (Logical.OR.equals(logical)) {
+                boolean hasAtLeastOnePermission = false;
+                for (final String permission : permissionsString) {
+                    if (subject.isPermitted(permission)) {
+                        hasAtLeastOnePermission = true;
+                        break;
+                    }
+                }
+
+                // Cause the exception if none match
+                if (!hasAtLeastOnePermission) {
+                    subject.checkPermission(permissionsString[0]);
+                }
+            }
+        } catch (AuthorizationException e) {
+            throw new SecurityApiException(e, ErrorCode.SECURITY_NOT_ENOUGH_PERMISSIONS);
+        }
+    }
+
+    private String[] getAllPermissionsAsStrings() {
+        if (allPermissions[0] == null) {
+            synchronized (allPermissions) {
+                if (allPermissions[0] == null) {
+                    final Permission[] killbillPermissions = Permission.values();
+                    for (int i = 0; i < killbillPermissions.length; i++) {
+                        allPermissions[i] = killbillPermissions[i].toString();
+                    }
+                }
+            }
+        }
+
+        return allPermissions;
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/security/api/NoOpSecurityApi.java b/util/src/main/java/com/ning/billing/util/security/api/NoOpSecurityApi.java
new file mode 100644
index 0000000..0b02ebe
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/security/api/NoOpSecurityApi.java
@@ -0,0 +1,40 @@
+/*
+ * 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.api;
+
+import java.util.List;
+import java.util.Set;
+
+import com.ning.billing.security.Logical;
+import com.ning.billing.security.Permission;
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.util.callcontext.TenantContext;
+
+import com.google.common.collect.ImmutableSet;
+
+public class NoOpSecurityApi implements SecurityApi {
+
+    @Override
+    public Set<Permission> getCurrentUserPermissions(final TenantContext context) {
+        return ImmutableSet.<Permission>copyOf(Permission.values());
+    }
+
+    @Override
+    public void checkCurrentUserPermissions(final List<Permission> permissions, final Logical logical, final TenantContext context) throws SecurityException {
+        // No-Op
+    }
+}
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
index b9b1c50..8869e40 100644
--- a/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationHandler.java
+++ b/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationHandler.java
@@ -23,15 +23,24 @@ 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.Permission;
 import com.ning.billing.security.RequiresPermissions;
+import com.ning.billing.security.SecurityApiException;
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.util.callcontext.DefaultTenantContext;
+import com.ning.billing.util.callcontext.TenantContext;
+
+import com.google.common.collect.ImmutableList;
 
 public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {
 
-    public PermissionAnnotationHandler() {
+    private final TenantContext context = new DefaultTenantContext(null);
+    private final SecurityApi securityApi;
+
+    public PermissionAnnotationHandler(final SecurityApi securityApi) {
         super(RequiresPermissions.class);
+        this.securityApi = securityApi;
     }
 
     public void assertAuthorized(final Annotation annotation) throws AuthorizationException {
@@ -40,28 +49,15 @@ public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {
         }
 
         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]);
+        try {
+            securityApi.checkCurrentUserPermissions(ImmutableList.<Permission>copyOf(requiresPermissions.value()), requiresPermissions.logical(), context);
+        } catch (SecurityApiException e) {
+            if (e.getCause() != null && e.getCause() instanceof AuthorizationException) {
+                throw (AuthorizationException) e.getCause();
+            } else if (e.getCause() != null) {
+                throw new AuthorizationException(e.getCause());
+            } else {
+                throw new AuthorizationException(e);
             }
         }
     }
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
index cc3c837..11567da 100644
--- a/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java
+++ b/util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java
@@ -19,9 +19,11 @@ package com.ning.billing.util.security;
 import org.apache.shiro.aop.AnnotationResolver;
 import org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor;
 
+import com.ning.billing.security.api.SecurityApi;
+
 public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {
 
-    public PermissionAnnotationMethodInterceptor(final AnnotationResolver resolver) {
-        super(new PermissionAnnotationHandler(), resolver);
+    public PermissionAnnotationMethodInterceptor(final SecurityApi securityApi, final AnnotationResolver resolver) {
+        super(new PermissionAnnotationHandler(securityApi), resolver);
     }
 }
\ No newline at end of file
diff --git a/util/src/test/java/com/ning/billing/util/glue/TestUtilModuleNoDB.java b/util/src/test/java/com/ning/billing/util/glue/TestUtilModuleNoDB.java
index ec302c8..70b886f 100644
--- a/util/src/test/java/com/ning/billing/util/glue/TestUtilModuleNoDB.java
+++ b/util/src/test/java/com/ning/billing/util/glue/TestUtilModuleNoDB.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.util.glue;
 
+import org.apache.shiro.guice.aop.ShiroAopModule;
 import org.skife.config.ConfigSource;
 
 import com.ning.billing.GuicyKillbillTestNoDBModule;
@@ -50,5 +51,7 @@ public class TestUtilModuleNoDB extends TestUtilModule {
         install(new MockNotificationQueueModule(configSource));
 
         installAuditMock();
+
+        install(new SecurityModule());
     }
 }
diff --git a/util/src/test/java/com/ning/billing/util/security/api/TestDefaultSecurityApi.java b/util/src/test/java/com/ning/billing/util/security/api/TestDefaultSecurityApi.java
new file mode 100644
index 0000000..e76f64b
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/security/api/TestDefaultSecurityApi.java
@@ -0,0 +1,52 @@
+/*
+ * 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.api;
+
+import java.util.Set;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.security.Permission;
+import com.ning.billing.security.api.SecurityApi;
+import com.ning.billing.util.UtilTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultSecurityApi extends UtilTestSuiteNoDB {
+
+    @Test(groups = "fast")
+    public void testRetrievePermissions() throws Exception {
+        configureShiro();
+
+        // We don't want the Guice injected one (it has Shiro disabled)
+        final SecurityApi securityApi = new DefaultSecurityApi();
+
+        final Set<Permission> anonsPermissions = securityApi.getCurrentUserPermissions(callContext);
+        Assert.assertEquals(anonsPermissions.size(), 0);
+
+        login("pierre");
+        final Set<Permission> pierresPermissions = securityApi.getCurrentUserPermissions(callContext);
+        Assert.assertEquals(pierresPermissions.size(), 2);
+        Assert.assertTrue(pierresPermissions.containsAll(ImmutableList.<Permission>of(Permission.INVOICE_CAN_CREDIT, Permission.INVOICE_CAN_ITEM_ADJUST)));
+
+        login("stephane");
+        final Set<Permission> stephanesPermissions = securityApi.getCurrentUserPermissions(callContext);
+        Assert.assertEquals(stephanesPermissions.size(), 1);
+        Assert.assertTrue(stephanesPermissions.containsAll(ImmutableList.<Permission>of(Permission.PAYMENT_CAN_REFUND)));
+    }
+}
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
index 8978060..fd9247b 100644
--- a/util/src/test/java/com/ning/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
+++ b/util/src/test/java/com/ning/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
@@ -131,33 +131,4 @@ public class TestPermissionAnnotationMethodInterceptor extends UtilTestSuiteNoDB
         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);
-    }
 }
diff --git a/util/src/test/java/com/ning/billing/util/UtilTestSuiteNoDB.java b/util/src/test/java/com/ning/billing/util/UtilTestSuiteNoDB.java
index f82fc0d..a7ab18f 100644
--- a/util/src/test/java/com/ning/billing/util/UtilTestSuiteNoDB.java
+++ b/util/src/test/java/com/ning/billing/util/UtilTestSuiteNoDB.java
@@ -18,12 +18,23 @@ package com.ning.billing.util;
 
 import javax.inject.Inject;
 
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+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.apache.shiro.util.ThreadContext;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 
 import com.ning.billing.GuicyKillbillTestSuiteNoDB;
 import com.ning.billing.bus.api.PersistentBus;
+import com.ning.billing.security.Permission;
+import com.ning.billing.security.api.SecurityApi;
 import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.audit.dao.AuditDao;
 import com.ning.billing.util.cache.CacheControllerDispatcher;
@@ -37,7 +48,6 @@ import com.google.inject.Stage;
 
 public class UtilTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
 
-
     @Inject
     protected PersistentBus eventBus;
     @Inject
@@ -52,6 +62,8 @@ public class UtilTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     protected AuditDao auditDao;
     @Inject
     protected AuditUserApi auditUserApi;
+    @Inject
+    protected SecurityApi securityApi;
 
     @BeforeClass(groups = "fast")
     public void beforeClass() throws Exception {
@@ -69,4 +81,37 @@ public class UtilTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
         eventBus.stop();
     }
 
+    // Security helpers
+
+    protected void login(final String username) {
+        logout();
+
+        final AuthenticationToken token = new UsernamePasswordToken(username, "password");
+        final Subject currentUser = SecurityUtils.getSubject();
+        currentUser.login(token);
+    }
+
+    protected void logout() {
+        final Subject currentUser = SecurityUtils.getSubject();
+        if (currentUser.isAuthenticated()) {
+            currentUser.logout();
+        }
+    }
+
+    protected 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() + "," + Permission.INVOICE_CAN_ITEM_ADJUST.toString());
+        config.getSection("roles").put("refunder", Permission.PAYMENT_CAN_REFUND.toString());
+
+        // Reset the security manager
+        ThreadContext.unbindSecurityManager();
+
+        final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config);
+        final SecurityManager securityManager = factory.getInstance();
+        SecurityUtils.setSecurityManager(securityManager);
+    }
 }