[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