CartoDB/cartodb20

View on GitHub
app/controllers/admin/organization_users_controller.rb

Summary

Maintainability
D
2 days
Test Coverage
require_dependency 'carto/controller_helper'
require_dependency 'dummy_password_generator'

class Admin::OrganizationUsersController < Admin::AdminController
  include OrganizationUsersHelper
  include DummyPasswordGenerator

  # Organization actions
  ssl_required  :new, :create, :edit, :update, :destroy
  # Data of single users
  ssl_required  :profile, :account, :oauth, :api_key, :regenerate_api_key

  before_action :get_config
  before_action :login_required, :check_permissions, :load_organization
  before_action :load_user, only: [:edit, :update, :destroy, :regenerate_api_key]
  before_action :ensure_edit_permissions, only: [:edit, :update, :destroy, :regenerate_api_key]

  layout 'application'

  def new
    @user = ::User.new
    @user.quota_in_bytes = [@organization.unassigned_quota, @organization.default_quota_in_bytes].min

    @user.soft_geocoding_limit = current_user.soft_geocoding_limit
    @user.soft_here_isolines_limit = current_user.soft_here_isolines_limit
    @user.soft_twitter_datasource_limit = current_user.soft_twitter_datasource_limit
    @user.soft_mapzen_routing_limit = current_user.soft_mapzen_routing_limit

    @user.viewer = @organization.remaining_seats <= 0 && @organization.remaining_viewer_seats > 0

    respond_to do |format|
      format.html { render 'new' }
    end
  end

  def edit
    set_flash_flags
    respond_to do |format|
      format.html { render 'edit' }
    end
  end

  def create
    @user = ::User.new

    # Validation is done on params to allow checking the change of the value.
    # The error is deferred to display values in the form in the error scenario.
    validation_failure = !soft_limits_validation(@user, params[:user], @organization.owner)

    # set organization first, so some validations related to org users are applied (i.e. strong passwords)
    @user.org_admin = params[:user][:org_admin] unless params[:user][:org_admin].nil?
    @user.organization = @organization

    if !@organization.auth_username_password_enabled &&
       !params[:user][:password].present? &&
       !params[:user][:password_confirmation].present?
      dummy_password = generate_dummy_password
      params[:user][:password] = dummy_password
      params[:user][:password_confirmation] = dummy_password
    end

    @user.set_fields(
      params[:user],
      [
        :username, :email, :password, :quota_in_bytes, :password_confirmation,
        :twitter_datasource_enabled, :soft_geocoding_limit, :soft_here_isolines_limit,
        :soft_mapzen_routing_limit
      ]
    )
    @user.viewer = params[:user][:viewer] == 'true'
    current_user.copy_account_features(@user)

    # Validate password first, so nicer errors are displayed
    model_validation_ok = @user.valid_password?(:password,
                                                params[:user][:password],
                                                params[:user][:password_confirmation]) &&
                          @user.valid_creation?(current_user)

    valid_password_confirmation
    unless model_validation_ok
      raise Sequel::ValidationFailed.new("Validation failed: #{@user.errors.full_messages.join(', ')}")
    end
    raise Carto::UnprocesableEntityError.new("Soft limits validation error") if validation_failure

    if Cartodb::Central.api_sync_enabled?
      response = central_new_organization_user_validation(@user)
      raise Sequel::ValidationFailed, "Validation failed: #{response['error']}" unless response['valid']
    end

    @user.save(raise_on_failure: true)
    @user.create_in_central
    common_data_url = CartoDB::Visualization::CommonDataService.build_url(self)
    ::Resque.enqueue(::Resque::UserDBJobs::CommonData::LoadCommonData, @user.id, common_data_url)
    @user.notify_new_organization_user
    @user.organization.notify_if_seat_limit_reached unless @user.viewer?
    CartoGearsApi::Events::EventManager.instance.notify(
      CartoGearsApi::Events::UserCreationEvent.new(
        CartoGearsApi::Events::UserCreationEvent::CREATED_VIA_ORG_ADMIN, @user
      )
    )
    redirect_to CartoDB.url(self, 'organization', user: current_user),
                flash: { success: "New user created successfully" }
  rescue Carto::UnprocesableEntityError => e
    log_error(message: "Validation error", exception: e)
    set_flash_flags
    flash.now[:error] = e.user_message
    render 'new', status: 422
  rescue CartoDB::CentralCommunicationFailure => e
    CartoDB.report_exception(e)
    begin
      @user.destroy
    rescue StandardError => ee
      CartoDB.report_exception(ee)
    end
    set_flash_flags
    flash.now[:error] = e.user_message
    @user = default_user
    render 'new'
  rescue Carto::PasswordConfirmationError => e
    flash.now[:error] = e.message
    render action: 'new', status: e.status
  rescue Sequel::ValidationFailed => e
    flash.now[:error] = e.message
    render 'new'
  end

  def update
    valid_password_confirmation
    session[:show_dashboard_details_flash] = params[:show_dashboard_details_flash].present?
    session[:show_account_settings_flash] = params[:show_account_settings_flash].present?

    # Validation is done on params to allow checking the change of the value.
    # The error is deferred to display values in the form in the error scenario.
    validation_failure = !soft_limits_validation(@user, params[:user])

    attributes = params[:user]
    @user.set_fields(attributes, [:email]) if attributes[:email].present? && !@user.google_sign_in
    @user.set_fields(attributes, [:quota_in_bytes]) if attributes[:quota_in_bytes].present?

    @user.set_fields(attributes, [:disqus_shortname]) if attributes[:disqus_shortname].present?
    @user.set_fields(attributes, [:available_for_hire]) if attributes[:available_for_hire].present?
    @user.set_fields(attributes, [:name]) if attributes[:name].present?
    @user.set_fields(attributes, [:website]) if attributes[:website].present?
    @user.set_fields(attributes, [:description]) if attributes[:description].present?
    @user.set_fields(attributes, [:twitter_username]) if attributes[:twitter_username].present?
    @user.set_fields(attributes, [:location]) if attributes[:location].present?
    @user.set_fields(attributes, [:org_admin]) if attributes[:org_admin].present?

    @user.viewer = attributes[:viewer] == 'true'

    @user.password = attributes[:password] if attributes[:password].present?
    @user.password_confirmation = attributes[:password_confirmation] if attributes[:password_confirmation].present?
    @user.soft_geocoding_limit = attributes[:soft_geocoding_limit] if attributes[:soft_geocoding_limit].present?
    @user.soft_here_isolines_limit = attributes[:soft_here_isolines_limit] if attributes[:soft_here_isolines_limit].present?
    @user.twitter_datasource_enabled = attributes[:twitter_datasource_enabled] if attributes[:twitter_datasource_enabled].present?
    @user.soft_twitter_datasource_limit = attributes[:soft_twitter_datasource_limit] if attributes[:soft_twitter_datasource_limit].present?
    @user.soft_mapzen_routing_limit = attributes[:soft_mapzen_routing_limit] if attributes[:soft_mapzen_routing_limit].present?

    model_validation_ok = @user.valid_update?(current_user)
    if attributes[:password].present? || attributes[:password_confirmation].present?
      model_validation_ok &&= @user.valid_password?(:password, attributes[:password], attributes[:password_confirmation])
    end

    unless model_validation_ok
      raise Sequel::ValidationFailed.new("Validation failed: #{@user.errors.full_messages.join(', ')}")
    end

    raise Carto::UnprocesableEntityError.new("Soft limits validation error") if validation_failure

    ActiveRecord::Base.transaction do
      if attributes[:mfa].present?
        service = Carto::UserMultifactorAuthUpdateService.new(user_id: @user.id)
        service.update(enabled: attributes[:mfa] == '1')
      end

      # update_in_central is duplicated because we don't wan ta local save if Central fails,
      # but before/after save at user can change some attributes that we also want to persist.
      # Since those callbacks aren't idempotent there's no much better solution without a big refactor.
      @user.update_in_central

      @user.save(raise_on_failure: true)

      @user.update_in_central
    end

    redirect_to CartoDB.url(self, 'edit_organization_user', params: { id: @user.username }, user: current_user),
                flash: { success: "Your changes have been saved correctly." }
  rescue Carto::UnprocesableEntityError => e
    log_error(message: 'Validation error', exception: e)
    set_flash_flags
    flash.now[:error] = e.user_message
    render 'edit', status: 422
  rescue CartoDB::CentralCommunicationFailure => e
    set_flash_flags
    flash.now[:error] = "There was a problem while updating this user. Please, try again and contact us if the problem persists. #{e.user_message}"
    render 'edit'
  rescue Carto::PasswordConfirmationError => e
    flash.now[:error] = e.message
    render action: 'edit', status: e.status
  rescue Sequel::ValidationFailed, ActiveRecord::RecordInvalid => e
    flash.now[:error] = e.message
    render 'edit', status: 422
  end

  def destroy
    valid_password_confirmation
    raise "Can't delete user. Has shared entities" if @user.has_shared_entities?

    @user.destroy
    @user.delete_in_central
    flash[:success] = "User was successfully deleted."
    redirect_to CartoDB.url(self, 'organization', user: current_user)
  rescue CartoDB::CentralCommunicationFailure => e
    if e.user_message =~ /No organization user found with username/
      flash[:success] = "User was successfully deleted."
      redirect_to CartoDB.url(self, 'organization', user: current_user)
    else
      log_error(message: 'Error deleting organizational user from central', exception: e, target_user: @user)
      flash[:success] = "#{e.user_message}. User was deleted from the organization server."
      redirect_to organization_path(user_domain: params[:user_domain])
    end
  rescue Carto::PasswordConfirmationError => e
    flash[:error] = e.message
    redirect_to organization_path(user_domain: params[:user_domain])
  rescue StandardError => e
    log_error(message: 'Error deleting organizational user', exception: e, target_user: @user)
    flash[:error] = "User was not deleted. #{e.message}"
    redirect_to organization_path(user_domain: params[:user_domain])
  end

  def regenerate_api_key
    valid_password_confirmation
    @user.regenerate_all_api_keys
    flash[:success] = "User API key regenerated successfully"
    redirect_to CartoDB.url(self, 'edit_organization_user', params: { id: @user.username }, user: current_user),
                flash: { success: "Your changes have been saved correctly." }
  rescue Carto::PasswordConfirmationError => e
    flash[:error] = e.message
    render action: 'edit', status: e.status
  rescue StandardError => e
    CartoDB.notify_exception(e, { user_id: @user.id, current_user: current_user.id })
    flash[:error] = "There was an error regenerating the API key. Please, try again and contact us if the problem persists"
    render 'edit'
  end

  private

  def default_user
    ::User.new(username: @user.username, email: @user.email, quota_in_bytes: @user.quota_in_bytes, twitter_datasource_enabled: @user.twitter_datasource_enabled)
  end

  def extras_enabled?
    extra_geocodings_enabled? || extra_here_isolines_enabled? || extra_tweets_enabled?
  end

  def extra_geocodings_enabled?
    !Cartodb.get_config(:geocoder, 'app_id').blank?
  end

  def extra_here_isolines_enabled?
    true
  end

  def extra_tweets_enabled?
    !Cartodb.get_config(:datasource_search, 'twitter_search', 'standard', 'username').blank?
  end

  def set_flash_flags(show_dashboard_details_flash = nil, show_account_settings_flash = nil)
    @show_dashboard_details_flash = session[:show_dashboard_details_flash] || show_dashboard_details_flash
    @show_account_settings_flash = session[:show_account_settings_flash] || show_account_settings_flash
    session[:show_dashboard_details_flash] = nil
    session[:show_account_settings_flash] = nil
  end

  def get_config
    @extras_enabled = extras_enabled?
    @extra_geocodings_enabled = extra_geocodings_enabled?
    @extra_tweets_enabled = extra_tweets_enabled?
  end

  def check_permissions
    raise RecordNotFound unless current_user.organization_admin?
  end

  def load_user
    @user = @organization.users.find_by(username: params[:id])&.sequel_user
    raise RecordNotFound unless @user
  end

  def load_organization
    @organization = current_user.organization
  end

  def ensure_edit_permissions
    render_403 unless @user.editable_by?(current_user)
  end
end