2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package edu.internet2.middleware.shibboleth.runner;
19 import javax.naming.directory.Attributes;
20 import javax.naming.directory.BasicAttributes;
22 import org.apache.log4j.ConsoleAppender;
23 import org.apache.log4j.Layout;
24 import org.apache.log4j.Level;
25 import org.apache.log4j.Logger;
26 import org.apache.log4j.PatternLayout;
27 import org.opensaml.SAMLBinding;
28 import org.opensaml.SAMLConfig;
30 import com.mockrunner.mock.web.MockFilterConfig;
31 import com.mockrunner.mock.web.MockHttpServletRequest;
32 import com.mockrunner.mock.web.MockHttpServletResponse;
33 import com.mockrunner.mock.web.MockServletContext;
34 import com.mockrunner.mock.web.WebMockObjectFactory;
35 import com.mockrunner.servlet.ServletTestModule;
37 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
38 import edu.internet2.middleware.shibboleth.idp.IdPResponder;
39 import edu.internet2.middleware.shibboleth.resource.AuthenticationFilter;
40 import edu.internet2.middleware.shibboleth.serviceprovider.AssertionConsumerServlet;
41 import edu.internet2.middleware.shibboleth.serviceprovider.FilterSupportImpl;
42 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig;
43 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderContext;
46 * A JUnit Test support class for Shibboleth.
48 * <p>This class can create the Mockrunner control blocks to
49 * interface to an instance of the SP and one or more instances
50 * of the IdP and Resource Filter. Each instance is initialized
51 * with its own set of configuration files. The test only needs
52 * to create the objects it needs.</p>
54 * <p>Look at *.integration.IntegrationTest for an example of use.</p>
56 * @author Howard Gilbert.
58 public class ShibbolethRunner {
60 // Default values used to define each context
61 public static String REMOTE_ADDR = "192.168.0.99";
62 public static String SCHEME = "https";
63 public static String PROTOCOL = "HTTP/1.1";
64 public static int SERVER_PORT = 443;
67 public static String IDP_SERVER_NAME = "idp.example.org";
68 public static String IDP_CONTEXT_PATH = "/shibboleth-idp";
69 public static String IDP_CONTEXT_URL = SCHEME+"://"+IDP_SERVER_NAME+IDP_CONTEXT_PATH+"/";
72 public static String SP_SERVER_NAME = "sp.example.org";
73 public static String SP_CONTEXT_PATH = "/shibboleth-sp";
74 public static String SP_CONTEXT_URL = SCHEME+"://"+SP_SERVER_NAME+SP_CONTEXT_PATH+"/";
77 public static int RESOURCE_SERVER_PORT = 9443;
78 public static String RESOURCE_CONTEXT_PATH = "/secure";
79 public static String RESOURCE_CONTEXT_URL = SCHEME+"://"+SP_SERVER_NAME+":"+RESOURCE_SERVER_PORT+RESOURCE_CONTEXT_PATH+"/";
82 private static SAMLConfig samlConfig; // See constructor for use
87 /********************* Static Methods **********************/
92 * For automated test cases that normally just work, you
93 * probably want to leave the logging level to ERROR. However,
94 * if you are running a custom test case to discover the source
95 * of a problem, or when building a new test case, then you
96 * may want to set the logging level to DEBUG.
98 * You can change the loglevel variable from the test case
99 * code before calling setupLogging().
101 public static Level loglevel = Level.INFO;
103 private static Logger clientLogger = Logger.getLogger("edu.internet2.middleware.shibboleth");
104 private static Logger initLogger = Logger.getLogger("shibboleth.init");
105 private static Logger samlLogger = Logger.getLogger("org.opensaml");
106 private static boolean manageLogs = false;
109 * You will almost always call setupLogging first, but it
110 * it not automatic in case you have exotic logging
113 * <p>Restriction: avoid any static initialization that generates
114 * log messages because this method can only be called after
115 * static initialation.</p>
117 public static void setupLogging() {
119 Logger root = Logger.getRootLogger();
120 Layout initLayout = new PatternLayout("%d{HH:mm} %-5p %m%n");
121 ConsoleAppender consoleAppender= new ConsoleAppender(initLayout,ConsoleAppender.SYSTEM_OUT);
122 root.removeAllAppenders();
123 root.addAppender(consoleAppender);
124 root.setLevel(Level.ERROR);
125 clientLogger.removeAllAppenders();
126 clientLogger.setLevel(loglevel);
127 initLogger.removeAllAppenders();
128 initLogger.setLevel(loglevel);
129 samlLogger.removeAllAppenders();
130 samlLogger.setLevel(loglevel);
134 * Sometimes (as in IdP initialization) the logging levels
135 * get reset to some unintended level. This resets them
136 * to whatever we want for testing.
138 public static void resetLoggingLevels() {
139 if (!manageLogs) return; // If setupLogging was never called.
140 clientLogger.removeAllAppenders();
141 clientLogger.setLevel(loglevel);
142 initLogger.removeAllAppenders();
143 initLogger.setLevel(loglevel);
144 samlLogger.removeAllAppenders();
145 samlLogger.setLevel(loglevel);
153 /********************* Constructors ************************
154 * Initialization logic goes here.
155 * <p>Reqires that Log4J already be configured.</p>
157 public ShibbolethRunner() {
158 configureTestSAMLQueries();
162 * SAML has a list of BindingProviders that access the IdP.
163 * Normally the SOAP HTTP BindingProvider is the default and
164 * it accesses the IdP by creating a URL socket. This code
165 * replaces that default with a Mockrunner BindingProvider.
166 * So when the SP does an AA or Artifact Query, the IdP is
167 * called using its Mockrunner simulated Servlet context.
169 * <p>Note: This method depends on a real IdP context created
170 * using the IdPTestContext. Use another method if you want
171 * to feed back pre-created test responses.</p>
173 private void configureTestSAMLQueries() {
174 samlConfig = SAMLConfig.instance();
175 samlConfig.setDefaultBindingProvider(SAMLBinding.SOAP,
176 "edu.internet2.middleware.shibboleth.runner.MockHTTPBindingProvider" );
186 /************************* Service Provider ********************
187 * The SP is represented by an SPContext object and the objects
188 * SessionManager, SPConfig, etc. chained off it. The context
189 * is initialized and then the main configuration file is read
190 * in to create the Config object.
193 private String spConfigFileName = "/basicSpHome/spconfig.xml";
195 * If you are goint to change the SP Config File
196 * do it before calling initServiceProvider or constructing
199 * @param spConfigFileName
201 public void setSpConfigFileName(String spConfigFileName) {
202 this.spConfigFileName = spConfigFileName;
205 private static boolean SPinitialized = false; // don't do it twice
206 public static SPTestContext consumer = null;
209 * Initialize an instance of the SP context and configuration.
211 * @throws ShibbolethConfigurationException if bad config file
213 public void initializeSP()
214 throws ShibbolethConfigurationException{
215 if (SPinitialized) return;
218 ServiceProviderContext context = ServiceProviderContext.getInstance();
219 context.initialize();
221 ServiceProviderConfig config = new ServiceProviderConfig();
222 context.setServiceProviderConfig(config);
223 config.loadConfigObjects(spConfigFileName);
225 // Plug an instance of FilterSupportImpl into the Filter
226 FilterSupportImpl service = new FilterSupportImpl();
227 AuthenticationFilter.setFilterSupport(service);
233 * A MockRunner interface object for the AssertionConsumerServlet.
235 * <p>The SP itself is a static set of objects initialized
236 * under the ServiceProviderContext. There can be only one
237 * SP per ClassLoader, so there is no way to test multiple
238 * SPs at the same time. However, SPs don't interact, so it
239 * doesn't matter.</p>
241 * <p>If more than one SPTestContext object is created, they
242 * share the same SPContext objects.
244 public class SPTestContext {
246 // The Factory creates the Request, Response, Session, etc.
247 public WebMockObjectFactory factory = new WebMockObjectFactory();
249 // The TestModule runs the Servlet and Filter methods in the simulated container
250 public ServletTestModule testModule = new ServletTestModule(factory);
252 // Now simulated Servlet API objects
253 public MockServletContext servletContext= factory.getMockServletContext();
254 public MockFilterConfig filterConfig= factory.getMockFilterConfig();
255 public MockHttpServletResponse response = factory.getMockResponse();
256 public MockHttpServletRequest request = factory.getMockRequest();
258 public AssertionConsumerServlet spServlet;
262 * Construct the related objects
263 * @throws ShibbolethConfigurationException
265 public SPTestContext() throws ShibbolethConfigurationException {
268 servletContext.setServletContextName("dummy SP Context");
269 servletContext.setInitParameter("ServiceProviderConfigFile", spConfigFileName);
271 // Create the Servlet object, but do not run its init()
272 // instead use the initializeSP() routine which does
273 // the same initialize in the test environment
274 spServlet = new AssertionConsumerServlet();
275 testModule.setServlet(spServlet, false);
281 * Set the fields of the request that depend on a suffix,
283 public void resetRequest(String suffix) {
287 // Unchanging properties of the HttpServletRequest
288 request.setRemoteAddr(REMOTE_ADDR);
289 request.setContextPath(SP_CONTEXT_PATH);
290 request.setProtocol(PROTOCOL);
291 request.setScheme(SCHEME);
292 request.setServerName(SP_SERVER_NAME);
293 request.setServerPort(SERVER_PORT);
295 request.setRequestURI(SP_CONTEXT_URL+suffix);
296 request.setRequestURL(SP_CONTEXT_URL+suffix);
297 request.setServletPath(SP_CONTEXT_PATH+"/"+suffix);
307 /************************ IdP ******************************
308 * Setup the IdP interface object
310 * The IdP associates its "context" of cached data and configured
311 * objects with the IdPResponder Servlet object. They are
312 * initialized when the Servlet init() is called. It is
313 * possible to create more than one IdpTestContext object
314 * representing different configuration files, or a new
315 * IdpTestContext can be created with fresh Mockrunner object
316 * on top of an existing initialized IdP.
318 * To direct the AA and Artifact queries back to the Idp object,
319 * a call to SAML sets up the MockHTTPBindingProvider to replace
320 * the normal HTTPBindingProvider. Thus instead of creating URL
321 * and sockets to talk to the IdP, a simulated Request object is
322 * configured and the IdP is called through MockRunner.
324 public String idpConfigFileName = "/basicIdpHome/idpconfig.xml";
325 public void setIdpConfigFileName(String idpConfigFileName) {
326 this.idpConfigFileName = idpConfigFileName;
330 * Although it is possible in theory to have more than one IdP
331 * running in a TestCase, this one static IdpTestContext
332 * pointer tells the MockHTTPBindingProvider which IdP
333 * to use for AA and Artifact queries. If you have more
334 * that one IdP, the TestCase has to figure out how to swap this
335 * pointer between them.
337 public static IdpTestContext idp = null;
340 * Initializes the IdP if necessary, then returns a
341 * pointer to the MockRunner interface object
343 * @return IdpTestContext with Mockrunner objects
345 public IdpTestContext getIdp() {
347 idp = new IdpTestContext();
354 * A set of Mockrunner control blocks to call a newly initialized
355 * or previously created IdP.
357 * <p>By default, an IdpTestContext creates a new instance of the
358 * IdP using the current configuration file. However, if an
359 * already intialized IdPResponder servlet is passed to the
360 * constructor, then new Mockrunner blocks are created but
361 * the existing IdP is reused.</p>
363 * <p>The IdP is initialized when the IdpResponder servlet init() is
364 * called. This establishes the static context of tables that
365 * allow the IdP to issue a Subject and then respond when that
366 * Subject is returned in an Attribute Query.</p>
368 * <p>This class creates the Mockrunner control blocks needed to
369 * call the IdP and, by creating the IdP Servlet object, also
370 * initializes an instance of the IdP. It depends on a configuration
371 * file located as a resource in the classpath, typically in the
372 * /testresources directory of the project.</p>
374 public class IdpTestContext {
378 // The Factory creates the Request, Response, Session, etc.
379 public WebMockObjectFactory factory = new WebMockObjectFactory();
381 // The TestModule runs the Servlet and Filter methods in the simulated container
382 public ServletTestModule testModule = new ServletTestModule(factory);
384 // Now simulated Servlet API objects
385 public MockServletContext servletContext= factory.getMockServletContext();
386 public MockFilterConfig filterConfig= factory.getMockFilterConfig();
387 public MockHttpServletResponse response = factory.getMockResponse();
388 public MockHttpServletRequest request = factory.getMockRequest();
391 // The IdP Servlet that processes SSO, AA, and Artifact requests
392 public IdPResponder idpServlet;
395 * Construct new context and new IdP from the configuration file.
397 public IdpTestContext() {
402 * Create a new Mockrunner context. If an previous
403 * IdP was initialized in a prior context, reuse it
404 * and therefore only refresh the Mockrunner objects.
405 * Otherwise, initialize a new instance of the IdP.
407 public IdpTestContext(IdPResponder oldidp) {
410 servletContext.setServletContextName("dummy IdP Context");
411 servletContext.setInitParameter("IdPConfigFile", idpConfigFileName);
414 idpServlet = new IdPResponder();
415 // NOTE: The IdP reads its configuration file and initializes
416 // itself within this call.
417 testModule.setServlet(idpServlet,true);
418 resetLoggingLevels();
420 // reuse an existing initialized servlet
427 * Set the fields of the request that depend on a suffix,
428 * normally SSO, AA, or Artifact
430 public void resetRequest(String suffix) {
435 // Unchanging properties of the HttpServletRequest
436 request.setRemoteAddr(REMOTE_ADDR);
437 request.setContextPath(IDP_CONTEXT_PATH);
438 request.setProtocol(PROTOCOL);
439 request.setScheme(SCHEME);
440 request.setServerName(IDP_SERVER_NAME);
441 request.setServerPort(SERVER_PORT);
443 request.setRequestURI(IDP_CONTEXT_URL+suffix);
444 request.setRequestURL(IDP_CONTEXT_URL+suffix);
445 request.setServletPath(IDP_CONTEXT_PATH+"/"+suffix);
454 /********************** Attribute Source ***********************
455 * Here we keep a static reference to a Collection of Attributes.
457 * The Test can clear the collection and add attributes. When
458 * the IdP needs attributes, it treats this collection as the
459 * starting point and processes them through ARP. When then get
460 * to the SP they go through AAP. So you can test the Attribute
461 * processing logic in both components by creating Attributes
462 * with names and values that are accepted or rejected.
465 public static BasicAttributes attributes = new BasicAttributes();
468 * The Test should obtain a reference to the Attribute collection and add
469 * such attributes as it wants the IdP to return for a Principal.
470 * @return Attributes collection
472 public Attributes getAttributesCollection() {
480 /*************************** Resource Manage Filter *****************
481 * The Filter depends on a Servlet environment simulated by MockRunner.
482 * We give it its own set of MockRunner blocks because in real life
483 * it runs in a separate context from the SP or IdP.
485 * The Filter depends on the SP and, once initialized, has a reference
486 * to FilterSupportImpl and through it the SP configuration and Sessions.
488 private AuthenticationFilterContext filter;
489 public AuthenticationFilterContext getFilter() throws ShibbolethConfigurationException {
491 filter=new AuthenticationFilterContext();
496 * Create the MockRunning interface for running the ServletFilter.
498 * <p>The AuthenticationFilter object itself contains no
499 * meaningful state, so you can create multiple instances
500 * of this interface object to represent more than one
501 * Resource context being managed by the same SP.</p>
504 public class AuthenticationFilterContext {
507 // The Factory creates the Request, Response, Session, etc.
508 public WebMockObjectFactory factory = new WebMockObjectFactory();
510 // The TestModule runs the Servlet and Filter methods in the simulated container
511 public ServletTestModule testModule = new ServletTestModule(factory);
513 // Now simulated Servlet API objects
514 public MockServletContext servletContext= factory.getMockServletContext();
515 public MockFilterConfig filterConfig= factory.getMockFilterConfig();
516 public MockHttpServletResponse response = factory.getMockResponse();
517 public MockHttpServletRequest request = factory.getMockRequest();
520 private AuthenticationFilter filter;
522 public AuthenticationFilterContext() {
524 // Dummy web.xml for Resouce context
525 servletContext.setServletContextName("dummy Servlet Context");
527 // Dummy <Filter> in dummy web.xml
528 MockServletContext filterParameters = new MockServletContext();
529 filterParameters.setInitParameter("requireId", ".+/test.+");
530 filterConfig.setupServletContext(filterParameters);
531 filterConfig.setFilterName("Test Filter under JUnit");
535 * Call after any changes to Context init-param values to
536 * initialize the filter object and connect to the SP.
538 * @throws ShibbolethConfigurationException from SP init.
540 public void setUp() throws ShibbolethConfigurationException {
542 // Create instance of Filter class, add to chain, call its init()
543 filter = new AuthenticationFilter();
544 testModule.addFilter(filter,true);
546 // Note: if the SP is already initialized, this noops.
552 public void resetRequest(String suffix) {
554 request.setRemoteAddr(REMOTE_ADDR);
555 request.setContextPath(RESOURCE_CONTEXT_PATH);
556 request.setProtocol(PROTOCOL);
557 request.setScheme(SCHEME);
558 request.setServerName(SP_SERVER_NAME);
559 request.setServerPort(RESOURCE_SERVER_PORT);
561 request.setMethod("GET");
562 request.setRequestURI(RESOURCE_CONTEXT_URL+suffix);
563 request.setRequestURL(RESOURCE_CONTEXT_URL+suffix);
564 request.setServletPath(RESOURCE_CONTEXT_PATH+"/"+suffix);