killbill-aplcache

Details

diff --git a/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java b/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
new file mode 100644
index 0000000..14c5eda
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015 Groupon, Inc
+ * Copyright 2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.apache.shiro.authc.pam;
+
+import java.util.Collection;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.realm.Realm;
+import org.killbill.billing.server.security.FirstSuccessfulStrategyWith540;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// See https://issues.apache.org/jira/browse/SHIRO-540
+public class ModularRealmAuthenticatorWith540 extends ModularRealmAuthenticator {
+
+    private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
+
+    public ModularRealmAuthenticatorWith540(final ModularRealmAuthenticator delegate) {
+        setRealms(delegate.getRealms());
+        setAuthenticationStrategy(delegate.getAuthenticationStrategy());
+    }
+
+    /**
+     * Performs the multi-realm authentication attempt by calling back to a {@link AuthenticationStrategy} object
+     * as each realm is consulted for {@code AuthenticationInfo} for the specified {@code token}.
+     *
+     * @param realms the multiple realms configured on this Authenticator instance.
+     * @param token  the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials.
+     * @return an aggregated AuthenticationInfo instance representing account data across all the successfully
+     * consulted realms.
+     */
+    protected AuthenticationInfo doMultiRealmAuthentication(final Collection<Realm> realms, final AuthenticationToken token) {
+
+        final AuthenticationStrategy strategy = getAuthenticationStrategy();
+
+        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
+
+        if (log.isTraceEnabled()) {
+            log.trace("Iterating through {} realms for PAM authentication", realms.size());
+        }
+
+        for (final Realm realm : realms) {
+
+            aggregate = strategy.beforeAttempt(realm, token, aggregate);
+
+            if (realm.supports(token)) {
+
+                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
+
+                AuthenticationInfo info = null;
+                Throwable t = null;
+                try {
+                    info = realm.getAuthenticationInfo(token);
+                } catch (final Throwable throwable) {
+                    t = throwable;
+                    if (log.isDebugEnabled()) {
+                        final String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
+                        log.debug(msg, t);
+                    }
+                }
+
+                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
+
+                if (strategy instanceof FirstSuccessfulStrategyWith540) {
+                    // check if we should check the next realm, or just stop here.
+                    if (!((FirstSuccessfulStrategyWith540) strategy).continueAfterAttempt(info, aggregate, t)) {
+                        log.trace("Will not consult any other realms for authentication, last realm [{}].", realm);
+                        break;
+                    }
+                }
+
+            } else {
+                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
+            }
+        }
+
+        aggregate = strategy.afterAllAttempts(token, aggregate);
+
+        return aggregate;
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/apache/shiro/guice/web/ShiroWebModuleWith435.java b/profiles/killbill/src/main/java/org/apache/shiro/guice/web/ShiroWebModuleWith435.java
new file mode 100644
index 0000000..f877740
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/apache/shiro/guice/web/ShiroWebModuleWith435.java
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+package org.apache.shiro.guice.web;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.name.Names;
+import com.google.inject.servlet.ServletModule;
+import org.apache.shiro.guice.ShiroModule;
+import org.apache.shiro.config.ConfigurationException;
+import org.apache.shiro.env.Environment;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.session.mgt.SessionManager;
+import org.apache.shiro.web.env.WebEnvironment;
+import org.apache.shiro.web.filter.PathMatchingFilter;
+import org.apache.shiro.web.filter.authc.*;
+import org.apache.shiro.web.filter.authz.*;
+import org.apache.shiro.web.filter.mgt.FilterChainResolver;
+import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
+import org.apache.shiro.web.mgt.WebSecurityManager;
+import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
+
+import javax.servlet.Filter;
+import javax.servlet.ServletContext;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default
+ * {@link org.apache.shiro.web.mgt.WebSecurityManager}, {@link org.apache.shiro.mgt.SecurityManager} and {@link org.apache.shiro.session.mgt.SessionManager}.  At least one realm must be added by
+ * using {@link #bindRealm() bindRealm}.
+ * <p/>
+ * Also provides for the configuring of filter chains and binds a {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information.
+ * @see <a href="https://issues.apache.org/jira/browse/SHIRO-435">https://issues.apache.org/jira/browse/SHIRO-435</a>
+ */
+public abstract class ShiroWebModuleWith435 extends ShiroModule {
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<PortFilter> PORT = Key.get(PortFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<UserFilter> USER = Key.get(UserFilter.class);
+
+
+    static final String NAME = "SHIRO";
+
+    /**
+     * We use a LinkedHashMap here to ensure that iterator order is the same as add order.  This is important, as the
+     * FilterChainResolver uses iterator order when searching for a matching chain.
+     */
+    private final Map<String, Key<? extends Filter>[]> filterChains = new LinkedHashMap<String, Key<? extends Filter>[]>();
+    private final ServletContext servletContext;
+
+    public ShiroWebModuleWith435(ServletContext servletContext) {
+        this.servletContext = servletContext;
+    }
+
+    public static void bindGuiceFilter(Binder binder) {
+        binder.install(guiceFilterModule());
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static void bindGuiceFilter(final String pattern, Binder binder) {
+        binder.install(guiceFilterModule(pattern));
+    }
+
+    public static ServletModule guiceFilterModule() {
+        return guiceFilterModule("/*");
+    }
+
+    public static ServletModule guiceFilterModule(final String pattern) {
+        return new ServletModule() {
+            @Override
+            protected void configureServlets() {
+                filter(pattern).through(GuiceShiroFilter.class);
+            }
+        };
+    }
+
+    @Override
+    protected final void configureShiro() {
+        bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
+        bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext);
+        bindWebSecurityManager(bind(WebSecurityManager.class));
+        bindWebEnvironment(bind(WebEnvironment.class));
+        bind(GuiceShiroFilter.class).asEagerSingleton();
+        expose(GuiceShiroFilter.class);
+
+        this.configureShiroWeb();
+
+        setupFilterChainConfigs();
+
+        bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(filterChains));
+    }
+
+    private void setupFilterChainConfigs() {
+        Table<Key<? extends PathMatchingFilter>, String, String> configs = HashBasedTable.create();
+
+        for (Map.Entry<String, Key<? extends Filter>[]> filterChain : filterChains.entrySet()) {
+            for (int i = 0; i < filterChain.getValue().length; i++) {
+                Key<? extends Filter> key = filterChain.getValue()[i];
+                if (key instanceof FilterConfigKey) {
+                    FilterConfigKey<? extends PathMatchingFilter> configKey = (FilterConfigKey<? extends PathMatchingFilter>) key;
+                    key = configKey.getKey();
+                    filterChain.getValue()[i] = key;
+                    if (!PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
+                        throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType());
+                    }
+                    configs.put(castToPathMatching(key), filterChain.getKey(), configKey.getConfigValue());
+                } else if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
+                    configs.put(castToPathMatching(key), filterChain.getKey(), "");
+                }
+            }
+        }
+        for (Key<? extends PathMatchingFilter> filterKey : configs.rowKeySet()) {
+            bindPathMatchingFilter(filterKey, configs.row(filterKey));
+        }
+    }
+
+    private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) {
+        bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton();
+    }
+
+    @SuppressWarnings({"unchecked"})
+    private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) {
+        return (Key<? extends PathMatchingFilter>) key;
+    }
+
+    protected abstract void configureShiroWeb();
+
+    @SuppressWarnings({"unchecked"})
+    @Override
+    protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) {
+        bind.to(WebSecurityManager.class); // SHIRO-435
+    }
+
+    /**
+     * Binds the security manager.  Override this method in order to provide your own security manager binding.
+     * <p/>
+     * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton.
+     *
+     * @param bind
+     */
+    protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) {
+        try {
+            bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton();
+        } catch (NoSuchMethodException e) {
+            throw new ConfigurationException("This really shouldn't happen.  Either something has changed in Shiro, or there's a bug in ShiroModule.", e);
+        }
+    }
+
+    /**
+     * Binds the session manager.  Override this method in order to provide your own session manager binding.
+     * <p/>
+     * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton.
+     *
+     * @param bind
+     */
+    @Override
+    protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
+        bind.to(ServletContainerSessionManager.class).asEagerSingleton();
+    }
+
+    @Override
+    protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) {
+        bind.to(WebEnvironment.class); // SHIRO-435
+    }
+
+    protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) {
+        bind.to(WebGuiceEnvironment.class).asEagerSingleton();
+    }
+
+    /**
+     * Adds a filter chain to the shiro configuration.
+     * <p/>
+     * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter}, it will be registered with a proper
+     * provider.
+     *
+     * @param pattern
+     * @param keys
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) {
+        filterChains.put(pattern, keys);
+    }
+
+    protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) {
+        return new FilterConfigKey<T>(baseKey, configValue);
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) {
+        return config(Key.get(typeLiteral), configValue);
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) {
+        return config(Key.get(type), configValue);
+    }
+
+    private static class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> {
+        private Key<T> key;
+        private String configValue;
+
+        private FilterConfigKey(Key<T> key, String configValue) {
+            super();
+            this.key = key;
+            this.configValue = configValue;
+        }
+
+        public Key<T> getKey() {
+            return key;
+        }
+
+        public String getConfigValue() {
+            return configValue;
+        }
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java
new file mode 100644
index 0000000..2fb4840
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.server.modules;
+
+import javax.inject.Named;
+import javax.sql.DataSource;
+
+import org.apache.shiro.cache.CacheManager;
+import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
+import org.killbill.billing.util.config.SecurityConfig;
+import org.killbill.billing.util.glue.ShiroEhCacheInstrumentor;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class KillbillJdbcTenantRealmProvider implements Provider<KillbillJdbcTenantRealm> {
+
+    private final SecurityConfig securityConfig;
+    private final CacheManager cacheManager;
+    private final ShiroEhCacheInstrumentor ehCacheInstrumentor;
+    private final DataSource dataSource;
+
+    @Inject
+    public KillbillJdbcTenantRealmProvider(final SecurityConfig securityConfig, final CacheManager cacheManager, final ShiroEhCacheInstrumentor ehCacheInstrumentor, @Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource) {
+        this.securityConfig = securityConfig;
+        this.cacheManager = cacheManager;
+        this.ehCacheInstrumentor = ehCacheInstrumentor;
+        this.dataSource = dataSource;
+    }
+
+    @Override
+    public KillbillJdbcTenantRealm get() {
+        final KillbillJdbcTenantRealm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource, securityConfig);
+
+        // Set the cache manager
+        // Note: the DefaultWebSecurityManager used for RBAC will have all of its realms (set in KillBillShiroWebModule)
+        // automatically configured with the EhCache manager (see EhCacheManagerProvider)
+        killbillJdbcTenantRealm.setCacheManager(cacheManager);
+
+        // Instrument the cache
+        ehCacheInstrumentor.instrument(killbillJdbcTenantRealm);
+
+        return killbillJdbcTenantRealm;
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
index 0617b12..721c7a7 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
@@ -23,30 +23,47 @@ import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
+import org.apache.shiro.authc.pam.ModularRealmAuthenticatorWith540;
 import org.apache.shiro.cache.CacheManager;
-import org.apache.shiro.guice.web.ShiroWebModule;
+import org.apache.shiro.guice.web.ShiroWebModuleWith435;
+import org.apache.shiro.realm.Realm;
 import org.apache.shiro.session.mgt.SessionManager;
+import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
 import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
+import org.apache.shiro.web.mgt.WebSecurityManager;
 import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
 import org.apache.shiro.web.util.WebUtils;
 import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.server.security.FirstSuccessfulStrategyWith540;
+import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
 import org.killbill.billing.util.config.RbacConfig;
 import org.killbill.billing.util.glue.EhCacheManagerProvider;
 import org.killbill.billing.util.glue.IniRealmProvider;
 import org.killbill.billing.util.glue.JDBCSessionDaoProvider;
 import org.killbill.billing.util.glue.KillBillShiroModule;
+import org.killbill.billing.util.glue.ShiroEhCacheInstrumentor;
 import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
 import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
 import org.skife.config.ConfigSource;
 import org.skife.config.ConfigurationObjectFactory;
 
+import com.google.inject.Inject;
 import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
 import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.matcher.AbstractMatcher;
+import com.google.inject.matcher.Matchers;
+import com.google.inject.spi.InjectionListener;
+import com.google.inject.spi.TypeEncounter;
+import com.google.inject.spi.TypeListener;
 
 // For Kill Bill server only.
 // See org.killbill.billing.util.glue.KillBillShiroModule for Kill Bill library.
-public class KillBillShiroWebModule extends ShiroWebModule {
+public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
 
     private final ConfigSource configSource;
 
@@ -56,26 +73,52 @@ public class KillBillShiroWebModule extends ShiroWebModule {
     }
 
     @Override
+    public void configure() {
+        super.configure();
+
+        bind(ShiroEhCacheInstrumentor.class).asEagerSingleton();
+    }
+
+    @Override
     protected void configureShiroWeb() {
+        // Magic provider to configure the cache manager
+        bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+
+        configureShiroForRBAC();
+
+        configureShiroForTenants();
+    }
+
+    private void configureShiroForRBAC() {
         final RbacConfig config = new ConfigurationObjectFactory(configSource).build(RbacConfig.class);
         bind(RbacConfig.class).toInstance(config);
 
+        // Note: order matters (the first successful match will win, see below)
         bindRealm().toProvider(IniRealmProvider.class).asEagerSingleton();
-
         bindRealm().to(KillBillJdbcRealm.class).asEagerSingleton();
-
         if (KillBillShiroModule.isLDAPEnabled()) {
             bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
         }
 
-        // Magic provider to configure the cache manager
-        bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+        bindListener(new AbstractMatcher<TypeLiteral<?>>() {
+                         @Override
+                         public boolean matches(final TypeLiteral<?> o) {
+                             return Matchers.subclassesOf(WebSecurityManager.class).matches(o.getRawType());
+                         }
+                     },
+                     new DefaultWebSecurityManagerTypeListener(getProvider(ShiroEhCacheInstrumentor.class)));
 
         if (KillBillShiroModule.isRBACEnabled()) {
             addFilterChain(JaxrsResource.PREFIX + "/**", Key.get(CorsBasicHttpAuthenticationFilter.class));
         }
     }
 
+    private void configureShiroForTenants() {
+        // Realm binding for the tenants (see TenantFilter)
+        bind(KillbillJdbcTenantRealm.class).toProvider(KillbillJdbcTenantRealmProvider.class).asEagerSingleton();
+        expose(KillbillJdbcTenantRealm.class);
+    }
+
     @Override
     protected void bindSessionManager(final AnnotatedBindingBuilder<SessionManager> bind) {
         // Bypass the servlet container completely for session management and delegate it to Shiro.
@@ -97,4 +140,36 @@ public class KillBillShiroWebModule extends ShiroWebModule {
             return "OPTIONS".equalsIgnoreCase(httpMethod) || super.isAccessAllowed(request, response, mappedValue);
         }
     }
+
+    private static final class DefaultWebSecurityManagerTypeListener implements TypeListener {
+
+        private final Provider<ShiroEhCacheInstrumentor> instrumentorProvider;
+
+        @Inject
+        public DefaultWebSecurityManagerTypeListener(final Provider<ShiroEhCacheInstrumentor> instrumentorProvider) {
+            this.instrumentorProvider = instrumentorProvider;
+        }
+
+        @Override
+        public <I> void hear(final TypeLiteral<I> typeLiteral, final TypeEncounter<I> typeEncounter) {
+            typeEncounter.register(new InjectionListener<I>() {
+                @Override
+                public void afterInjection(final Object o) {
+                    final ShiroEhCacheInstrumentor ehCacheInstrumentor = instrumentorProvider.get();
+                    ehCacheInstrumentor.instrument(CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME);
+
+                    final DefaultWebSecurityManager webSecurityManager = (DefaultWebSecurityManager) o;
+                    if (webSecurityManager.getAuthenticator() instanceof ModularRealmAuthenticator) {
+                        final ModularRealmAuthenticator authenticator = (ModularRealmAuthenticator) webSecurityManager.getAuthenticator();
+                        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategyWith540());
+                        webSecurityManager.setAuthenticator(new ModularRealmAuthenticatorWith540(authenticator));
+
+                        for (final Realm realm : webSecurityManager.getRealms()) {
+                            ehCacheInstrumentor.instrument(realm);
+                        }
+                    }
+                }
+            });
+        }
+    }
 }
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/FirstSuccessfulStrategyWith540.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/FirstSuccessfulStrategyWith540.java
new file mode 100644
index 0000000..cd39ac8
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/FirstSuccessfulStrategyWith540.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 Groupon, Inc
+ * Copyright 2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.server.security;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
+
+public class FirstSuccessfulStrategyWith540 extends FirstSuccessfulStrategy {
+
+    public boolean continueAfterAttempt(final AuthenticationInfo singleRealmInfo, final AuthenticationInfo aggregateInfo, final Throwable t) {
+        return !(aggregateInfo != null && aggregateInfo == singleRealmInfo);
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
index 85b17d9..20b2d7a 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
@@ -46,6 +46,10 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
         this.dataSource = dataSource;
         this.securityConfig = securityConfig;
 
+        // Note: we don't support updating tenants credentials via API
+        // See JavaDoc warning: https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/AuthenticatingRealm.html
+        setAuthenticationCachingEnabled(true);
+
         configureSecurity();
         configureQueries();
         configureDataSource();
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
index 82f63d1..a0af60a 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
@@ -21,7 +21,6 @@ package org.killbill.billing.server.security;
 import java.io.IOException;
 
 import javax.inject.Inject;
-import javax.inject.Named;
 import javax.inject.Singleton;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -31,7 +30,6 @@ import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
 
 import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.authc.AuthenticationToken;
@@ -40,11 +38,9 @@ import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
 import org.apache.shiro.realm.Realm;
 import org.killbill.billing.jaxrs.resources.JaxrsResource;
 import org.killbill.billing.server.listeners.KillbillGuiceListener;
-import org.killbill.billing.server.modules.KillbillPlatformModule;
 import org.killbill.billing.tenant.api.Tenant;
 import org.killbill.billing.tenant.api.TenantApiException;
 import org.killbill.billing.tenant.api.TenantUserApi;
-import org.killbill.billing.util.config.SecurityConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -61,17 +57,12 @@ public class TenantFilter implements Filter {
     @Inject
     protected TenantUserApi tenantUserApi;
     @Inject
-    protected SecurityConfig securityConfig;
-
-    @Inject
-    @Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED)
-    protected DataSource dataSource;
+    protected KillbillJdbcTenantRealm killbillJdbcTenantRealm;
 
     private ModularRealmAuthenticator modularRealmAuthenticator;
 
     @Override
     public void init(final FilterConfig filterConfig) throws ServletException {
-        final Realm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource, securityConfig);
         // We use Shiro to verify the api credentials - but the Shiro Subject is only used for RBAC
         modularRealmAuthenticator = new ModularRealmAuthenticator();
         modularRealmAuthenticator.setRealms(ImmutableList.<Realm>of(killbillJdbcTenantRealm));
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
index fa48420..5044598 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
@@ -22,6 +22,7 @@ import java.util.EventListener;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -196,6 +197,9 @@ public class TestJaxrsBase extends KillbillClient {
         clock.resetDeltaFromReality();
         clock.setDay(new LocalDate(2012, 8, 25));
 
+        // Make sure to re-generate the api key and secret (could be cached by Shiro)
+        DEFAULT_API_KEY = UUID.randomUUID().toString();
+        DEFAULT_API_SECRET = UUID.randomUUID().toString();
         loginTenant(DEFAULT_API_KEY, DEFAULT_API_SECRET);
 
         // Recreate the tenant (tables have been cleaned-up)
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
index 5b94fb4..f01e061 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
  *
  * The Billing Project 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
diff --git a/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
index d8b92a7..5603b73 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
@@ -24,27 +24,16 @@ import javax.inject.Provider;
 import org.apache.shiro.cache.ehcache.EhCacheManager;
 import org.apache.shiro.mgt.DefaultSecurityManager;
 import org.apache.shiro.mgt.SecurityManager;
-import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.ehcache.InstrumentedEhcache;
-import net.sf.ehcache.CacheException;
 import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
 
 public class EhCacheManagerProvider implements Provider<EhCacheManager> {
 
-    private static final Logger logger = LoggerFactory.getLogger(EhCacheManagerProvider.class);
-
-    private final MetricRegistry metricRegistry;
     private final SecurityManager securityManager;
     private final CacheManager ehCacheCacheManager;
 
     @Inject
-    public EhCacheManagerProvider(final MetricRegistry metricRegistry, final SecurityManager securityManager, final CacheManager ehCacheCacheManager) {
-        this.metricRegistry = metricRegistry;
+    public EhCacheManagerProvider(final SecurityManager securityManager, final CacheManager ehCacheCacheManager) {
         this.securityManager = securityManager;
         this.ehCacheCacheManager = ehCacheCacheManager;
     }
@@ -55,21 +44,8 @@ public class EhCacheManagerProvider implements Provider<EhCacheManager> {
         // Same EhCache manager instance as the rest of the system
         shiroEhCacheManager.setCacheManager(ehCacheCacheManager);
 
-        // It looks like Shiro's cache manager is not thread safe. Concurrent requests on startup
-        // can throw org.apache.shiro.cache.CacheException: net.sf.ehcache.ObjectExistsException: Cache shiro-activeSessionCache already exists
-        // As a workaround, create the cache manually here
-        shiroEhCacheManager.getCache(CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME);
-
-        // Instrument the cache
-        final Ehcache shiroActiveSessionEhcache = ehCacheCacheManager.getEhcache(CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME);
-        final Ehcache decoratedCache = InstrumentedEhcache.instrument(metricRegistry, shiroActiveSessionEhcache);
-        try {
-            ehCacheCacheManager.replaceCacheWithDecoratedCache(shiroActiveSessionEhcache, decoratedCache);
-        } catch (final CacheException e) {
-            logger.warn("Unable to instrument cache {}: {}", shiroActiveSessionEhcache.getName(), e.getMessage());
-        }
-
         if (securityManager instanceof DefaultSecurityManager) {
+            // For RBAC only (see also KillbillJdbcTenantRealmProvider)
             ((DefaultSecurityManager) securityManager).setCacheManager(shiroEhCacheManager);
         }
 
diff --git a/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java b/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
index 379e3b6..8e74e76 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
@@ -53,17 +53,26 @@ public class IniRealmProvider implements Provider<IniRealm> {
             // by going through IniSecurityManagerFactory.
             final DefaultSecurityManager securityManager = (DefaultSecurityManager) factory.getInstance();
             final Collection<Realm> realms = securityManager.getRealms();
-            if (realms == null || realms.isEmpty()) {
-                return new IniRealm(securityConfig.getShiroResourcePath());
-            }
 
-            for (final Realm cur : realms) {
-                if (cur instanceof IniRealm) {
-                    return (IniRealm) cur;
+            IniRealm iniRealm = null;
+            if (realms == null || realms.isEmpty()) {
+                iniRealm = new IniRealm(securityConfig.getShiroResourcePath());
+            } else {
+                for (final Realm cur : realms) {
+                    if (cur instanceof IniRealm) {
+                        iniRealm = (IniRealm) cur;
+                        break;
+                    }
                 }
             }
-            throw new ConfigurationException();
+            if (iniRealm != null) {
+                // See JavaDoc warning: https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/AuthenticatingRealm.html
+                iniRealm.setAuthenticationCachingEnabled(true);
 
+                return iniRealm;
+            } else {
+                throw new ConfigurationException();
+            }
         } catch (final ConfigurationException e) {
             log.warn("Unable to configure RBAC", e);
             return new IniRealm();
diff --git a/util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java b/util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java
new file mode 100644
index 0000000..6a0bb91
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+
+import org.apache.shiro.cache.CacheManager;
+import org.apache.shiro.realm.AuthenticatingRealm;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.realm.Realm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ehcache.InstrumentedEhcache;
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.Ehcache;
+
+public class ShiroEhCacheInstrumentor {
+
+    private static final Logger logger = LoggerFactory.getLogger(ShiroEhCacheInstrumentor.class);
+
+    private final MetricRegistry metricRegistry;
+    private final CacheManager shiroEhCacheManager;
+    private final net.sf.ehcache.CacheManager ehCacheCacheManager;
+
+    @Inject
+    public ShiroEhCacheInstrumentor(final MetricRegistry metricRegistry, final CacheManager shiroEhCacheManager, final net.sf.ehcache.CacheManager ehCacheCacheManager) {
+        this.metricRegistry = metricRegistry;
+        this.shiroEhCacheManager = shiroEhCacheManager;
+        this.ehCacheCacheManager = ehCacheCacheManager;
+    }
+
+    public void instrument(final Realm realm) {
+        if (realm instanceof AuthorizingRealm) {
+            instrument((AuthorizingRealm) realm);
+        } else if (realm instanceof AuthenticatingRealm) {
+            instrument((AuthenticatingRealm) realm);
+        }
+    }
+
+    public void instrument(final AuthorizingRealm realm) {
+        instrument(realm.getAuthenticationCacheName());
+        instrument(realm.getAuthorizationCacheName());
+    }
+
+    public void instrument(final AuthenticatingRealm realm) {
+        instrument(realm.getAuthenticationCacheName());
+    }
+
+    public void instrument(final String cacheName) {
+        // Initialize the cache, if it doesn't exist yet
+        // Note: Shiro's cache manager is not thread safe. Concurrent requests on startup
+        // can throw org.apache.shiro.cache.CacheException: net.sf.ehcache.ObjectExistsException: Cache shiro-activeSessionCache already exists
+        shiroEhCacheManager.getCache(cacheName);
+
+        final Ehcache shiroEhcache = ehCacheCacheManager.getEhcache(cacheName);
+        final Ehcache decoratedCache = InstrumentedEhcache.instrument(metricRegistry, shiroEhcache);
+        try {
+            ehCacheCacheManager.replaceCacheWithDecoratedCache(shiroEhcache, decoratedCache);
+        } catch (final CacheException e) {
+            logger.warn("Unable to instrument cache {}: {}", shiroEhcache.getName(), e.getMessage());
+        }
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
index 380258b..87c8588 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
@@ -42,6 +42,10 @@ public class KillBillJdbcRealm extends JdbcRealm {
         this.dataSource = dataSource;
         this.securityConfig = securityConfig;
 
+        // TODO Enable when we add support for cache invalidation
+        // See JavaDoc warning: https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/AuthenticatingRealm.html
+        //setAuthenticationCachingEnabled(true);
+
         // Tweak JdbcRealm defaults
         setPermissionsLookupEnabled(true);
         setAuthenticationQuery(KILLBILL_SALTED_AUTHENTICATION_QUERY);