Configure logging with <Logging> element in origin.xml.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / AAServlet.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved
4  * 
5  * 
6  * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
7  * following conditions are met:
8  * 
9  * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
10  * disclaimer.
11  * 
12  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
13  * disclaimer in the documentation and/or other materials provided with the distribution, if any, must include the
14  * following acknowledgment: "This product includes software developed by the University Corporation for Advanced
15  * Internet Development <http://www.ucaid.edu> Internet2 Project. Alternately, this acknowledegement may appear in the
16  * software itself, if and wherever such third-party acknowledgments normally appear.
17  * 
18  * Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor the University Corporation for
19  * Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote products derived from this software
20  * without specific prior written permission. For written permission, please contact shibboleth@shibboleth.org
21  * 
22  * Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the University Corporation
23  * for Advanced Internet Development, nor may Shibboleth appear in their name, without prior written permission of the
24  * University Corporation for Advanced Internet Development.
25  * 
26  * 
27  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR
28  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
29  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE,
30  * ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
31  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
32  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
33  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36  */
37
38 package edu.internet2.middleware.shibboleth.aa;
39
40 import java.io.IOException;
41 import java.net.URI;
42 import java.net.URISyntaxException;
43 import java.security.Principal;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.Date;
48 import java.util.Iterator;
49
50 import javax.servlet.ServletException;
51 import javax.servlet.UnavailableException;
52 import javax.servlet.http.HttpServlet;
53 import javax.servlet.http.HttpServletRequest;
54 import javax.servlet.http.HttpServletResponse;
55
56 import org.apache.log4j.Logger;
57 import org.apache.log4j.MDC;
58 import org.opensaml.QName;
59 import org.opensaml.SAMLAssertion;
60 import org.opensaml.SAMLAttribute;
61 import org.opensaml.SAMLAttributeQuery;
62 import org.opensaml.SAMLAttributeStatement;
63 import org.opensaml.SAMLAudienceRestrictionCondition;
64 import org.opensaml.SAMLBinding;
65 import org.opensaml.SAMLCondition;
66 import org.opensaml.SAMLException;
67 import org.opensaml.SAMLIdentifier;
68 import org.opensaml.SAMLRequest;
69 import org.opensaml.SAMLResponse;
70 import org.opensaml.SAMLStatement;
71 import org.opensaml.SAMLSubject;
72 import org.w3c.dom.Document;
73 import org.w3c.dom.Element;
74 import org.w3c.dom.NodeList;
75
76 import sun.misc.BASE64Decoder;
77 import edu.internet2.middleware.shibboleth.aa.arp.ArpEngine;
78 import edu.internet2.middleware.shibboleth.aa.arp.ArpException;
79 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
80 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolverException;
81 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
82 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
83 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
84 import edu.internet2.middleware.shibboleth.common.NameMapper;
85 import edu.internet2.middleware.shibboleth.common.OriginConfig;
86 import edu.internet2.middleware.shibboleth.common.RelyingParty;
87 import edu.internet2.middleware.shibboleth.common.SAMLBindingFactory;
88 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapperException;
89 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
90 import edu.internet2.middleware.shibboleth.common.ShibbolethOriginConfig;
91
92 /**
93  * @author Walter Hoehn
94  */
95
96 public class AAServlet extends HttpServlet {
97
98         private AAConfig configuration;
99         protected AAResponder responder;
100         private NameMapper nameMapper;
101         private SAMLBinding binding;
102         private static Logger transactionLog = Logger.getLogger("Shibboleth-TRANSACTION");
103         private AAServiceProviderMapper targetMapper;
104
105         private static Logger log = Logger.getLogger(AAServlet.class.getName());
106
107         public void init() throws ServletException {
108                 super.init();
109
110                 MDC.put("serviceId", "[AA] Core");
111                 log.info("Initializing Attribute Authority.");
112
113                 try {
114                         nameMapper = new NameMapper();
115                         loadConfiguration();
116
117                         binding = SAMLBindingFactory.getInstance(SAMLBinding.SAML_SOAP_HTTPS);
118
119                         log.info("Attribute Authority initialization complete.");
120
121                 } catch (ShibbolethConfigurationException ae) {
122                         log.fatal("The AA could not be initialized: " + ae);
123                         throw new UnavailableException("Attribute Authority failed to initialize.");
124                 } catch (SAMLException se) {
125                         log.fatal("SAML SOAP binding could not be loaded: " + se);
126                         throw new UnavailableException("Attribute Authority failed to initialize.");
127                 }
128         }
129
130         protected void loadConfiguration() throws ShibbolethConfigurationException {
131
132                 Document originConfig = OriginConfig.getOriginConfig(this.getServletContext());
133
134                 //Load global configuration properties
135                 configuration = new AAConfig(originConfig.getDocumentElement());
136
137                 //Load name mappings
138                 NodeList itemElements =
139                         originConfig.getDocumentElement().getElementsByTagNameNS(
140                                 NameIdentifierMapping.mappingNamespace,
141                                 "NameMapping");
142
143                 for (int i = 0; i < itemElements.getLength(); i++) {
144                         try {
145                                 nameMapper.addNameMapping((Element) itemElements.item(i));
146                         } catch (NameIdentifierMappingException e) {
147                                 log.error("Name Identifier mapping could not be loaded: " + e);
148                         }
149                 }
150
151                 //Load relying party config
152                 try {
153                         targetMapper = new AAServiceProviderMapper(originConfig.getDocumentElement(), configuration);
154                 } catch (ServiceProviderMapperException e) {
155                         log.error("Could not load origin configuration: " + e);
156                         throw new ShibbolethConfigurationException("Could not load origin configuration.");
157                 }
158
159                 try {
160                         //Startup Attribute Resolver
161                         AttributeResolver resolver = new AttributeResolver(configuration);
162
163                         //Startup ARP Engine
164                         ArpEngine arpEngine = null;
165                         itemElements =
166                                 originConfig.getDocumentElement().getElementsByTagNameNS(
167                                         ShibbolethOriginConfig.originConfigNamespace,
168                                         "ReleasePolicyEngine");
169
170                         if (itemElements.getLength() > 1) {
171                                 log.warn("Encountered multiple <ReleasePolicyEngine> configuration elements.  Using first...");
172                         }
173                         if (itemElements.getLength() < 1) {
174                                 arpEngine = new ArpEngine();
175                         } else {
176                                 arpEngine = new ArpEngine((Element) itemElements.item(0));
177                         }
178
179                         //Startup responder
180                         responder = new AAResponder(arpEngine, resolver);
181
182                 } catch (ArpException ae) {
183                         log.fatal("The AA could not be initialized due to a problem with the ARP Engine configuration: " + ae);
184                         throw new ShibbolethConfigurationException("Could not load ARP Engine.");
185                 } catch (AttributeResolverException ne) {
186                         log.fatal(
187                                 "The AA could not be initialized due to a problem with the Attribute Resolver configuration: " + ne);
188                         throw new ShibbolethConfigurationException("Could not load Attribute Resolver.");
189                 }
190
191         }
192         public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
193
194                 MDC.put("serviceId", "[AA] " + new SAMLIdentifier().toString());
195                 MDC.put("remoteAddr", req.getRemoteAddr());
196                 log.info("Handling request.");
197
198                 AARelyingParty relyingParty = null;
199
200                 //Parse SOAP request
201                 SAMLRequest samlRequest = null;
202                 StringBuffer credentialName = new StringBuffer();
203                 try {
204                         samlRequest = binding.receive(req, credentialName);
205
206                 } catch (SAMLException e) {
207                         log.fatal("Unable to parse request: " + e);
208                         throw new ServletException("Request failed.");
209                 }
210
211                 try {
212                         if (samlRequest.getQuery() == null || !(samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
213                                 throw new SAMLException(
214                                         SAMLException.REQUESTER,
215                                         "This SAML authority only responds to attribute queries.");
216                         }
217                         SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
218
219                         //Identify a Relying Party
220                         if (attributeQuery.getResource() == null || attributeQuery.getResource().equals("")) {
221                                 log.error("Request from an unidentified service provider.");
222                         }
223                         log.info("Request from service provider: (" + attributeQuery.getResource() + ").");
224                         relyingParty = targetMapper.getRelyingParty(attributeQuery.getResource());
225
226                         //Map Subject to local principal
227                         if (relyingParty.getProviderId() != null
228                                 && !relyingParty.getProviderId().equals(attributeQuery.getSubject().getName().getNameQualifier())) {
229                                 log.error(
230                                         "The name qualifier for the referenced subject ("
231                                                 + attributeQuery.getSubject().getName().getNameQualifier()
232                                                 + ") is not valid for this identiy provider.");
233                                 throw new NameIdentifierMappingException(
234                                         "The name qualifier for the referenced subject ("
235                                                 + attributeQuery.getSubject().getName().getNameQualifier()
236                                                 + ") is not valid for this identiy provider.");
237                         }
238
239                         Principal principal = null;
240                         try {
241                                 if (attributeQuery.getSubject().getName().getName().equalsIgnoreCase("foo")) {
242                                         // for testing
243                                         principal = new AuthNPrincipal("test-handle");
244                                 } else {
245                                         principal =
246                                                 nameMapper.getPrincipal(
247                                                         attributeQuery.getSubject().getName(),
248                                                         relyingParty,
249                                                         relyingParty.getIdentityProvider());
250                                 }
251                                 log.info("Request is for principal (" + principal.getName() + ").");
252
253                                 //TODO Do something about these silly passthru errors
254
255                         } catch (NameIdentifierMappingException e) {
256                                 log.info("Could not associate the request subject with a principal: " + e);
257                                 try {
258                                         //TODO this doesn't always make sense anymore
259                                         QName[] codes =
260                                                 {
261                                                         SAMLException.REQUESTER,
262                                                         new QName(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS, "InvalidHandle")};
263                                         if (relyingParty.passThruErrors()) {
264                                                 sendFailure(
265                                                         resp,
266                                                         samlRequest,
267                                                         new SAMLException(Arrays.asList(codes), "The supplied Subject was unrecognized.", e));
268
269                                         } else {
270                                                 sendFailure(
271                                                         resp,
272                                                         samlRequest,
273                                                         new SAMLException(Arrays.asList(codes), "The supplied Subject was unrecognized."));
274                                         }
275                                         return;
276                                 } catch (Exception ee) {
277                                         log.fatal("Could not construct a SAML error response: " + ee);
278                                         throw new ServletException("Attribute Authority response failure.");
279                                 }
280                         }
281
282                         if (credentialName == null || credentialName.toString().equals("")) {
283                                 log.info("Request is from an unauthenticated service provider.");
284                         } else {
285                                 log.info("Request is from service provider: (" + credentialName + ").");
286                         }
287
288                         SAMLAttribute[] attrs;
289                         Iterator requestedAttrsIterator = attributeQuery.getDesignators();
290                         if (requestedAttrsIterator.hasNext()) {
291                                 log.info("Request designates specific attributes, resolving this set.");
292                                 ArrayList requestedAttrs = new ArrayList();
293                                 while (requestedAttrsIterator.hasNext()) {
294                                         SAMLAttribute attribute = (SAMLAttribute) requestedAttrsIterator.next();
295                                         try {
296                                                 log.debug("Designated attribute: (" + attribute.getName() + ")");
297                                                 requestedAttrs.add(new URI(attribute.getName()));
298                                         } catch (URISyntaxException use) {
299                                                 log.error(
300                                                         "Request designated an attribute name that does not conform to the required URI syntax ("
301                                                                 + attribute.getName()
302                                                                 + ").  Ignoring this attribute");
303                                         }
304                                 }
305                                 attrs =
306                                         responder.getReleaseAttributes(
307                                                 principal,
308                                                 credentialName.toString(),
309                                                 null,
310                                                 (URI[]) requestedAttrs.toArray(new URI[0]));
311                         } else {
312                                 log.info("Request does not designate specific attributes, resolving all available.");
313                                 attrs = responder.getReleaseAttributes(principal, credentialName.toString(), null);
314                         }
315
316                         log.info("Found " + attrs.length + " attribute(s) for " + principal.getName());
317                         sendResponse(resp, attrs, samlRequest, relyingParty, null);
318                         log.info("Successfully responded about " + principal.getName());
319
320                         //TODO place transaction log statement here
321
322                 } catch (Exception e) {
323                         log.error("Error while processing request: " + e);
324                         try {
325                                 if (relyingParty != null && relyingParty.passThruErrors()) {
326                                         sendFailure(
327                                                 resp,
328                                                 samlRequest,
329                                                 new SAMLException(SAMLException.RESPONDER, "General error processing request.", e));
330                                 } else if (configuration.passThruErrors()) {
331                                         sendFailure(
332                                                 resp,
333                                                 samlRequest,
334                                                 new SAMLException(SAMLException.RESPONDER, "General error processing request.", e));
335                                 } else {
336                                         sendFailure(
337                                                 resp,
338                                                 samlRequest,
339                                                 new SAMLException(SAMLException.RESPONDER, "General error processing request."));
340                                 }
341                                 return;
342                         } catch (Exception ee) {
343                                 log.fatal("Could not construct a SAML error response: " + ee);
344                                 throw new ServletException("Attribute Authority response failure.");
345                         }
346
347                 }
348         }
349         
350         public void destroy() {
351                 log.info("Cleaning up resources.");
352                 responder.destroy();
353                 nameMapper.destroy();
354         }
355         public void sendResponse(
356                 HttpServletResponse resp,
357                 SAMLAttribute[] attrs,
358                 SAMLRequest samlRequest,
359                 RelyingParty relyingParty,
360                 SAMLException exception)
361                 throws IOException {
362
363                 SAMLException ourSE = null;
364                 SAMLResponse samlResponse = null;
365
366                 try {
367                         if (attrs == null || attrs.length == 0) {
368                                 //No attribute found
369                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, exception);
370                         } else {
371
372                                 if (samlRequest.getQuery() == null || !(samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
373                                         throw new SAMLException(
374                                                 SAMLException.REQUESTER,
375                                                 "This SAML authority only responds to attribute queries");
376                                 }
377                                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
378
379                                 //Reference requested subject
380                                 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
381
382                                 //Set appropriate audience
383                                 ArrayList audiences = new ArrayList();
384                                 if (relyingParty.getProviderId() != null) {
385                                         audiences.add(relyingParty.getProviderId());
386                                 }
387                                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
388                                         audiences.add(relyingParty.getName());
389                                 }
390                                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
391
392                                 //Put all attributes into an assertion
393                                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, Arrays.asList(attrs));
394
395                                 //Set assertion expiration to longest attribute expiration
396                                 long max = 0;
397                                 for (int i = 0; i < attrs.length; i++) {
398                                         if (max < attrs[i].getLifetime()) {
399                                                 max = attrs[i].getLifetime();
400                                         }
401                                 }
402                                 Date now = new Date();
403                                 Date then = new Date(now.getTime() + max);
404
405                                 SAMLAssertion sAssertion =
406                                         new SAMLAssertion(
407                                                 relyingParty.getIdentityProvider().getProviderId(),
408                                                 now,
409                                                 then,
410                                                 Collections.singleton(condition),
411                                                 null,
412                                                 Collections.singleton(statement));
413
414                                 samlResponse =
415                                         new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), exception);
416                         }
417                 } catch (SAMLException se) {
418                         ourSE = se;
419                 } catch (CloneNotSupportedException ex) {
420                         ourSE = new SAMLException(SAMLException.RESPONDER, ex);
421
422                 } finally {
423
424                         if (log.isDebugEnabled()) {
425                                 try {
426                                         log.debug(
427                                                 "Dumping generated SAML Response:"
428                                                         + System.getProperty("line.separator")
429                                                         + new String(
430                                                                 new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
431                                                                 "UTF8"));
432                                 } catch (SAMLException e) {
433                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
434                                 } catch (IOException e) {
435                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
436                                 }
437                         }
438
439                         binding.respond(resp, samlResponse, ourSE);
440                 }
441         }
442
443         public void sendFailure(HttpServletResponse httpResponse, SAMLRequest samlRequest, SAMLException exception)
444                 throws IOException {
445                 try {
446                         SAMLResponse samlResponse =
447                                 new SAMLResponse((samlRequest != null) ? samlRequest.getId() : null, null, null, exception);
448                         if (log.isDebugEnabled()) {
449                                 try {
450                                         log.debug(
451                                                 "Dumping generated SAML Error Response:"
452                                                         + System.getProperty("line.separator")
453                                                         + new String(
454                                                                 new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
455                                                                 "UTF8"));
456                                 } catch (IOException e) {
457                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
458                                 }
459                         }
460                         binding.respond(httpResponse, samlResponse, null);
461                         log.debug("Returning SAML Error Response.");
462                 } catch (SAMLException se) {
463                         binding.respond(httpResponse, null, exception);
464                         log.error("AA failed to make an error message: " + se);
465                 }
466         }
467
468 }