content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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 org.apache.causeway.applib.services.user;
  
  import java.util.List;
  import java.util.Optional;
  
  import jakarta.annotation.Priority;
  import jakarta.inject.Inject;
  import jakarta.inject.Named;
  import jakarta.inject.Provider;
  
  import org.springframework.beans.factory.annotation.Qualifier;
  import org.jspecify.annotations.Nullable;
  import org.springframework.stereotype.Service;
  
  import org.apache.causeway.applib.CausewayModuleApplib;
  import org.apache.causeway.applib.annotation.PriorityPrecedence;
  import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
  import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
  import org.apache.causeway.applib.services.keyvaluestore.KeyValueSessionStore;
  import org.apache.causeway.applib.services.sudo.SudoService;
  import org.apache.causeway.commons.internal.assertions._Assert;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  
  import lombok.RequiredArgsConstructor;
  
  /**
   * Allows the domain object to obtain the identity of the user interacting with
   * said object.
   *
   * <p>
   * If {@link SudoService} has been used to temporarily override the user and/or
   * roles, then this service will report the overridden values instead. This is
   * within the context of a thread.
   * </p>
   *
   * <p>
   * In addition, if impersonation has been invoked through the
   * {@link ImpersonateMenu}, then this service will report the impersonated user,
   * with the companion {@link KeyValueSessionStore} taking responsibilty for
   * remembering the impersonated user over multiple (http) requests, eg using an
   * http session. It's important to note that under these circumstances the user
   * reported by this service (the &quot;effective&quot; user) will <i>not</i> be
   * the same as the user held in the {@link InteractionContext}, as obtained by
   * {@link InteractionLayerTracker#currentInteractionContext()
   * InteractionLayerTracker} (the &quot;real&quot; user).
   * </p>
   *
   * @see org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker
   * @see org.apache.causeway.applib.services.iactnlayer.InteractionContext
   * @see SudoService
   * @see ImpersonateMenu
   *
   * @since 1.x revised in 2.0 {@index}
   */
  @Service
  @Named(UserService.LOGICAL_TYPE_NAME)
  @Priority(PriorityPrecedence.MIDPOINT)
  @Qualifier("Default")
  @RequiredArgsConstructor(onConstructor_ = { @Inject })
  public class UserService {
  
      static final String LOGICAL_TYPE_NAME = CausewayModuleApplib.NAMESPACE + ".UserService";
  
      /**
       * Default returned from {@link #currentUserNameElseNobody()}.
       */
      public static final String NOBODY = "__causeway_nobody";
  
      private static final String SESSION_KEY_IMPERSONATED_USER = UserService.class.getName() + "#sudo";
  
      private final Provider<InteractionLayerTracker> interactionLayerTrackerProvider;
      private final Optional<KeyValueSessionStore> keyValueSessionStore;
  
      /**
       * Returns the details about the current user, either the &quot;effective&quot;
       * user (if being {@link #impersonateUser(String, List, String) impersonated})
       * otherwise the &quot;real&quot; user (as obtained from the
       * {@link InteractionContext} of the current thread).
       */
      public Optional<UserMemento> currentUser() {
          var currentUser = interactionLayerTrackerProvider.get()
                      .currentInteractionContext()
                      .map(InteractionContext::getUser);
  
          currentUser.ifPresent(
                  u->_Assert.assertEquals(u.isImpersonating(), isImpersonating(), ()->
                      "framework bug: InteractionService and UserService disagree on impersonation state\n\t"));
  
          return currentUser;
      }
  
      /**
       * Whether the current user is the <i>system user</i> (as obtained from the
       * {@link InteractionContext} of the current thread).
       */
      public boolean isCurrentUserWithSudoAccessAllRole() {
          return currentUser()
                  .map(UserMemento::hasSudoAccessAllRole)
                  .orElse(false);
      }
  
      /**
       * Gets the details about the {@link #currentUser()} current user, if any (and
       * returning <code>null</code> if there is none).
       */
      @Nullable
      public UserMemento getUser() {
          return currentUser().orElse(null);
      }
  
      /**
       * Gets the details about the {@link #currentUser()} current user, throwing an
       * exception if there is none.
       *
       * @throws IllegalStateException if no {@link InteractionContext} can be found
       *                               with the current thread's context.
       */
      public UserMemento currentUserElseFail() {
          return currentUser()
                  .orElseThrow(() -> _Exceptions.illegalState("Current thread has no InteractionContext."));
      }
  
      /**
       * Optionally gets the {@link #currentUser() current user}'s name, obtained from
       * {@link UserMemento}.
       */
      public Optional<String> currentUserName() {
          return currentUser().map(UserMemento::name);
      }
  
      /**
       * Returns either the current user's name or else {@link #NOBODY}.
       */
      public String currentUserNameElseNobody() {
          return currentUserName().orElse(NOBODY);
      }
  
      /**
       * 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, String)
       * @see #stopImpersonating()
       */
      public boolean isImpersonating() {
          return lookupSudoUser().isPresent();
      }
  
      /**
       * Whether impersonation is available for this request.
       *
       * <p>
       * The typical implementation uses an HTTP session, which is not guaranteed to
       * be available for all viewers. Specifically, the Wicket viewer <i>does</i> use
       * HTTP sessions and therefore supports impersonation, but the RestfulObjects
       * viewer does <i>not</i>. This means that the result of this call varies on a
       * request-by-request basis.
       * </p>
       *
       * @see #impersonateUser(String, List, String)
       * @see #isImpersonating()
       * @see #stopImpersonating()
       *
       * @return whether impersonation is supported in the context of this (http)
       *         request.
       */
      public boolean supportsImpersonation() {
          return keyValueSessionStore
              .map(KeyValueSessionStore::isSessionAvailable)
              .orElse(false);
      }
  
      /**
       * Allows implementations to override the current user with another user.
       *
       * <p>
       * If this service (for this request) does not {@link #supportsImpersonation()
       * support impersonation}, then the request is just ignored.
       * </p>
       *
       * <p>
       * IMPORTANT: 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 #isImpersonating()
       * @see #stopImpersonating()
       * @param userName          - the name of the user to be impersonated
       * @param roles             - the collection of roles for the impersonated user
       *                          to have.
       * @param multiTenancyToken
       */
      public void impersonateUser(
              final String userName,
              final List<String> roles,
              final String multiTenancyToken) {
          setSudoUser(UserMemento.ofNameAndRoleNames(userName, roles)
                  .withImpersonating(true)
                  .withMultiTenancyToken(multiTenancyToken));
      }
  
      /**
       * Optionally the impersonated user, based on whether
       * a call to {@link #impersonateUser(String, List, String)}
       * was made within the current HTTP session.
       */
      public Optional<UserMemento> lookupImpersonatedUser() {
          return lookupSudoUser();
      }
  
      /**
       * For implementations that support impersonation, this is to programmatically
       * stop impersonating a user
       *
       * <p>
       * If this service (for this request) does not {@link #supportsImpersonation()
       * support impersonation}, then the request is just ignored.
       * </p>
       *
       * <p>
       * Intended to be called at some point after
       * {@link #impersonateUser(String, List, String)} would have been called.
       * </p>
       *
       * @see #supportsImpersonation()
       * @see #impersonateUser(String, List, String)
       * @see #isImpersonating()
       */
      public void stopImpersonating() {
          clearSudoUser();
      }
  
      // -- HELPER - KEY STORE FOR IMPERSONATING
  
      private void setSudoUser(final @Nullable UserMemento userMemento) {
          if(userMemento==null) {
              clearSudoUser();
              return;
          }
          keyValueSessionStore
              .ifPresent(store->store.put(SESSION_KEY_IMPERSONATED_USER, userMemento));
      }
  
      private Optional<UserMemento> lookupSudoUser() {
          return keyValueSessionStore
              .flatMap(store->store.lookupAs(SESSION_KEY_IMPERSONATED_USER, UserMemento.class));
      }
  
      private void clearSudoUser() {
          keyValueSessionStore
              .ifPresent(store->store.clear(SESSION_KEY_IMPERSONATED_USER));
      }
  
  }
  