<commit_before>package bio.terra.cli.businessobject;

import bio.terra.cli.app.utils.AppDefaultCredentialUtils;
import bio.terra.cli.command.auth.Login.LogInMode;
import bio.terra.cli.exception.SystemException;
import bio.terra.cli.serialization.persisted.PDUser;
import bio.terra.cli.service.GoogleOauth;
import bio.terra.cli.service.SamService;
import bio.terra.cli.service.utils.HttpUtils;
import bio.terra.cli.utils.FileUtils;
import bio.terra.cli.utils.UserIO;
import com.google.api.client.http.HttpStatusCodes;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatusInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Internal representation of a user. An instance of this class is part of the current context or
 * state.
 */
@SuppressFBWarnings(
    value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE",
    justification =
        "Known false positive for certain try-with-resources blocks, which are used in several methods in this class. https://github.com/spotbugs/spotbugs/issues/1338 (Other similar issues linked from there.)")
public class User {
  private static final Logger logger = LoggerFactory.getLogger(User.class);

  // id that Terra uses to identify a user. the CLI queries SAM for a user's subject id to populate
  // this field.
  private String id;

  // name that Terra associates with this user. the CLI queries SAM for a user's email to populate
  // this field.
  private String email;

  // proxy group email that Terra associates with this user. permissions granted to the proxy group
  // are transitively granted to the user and all of their pet SAs. the CLI queries SAM to populate
  // this field.
  private String proxyGroupEmail;

  // pet SA email that Terra associates with this user. the CLI queries SAM to populate this field.
  private String petSAEmail;

  // Google credentials for the user. This can be either 1) end-user google credentials from an
  // oauth browser flow, or 2) end-user or pet sa google credentials pulled from the application
  // default credentials.
  private GoogleCredentials googleCredentials;

  /**
   * User specified what mode to log-in. When log-in mode is {@code APP_DEFAULT_CREDENTIALS}, check
   * for ADC instead of user credentials that are stored on disk in the Terra CLI credential store
   * ($home/.terra).
   */
  private LogInMode logInMode;

  // these are the same scopes requested by Terra service swagger pages
  @VisibleForTesting
  public static final List<String> USER_SCOPES = ImmutableList.of("openid", "email", "profile");

  // these are the same scopes requested by Terra service swagger pages, plus the cloud platform
  // scope. pet SAs need the cloud platform scope to talk to GCP directly (e.g. to check the status
  // of a GCP notebook)
  private static final List<String> PET_SA_SCOPES =
      ImmutableList.of(
          "openid", "email", "profile", "https://www.googleapis.com/auth/cloud-platform");

  // google OAuth client secret file
  // (https://developers.google.com/adwords/api/docs/guides/authentication#create_a_client_id_and_client_secret)
  private static final String CLIENT_SECRET_FILENAME = "client_secret.json";

  // URL of the landing page shown in the browser after completing the OAuth part of login
  // ideally this should point to product documentation or perhaps the UI, but for now the CLI
  // README seems like the best option. in the future, if we want to make this server-specific, then
  // it should become a property of the Server
  private static final String LOGIN_LANDING_PAGE =
      "https://github.com/DataBiosphere/terra-cli/blob/main/README.md";

  /** Build an instance of this class from the serialized format on disk. */
  public User(PDUser configFromDisk) {
    this.id = configFromDisk.id;
    this.email = configFromDisk.email;
    this.proxyGroupEmail = configFromDisk.proxyGroupEmail;
    this.petSAEmail = configFromDisk.petSAEmail;
    this.logInMode = configFromDisk.useApplicationDefaultCredentials;
  }

  /** Build an empty instance of this class. */
  private User() {}

  /**
   * Load any existing credentials for this user. Return silently, do not prompt for login, if they
   * are expired or do not exist on disk.
   */
  public void loadExistingCredentials() {
    // load existing user credentials from disk
    if (logInMode == LogInMode.APP_DEFAULT_CREDENTIALS) {
      loadAppDefaultCredentials();
      return;
    }
    try (InputStream inputStream =
        User.class.getClassLoader().getResourceAsStream(CLIENT_SECRET_FILENAME)) {
      googleCredentials =
          GoogleOauth.getExistingUserCredential(
              USER_SCOPES, inputStream, Context.getContextDir().toFile());
    } catch (IOException | GeneralSecurityException ex) {
      throw new SystemException("Error fetching user credentials.", ex);
    }

    // load existing pet SA credentials from disk
    if (Context.getWorkspace().isEmpty() || !Context.requireWorkspace().getIsLoaded()) {
      logger.debug(
          "Current workspace is either not defined or not loaded, so there are no pet SA credentials.");
      return;
    }
    Path jsonKeyPath = Context.getPetSaKeyFile(this);
    logger.debug("Looking for pet SA key file at: {}", jsonKeyPath);
    if (!jsonKeyPath.toFile().exists()) {
      logger.debug("Pet SA key file not found for current user + workspace");
      return;
    }
  }

  /**
   * Load any existing credentials for this user. Prompt for browser login if they are expired or do
   * not exist.
   */
  public static void login() {
    login(/*logInMode=*/ LogInMode.BROWSER);
  }

  /**
   * Load any existing credentials for this user. Prompt for login if they are expired or do not
   * exist.
   *
   * @param logInMode when logInMode is APP_DEFAULT_CREDENTIALS, use the application default
   *     credentials to log-in instead of oauth flow in the browser.
   */
  public static void login(LogInMode logInMode) {
    Optional<User> currentUser = Context.getUser();
    currentUser.ifPresent(User::loadExistingCredentials);

    // populate the current user object or build a new one
    User user = currentUser.orElseGet(User::new);
    user.logInMode = logInMode;

    if (user.logInMode == LogInMode.APP_DEFAULT_CREDENTIALS) {
      user.loadAppDefaultCredentials();
    } else {
      if (currentUser.isEmpty() || currentUser.get().requiresReauthentication()) {
        user.doOauthLoginFlow();
      }
    }<commit_msg>[PF-1333]: Fix bug when the log-in mode get lost in the follow-on command (#214)<commit_after>package bio.terra.cli.businessobject;

import bio.terra.cli.app.utils.AppDefaultCredentialUtils;
import bio.terra.cli.command.auth.Login.LogInMode;
import bio.terra.cli.exception.SystemException;
import bio.terra.cli.serialization.persisted.PDUser;
import bio.terra.cli.service.GoogleOauth;
import bio.terra.cli.service.SamService;
import bio.terra.cli.service.utils.HttpUtils;
import bio.terra.cli.utils.FileUtils;
import bio.terra.cli.utils.UserIO;
import com.google.api.client.http.HttpStatusCodes;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatusInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Internal representation of a user. An instance of this class is part of the current context or
 * state.
 */
@SuppressFBWarnings(
    value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE",
    justification =
        "Known false positive for certain try-with-resources blocks, which are used in several methods in this class. https://github.com/spotbugs/spotbugs/issues/1338 (Other similar issues linked from there.)")
public class User {
  private static final Logger logger = LoggerFactory.getLogger(User.class);

  // id that Terra uses to identify a user. the CLI queries SAM for a user's subject id to populate
  // this field.
  private String id;

  // name that Terra associates with this user. the CLI queries SAM for a user's email to populate
  // this field.
  private String email;

  // proxy group email that Terra associates with this user. permissions granted to the proxy group
  // are transitively granted to the user and all of their pet SAs. the CLI queries SAM to populate
  // this field.
  private String proxyGroupEmail;

  // pet SA email that Terra associates with this user. the CLI queries SAM to populate this field.
  private String petSAEmail;

  // Google credentials for the user. This can be either 1) end-user google credentials from an
  // oauth browser flow, or 2) end-user or pet sa google credentials pulled from the application
  // default credentials.
  private GoogleCredentials googleCredentials;

  /**
   * User specified what mode to log-in. When log-in mode is {@code APP_DEFAULT_CREDENTIALS}, check
   * for ADC instead of user credentials that are stored on disk in the Terra CLI credential store
   * ($home/.terra).
   */
  private LogInMode logInMode;

  // these are the same scopes requested by Terra service swagger pages
  @VisibleForTesting
  public static final List<String> USER_SCOPES = ImmutableList.of("openid", "email", "profile");

  // these are the same scopes requested by Terra service swagger pages, plus the cloud platform
  // scope. pet SAs need the cloud platform scope to talk to GCP directly (e.g. to check the status
  // of a GCP notebook)
  private static final List<String> PET_SA_SCOPES =
      ImmutableList.of(
          "openid", "email", "profile", "https://www.googleapis.com/auth/cloud-platform");

  // google OAuth client secret file
  // (https://developers.google.com/adwords/api/docs/guides/authentication#create_a_client_id_and_client_secret)
  private static final String CLIENT_SECRET_FILENAME = "client_secret.json";

  // URL of the landing page shown in the browser after completing the OAuth part of login
  // ideally this should point to product documentation or perhaps the UI, but for now the CLI
  // README seems like the best option. in the future, if we want to make this server-specific, then
  // it should become a property of the Server
  private static final String LOGIN_LANDING_PAGE =
      "https://github.com/DataBiosphere/terra-cli/blob/main/README.md";

  /** Build an instance of this class from the serialized format on disk. */
  public User(PDUser configFromDisk) {
    this.id = configFromDisk.id;
    this.email = configFromDisk.email;
    this.proxyGroupEmail = configFromDisk.proxyGroupEmail;
    this.petSAEmail = configFromDisk.petSAEmail;
    this.logInMode = configFromDisk.useApplicationDefaultCredentials;
  }

  /** Build an empty instance of this class. */
  private User() {}

  /**
   * Load any existing credentials for this user. Return silently, do not prompt for login, if they
   * are expired or do not exist on disk.
   */
  public void loadExistingCredentials() {
    // load existing user credentials from disk
    if (logInMode == LogInMode.APP_DEFAULT_CREDENTIALS) {
      loadAppDefaultCredentials();
      return;
    }
    try (InputStream inputStream =
        User.class.getClassLoader().getResourceAsStream(CLIENT_SECRET_FILENAME)) {
      googleCredentials =
          GoogleOauth.getExistingUserCredential(
              USER_SCOPES, inputStream, Context.getContextDir().toFile());
    } catch (IOException | GeneralSecurityException ex) {
      throw new SystemException("Error fetching user credentials.", ex);
    }

    // load existing pet SA credentials from disk
    if (Context.getWorkspace().isEmpty() || !Context.requireWorkspace().getIsLoaded()) {
      logger.debug(
          "Current workspace is either not defined or not loaded, so there are no pet SA credentials.");
      return;
    }
    Path jsonKeyPath = Context.getPetSaKeyFile(this);
    logger.debug("Looking for pet SA key file at: {}", jsonKeyPath);
    if (!jsonKeyPath.toFile().exists()) {
      logger.debug("Pet SA key file not found for current user + workspace");
      return;
    }
  }

  /** Load any existing credentials for this user. Use the {@code user.logInMode} if it's set. */
  public static void login() {
    login(Context.getUser().orElseGet(User::new).logInMode);
  }

  /**
   * Load any existing credentials for this user. Prompt for login if they are expired or do not
   * exist.
   *
   * @param logInMode when logInMode is APP_DEFAULT_CREDENTIALS, use the application default
   *     credentials to log-in instead of oauth flow in the browser.
   */
  public static void login(LogInMode logInMode) {
    Optional<User> currentUser = Context.getUser();
    currentUser.ifPresent(User::loadExistingCredentials);

    // populate the current user object or build a new one
    User user = currentUser.orElseGet(User::new);
    user.logInMode = logInMode;

    if (user.logInMode == LogInMode.APP_DEFAULT_CREDENTIALS) {
      user.loadAppDefaultCredentials();
    } else {
      if (currentUser.isEmpty() || currentUser.get().requiresReauthentication()) {
        user.doOauthLoginFlow();
      }
    }
{'fraction_non_alphanumeric': 0.0888143815094878, 'fraction_numerical': 0.0020687687259238123, 'mean_word_length': 3.7281618887015178, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 9, 'https://': 8, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '974799', 'n_tokens_mistral': 4033, 'n_tokens_neox': 3955, 'n_words': 1337}