[java-idp-plugin-duo] branch main updated: JDUO-82 - API to access enrollment information

Phil Smart philip.smart at jisc.ac.uk
Tue Jan 16 11:59:47 UTC 2024


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

philsmart pushed a commit to branch main
in repository java-idp-plugin-duo.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=eee92751020aa51dfd8042fc4960650ff24dc168

The following commit(s) were added to refs/heads/main by this push:
     new eee92751 JDUO-82 - API to access enrollment information
eee92751 is described below

commit eee92751020aa51dfd8042fc4960650ff24dc168
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Jan 16 11:59:45 2024 +0000

    JDUO-82 - API to access enrollment information
    
     - Make a new (more convient) Generic method for retrieving information
    from the DuoAdmin API that specifically returns a List of Maps.
    
    https://shibboleth.atlassian.net/browse/JDUO-82
---
 .../idp/plugin/authn/duo/DuoAdminClient.java       |  25 ++-
 .../authn/duo/DuoAdminListMapResponseWrapper.java  |  48 ++++++
 .../authn/duo/PasswordlessEnrollmentCondition.java |   2 +-
 .../authn/duo/impl/DefaultDuoAdminClient.java      |  14 ++
 .../authn/duo/impl/DefaultDuoAdminClientTest.java  | 175 ++++++++++++++++++++-
 5 files changed, 260 insertions(+), 4 deletions(-)

diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java
index 28100c7d..9737cd3c 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java
@@ -49,7 +49,8 @@ public interface DuoAdminClient {
             throws DuoException;
     
     /**
-     * Generic method for returning a {@link DuoAdminResponseWrapper} response from the given path and parameters.
+     * Generic method for returning a type of {@link DuoAdminResponseWrapper} response from the given path and 
+     * parameters.
      * 
      *  <p>Note, only GET requests should be allowed and hence only retrieval operations should be supported.</p>
      * 
@@ -57,7 +58,7 @@ public interface DuoAdminClient {
      * @param context the profile request context, typically used in locating the {@link DuoIntegration} to use
      * @param path the path component of the API endpoint
      * @param parameters any name value pairs to add to the HTTP request parameters
-     * @param wrapperTypeRef the type of response to return inside the {@link DuoAdminResponseWrapper}
+     * @param wrapperTypeRef the type of response to return
      * 
      * @return a {@link DuoAdminResponseWrapper} with the correct response type embedded
      * 
@@ -66,5 +67,25 @@ public interface DuoAdminClient {
     @Nonnull <T extends DuoAdminResponseWrapper<?>> T retrieve(@Nonnull final ProfileRequestContext context,
             @Nonnull @NotEmpty final String path, @Nullable @NonnullElements final Map<String, String> parameters,
             @Nonnull final TypeReference<T> wrapperTypeRef) throws DuoException;
+    
+    
+    
+    /**
+     * Generic method for returning a {@link DuoAdminListMapResponseWrapper} response from the given path and 
+     * parameters. 
+     * 
+     * <p>Note, only GET requests should be allowed and hence only retrieval operations should be supported.</p>
+     * 
+     * @param context the profile request context, typically used in locating the {@link DuoIntegration} to use
+     * @param path the path component of the API endpoint
+     * @param parameters any name value pairs to add to the HTTP request parameters
+     * 
+     * @return a {@link DuoAdminListMapResponseWrapper} with a List of Maps response type embedded
+     * 
+     * @throws DuoException on error retrieving a response from the API
+     */
+    @Nonnull public DuoAdminListMapResponseWrapper retrieve(@Nonnull final ProfileRequestContext context,
+            @Nonnull @NotEmpty final String path, @Nullable @NonnullElements final Map<String, String> parameters) 
+                    throws DuoException;
 
 }
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminListMapResponseWrapper.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminListMapResponseWrapper.java
new file mode 100644
index 00000000..4519cb9a
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminListMapResponseWrapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.idp.plugin.authn.duo;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Handle the specific case of a {@link List} of {@link Map Maps} response from the Duo AdminAPI. If a single object 
+ * is returned in the response instead of a List, it is convert to a single valued list for consistency. 
+ */
+public class DuoAdminListMapResponseWrapper extends DuoAdminResponseWrapper<List<Map<String, Object>>>{
+    
+    /** the inner response. */
+    @JsonProperty("response")
+    @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
+    private List<Map<String, Object>> response;
+    
+    
+    /**
+     * Get the inner response.
+     * 
+     * @return inner response
+     */
+    @Override
+    @Nonnull public List<Map<String, Object>> getResponse() {
+        assert response != null;
+        return response;
+    }
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/PasswordlessEnrollmentCondition.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/PasswordlessEnrollmentCondition.java
index 16f77b25..3cdbd517 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/PasswordlessEnrollmentCondition.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/PasswordlessEnrollmentCondition.java
@@ -94,7 +94,7 @@ public class PasswordlessEnrollmentCondition extends AbstractInitializableCompon
         checkComponentActive();
         
         if (profileRequestContext == null || username == null) {
-            log.trace("Unable to checl passwordless enrollment status for '{}'", username);
+            log.trace("Unable to check passwordless enrollment status for '{}'", username);
             return false;
         }
         
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java
index a849eac2..1118388e 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java
@@ -46,6 +46,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import net.shibboleth.idp.authn.duo.DuoAuthAPI;
 import net.shibboleth.idp.authn.duo.DuoIntegration;
 import net.shibboleth.idp.plugin.authn.duo.DuoAdminClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoAdminListMapResponseWrapper;
 import net.shibboleth.idp.plugin.authn.duo.DuoAdminResponseWrapper;
 import net.shibboleth.idp.plugin.authn.duo.DuoException;
 import net.shibboleth.idp.plugin.authn.duo.model.User;
@@ -197,6 +198,19 @@ public class DefaultDuoAdminClient extends AbstractIdentifiableInitializableComp
         }
     }
     
+    /** {@inheritDoc} */
+    @Nonnull public DuoAdminListMapResponseWrapper retrieve(@Nonnull final ProfileRequestContext context, 
+            @Nonnull @NotEmpty final String path, @Nullable @NonnullElements final Map<String, String> parameters) 
+                    throws DuoException {
+        try {
+            final ClassicHttpRequest request = buildRequest(context, path, parameters);
+            return doAPIRequest(request, new TypeReference<DuoAdminListMapResponseWrapper>() {});
+        } catch (final DuoException | IOException e) {
+            //Wrap the exception
+            throw new DuoException(e);
+        }
+    }
+    
     
     /**
      * Check the component is active and if it is get the {@link DuoIntegration} to use.
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClientTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClientTest.java
index 65a1028a..c49875b1 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClientTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClientTest.java
@@ -37,6 +37,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import net.shibboleth.idp.authn.duo.BasicDuoIntegration;
+import net.shibboleth.idp.plugin.authn.duo.DuoAdminListMapResponseWrapper;
 import net.shibboleth.idp.plugin.authn.duo.DuoAdminResponseWrapper;
 import net.shibboleth.idp.plugin.authn.duo.DuoException;
 import net.shibboleth.idp.plugin.authn.duo.model.User;
@@ -147,6 +148,22 @@ public class DefaultDuoAdminClientTest {
             }          
             """;
     
+    /** A mocked bypass codes response with metadata. Is a single object and not an array type*/
+    private static final String GOOD_RESPONSE_BYPASSCODES_WITH_NO_METADATA_SINGLE_OBJECT = 
+            """
+            {
+            "response": 
+                {
+                "bypass_code_id": "DB2A9F0012RL54001FA3",
+                "created": 1522260759,
+                "expiration": 1522264359,
+                "reuse_count": 1
+                }
+              ,
+            "stat": "OK"
+            }          
+            """;
+    
     /** Two users in response. We require one.*/
     private static final String TWO_USER_RESPONSE = 
             """
@@ -214,6 +231,30 @@ public class DefaultDuoAdminClientTest {
             }         
             """;
     
+    /** A verification push response as an object (the normal output).*/
+    private static final String VERIFICATION_PUSH_RESPONSE_OBJECT = 
+            """
+             {
+              "response": {
+                "push_id": "123abc45-6def-789g-h012-34567ijk8901",
+                "result": "approve"
+              },
+              "stat": "OK"
+            }
+            """;
+    
+    /** A verification push response as an array (not normal, only for testing).*/
+    private static final String VERIFICATION_PUSH_RESPONSE_ARRAY = 
+            """
+             {
+              "response": [{
+                "push_id": "123abc45-6def-789g-h012-34567ijk8901",
+                "result": "approve"
+              }],
+              "stat": "OK"
+            }
+            """;
+    
     @BeforeMethod
     public void setup() {
         client = new DefaultDuoAdminClient();
@@ -431,6 +472,31 @@ public class DefaultDuoAdminClientTest {
         assertEquals(userWrapper.getResponse().get(0).getUsername(), "jdoe");
     }
     
+    @Test
+    public void testClientRetrieve_OK_MapType() throws Exception {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(GOOD_RESPONSE));
+        Mockito.when(httpClient.executeOpen((HttpHost) Mockito.any(), (ClassicHttpRequest) Mockito.any(), 
+                (HttpContext) Mockito.any())).thenReturn(httpResponse);
+        
+        client.setHttpClient(httpClient);
+        client.setObjectMapper(new ObjectMapper());
+        client.initialize();
+        
+        final DuoAdminResponseWrapper<List<Map<String, Object>>> userWrapper = 
+                client.retrieve(new ProfileRequestContext(),"/admin/v1/users", Map.of("username","jdoe"));
+        
+        assertNotNull(userWrapper);
+        assertNotNull(userWrapper.getResponse());
+        assertEquals(userWrapper.getResponse().size(), 1);
+        assertEquals(userWrapper.getResponse().get(0).get("username"), "jdoe");
+        assertNotNull(userWrapper.getResponse().get(0).get("webauthncredentials"));
+
+    }
+    
     @Test
     public void testClientRetrieve_ByPassCode_OK_WithPagingMetadata() throws Exception {
         final HttpClient httpClient = Mockito.mock(HttpClient.class);
@@ -477,7 +543,7 @@ public class DefaultDuoAdminClientTest {
         client.initialize();
         
         final DuoAdminResponseWrapper<List<Map<String,Object>>> userWrapper = 
-                client.retrieve(new ProfileRequestContext(),"/admin/v1/users/1/bypass_codes", Map.of("username","jdoe"), 
+                client.retrieve(new ProfileRequestContext(),"/admin/v1/users/1/bypass_codes",Map.of("username","jdoe"), 
                 new TypeReference<DuoAdminResponseWrapper<List<Map<String,Object>>>>() {});
         
         assertNotNull(userWrapper);
@@ -487,6 +553,113 @@ public class DefaultDuoAdminClientTest {
         assertNull(userWrapper.getMetadata());
 
     }
+    
+    @Test
+    public void testClientRetrieve_ByPassCodes_MapType_OK_WithoutPagingMetadata() throws Exception {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(GOOD_RESPONSE_BYPASSCODES_WITH_NO_METADATA));
+        Mockito.when(httpClient.executeOpen((HttpHost) Mockito.any(), (ClassicHttpRequest) Mockito.any(), 
+                (HttpContext) Mockito.any())).thenReturn(httpResponse);
+        
+        client.setHttpClient(httpClient);
+        client.setObjectMapper(new ObjectMapper());
+        client.initialize();
+        
+        final DuoAdminListMapResponseWrapper userWrapper = 
+                client.retrieve(new ProfileRequestContext(),"/admin/v1/users/1/bypass_codes",Map.of("username","jdoe"));
+        
+        assertNotNull(userWrapper);
+        assertNotNull(userWrapper.getResponse());
+        assertEquals(userWrapper.getResponse().size(), 1);
+        assertNotNull(userWrapper.getResponse().get(0));
+        assertEquals(userWrapper.getResponse().get(0).get("bypass_code_id"), "DB2A9F0012RL54001FA3");
+        
+        assertNull(userWrapper.getMetadata());
+
+    }
+    
+    @Test
+    public void testClientRetrieve_ByPassCodes_MapType_SingleObject_OK_WithoutPagingMetadata() throws Exception {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(
+                new StringEntity(GOOD_RESPONSE_BYPASSCODES_WITH_NO_METADATA_SINGLE_OBJECT));
+        Mockito.when(httpClient.executeOpen((HttpHost) Mockito.any(), (ClassicHttpRequest) Mockito.any(), 
+                (HttpContext) Mockito.any())).thenReturn(httpResponse);
+        
+        client.setHttpClient(httpClient);
+        client.setObjectMapper(new ObjectMapper());
+        client.initialize();
+        
+        final DuoAdminListMapResponseWrapper userWrapper = 
+                client.retrieve(new ProfileRequestContext(),"/admin/v1/users/1/bypass_codes",Map.of("username","jdoe"));
+
+        assertNotNull(userWrapper);
+        assertNotNull(userWrapper.getResponse());
+        assertEquals(userWrapper.getResponse().size(), 1);
+        assertNotNull(userWrapper.getResponse().get(0));
+        assertEquals(userWrapper.getResponse().get(0).get("bypass_code_id"), "DB2A9F0012RL54001FA3");
+        
+        assertNull(userWrapper.getMetadata());
+
+    }
+    
+    @Test
+    public void testClientRetrieve_MapType_Object_VerificationPush_OK() throws Exception {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(VERIFICATION_PUSH_RESPONSE_OBJECT));
+        Mockito.when(httpClient.executeOpen((HttpHost) Mockito.any(), (ClassicHttpRequest) Mockito.any(), 
+                (HttpContext) Mockito.any())).thenReturn(httpResponse);
+        
+        client.setHttpClient(httpClient);
+        client.setObjectMapper(new ObjectMapper());
+        client.initialize();
+        
+        final DuoAdminListMapResponseWrapper userWrapper = 
+                client.retrieve(new ProfileRequestContext(),
+                        "/admin/v1/users/1/verification_push_response", Map.of("username","jdoe"));
+        
+        assertNotNull(userWrapper);
+        assertNotNull(userWrapper.getResponse());
+        assertEquals(userWrapper.getResponse().size(), 1);
+        assertNotNull(userWrapper.getResponse().get(0));
+        assertEquals(userWrapper.getResponse().get(0).get("push_id"), "123abc45-6def-789g-h012-34567ijk8901");
+
+    }
+    
+    @Test
+    public void testClientRetrieve_MapType_Array_VerificationPush_OK() throws Exception {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(VERIFICATION_PUSH_RESPONSE_ARRAY));
+        Mockito.when(httpClient.executeOpen((HttpHost) Mockito.any(), (ClassicHttpRequest) Mockito.any(), 
+                (HttpContext) Mockito.any())).thenReturn(httpResponse);
+        
+        client.setHttpClient(httpClient);
+        client.setObjectMapper(new ObjectMapper());
+        client.initialize();
+        
+        final DuoAdminListMapResponseWrapper userWrapper = 
+                client.retrieve(new ProfileRequestContext(),
+                        "/admin/v1/users/1/verification_push_response", Map.of("username","jdoe"));
+        
+        assertNotNull(userWrapper);
+        assertNotNull(userWrapper.getResponse());
+        assertEquals(userWrapper.getResponse().size(), 1);
+        assertNotNull(userWrapper.getResponse().get(0));
+        assertEquals(userWrapper.getResponse().get(0).get("push_id"), "123abc45-6def-789g-h012-34567ijk8901");
+
+    }
 
 
 }

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


More information about the commits mailing list