[java-plugin-shibd-saml] branch main updated: Unit testing.

Scott Cantor cantor.2 at osu.edu
Wed Sep 18 21:59:52 UTC 2024


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-plugin-shibd-saml.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd-saml.git;a=commit;h=64bd80851c2500f085182b3be06d33f0f854880d

The following commit(s) were added to refs/heads/main by this push:
     new 64bd808  Unit testing.
64bd808 is described below

commit 64bd80851c2500f085182b3be06d33f0f854880d
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Sep 18 17:59:50 2024 -0400

    Unit testing.
---
 sp-saml-impl/pom.xml                               |  20 ++
 .../saml2/profile/impl/ExtractSAMLAttributes.java  |  25 ++-
 .../saml2/profile/impl/PrepareAgentResponse.java   |  78 ++++----
 .../profile/impl/PrepareAgentResponseTest.java     | 217 +++++++++++++++++++++
 4 files changed, 292 insertions(+), 48 deletions(-)

diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 69510b0..1e3c31b 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -91,6 +91,20 @@
         </dependency>
         
         <!-- Test Dependencies -->
+        <dependency>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-server-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-server-impl</artifactId>
+            <version>${shibd.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-testing</artifactId>
@@ -103,6 +117,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-profile-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-core-impl</artifactId>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
index acc6416..cd6df43 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
@@ -317,26 +317,25 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
         final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
         assert mapped != null;
 
+        final Subject subject = samlTokenContext.getSubject();
+        final NameID nameID = subject != null ? subject.getNameID() : null;
+        
         final AuthnStatement theStatement = samlTokenContext.getAuthnStatement();
         assert theStatement != null;
         
         try (final ServiceableComponent<AttributeTranscoderRegistry> component =
                 ensureApplication().getAttributeTranscoderRegistry().getServiceableComponent()) {
+            
+            if (nameID != null) {
+                try {
+                    decodeNameID(component.getComponent(), profileRequestContext, nameID, mapped);
+                } catch (final AttributeDecodingException e) {
+                    log.error("{} Error decoding inbound NameID", getLogPrefix(), e);
+                }
+            }
+            
             final Response response = getResponse(profileRequestContext);
             for (final Assertion assertion : response.getAssertions()) {
-                
-                if (assertion == theStatement.getParent()) {
-                    final Subject subject = assertion.getSubject();
-                    final NameID nameID = subject != null ? subject.getNameID() : null;
-                    if (nameID != null) {
-                        try {
-                            decodeNameID(component.getComponent(), profileRequestContext, nameID, mapped);
-                        } catch (final AttributeDecodingException e) {
-                            log.error("{} Error decoding inbound NameID", getLogPrefix(), e);
-                        }
-                    }
-                }
-                
                 for (final AttributeStatement statement : assertion.getAttributeStatements()) {
                     for (final Attribute designator : statement.getAttributes()) {
                         assert designator!=null;
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
index 2c6c813..a2f630c 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
@@ -24,16 +24,17 @@ import javax.annotation.Nullable;
 import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.common.binding.SAMLBindingSupport;
-import org.opensaml.saml.saml2.core.Assertion;
 import org.opensaml.saml.saml2.core.AuthnStatement;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.Subject;
 import org.slf4j.Logger;
 
 import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -55,17 +56,21 @@ import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
  * is impossible, so null can be returned.</p>
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
  */
 public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
+
+    /** DOM configuration parameters used by LSSerializer to exclude XML declaration. */
+    @Nonnull private static final Map<String, Object> NO_XML_DECL_PARAMS;
     
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(PrepareAgentResponse.class);
 
     /** Strategy used to look up {@link SAMLTokenContext} to operate on. */
     @Nonnull private Function<ProfileRequestContext,SAMLTokenContext> samlTokenContextLookupStrategy;
-
-    /** DOM configuration parameters used by LSSerializer to exclude XML declaration. */
-    @Nonnull private static final Map<String, Object> NO_XML_DECL_PARAMS;
+    
+    /** Context to operate on. */
+    @NonnullBeforeExec private SAMLTokenContext samlTokenContext;
 
     /** Constructor. */
     public PrepareAgentResponse() {
@@ -88,6 +93,24 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
                 Constraint.isNotNull(strategy, "SAMLTokenContext lookup strategy cannot be null");
     }
     
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        samlTokenContext = samlTokenContextLookupStrategy.apply(profileRequestContext);
+        if (samlTokenContext == null) {
+            log.warn("{} No SAMLTokenContext found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
     /** {@inheritDoc} */
     @Override
     @Nullable protected String getStateToken(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -97,43 +120,28 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
     /** {@inheritDoc} */
     @Override
     @Nullable protected String getSessionData(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final SAMLTokenContext tokenContext = samlTokenContextLookupStrategy.apply(profileRequestContext);
-        if (tokenContext == null) {
-            log.warn("{} No SAMLTokenContext found", getLogPrefix());
-            return null;
-        }
         
-        final AuthnStatement statement = tokenContext.getAuthnStatement();
-        if (statement == null) {
-            log.warn("{} No AuthnStatement found in SAMLTokenContext", getLogPrefix());
+        final Subject subject = samlTokenContext.getSubject();
+        final NameID nameID = subject != null ? subject.getNameID() : null;
+        if (nameID == null) {
+            log.debug("{} No NameID found in assertion, no session data to attach", getLogPrefix());
             return null;
         }
-        
-        if (statement.getParent() instanceof Assertion assertion) {
-            
-            final Subject subject = assertion.getSubject();
-            final NameID nameID = subject != null ? subject.getNameID() : null;
-            if (nameID == null) {
-                log.debug("{} No NameID found in assertion, nothing to do", getLogPrefix());
-                return null;
-            }
             
-            final String sessionIndex = statement.getSessionIndex();
-            if (sessionIndex != null) {
-                // If you tell anyone I did this, I will be sad.
-                nameID.setSPProvidedID(sessionIndex);
-            }
-            
-            try {
-                final String xml = SerializeSupport.nodeToString(XMLObjectSupport.marshall(nameID), NO_XML_DECL_PARAMS);
-                return Base64Support.encodeURLSafe(xml.getBytes(StandardCharsets.UTF_8));
-            } catch (final MarshallingException | EncodingException e) {
-                log.error("{} Error marshalling and encoding NameID", getLogPrefix(), e);
-                return null;
-            }
+        final AuthnStatement statement = samlTokenContext.getAuthnStatement();
+        final String sessionIndex = statement != null ? statement.getSessionIndex() : null;
+        if (sessionIndex != null) {
+            // If you tell anyone I did this, I will be sad.
+            nameID.setSPProvidedID(sessionIndex);
+        }
+        
+        try {
+            final String xml = SerializeSupport.nodeToString(XMLObjectSupport.marshall(nameID), NO_XML_DECL_PARAMS);
+            return Base64Support.encodeURLSafe(xml.getBytes(StandardCharsets.UTF_8));
+        } catch (final MarshallingException | EncodingException e) {
+            log.error("{} Error marshalling and encoding NameID", getLogPrefix(), e);
         }
         
-        log.warn("{} No parent Assertion available via AuthnStatement", getLogPrefix());
         return null;
     }
 
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
new file mode 100644
index 0000000..f8d4623
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.saml.saml2.profile.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.attribute.ByteAttributeValue;
+import net.shibboleth.idp.attribute.DateTimeAttributeValue;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
+
+/**
+ * Unit test for {@link PrepareAgentResponse} action.
+ */
+public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
+
+    /** Test request URL. */
+    @Nonnull @NotEmpty private final static String TEST_URL = "https://sp.example.org/cgi-bin/test.cgi";
+
+    /** Test query string. */
+    @Nonnull @NotEmpty private final static String TEST_QUERY = "foo=bar%20baz&frobnitz=zorkmid";
+
+    private SAMLTokenContext samlTokenContext;
+    private AttributeContext attributeContext;
+    private PrepareAgentResponse action;
+        
+    /**
+     * Set up test.
+     * 
+     * @throws ComponentInitializationException
+     */
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.beforeMethod();
+
+        samlTokenContext = prc.ensureSubcontext(SAMLTokenContext.class);
+        attributeContext = samlTokenContext.ensureSubcontext(AttributeContext.class);
+        
+        action = new PrepareAgentResponse();
+        action.initialize();
+    }
+    
+    /**
+     * Tear down test.
+     */
+    @AfterMethod
+    public void tearDown() {
+        action.destroy();
+    }
+    
+    /**
+     * Unit test when no SAMLTokenContext exists.
+     */
+    @Test
+    public void testNoSAMLContext() {
+        prc.removeSubcontext(samlTokenContext);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+    }
+
+    /**
+     * Unit test when no data exists, so output is valid but empty.
+     */
+    @Test
+    public void testNoOutput() {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF out = arc.getOutput();
+        assert out != null;
+        Assert.assertTrue(out.isstruct());
+        Assert.assertTrue(out.getmember("session").isnull());
+        
+        final DDF http = out.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
+        Assert.assertTrue(http.isstruct());
+        Assert.assertEquals(http.getmember("redirect").unsafe_string(), new byte[] {'/'});
+        Assert.assertTrue(http.getmember("response").isnull());
+        Assert.assertTrue(http.getmember("headers").isnull());
+    }
+
+    /**
+     * Unit test for getting attributes serialized into output.
+     */
+    @Test
+    public void testAttributes() {
+        final Instant ts = Instant.now();
+        buildAttributes(ts);
+
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.BASE_URL).unsafe_string("/secure/index.html".getBytes(StandardCharsets.UTF_8));
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF out = arc.getOutput();
+        assert out != null;
+        Assert.assertTrue(out.isstruct());
+    
+        final DDF http = out.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
+        Assert.assertTrue(http.isstruct());
+        Assert.assertEquals(http.getmember("redirect").unsafe_string(), "/secure/index.html".getBytes(StandardCharsets.UTF_8));
+
+        final DDF attrs = out.getmember(ConsumerConstants.SESSION_ATTRIBUTES);
+        Assert.assertTrue(attrs.islist());
+        
+        for (final DDF child : attrs.asList()) {
+            final String id = child.name();
+            assert id != null;
+            switch (id) {
+                case "strings":
+                    Assert.assertEquals(child.asList().stream().map(DDF::string).toList(),
+                            CollectionSupport.listOf("one", "two"));
+                    break;
+                
+                case "scoped_strings":
+                    Assert.assertEquals(child.asList().stream().map(obj -> obj.getmember("value").string()).toList(),
+                            CollectionSupport.listOf("one", "two"));
+                    Assert.assertEquals(child.asList().stream().map(obj -> obj.getmember("scope").string()).toList(),
+                            CollectionSupport.listOf("domain1", "domain2"));
+                    break;
+
+                case "times":
+                    Assert.assertEquals(child.asList().stream().map(DDF::longinteger).toList(),
+                            CollectionSupport.listOf(ts.getEpochSecond(), ts.plusSeconds(3600).getEpochSecond()));
+                    break;
+                
+                case "bytes":
+                    int i = 0;
+                    for (final DDF value : child.asList()) {
+                        if (i == 0) {
+                            Assert.assertEquals(value.unsafe_string(), "one".getBytes(StandardCharsets.UTF_8));
+                        } else if (i == 1) {
+                            Assert.assertEquals(value.unsafe_string(), "two".getBytes(StandardCharsets.UTF_8));
+                        } else {
+                            Assert.fail("Too many values");
+                        }
+                        ++i;
+                    }
+                    break;
+                
+                default:
+                    Assert.fail("Unexpected child name");
+            }
+        }
+    }
+    
+    /**
+     * Adds mock attributes to context for serializing.
+     * 
+     * @param ts timestamp for values
+     */
+    private void buildAttributes(@Nonnull final Instant ts) {
+        final IdPAttribute attr1 = new IdPAttribute("strings");
+        attr1.setValues(CollectionSupport.listOf(
+                StringAttributeValue.valueOf("one"),
+                StringAttributeValue.valueOf(""),
+                StringAttributeValue.valueOf("two")
+                ));
+        
+        final IdPAttribute attr2 = new IdPAttribute("scoped_strings");
+        attr2.setValues(CollectionSupport.listOf(
+                ScopedStringAttributeValue.valueOf("one", "domain1"),
+                ScopedStringAttributeValue.valueOf("", "domain"),
+                ScopedStringAttributeValue.valueOf("two", "domain2")
+                ));
+        
+        final IdPAttribute attr3 = new IdPAttribute("times");
+        attr3.setValues(CollectionSupport.listOf(
+                new DateTimeAttributeValue(ts),
+                new DateTimeAttributeValue(ts.plusSeconds(3600))
+                ));
+
+        final IdPAttribute attr4= new IdPAttribute("bytes");
+        attr4.setValues(CollectionSupport.listOf(
+                new ByteAttributeValue("one".getBytes(StandardCharsets.UTF_8)),
+                new ByteAttributeValue("two".getBytes(StandardCharsets.UTF_8))
+                ));
+        
+        attributeContext.setIdPAttributes(CollectionSupport.listOf(attr1, attr2, attr3, attr4));
+    }
+
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list