Add support for a defining a precedence of NameID formats - SC-145
authorlajoie <lajoie@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Tue, 15 Mar 2011 14:51:51 +0000 (14:51 +0000)
committerlajoie <lajoie@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Tue, 15 Mar 2011 14:51:51 +0000 (14:51 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/branches/REL_2@2997 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/main/java/edu/internet2/middleware/shibboleth/idp/profile/AbstractSAMLProfileHandler.java

index 425363b..104194f 100644 (file)
@@ -91,7 +91,7 @@ public abstract class AbstractSAMLProfileHandler extends
 
     /** Resolver used to determine active security policy for an incoming request. */
     private SecurityPolicyResolver securityPolicyResolver;
-    
+
     /** Credential resolver for resolving keys from metadata. */
     private MetadataCredentialResolver metadataCredentialResolver;
 
@@ -183,13 +183,13 @@ public abstract class AbstractSAMLProfileHandler extends
 
     /**
      * A convenience method for obtaining a metadata credential resolver for the current metadata provider.
-     *
+     * 
      * @return the metadata credential resolver or null
      */
     public MetadataCredentialResolver getMetadataCredentialResolver() {
         // It's advisable to cache the metadata cred resolver instance from the factory
-        // for the life of the profile handler.  See SIDP-428.
-        synchronized(this) {
+        // for the life of the profile handler. See SIDP-428.
+        synchronized (this) {
             if (metadataCredentialResolver == null) {
                 MetadataCredentialResolverFactory mcrFactory = MetadataCredentialResolverFactory.getFactory();
                 MetadataProvider metadataProvider = getMetadataProvider();
@@ -448,22 +448,43 @@ public abstract class AbstractSAMLProfileHandler extends
 
         String requiredNameFormat = DatatypeHelper.safeTrimOrNullString(getRequiredNameIDFormat(requestContext));
         if (requiredNameFormat != null) {
-            log.debug("Attempting to build name identifier for relying party'{}' that requires format '{}'",
+            log.debug("Attempting to select name identifier attribute for relying party'{}' that requires format '{}'",
                     requestContext.getInboundMessageIssuer(), requiredNameFormat);
             return selectNameIDAttributeAndEncoderByRequiredFormat(requiredNameFormat, nameIdEncoderType,
                     requestContext);
         }
 
+        Map<String, BaseAttribute> principalAttributes = requestContext.getAttributes();
+        if (principalAttributes == null || principalAttributes.isEmpty()) {
+            log.debug("No attributes for principal '{}', no name identifier will be created for relying party '{}'",
+                    requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
+            return null;
+        }
+
+        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = null;
         List<String> supportedNameFormats = getNameFormats(requestContext);
         if (supportedNameFormats.isEmpty()) {
-            log.debug("Attempting to build name identifier for relying party '{}' that supports any format",
+            log.debug("Attempting to select name identifier attribute for relying party '{}' that supports any format",
                     requestContext.getInboundMessageIssuer());
+            nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoderByFormat(nameIdEncoderType, principalAttributes,
+                    null);
         } else {
-            log.debug("Attempting to build name identifier for relying party '{}' that supports the formats: {}",
+            log.debug(
+                    "Attempting to select name identifier attribute for relying party '{}' that supports the formats: {}",
                     requestContext.getInboundMessageIssuer(), supportedNameFormats);
+            nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoderBySupportedFormats(supportedNameFormats,
+                    nameIdEncoderType, requestContext);
         }
-        return selectNameIDAttributeAndEncoderBySupportedFormats(supportedNameFormats, nameIdEncoderType,
-                requestContext);
+
+        if (nameIdAttributeAndEncoder != null) {
+            log.debug("Name identifier for relying party '{}' will be built from attribute '{}'",
+                    requestContext.getInboundMessageIssuer(), nameIdAttributeAndEncoder.getFirst().getId());
+        } else {
+            log.debug(
+                    "No attributes for principal '{}' support encoding into a supported name identifier format for relying party '{}'",
+                    requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
+        }
+        return nameIdAttributeAndEncoder;
     }
 
     /**
@@ -481,48 +502,12 @@ public abstract class AbstractSAMLProfileHandler extends
     }
 
     /**
-     * Selects the principal attribute that can be encoded in to the required name identifier format.
-     * 
-     * @param <T> type of name identifier encoder the attribute must support
-     * @param requiredNameFormat required name identifier format type
-     * @param nameIdEncoderType type of name identifier encoder the attribute must support
-     * @param requestContext the current request context
-     * 
-     * @return the select attribute, and its encoder, to be used to build the name identifier
-     * 
-     * @throws ProfileException thrown if a specific name identifier format was required but not supported
-     */
-    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderByRequiredFormat(
-            String requiredNameFormat, Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext)
-            throws ProfileException {
-        String requiredNameFormatErr = "No attribute of principal '" + requestContext.getPrincipalName()
-                + "' can be encoded in to a NameIdentifier of " + "required format '" + requiredNameFormat
-                + "' for relying party '" + requestContext.getInboundMessageIssuer() + "'";
-
-        Map<String, BaseAttribute> principalAttributes = requestContext.getAttributes();
-        if (principalAttributes == null || principalAttributes.isEmpty()) {
-            log.debug("No attributes for principal '{}', no name identifier will be created for relying party '{}'",
-                    requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
-            log.warn(requiredNameFormatErr);
-            throw new ProfileException(requiredNameFormatErr);
-        }
-
-        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(nameIdEncoderType,
-                principalAttributes, java.util.Collections.singletonList(requiredNameFormat));
-        if (nameIdAttributeAndEncoder == null) {
-            log.warn(requiredNameFormatErr);
-            throw new ProfileException(requiredNameFormatErr);
-        }
-
-        return nameIdAttributeAndEncoder;
-    }
-
-    /**
      * Gets the name identifier formats to use when creating identifiers for the relying party.
      * 
      * @param requestContext current request context
      * 
-     * @return list of formats that may be used with the relying party, or an empty list for no preference
+     * @return list of formats, in preference order, that may be used with the relying party, or an empty list for no
+     *         preference
      * 
      * @throws ProfileException thrown if there is a problem determining the name identifier format to use
      */
@@ -540,6 +525,18 @@ public abstract class AbstractSAMLProfileHandler extends
         // If metadata contains the unspecified name format this means that any are supported
         if (nameFormats.contains(NameIdentifier.UNSPECIFIED)) {
             nameFormats.clear();
+            return nameFormats;
+        }
+
+        String[] formatPrecedence = requestContext.getRelyingPartyConfiguration().getNameIdFormatPrecedence();
+        if (!nameFormats.isEmpty() && formatPrecedence != null) {
+            for (int i = 0, j = 0; i < formatPrecedence.length; i++) {
+                if (nameFormats.contains(formatPrecedence[i])) {
+                    nameFormats.remove(formatPrecedence[i]);
+                    nameFormats.add(j, formatPrecedence[i]);
+                    j++;
+                }
+            }
         }
 
         return nameFormats;
@@ -576,53 +573,125 @@ public abstract class AbstractSAMLProfileHandler extends
     }
 
     /**
-     * Selects the principal attribute that can be encoded in to one of the supported name identifier formats.
+     * Selects the principal attribute that can be encoded in to the required name identifier format.
      * 
      * @param <T> type of name identifier encoder the attribute must support
-     * @param supportedNameFormats name identifier formats supported by the relaying part, or an empty list if all
-     *            formats are supported
+     * @param requiredNameFormat required name identifier format type
      * @param nameIdEncoderType type of name identifier encoder the attribute must support
      * @param requestContext the current request context
      * 
      * @return the select attribute, and its encoder, to be used to build the name identifier
      * 
-     * @throws ProfileException thrown if there is a problem selecting the attribute
+     * @throws ProfileException thrown if a specific name identifier format was required but not supported
      */
-    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderBySupportedFormats(
-            List<String> supportedNameFormats, Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext)
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderByRequiredFormat(
+            String requiredNameFormat, Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext)
             throws ProfileException {
+        String requiredNameFormatErr = "No attribute of principal '" + requestContext.getPrincipalName()
+                + "' can be encoded in to a NameIdentifier of " + "required format '" + requiredNameFormat
+                + "' for relying party '" + requestContext.getInboundMessageIssuer() + "'";
+
         Map<String, BaseAttribute> principalAttributes = requestContext.getAttributes();
         if (principalAttributes == null || principalAttributes.isEmpty()) {
             log.debug("No attributes for principal '{}', no name identifier will be created for relying party '{}'",
                     requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
-            return null;
+            log.warn(requiredNameFormatErr);
+            throw new ProfileException(requiredNameFormatErr);
         }
 
-        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = null;
-        nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(nameIdEncoderType, principalAttributes,
-                supportedNameFormats);
+        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoderByFormat(nameIdEncoderType,
+                principalAttributes, requiredNameFormat);
         if (nameIdAttributeAndEncoder == null) {
-            log
-                    .debug(
-                            "No attributes for principal '{}' support encoding into a supported name identifier format for relying party '{}'",
-                            requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
+            log.warn(requiredNameFormatErr);
+            throw new ProfileException(requiredNameFormatErr);
         }
 
         return nameIdAttributeAndEncoder;
     }
 
     /**
+     * Selects the principal attribute that can be encoded in to one of the supported name identifier formats.
+     * 
+     * @param <T> type of name identifier encoder the attribute must support
+     * @param supportedNameFormats name identifier formats, in preference order, supported by the relaying part, or an
+     *            empty list if all formats are supported
+     * @param nameIdEncoderType type of name identifier encoder the attribute must support
+     * @param requestContext the current request context
+     * 
+     * @return the select attribute, and its encoder, to be used to build the name identifier
+     * 
+     * @throws ProfileException thrown if there is a problem selecting the attribute
+     */
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderBySupportedFormats(
+            List<String> supportedNameFormats, Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        Map<String, BaseAttribute> principalAttributes = requestContext.getAttributes();
+
+        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = null;
+        for (String nameFormat : supportedNameFormats) {
+            nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoderByFormat(nameIdEncoderType, principalAttributes,
+                    nameFormat);
+            if (nameIdAttributeAndEncoder != null) {
+                return nameIdAttributeAndEncoder;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Selects an attribute, resolved previously, to encode as a NameID.
+     * 
+     * @param <T> type of name identifier encoder the attribute must support
+     * @param nameIdEncoderType type of name identifier encoder the attribute must support, must not be null
+     * @param principalAttributes resolved attributes, must not be null
+     * @param nameFormat format of the NameID that needs to be supported by the selected attribute, may be null if any
+     *            format is acceptable
+     * 
+     * @return the attribute and its associated NameID encoder or null if no attribute can be encoded to the given
+     *         format
+     */
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderByFormat(
+            Class<T> nameIdEncoderType, Map<String, BaseAttribute> principalAttributes, String nameFormat) {
+
+        T nameIdEncoder = null;
+
+        for (BaseAttribute<?> attribute : principalAttributes.values()) {
+            if (attribute == null) {
+                continue;
+            }
+
+            for (AttributeEncoder encoder : attribute.getEncoders()) {
+                if (encoder == null) {
+                    continue;
+                }
+
+                if (nameIdEncoderType.isInstance(encoder)) {
+                    nameIdEncoder = nameIdEncoderType.cast(encoder);
+                    if (nameFormat == null || nameFormat.equals(nameIdEncoder.getNameFormat())) {
+                        return new Pair<BaseAttribute, T>(attribute, nameIdEncoder);
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
      * Selects an attribute, resolved previously, to encode as a NameID.
      * 
      * @param <T> type of name identifier encoder the attribute must support
      * @param nameIdEncoderType type of name identifier encoder the attribute must support
      * @param principalAttributes resolved attributes
-     * @param supportedNameFormats NameID formats supported by the relying party or an empty list if all formats are
-     *            acceptable
+     * @param supportedNameFormats NameID formats, in order of preference, supported by the relying party or an empty
+     *            list if all formats are acceptable
      * 
      * @return the attribute and its associated NameID encoder
      * 
      * @throws ProfileException thrown if no attribute can be encoded in to a NameID of the required type
+     * 
+     * @deprecated
      */
     protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoder(
             Class<T> nameIdEncoderType, Map<String, BaseAttribute> principalAttributes,
@@ -718,8 +787,8 @@ public abstract class AbstractSAMLProfileHandler extends
                 }
             }
 
-            log.debug("Encoding response to SAML request {} from relying party {}", requestContext
-                    .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
+            log.debug("Encoding response to SAML request {} from relying party {}",
+                    requestContext.getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
 
             requestContext.setMessageEncoder(encoder);
             encoder.encode(requestContext);