You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/04/11 10:52:29 UTC

[isis] 02/03: ISIS-2550: extends UserService API with the concept of impersonation

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2550
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 2c54de0838c1029cdb6906af8c2927cc652738be
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Apr 11 11:51:28 2021 +0100

    ISIS-2550: extends UserService API with the concept of impersonation
    
    ... and modifies UserServiceDefault to support impersonation, delegating the details to a new service, ImpersonatedUserHolder (that uses HttpSession)
---
 .../isis/applib/services/user/UserService.java     | 78 ++++++++++++++++++++++
 .../IsisModuleCoreRuntimeServices.java             |  2 +
 ...iceDefault.java => ImpersonatedUserHolder.java} | 45 +++++++++----
 .../runtimeservices/user/UserServiceDefault.java   | 46 +++++++++++--
 4 files changed, 152 insertions(+), 19 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java
index af32a15..3adf804 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java
@@ -18,6 +18,7 @@
  */
 package org.apache.isis.applib.services.user;
 
+import java.util.List;
 import java.util.Optional;
 
 import javax.annotation.Nullable;
@@ -84,4 +85,81 @@ public interface UserService {
                 .orElse("Nobody");
     }
 
+    /**
+     * Allows implementations to override the current user with another user.
+     *
+     * <p>
+     *     This is intended for non-production environments only, where it can
+     *     be invaluable (from a support perspective) to be able to quickly
+     *     use the application &quot;as if&quot; logged in as another user.
+     * </p>
+     *
+     * @see #supportsImpersonation()
+     * @see #getImpersonatedUser()
+     * @see #isImpersonating()
+     * @see #stopImpersonating()
+     *
+     * @param userName
+     * @param roles
+     */
+    default void impersonateUser(final String userName, final List<String> roles) {
+        throw new RuntimeException("Not implemented");
+    }
+
+    /**
+     * For implementations that support impersonation, this is to
+     * programmatically stop impersonating a user
+     *
+     * <p>
+     *     Intended to be called at some point after
+     *     {@link #impersonateUser(String, List)} would have been called.
+     * </p>
+     *
+     * @see #supportsImpersonation()
+     * @see #impersonateUser(String, List)
+     * @see #getImpersonatedUser()
+     * @see #isImpersonating()
+     */
+    default void stopImpersonating() {
+        throw new RuntimeException("Not implemented");
+    }
+
+    /**
+     * Whether this implementation supports impersonation.
+     *
+     * @see #impersonateUser(String, List)
+     * @see #getImpersonatedUser()
+     * @see #isImpersonating()
+     * @see #stopImpersonating()
+     */
+    default boolean supportsImpersonation() {
+        return false;
+    }
+
+    /**
+     * The impersonated user, if it has previously been set.
+     *
+     * @see #supportsImpersonation()
+     * @see #impersonateUser(String, List)
+     * @see #isImpersonating()
+     * @see #stopImpersonating()
+     */
+    default Optional<UserMemento> getImpersonatedUser() {
+        return Optional.empty();
+    }
+
+    /**
+     * Whether or not the user currently reported (in {@link #currentUser()}
+     * and similar) is actually an impersonated user.
+     *
+     * @see #currentUser()
+     * @see #supportsImpersonation()
+     * @see #impersonateUser(String, List)
+     * @see #getImpersonatedUser()
+     * @see #stopImpersonating()
+     */
+    default boolean isImpersonating() {
+        return getImpersonatedUser().isPresent();
+    }
+
 }
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
index 307f7c3..2692536 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
@@ -59,6 +59,7 @@ import org.apache.isis.core.runtimeservices.session.InteractionFactoryDefault;
 import org.apache.isis.core.runtimeservices.sudo.SudoServiceDefault;
 import org.apache.isis.core.runtimeservices.transaction.TransactionServiceSpring;
 import org.apache.isis.core.runtimeservices.urlencoding.UrlEncodingServiceWithCompression;
+import org.apache.isis.core.runtimeservices.user.ImpersonatedUserHolder;
 import org.apache.isis.core.runtimeservices.user.UserServiceDefault;
 import org.apache.isis.core.runtimeservices.userprof.UserProfileServiceDefault;
 import org.apache.isis.core.runtimeservices.userreg.EmailNotificationServiceDefault;
@@ -88,6 +89,7 @@ import org.apache.isis.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefaul
         EventBusServiceSpring.class,
         FactoryServiceDefault.class,
         HomePageResolverServiceDefault.class,
+        ImpersonatedUserHolder.class,
         InteractionDtoFactoryDefault.class,
         InteractionFactoryDefault.class,
         JaxbServiceDefault.class,
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonatedUserHolder.java
similarity index 53%
copy from core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java
copy to core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonatedUserHolder.java
index 5bce6c3..9e26dab 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonatedUserHolder.java
@@ -22,32 +22,51 @@ import java.util.Optional;
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.servlet.http.HttpSession;
 
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.annotation.Primary;
 import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.OrderPrecedence;
-import org.apache.isis.applib.services.iactn.ExecutionContext;
 import org.apache.isis.applib.services.user.UserMemento;
-import org.apache.isis.applib.services.user.UserService;
-import org.apache.isis.core.interaction.session.InteractionTracker;
 
+/**
+ * Used by {@link UserServiceDefault} to allow the current user to be
+ * temporarily impersonated.
+ *
+ * <p>
+ *     Intended for non-production environments only.
+ * </p>
+ *
+ * @since 2.0 {@index}
+ */
 @Service
-@Named("isis.runtimeservices.UserServiceDefault")
+@Named("isis.runtimeservices.ImpersonatedUserHolder")
 @Order(OrderPrecedence.MIDPOINT)
 @Primary
 @Qualifier("Default")
-public class UserServiceDefault implements UserService {
-    
-    @Inject private InteractionTracker isisInteractionTracker;
-    
-    @Override
-    public Optional<UserMemento> currentUser() {
-        return isisInteractionTracker.currentExecutionContext()
-                .map(ExecutionContext::getUser);
+public class ImpersonatedUserHolder {
+
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    @Inject private HttpSession httpSession;
+
+    private static final String HTTP_SESSION_KEY_IMPERSONATED_USER = ImpersonatedUserHolder.class.getName() + "#userMemento";
+
+    public void setUserMemento(final UserMemento userMemento) {
+        this.httpSession.setAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER, userMemento);
+    }
+
+    public Optional<UserMemento> getUserMemento() {
+        final Object attribute = this.httpSession.getAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER);
+        return attribute instanceof UserMemento
+                ? Optional.of((UserMemento)attribute)
+                : Optional.empty();
     }
-    
 
+    public void clearUserMemento() {
+        this.httpSession.removeAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER);
+    }
 }
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java
index 5bce6c3..69d570a 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/UserServiceDefault.java
@@ -18,6 +18,8 @@
  */
 package org.apache.isis.core.runtimeservices.user;
 
+import java.util.Arrays;
+import java.util.List;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -34,20 +36,52 @@ import org.apache.isis.applib.services.user.UserMemento;
 import org.apache.isis.applib.services.user.UserService;
 import org.apache.isis.core.interaction.session.InteractionTracker;
 
+import lombok.RequiredArgsConstructor;
+
 @Service
 @Named("isis.runtimeservices.UserServiceDefault")
 @Order(OrderPrecedence.MIDPOINT)
 @Primary
 @Qualifier("Default")
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
 public class UserServiceDefault implements UserService {
-    
-    @Inject private InteractionTracker isisInteractionTracker;
-    
+
+    private final InteractionTracker isisInteractionTracker;
+    private final ImpersonatedUserHolder impersonatedUserHolder;
+
+    /**
+     * Either the current user or the one being impersonated.
+     */
     @Override
     public Optional<UserMemento> currentUser() {
-        return isisInteractionTracker.currentExecutionContext()
-                .map(ExecutionContext::getUser);
+        return or(getImpersonatedUser(),
+                    isisInteractionTracker.currentExecutionContext()
+                        .map(ExecutionContext::getUser)
+        );
+    }
+
+    private static <T> Optional<T> or(Optional<T> optional, Optional<T> fallback) {
+        return optional.isPresent() ? optional : fallback;
+    }
+
+    @Override
+    public void impersonateUser(final String userName, final List<String> roles) {
+        impersonatedUserHolder.setUserMemento(UserMemento.ofNameAndRoleNames(userName, roles));
+    }
+
+    @Override
+    public void stopImpersonating() {
+        impersonatedUserHolder.clearUserMemento();
+    }
+
+    @Override
+    public boolean supportsImpersonation() {
+        return true;
+    }
+
+    @Override
+    public Optional<UserMemento> getImpersonatedUser() {
+        return impersonatedUserHolder.getUserMemento();
     }
-    
 
 }