<commit_before>package org.pmiops.workbench.db.dao;

import java.io.IOException;
import java.sql.Timestamp;
import java.time.Clock;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Provider;

import com.google.api.client.http.HttpStatusCodes;
import org.pmiops.workbench.compliance.ComplianceService;
import org.pmiops.workbench.config.WorkbenchConfig;
import org.pmiops.workbench.db.model.AdminActionHistory;
import org.pmiops.workbench.db.model.CommonStorageEnums;
import org.pmiops.workbench.db.model.User;
import org.pmiops.workbench.exceptions.ConflictException;
import org.pmiops.workbench.exceptions.NotFoundException;
import org.pmiops.workbench.firecloud.ApiClient;
import org.pmiops.workbench.firecloud.FireCloudService;
import org.pmiops.workbench.firecloud.api.NihApi;
import org.pmiops.workbench.firecloud.model.NihStatus;
import org.pmiops.workbench.google.DirectoryService;
import org.pmiops.workbench.model.BillingProjectStatus;
import org.pmiops.workbench.model.DataAccessLevel;
import org.pmiops.workbench.model.EmailVerificationStatus;
import org.pmiops.workbench.moodle.model.BadgeDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;

/**
 * A higher-level service class containing user manipulation and business logic which can't be
 * represented by automatic query generation in UserDao.
 *
 * A large portion of this class is dedicated to:
 *
 * (1) making it easy to consistently modify a subset of fields in a User entry, with retries
 * (2) ensuring we call a single updateDataAccessLevel method whenever a User entry is saved.
 */
@Service
public class UserService {

  private final int MAX_RETRIES = 3;

  private final Provider<User> userProvider;
  private final UserDao userDao;
  private final AdminActionHistoryDao adminActionHistoryDao;
  private final Clock clock;
  private final Random random;
  private final FireCloudService fireCloudService;
  private final Provider<WorkbenchConfig> configProvider;
  private final ComplianceService complianceService;
  private final DirectoryService directoryService;
  private static final Logger log = Logger.getLogger(UserService.class.getName());

  @Autowired
  public UserService(Provider<User> userProvider,
      UserDao userDao,
      AdminActionHistoryDao adminActionHistoryDao,
      Clock clock,
      Random random,
      FireCloudService fireCloudService,
      Provider<WorkbenchConfig> configProvider,
      ComplianceService complianceService,
      DirectoryService directoryService) {
    this.userProvider = userProvider;
    this.userDao = userDao;
    this.adminActionHistoryDao = adminActionHistoryDao;
    this.clock = clock;
    this.random = random;
    this.fireCloudService = fireCloudService;
    this.configProvider = configProvider;
    this.complianceService = complianceService;
    this.directoryService = directoryService;
  }

  /**
   * Updates the currently-authenticated user with a modifier function.
   *
   * Ensures that the data access level for the user reflects the state of other fields on the
   * user; handles conflicts with concurrent updates by retrying.
   */
  private User updateUserWithRetries(Function<User, User> modifyUser) {
    User user = userProvider.get();
    return updateUserWithRetries(modifyUser, user);
  }

  /**
   * Updates a user record with a modifier function.
   *
   * Ensures that the data access level for the user reflects the state of other fields on the
   * user; handles conflicts with concurrent updates by retrying.
   */
    public User updateUserWithRetries(Function<User, User> userModifier, User user) {
    int numAttempts = 0;
    while (true) {
      user = userModifier.apply(user);
      updateDataAccessLevel(user);
      try {
        user = userDao.save(user);
        return user;
      } catch (ObjectOptimisticLockingFailureException e) {
        if (numAttempts < MAX_RETRIES) {
          user = userDao.findOne(user.getUserId());
          numAttempts++;
        } else {
          throw new ConflictException(String.format("Could not update user %s", user.getUserId()));
        }
      }
    }
  }

  private void updateDataAccessLevel(User user) {
    boolean dataUseAgreementCompliant = user.getDataUseAgreementCompletionTime() != null ||
      user.getDataUseAgreementBypassTime() != null || !configProvider.get().access.enableDataUseAgreement;
    boolean eraCommonsCompliant = user.getEraCommonsBypassTime() != null ||
      !configProvider.get().access.enableEraCommons || user.getEraCommonsCompletionTime() != null;
    boolean complianceTrainingCompliant = user.getComplianceTrainingCompletionTime() != null ||
      user.getComplianceTrainingBypassTime() != null || !configProvider.get().access.enableComplianceTraining;
    boolean betaAccessGranted = user.getBetaAccessBypassTime() != null ||
            !configProvider.get().access.enableBetaAccess;
    boolean twoFactorAuthComplete = user.getTwoFactorAuthCompletionTime() != null ||
      user.getTwoFactorAuthBypassTime() != null;

    // TODO: can take out other checks once we're entirely moved over to the 'module' columns
    boolean shouldBeRegistered = !user.getDisabled()
        && complianceTrainingCompliant
        && eraCommonsCompliant
        && betaAccessGranted
        && twoFactorAuthComplete
        && EmailVerificationStatus.SUBSCRIBED.equals(user.getEmailVerificationStatusEnum());
    boolean isInGroup = this.fireCloudService.
            isUserMemberOfGroup(user.getEmail(), configProvider.get().firecloud.registeredDomainName);
    if (shouldBeRegistered) {
      if (!isInGroup) {
        this.fireCloudService.addUserToGroup(user.getEmail(),
            configProvider.get().firecloud.registeredDomainName);
      }
      user.setDataAccessLevelEnum(DataAccessLevel.REGISTERED);
    } else {
      if (isInGroup) {
        this.fireCloudService.removeUserFromGroup(user.getEmail(),
            configProvider.get().firecloud.registeredDomainName);
      }<commit_msg>enforce data use agreement to set data access level (#2051)<commit_after>package org.pmiops.workbench.db.dao;

import java.io.IOException;
import java.sql.Timestamp;
import java.time.Clock;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Provider;

import com.google.api.client.http.HttpStatusCodes;
import org.pmiops.workbench.compliance.ComplianceService;
import org.pmiops.workbench.config.WorkbenchConfig;
import org.pmiops.workbench.db.model.AdminActionHistory;
import org.pmiops.workbench.db.model.CommonStorageEnums;
import org.pmiops.workbench.db.model.User;
import org.pmiops.workbench.exceptions.ConflictException;
import org.pmiops.workbench.exceptions.NotFoundException;
import org.pmiops.workbench.firecloud.ApiClient;
import org.pmiops.workbench.firecloud.FireCloudService;
import org.pmiops.workbench.firecloud.api.NihApi;
import org.pmiops.workbench.firecloud.model.NihStatus;
import org.pmiops.workbench.google.DirectoryService;
import org.pmiops.workbench.model.BillingProjectStatus;
import org.pmiops.workbench.model.DataAccessLevel;
import org.pmiops.workbench.model.EmailVerificationStatus;
import org.pmiops.workbench.moodle.model.BadgeDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;

/**
 * A higher-level service class containing user manipulation and business logic which can't be
 * represented by automatic query generation in UserDao.
 *
 * A large portion of this class is dedicated to:
 *
 * (1) making it easy to consistently modify a subset of fields in a User entry, with retries
 * (2) ensuring we call a single updateDataAccessLevel method whenever a User entry is saved.
 */
@Service
public class UserService {

  private final int MAX_RETRIES = 3;

  private final Provider<User> userProvider;
  private final UserDao userDao;
  private final AdminActionHistoryDao adminActionHistoryDao;
  private final Clock clock;
  private final Random random;
  private final FireCloudService fireCloudService;
  private final Provider<WorkbenchConfig> configProvider;
  private final ComplianceService complianceService;
  private final DirectoryService directoryService;
  private static final Logger log = Logger.getLogger(UserService.class.getName());

  @Autowired
  public UserService(Provider<User> userProvider,
      UserDao userDao,
      AdminActionHistoryDao adminActionHistoryDao,
      Clock clock,
      Random random,
      FireCloudService fireCloudService,
      Provider<WorkbenchConfig> configProvider,
      ComplianceService complianceService,
      DirectoryService directoryService) {
    this.userProvider = userProvider;
    this.userDao = userDao;
    this.adminActionHistoryDao = adminActionHistoryDao;
    this.clock = clock;
    this.random = random;
    this.fireCloudService = fireCloudService;
    this.configProvider = configProvider;
    this.complianceService = complianceService;
    this.directoryService = directoryService;
  }

  /**
   * Updates the currently-authenticated user with a modifier function.
   *
   * Ensures that the data access level for the user reflects the state of other fields on the
   * user; handles conflicts with concurrent updates by retrying.
   */
  private User updateUserWithRetries(Function<User, User> modifyUser) {
    User user = userProvider.get();
    return updateUserWithRetries(modifyUser, user);
  }

  /**
   * Updates a user record with a modifier function.
   *
   * Ensures that the data access level for the user reflects the state of other fields on the
   * user; handles conflicts with concurrent updates by retrying.
   */
    public User updateUserWithRetries(Function<User, User> userModifier, User user) {
    int numAttempts = 0;
    while (true) {
      user = userModifier.apply(user);
      updateDataAccessLevel(user);
      try {
        user = userDao.save(user);
        return user;
      } catch (ObjectOptimisticLockingFailureException e) {
        if (numAttempts < MAX_RETRIES) {
          user = userDao.findOne(user.getUserId());
          numAttempts++;
        } else {
          throw new ConflictException(String.format("Could not update user %s", user.getUserId()));
        }
      }
    }
  }

  private void updateDataAccessLevel(User user) {
    boolean dataUseAgreementCompliant = user.getDataUseAgreementCompletionTime() != null ||
      user.getDataUseAgreementBypassTime() != null || !configProvider.get().access.enableDataUseAgreement;
    boolean eraCommonsCompliant = user.getEraCommonsBypassTime() != null ||
      !configProvider.get().access.enableEraCommons || user.getEraCommonsCompletionTime() != null;
    boolean complianceTrainingCompliant = user.getComplianceTrainingCompletionTime() != null ||
      user.getComplianceTrainingBypassTime() != null || !configProvider.get().access.enableComplianceTraining;
    boolean betaAccessGranted = user.getBetaAccessBypassTime() != null ||
            !configProvider.get().access.enableBetaAccess;
    boolean twoFactorAuthComplete = user.getTwoFactorAuthCompletionTime() != null ||
      user.getTwoFactorAuthBypassTime() != null;

    // TODO: can take out other checks once we're entirely moved over to the 'module' columns
    boolean shouldBeRegistered = !user.getDisabled()
        && complianceTrainingCompliant
        && eraCommonsCompliant
        && betaAccessGranted
        && twoFactorAuthComplete
        && dataUseAgreementCompliant
        && EmailVerificationStatus.SUBSCRIBED.equals(user.getEmailVerificationStatusEnum());
    boolean isInGroup = this.fireCloudService.
            isUserMemberOfGroup(user.getEmail(), configProvider.get().firecloud.registeredDomainName);
    if (shouldBeRegistered) {
      if (!isInGroup) {
        this.fireCloudService.addUserToGroup(user.getEmail(),
            configProvider.get().firecloud.registeredDomainName);
      }
      user.setDataAccessLevelEnum(DataAccessLevel.REGISTERED);
    } else {
      if (isInGroup) {
        this.fireCloudService.removeUserFromGroup(user.getEmail(),
            configProvider.get().firecloud.registeredDomainName);
      }
{'fraction_non_alphanumeric': 0.08555443313388764, 'fraction_numerical': 0.0009324733856554511, 'mean_word_length': 4.32258064516129, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, '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': '1371206', 'n_tokens_mistral': 3604, 'n_tokens_neox': 3534, 'n_words': 826}