◆ Problem
You want to test your EJB security settings.
◆ Background
Manual EJB security testing is generally even more annoying than manual web resource security testing (see recipe 13.4). The typical approach is to build a web front end just to be able to invoke the EJBs directly, or you might be using a devel- opment environment that provides test client applications for this purpose.11 Such test clients are essentially interactive Java interpreters, in which you execute code by clicking hypertext links; and while it can be useful for quickly verifying that your EJBs are not entirely broken, they are not a substitute for or—at least in our opinion—a good tool for testing. What you need is a way to write security- based tests that simply provide credentials, look up the EJB, and try to use it.
These tests are easy to automate. This recipe describes how.
◆ Recipe
We recommend starting with Cactus, which helps you simulate having a specific user logged in on a per test basis. It uses a mechanism quite similar to HtmlUnit (see recipe 13.4). The general strategy is to provide Cactus with the appropriate authentication information to simulate a user being logged in, and then trying to invoke the protected EJB method. Remember that because we are using Cactus, these tests execute in the container. We will come back to that in the Discussion
10We do not have any specific product in mind. The culprits know who they are.
11IBM’s WebSphere Studio, for example, provides the Universal Test Client for this purpose.
TE AM FL Y
Team-Fly®
531 Test EJB resource security
section of this recipe. Listing 13.10 shows the first test: if there are no credentials (no one has logged in), then invoking an EJB method ought to fail. The EJB in question is a session bean that can provide the business logic for our Coffee Shop administrative application. This EJB performs pricing changes.
package junit.cookbook.coffee.model.ejb.test;
import java.rmi.ServerException;
import javax.ejb.EJBException;
import javax.naming.*;
import javax.rmi.PortableRemoteObject;
import junit.cookbook.coffee.model.ejb.*;
import org.apache.cactus.*;
public class PricingOperationsSecurityTest extends ServletTestCase { public void testNoCredentials() throws Exception {
Context context = new InitialContext();
Object object = context.lookup("ejb/PricingOperations");
PricingOperationsHome home =
(PricingOperationsHome) PortableRemoteObject.narrow(
object,
PricingOperationsHome.class);
try {
PricingOperations pricingOperations = home.create();
fail("No credentials and you got through?!");
}
catch (ServerException expected) {
Throwable serverExceptionCause = expected.getCause();
assertTrue(
"This caused the ServerException: "
+ serverExceptionCause,
serverExceptionCause instanceof EJBException);
EJBException ejbException =
(EJBException) serverExceptionCause;
Exception ejbExceptionCause =
ejbException.getCausedByException();
assertTrue(
"This caused the EJBException: " + ejbExceptionCause, ejbExceptionCause instanceof SecurityException);
} } }
Listing 13.10 PricingOperationsSecurityTest, a server-side Cactus test
532 CHAPTER 13
Testing J2EE applications
All the code in bold print verifies that the server-side exception is indeed a secu- rity exception, and not some incidental problem. It is common practice in Java to wrap exceptions within exceptions when propagating them up through the vari- ous layers of an application, which explains all the unwrapping here in the test.
We will refactor this into a separate method when we add another test or two.
Otherwise, this uses the technique we describe in recipe 2.8, “Test throwing the right exception.” Our next test verifies that a user with the required authorization can create a PricingOperations bean.
You need to follow a few simple instructions from the Cactus web site to be able to provide authentication information along with a request.12 After we augmented our web deployment descriptor, we added the following test, which verifies that an administrator (user admin) can invoke methods on both the home interface and on the bean itself.
public void beginAdministrator(WebRequest request) { request.setRedirectorName("ServletRedirectorSecure" );
request.setAuthentication(
new BasicAuthentication("admin", "adm1n"));
}
public void testAdministrator() throws Exception { PricingOperations pricingOperations = home.create();
pricingOperations.setPrice("762", Money.dollars(12, 50));
}
The Cactus web site instructed us to implement a “begin” method in order to set authentication for this test. Our test then simply creates a PricingOperations bean and invokes a method on it. As long as this test does not throw any security-related exceptions, it passes—that is why there are no assertions in it. If the test did throw an exception, we might want to add logic to verify exactly which exception it is throwing, and have the test fail only if it threw a security-related exception. We believe that the extra reward is not worth the extra effort, so we leave the test as it is. We have moved home into the test fixture and initialize it in setUp(), as follows:
public class PricingOperationsSecurityTest extends ServletTestCase { private PricingOperationsHome home;
protected void setUp() throws Exception { Context context = new InitialContext();
Object object = context.lookup("ejb/PricingOperations");
home =
(PricingOperationsHome) PortableRemoteObject.narrow(
12See http://jakarta.apache.org/cactus/writing/howto_security.html
533 Test EJB resource security
object,
PricingOperationsHome.class);
}
// Tests omitted }
The last test we include here tries to log in as a shopper and invoke the Pricing- Operations EJB, which ought to fail with “user not authorized.” When we wrote this test, we realized that we wanted the Cactus test redirector to work for any authenticated user, because the EJB layer was going to perform the stricter secu- rity check. After failing to figure out how to do that, we settled on a role that no user played, and named it test. We configured our test web application—the web application containing our Cactus tests—with a security constraint, but authorized all users to use the web application (by not preventing anyone from using it). This forces the user to be authenticated for purposes of testing the user’s authority to invoke EJBs, but any user can execute the tests. We think this is a good design for security-based tests.
We refactored the “no credentials” test, extracting the code that tries to invoke PricingOperationsHome.create(), unwraps the expected exception, and verifies that it is a SecurityException. We even added some code to check the Security- Exception message—now we need to distinguish between “no credentials pro- vided” and “user not authorized.” Listing 13.11 shows that method.
private void doTestExpectingSecurityException(
String testFailureMessage,
String expectedSecurityExceptionMessageContains) throws Exception {
try {
PricingOperations pricingOperations = home.create();
fail(testFailureMessage);
}
catch (ServerException expected) {
Throwable serverExceptionCause = expected.getCause();
assertTrue(
"This caused the ServerException: "
+ serverExceptionCause,
serverExceptionCause instanceof EJBException);
EJBException ejbException =
(EJBException) serverExceptionCause;
Exception ejbExceptionCause =
ejbException.getCausedByException();
Listing 13.11 Verifying the SecurityException message
534 CHAPTER 13
Testing J2EE applications
assertTrue(
"This caused the EJBException: " + ejbExceptionCause, ejbExceptionCause instanceof SecurityException);
SecurityException securityException = (SecurityException) ejbExceptionCause;
String securityExceptionMessage = securityException.getMessage();
assertTrue(
securityExceptionMessage,
securityExceptionMessage.matches(
".*"
+ expectedSecurityExceptionMessageContains + ".*"));
} }
We have highlighted in bold print the extra code to check the SecurityException message. With this change, the “no credentials” test now looks as follows:
public void testNoCredentials() throws Exception { doTestExpectingSecurityException(
"No credentials and you got through?!", "principal=null");
}
And the new test, which tries to invoke the EJB as a shopper, looks as follows.
public void beginShopper(WebRequest request) {
request.setRedirectorName("ServletRedirectorSecure");
request.setAuthentication(
new BasicAuthentication("shopper", "sh0pper"));
}
public void testShopper() throws Exception { doTestExpectingSecurityException(
"Only administrators should be allowed to do this!", "Insufficient method permissions");
}
As with the “administrator” test, first we implement the Cactus method beginShop- per() to impersonate a shopper, and then we implement the test itself, expecting to see “Insufficient method permissions” in the SecurityException message. This message text is specific to JBoss 3.2.2, and if we wrote any more of these tests, we would certainly extract that text into either a property file or at least a symbolic constant, to avoid massive duplication. If in JBoss 4.0, that message text changes, we do not want to have to change 25 strings scattered throughout our tests.
535 Test EJB resource security
So here we have examples of the three typical kinds of security tests: no user logged in, an authorized user logged in, and an unauthorized user logged in. You can use these as templates to write your own tests.
◆ Discussion
The Cactus tests are complex, but only in the sense that the runtime environment is complex. Writing the tests themselves involves a slight learning curve—in our opinion, not too steep a curve, either—and the usual tentative experiment or two while trying to figure out whether you have followed the instructions correctly.
The good news is that once you get going, the only real problem is that the tests are slower to execute than we would like. Certainly that is not the fault of Cac- tus,13 but rather is intrinsic to any kind of in-container testing strategy. The only drawback is that writing an exhaustive security test suite this way—trying all per- mutations of roles and so on—leads to an unhealthy amount of duplicated code.
If you refactor mercilessly, eventually you will end up with an engine that gen- erates the tests from a text-based description of the security roles. You could imag- ine an XML document that describes which roles are authorized to perform which actions, and that could be used to generate the tests. Does this sound familiar? We recognize it as a description of the declarative security feature of J2EE! To get your tests right, then, would be equivalent to getting your test-generating XML docu- ment right. Instead of this, we recommend just verifying the deployment descrip- tors themselves. Use XMLUnit to verify that you have specified the security settings correctly, and then use whatever strategies you need to verify the configuration files that are specific to your application server. These tests provide a warning sys- tem whenever someone changes security settings: the tests will fail, and then it will be up to the team to decide whether that change is correct. No more accidental security holes caused by someone who changed a file at 2 a.m. and neglected to warn anyone about it.14
◆ Related
■ Chapter 9—Testing and XML
■ 13.4—Test web resource security
■ Cactus Security HOWTO
(http://jakarta.apache.org/cactus/writing/howto_security.html)
13It must be Chet’s fault.
14We do not recommend working when tired, anyway, but we recognize that some people are pressured into doing it.
536 CHAPTER 13
Testing J2EE applications