killbill-memoizeit
Changes
server/src/main/webapp/WEB-INF/shiro.ini 15(+15 -0)
server/src/test/resources/shiro.ini 19(+10 -9)
util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java 6(+4 -2)
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();
}
server/src/main/webapp/WEB-INF/shiro.ini 15(+15 -0)
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()));
+ }
+}
server/src/test/resources/shiro.ini 19(+10 -9)
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);
+ }
}