killbill-aplcache
Changes
profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java 97(+97 -0)
profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java 60(+60 -0)
profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java 87(+81 -6)
profiles/killbill/src/main/java/org/killbill/billing/server/security/FirstSuccessfulStrategyWith540.java 28(+28 -0)
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);