From 89c32e9f161d343455c50d22ad6f4d70f4807a04 Mon Sep 17 00:00:00 2001
From: Nicholas Blair <nicholas.blair@wisc.edu>
Date: Tue, 17 May 2016 20:03:23 -0500
Subject: [PATCH] feat: add callback for UserDetailsService#loadUserByUsername

Allows downstream projects to mutate the UWUserDetails instance returned by that core method.
---
 .../local/LocalUWUserDetailsCallback.java     | 36 +++++++++++++++++++
 .../local/LocalUserDetailsManagerImpl.java    | 20 ++++++++++-
 .../LocalUserDetailsManagerImplTest.java      | 25 ++++++++++++-
 3 files changed, 79 insertions(+), 2 deletions(-)
 create mode 100644 uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUWUserDetailsCallback.java

diff --git a/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUWUserDetailsCallback.java b/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUWUserDetailsCallback.java
new file mode 100644
index 0000000..a12b41f
--- /dev/null
+++ b/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUWUserDetailsCallback.java
@@ -0,0 +1,36 @@
+package edu.wisc.uwss.local;
+
+import edu.wisc.uwss.UWUserDetails;
+
+/**
+ * Callback interface allowing downstream projects to mutate the result to be returned from
+ * {@link LocalUserDetailsManagerImpl#loadUserByUsername(String)}.
+ *
+ * Example usage:
+ *
+ <pre>
+ class MyCustomLocalUWUserDetailsCallback implements LocalUWUserDetailsCallback<MyUWUserDetailsImpl> {
+
+   @Autowired
+   private MySomethingDao somethingDao;
+
+   public void success(MyUWUserDetailsImpl userDetails) {
+     userDetails.setMySomethingField(somethingDao.getSomethingsForUser(userDetails.getUsername());
+   }
+ }
+ </pre>
+ *
+ * Consumers can register any number of Beans implementing this interface; see {@link org.springframework.core.Ordered}
+ * if you need a particular order of execution.
+ *
+ * @author Nicholas Blair
+ */
+public interface LocalUWUserDetailsCallback<T extends UWUserDetails> {
+
+  /**
+   * Callback executed upon successfully loading the {@link UWUserDetails} for a username.
+   *
+   * @param userDetails the userDetails to modify
+   */
+  void success(T userDetails);
+}
diff --git a/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUserDetailsManagerImpl.java b/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUserDetailsManagerImpl.java
index 9985a05..f657dd6 100644
--- a/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUserDetailsManagerImpl.java
+++ b/uw-spring-security-core/src/main/java/edu/wisc/uwss/local/LocalUserDetailsManagerImpl.java
@@ -3,6 +3,8 @@
  */
 package edu.wisc.uwss.local;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
@@ -39,6 +41,8 @@ public class LocalUserDetailsManagerImpl implements UserDetailsManager {
   private Logger logger = LoggerFactory.getLogger(this.getClass());
   private final Map<String, UWUserDetails> users = new ConcurrentHashMap<>();
 
+  @Autowired(required=false)
+  private List<LocalUWUserDetailsCallback> callbacks = new ArrayList<>();
   @Autowired(required=false)
   private LocalUserDetailsAttributesMapper localUserDetailsAttributeMapper = new LocalUserDetailsAttributesMapper.Default();
 
@@ -68,6 +72,16 @@ public class LocalUserDetailsManagerImpl implements UserDetailsManager {
   public void setDemoUsers(@Qualifier("demo-users") Properties properties) {
    this.properties = properties;
   }
+
+  /**
+   * Visible for testing.
+   *
+   * @param callbacks
+   */
+  void setCallbacks(List<LocalUWUserDetailsCallback> callbacks) {
+    this.callbacks = callbacks;
+  }
+
   /**
    * Add a user. Does nothing if the argument is null or the {@link UWUserDetailsImpl#getUsername()} is blank.
    * 
@@ -109,7 +123,11 @@ public class LocalUserDetailsManagerImpl implements UserDetailsManager {
     if(user == null) {
       throw new UsernameNotFoundException(username + " not found");
     }
-    return SerializationUtils.clone(user);
+    UWUserDetails result = SerializationUtils.clone(user);
+    for(LocalUWUserDetailsCallback callback: callbacks) {
+      callback.success(result);
+    }
+    return result;
   }
   /**
    * @param user
diff --git a/uw-spring-security-core/src/test/java/edu/wisc/uwss/local/LocalUserDetailsManagerImplTest.java b/uw-spring-security-core/src/test/java/edu/wisc/uwss/local/LocalUserDetailsManagerImplTest.java
index b07c9e6..c62f96f 100644
--- a/uw-spring-security-core/src/test/java/edu/wisc/uwss/local/LocalUserDetailsManagerImplTest.java
+++ b/uw-spring-security-core/src/test/java/edu/wisc/uwss/local/LocalUserDetailsManagerImplTest.java
@@ -10,6 +10,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Properties;
 
@@ -30,7 +31,7 @@ import edu.wisc.uwss.UWUserDetailsImpl;
 public class LocalUserDetailsManagerImplTest {
 
   /**
-   * Verify {@link LocalUserDetailsServiceImpl#loadUserByUsername(String)} throws 
+   * Verify {@link LocalUserDetailsManagerImpl#loadUserByUsername(String)} throws
    * {@link UsernameNotFoundException} for an unknown user.
    */
   @Test(expected=UsernameNotFoundException.class)
@@ -275,5 +276,27 @@ public class LocalUserDetailsManagerImplTest {
     manager.deleteUser(username);
     assertFalse(manager.userExists(username));
   }
+
+  @Test
+  public void loadUserByUsername_with_callback() {
+    boolean visited = false;
+    LocalUserDetailsManagerImpl manager = new LocalUserDetailsManagerImpl();
+    LocalUWUserDetailsCallback callback = new LocalUWUserDetailsCallback<UWUserDetailsImpl>() {
+      @Override
+      public void success(UWUserDetailsImpl userDetails) {
+        userDetails.setFirstName("something custom");
+      }
+    };
+    manager.setCallbacks(Arrays.asList(callback));
+
+    UWUserDetails user = new UWUserDetailsImpl("UW000A000", "foo", "bar", "Foo Bar", "foo@foo.wisc.edu");
+    // first name starts null
+    assertNull(user.getFirstName());
+    manager.addDemoUser(user);
+
+    UWUserDetails result = manager.loadUserByUsername("foo");
+    // observe firstname modified by the callback
+    assertEquals("something custom", result.getFirstName());
+  }
 }
   
\ No newline at end of file
-- 
GitLab