View Javadoc

1   /*
2    * Licensed to the University Corporation for Advanced Internet Development, 
3    * Inc. (UCAID) under one or more contributor license agreements.  See the 
4    * NOTICE file distributed with this work for additional information regarding
5    * copyright ownership. The UCAID licenses this file to You under the Apache 
6    * License, Version 2.0 (the "License"); you may not use this file except in 
7    * compliance with the License.  You may obtain a copy of the License at
8    *
9    *    http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package edu.internet2.middleware.shibboleth.idp.profile.saml2;
19  
20  import java.util.ArrayList;
21  
22  import org.opensaml.common.SAMLObjectBuilder;
23  import org.opensaml.common.binding.BasicEndpointSelector;
24  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
25  import org.opensaml.common.xml.SAMLConstants;
26  import org.opensaml.saml2.core.AttributeQuery;
27  import org.opensaml.saml2.core.AttributeStatement;
28  import org.opensaml.saml2.core.NameID;
29  import org.opensaml.saml2.core.Response;
30  import org.opensaml.saml2.core.Statement;
31  import org.opensaml.saml2.core.StatusCode;
32  import org.opensaml.saml2.core.Subject;
33  import org.opensaml.saml2.metadata.AssertionConsumerService;
34  import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
35  import org.opensaml.saml2.metadata.Endpoint;
36  import org.opensaml.saml2.metadata.EntityDescriptor;
37  import org.opensaml.saml2.metadata.SPSSODescriptor;
38  import org.opensaml.saml2.metadata.provider.MetadataProvider;
39  import org.opensaml.ws.message.decoder.MessageDecodingException;
40  import org.opensaml.ws.transport.http.HTTPInTransport;
41  import org.opensaml.ws.transport.http.HTTPOutTransport;
42  import org.opensaml.xml.security.SecurityException;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
47  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
48  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
49  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.AttributeQueryConfiguration;
50  import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
51  import edu.internet2.middleware.shibboleth.idp.session.Session;
52  
53  /** SAML 2.0 Attribute Query profile handler. */
54  public class AttributeQueryProfileHandler extends AbstractSAML2ProfileHandler {
55  
56      /** Class logger. */
57      private static Logger log = LoggerFactory.getLogger(AttributeQueryProfileHandler.class);
58  
59      /** Builder of NameID objects. */
60      private SAMLObjectBuilder<NameID> nameIDBuilder;
61  
62      /** Builder of assertion consumer service endpoints. */
63      private SAMLObjectBuilder<AssertionConsumerService> acsEndpointBuilder;
64  
65      /** Constructor. */
66      public AttributeQueryProfileHandler() {
67          super();
68  
69          nameIDBuilder = (SAMLObjectBuilder<NameID>) getBuilderFactory().getBuilder(
70                  NameID.DEFAULT_ELEMENT_NAME);
71          acsEndpointBuilder = (SAMLObjectBuilder<AssertionConsumerService>) getBuilderFactory().getBuilder(
72                  AssertionConsumerService.DEFAULT_ELEMENT_NAME);
73      }
74  
75      /** {@inheritDoc} */
76      public String getProfileId() {
77          return AttributeQueryConfiguration.PROFILE_ID;
78      }
79  
80      /** {@inheritDoc} */
81      public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
82          Response samlResponse;
83  
84          AttributeQueryContext requestContext = new AttributeQueryContext();
85  
86          try {
87              decodeRequest(requestContext, inTransport, outTransport);
88  
89              if (requestContext.getProfileConfiguration() == null) {
90                  String msg = "SAML 2 Attribute Query profile is not configured for relying party "
91                          + requestContext.getInboundMessage();
92                  requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI,
93                          msg));
94                  log.warn(msg);
95                  samlResponse = buildErrorResponse(requestContext);
96              } else {
97                  checkSamlVersion(requestContext);
98  
99                  // TODO add proper requested attribute filtering support
100                 AttributeQuery query = requestContext.getInboundSAMLMessage();
101                 if (query.getAttributes() != null && !query.getAttributes().isEmpty()) {
102                     log.warn("Specific attributes requested in query '{}'. This functionality is not yet supported",
103                             requestContext.getInboundMessageIssuer());
104                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI,
105                             StatusCode.REQUEST_UNSUPPORTED_URI,
106                             "Request of specific attributes during an attribute query is not supported"));
107                     throw new ProfileException();
108                 }
109 
110                 // Resolve attribute query name id to principal name and place in context
111                 resolvePrincipal(requestContext);
112 
113                 Session idpSession = getSessionManager().getSession(requestContext.getPrincipalName());
114                 if (idpSession != null) {
115                     requestContext.setUserSession(idpSession);
116                     AuthenticationMethodInformation authnInfo = idpSession.getAuthenticationMethods().get(
117                             requestContext.getInboundMessageIssuer());
118                     if (authnInfo != null) {
119                         requestContext.setPrincipalAuthenticationMethod(authnInfo.getAuthenticationMethod());
120                     }
121                 }
122 
123                 resolveAttributes(requestContext);
124 
125                 // Lookup principal name and attributes, create attribute statement from information
126                 ArrayList<Statement> statements = new ArrayList<Statement>();
127                 AttributeStatement attributeStatement = buildAttributeStatement(requestContext);
128                 if (attributeStatement != null) {
129                     requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
130                     statements.add(attributeStatement);
131                 }
132 
133                 // create the SAML response
134                 samlResponse = buildResponse(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:sender-vouches",
135                         statements);
136             }
137         } catch (ProfileException e) {
138             samlResponse = buildErrorResponse(requestContext);
139         }
140 
141         requestContext.setOutboundSAMLMessage(samlResponse);
142         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
143         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
144 
145         encodeResponse(requestContext);
146         writeAuditLogEntry(requestContext);
147     }
148 
149     /**
150      * Decodes an incoming request and populates a created request context with the resultant information.
151      * 
152      * @param inTransport inbound message transport
153      * @param outTransport outbound message transport *
154      * @param requestContext request context to which decoded information should be added
155      * 
156      * @throws ProfileException throw if there is a problem decoding the request
157      */
158     protected void decodeRequest(AttributeQueryContext requestContext, HTTPInTransport inTransport,
159             HTTPOutTransport outTransport) throws ProfileException {
160         if (log.isDebugEnabled()) {
161             log.debug("Decoding message with decoder binding '{}'", getInboundMessageDecoder(requestContext)
162                     .getBindingURI());
163         }
164 
165         requestContext.setCommunicationProfileId(getProfileId());
166 
167         MetadataProvider metadataProvider = getMetadataProvider();
168         requestContext.setMetadataProvider(metadataProvider);
169 
170         requestContext.setInboundMessageTransport(inTransport);
171         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
172         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
173         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
174 
175         requestContext.setOutboundMessageTransport(outTransport);
176         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
177 
178         try {
179             SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
180             requestContext.setMessageDecoder(decoder);
181             decoder.decode(requestContext);
182             log.debug("Decoded request from relying party '{}'", requestContext.getInboundMessageIssuer());
183 
184             if (!(requestContext.getInboundSAMLMessage() instanceof AttributeQuery)) {
185                 log.warn("Incoming message was not a AttributeQuery, it was a {}", requestContext
186                         .getInboundSAMLMessage().getClass().getName());
187                 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null,
188                         "Invalid SAML AttributeQuery message."));
189                 throw new ProfileException("Invalid SAML AttributeQuery message.");
190             }
191         } catch (MessageDecodingException e) {
192             String msg = "Error decoding attribute query message";
193             log.warn(msg, e);
194             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, msg));
195             throw new ProfileException(msg);
196         } catch (SecurityException e) {
197             String msg = "Message did not meet security requirements";
198             log.warn(msg, e);
199             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI, msg));
200             throw new ProfileException(msg, e);
201         } finally {
202             // Set as much information as can be retrieved from the decoded message
203             populateRequestContext(requestContext);
204         }
205     }
206 
207     /** {@inheritDoc} */
208     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
209             throws ProfileException {
210         super.populateRelyingPartyInformation(requestContext);
211 
212         EntityDescriptor relyingPartyMetadata = requestContext.getPeerEntityMetadata();
213         if (relyingPartyMetadata != null) {
214             requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
215             requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS));
216         }
217     }
218 
219     /** {@inheritDoc} */
220     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
221             throws ProfileException {
222         super.populateAssertingPartyInformation(requestContext);
223 
224         EntityDescriptor localEntityDescriptor = requestContext.getLocalEntityMetadata();
225         if (localEntityDescriptor != null) {
226             requestContext.setLocalEntityRole(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME);
227             requestContext.setLocalEntityRoleMetadata(localEntityDescriptor
228                     .getAttributeAuthorityDescriptor(SAMLConstants.SAML20P_NS));
229         }
230     }
231 
232     /**
233      * Populates the request context with information from the inbound SAML message.
234      * 
235      * This method requires the the following request context properties to be populated: inbound saml message
236      * 
237      * This methods populates the following request context properties: subject name identifier
238      * 
239      * @param requestContext current request context
240      * 
241      * @throws ProfileException thrown if the inbound SAML message or subject identifier is null
242      */
243     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
244         AttributeQuery query = (AttributeQuery) requestContext.getInboundSAMLMessage();
245         if (query != null) {
246             Subject subject = query.getSubject();
247             if (subject == null) {
248                 String msg = "Attribute query did not contain a proper subject";
249                 log.warn(msg);
250                 ((AttributeQueryContext) requestContext).setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null,
251                         msg));
252                 throw new ProfileException(msg);
253             }
254             requestContext.setSubjectNameIdentifier(subject.getNameID());
255         }
256     }
257 
258     /**
259      * Selects the appropriate endpoint for the relying party and stores it in the request context.
260      * 
261      * @param requestContext current request context
262      * 
263      * @return Endpoint selected from the information provided in the request context
264      */
265     protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) {
266         Endpoint endpoint;
267 
268         if (getInboundBinding().equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) {
269             endpoint = acsEndpointBuilder.buildObject();
270             endpoint.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI);
271         } else {
272             BasicEndpointSelector endpointSelector = new BasicEndpointSelector();
273             endpointSelector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
274             endpointSelector.setMetadataProvider(getMetadataProvider());
275             endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
276             endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
277             endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
278             endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
279             endpoint = endpointSelector.selectEndpoint();
280         }
281 
282         return endpoint;
283     }
284 
285     /** {@inheritDoc} */
286     protected NameID buildNameId(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
287         throws ProfileException {
288         
289         log.debug("Reusing NameID supplied in query");
290         NameID src = requestContext.getSubjectNameIdentifier();
291         if (src != null) {
292             NameID dest = nameIDBuilder.buildObject();
293             dest.setValue(src.getValue());
294             dest.setNameQualifier(src.getNameQualifier());
295             dest.setSPNameQualifier(src.getSPNameQualifier());
296             dest.setFormat(src.getFormat());
297             dest.setSPProvidedID(src.getSPProvidedID());
298 
299             if (dest.getValue() != null) {
300                 // TODO: this is a hack to satisfy the audit log, but we should fix the
301                 // context API to handle the NameID value directly
302                 BasicAttribute<String> attribute = new BasicAttribute<String>();
303                 attribute.setId("outboundQueryNameID");
304                 attribute.getValues().add(dest.getValue());
305                 requestContext.setNameIdentifierAttribute(attribute);
306             }
307 
308             return dest;
309         }
310         return null;
311     }
312 
313     
314     /** Basic data structure used to accumulate information as a request is being processed. */
315     protected class AttributeQueryContext extends
316             BaseSAML2ProfileRequestContext<AttributeQuery, Response, AttributeQueryConfiguration> {
317 
318     }
319 }