cartodb-4.42/app/models/carto/user_creation.rb

353 lines
12 KiB
Ruby
Raw Normal View History

2024-04-06 13:25:13 +08:00
require 'cartodb-common'
require_dependency 'carto/user_authenticator'
class Carto::UserCreation < ActiveRecord::Base
# Synced with CartoGearsApi::Events::UserCreationEvent
CREATED_VIA_SAML = 'saml'.freeze
CREATED_VIA_LDAP = 'ldap'.freeze
CREATED_VIA_ORG_SIGNUP = 'org_signup'.freeze
CREATED_VIA_API = 'api'.freeze
CREATED_VIA_HTTP_AUTENTICATION = 'http_authentication'.freeze
VALID_CREATED_VIA = [
CREATED_VIA_LDAP, CREATED_VIA_SAML, CREATED_VIA_ORG_SIGNUP,
CREATED_VIA_API, CREATED_VIA_HTTP_AUTENTICATION
].freeze
IN_PROGRESS_STATES = [:initial, :enqueuing, :creating_user, :validating_user, :saving_user, :promoting_user, :load_common_data, :creating_user_in_central]
FINAL_STATES = [:success, :failure]
scope :http_authentication, -> { where(created_via: CREATED_VIA_HTTP_AUTENTICATION) }
scope :in_progress, -> { where(state: IN_PROGRESS_STATES) }
belongs_to :log, class_name: Carto::Log
belongs_to :user, class_name: Carto::User
after_create :use_invitation
def self.new_user_signup(user, created_via = CREATED_VIA_ORG_SIGNUP)
# Normal validation breaks state_machine method generation
raise 'User needs username' unless user.username
raise 'User needs email' unless user.email
raise "Not valid #{created_via}: #{VALID_CREATED_VIA.join(', ')}" unless VALID_CREATED_VIA.include?(created_via)
user_creation = Carto::UserCreation.new
user_creation.username = user.username
user_creation.email = user.email
user_creation.crypted_password = user.crypted_password
user_creation.session_salt = user.session_salt
user_creation.organization_id = user.organization.nil? ? nil : user.organization.id
user_creation.quota_in_bytes = user.quota_in_bytes
user_creation.soft_geocoding_limit = user.soft_geocoding_limit
user_creation.soft_here_isolines_limit = user.soft_here_isolines_limit
user_creation.soft_obs_snapshot_limit = user.soft_obs_snapshot_limit
user_creation.soft_obs_general_limit = user.soft_obs_general_limit
user_creation.soft_twitter_datasource_limit = user.soft_twitter_datasource_limit.nil? ? false : user.soft_twitter_datasource_limit
user_creation.soft_mapzen_routing_limit = user.soft_mapzen_routing_limit
user_creation.google_sign_in = user.google_sign_in
user_creation.github_user_id = user.github_user_id
user_creation.log = Carto::Log.new_user_creation
user_creation.created_via = created_via
user_creation.viewer = user.viewer || false
user_creation.org_admin = user.org_admin || false
user_creation.last_password_change_date = user.last_password_change_date
user_creation
end
state_machine :state, :initial => :enqueuing do
before_transition any => any, :do => :log_transition_begin
after_transition any => any, :do => :log_transition_end
after_failure do
self.fail_user_creation
end
after_transition any => :creating_user, :do => :initialize_user
after_transition any => :validating_user, :do => :validate_user
after_transition any => :saving_user, :do => :save_user
after_transition any => :promoting_user, :do => :promote_user
after_transition any => :load_common_data, :do => :load_common_data
after_transition any => :creating_user_in_central, :do => :create_in_central
before_transition any => :success, :do => :close_creation
before_transition any => :failure, :do => :clean_user
event :next_creation_step do
transition :enqueuing => :creating_user,
:creating_user => :validating_user,
:validating_user => :saving_user,
:saving_user => :promoting_user
# This looks more complex than it actually is. The flow is always:
# promoting_user -> creating_user_in_central -> load_common_data -> success
# creating_user_in_central is skipped if central is not configured
# load_common_data is skipped for viewers
transition promoting_user: :creating_user_in_central, if: :sync_data_with_cartodb_central?
transition promoting_user: :load_common_data, unless: :viewer?
transition promoting_user: :success
transition creating_user_in_central: :load_common_data, unless: :viewer?
transition creating_user_in_central: :success
transition load_common_data: :success
end
event :fail_user_creation do
transition any => :failure
end
state all - [:success, :failure] do
def finished?
false
end
end
state :success, :failure do
def finished?
true
end
end
end
def set_owner_promotion(promote_to_organization_owner)
@promote_to_organization_owner = promote_to_organization_owner
end
def set_common_data_url(common_data_url)
@common_data_url = common_data_url
end
# TODO: Shorcut, search for a better solution to detect requirement
def requires_validation_email?
google_sign_in != true &&
!github_user_id &&
!has_valid_invitation? &&
!Carto::Ldap::Manager.new.configuration_present? &&
!created_via_api? &&
!created_via_http_authentication? &&
!created_via_saml?
end
def autologin?
state == 'success' && created_at > Time.now - 1.minute && enabled? && cartodb_user.dashboard_viewed_at.nil?
end
def subdomain
cartodb_user.subdomain
end
def with_invitation_token(invitation_token)
self.invitation_token = invitation_token
self
end
def created_via_api?
created_via == CREATED_VIA_API
end
def created_via_http_authentication?
created_via == CREATED_VIA_HTTP_AUTENTICATION
end
def created_via_saml?
created_via == CREATED_VIA_SAML
end
def has_valid_invitation?
return false unless invitation_token
!valid_invitation.nil?
end
def pertinent_invitation
@pertinent_invitation ||= select_valid_invitation_token(Carto::Invitation.query_with_unused_email(email).all)
end
def valid_invitation
@valid_invitation ||= select_valid_invitation_token(Carto::Invitation.query_with_valid_email(email).all)
end
private
def enabled?
cartodb_user.enable_account_token.nil? && cartodb_user.enabled
end
# Returns the first matching token invitation, and raises error if none is found
# but a token is set, since it might be fake
def select_valid_invitation_token(invitations)
return nil if invitations.empty?
invitation = invitations.select do |i|
i.token(email) == invitation_token &&
organization_id == i.organization_id
end.first
raise "Fake token sent for email #{email}, #{invitation_token}" if invitation_token && invitation.nil?
invitation
end
def cartodb_user
@cartodb_user ||= ::User.where(id: user_id).first
end
def log_transition_begin
log_transition('Beginning')
end
def log_transition_end
log_transition('End')
end
def log_transition(prefix)
self.log.append("#{prefix}: State: #{self.state}")
end
def initialize_user
@cartodb_user = ::User.new
@cartodb_user.username = username
@cartodb_user.email = email
@cartodb_user.crypted_password = crypted_password
@cartodb_user.session_salt = session_salt
@cartodb_user.google_sign_in = google_sign_in
@cartodb_user.github_user_id = github_user_id
@cartodb_user.invitation_token = invitation_token
@cartodb_user.enable_account_token = Carto::Common::EncryptionService.make_token if requires_validation_email?
unless organization_id.nil? || @promote_to_organization_owner
organization = ::Organization.where(id: organization_id).first
raise "Trying to copy organization settings from one without owner" if organization.owner.nil?
@cartodb_user.organization = organization
@cartodb_user.organization.owner.copy_account_features(@cartodb_user)
end
@cartodb_user.quota_in_bytes = quota_in_bytes unless quota_in_bytes.nil?
@cartodb_user.soft_geocoding_limit = soft_geocoding_limit unless soft_geocoding_limit.nil?
@cartodb_user.soft_here_isolines_limit = soft_here_isolines_limit unless soft_here_isolines_limit.nil?
@cartodb_user.soft_obs_snapshot_limit = soft_obs_snapshot_limit unless soft_obs_snapshot_limit.nil?
@cartodb_user.soft_obs_general_limit = soft_obs_general_limit unless soft_obs_general_limit.nil?
@cartodb_user.soft_twitter_datasource_limit = soft_twitter_datasource_limit unless soft_twitter_datasource_limit.nil?
@cartodb_user.soft_mapzen_routing_limit = soft_mapzen_routing_limit unless soft_mapzen_routing_limit.nil?
@cartodb_user.viewer = viewer if viewer
@cartodb_user.org_admin = org_admin if org_admin
@cartodb_user.last_password_change_date = last_password_change_date unless last_password_change_date.nil?
if valid_invitation
@cartodb_user.viewer = valid_invitation.viewer
end
@cartodb_user
rescue StandardError => e
handle_failure(e, mark_as_failure = true)
nil
end
# Central validation
def validate_user
@cartodb_user.validate_credentials_not_taken_in_central
raise "Credentials already used" unless @cartodb_user.errors.empty?
rescue StandardError => e
handle_failure(e, mark_as_failure = true)
end
def save_user
# INFO: until here we haven't user_id, so in-memory @cartodb_user is needed.
# After this, self.cartodb_user is used, which can be either @cartodb_user or loaded from database.
# This enables resuming.
@cartodb_user.save(raise_on_failure: true)
self.user_id = @cartodb_user.id
self.save
rescue StandardError => e
handle_failure(e, mark_as_failure = true)
end
def use_invitation
return unless invitation_token
invitation = pertinent_invitation
return unless invitation
invitation.use(email, invitation_token)
end
def promote_user
return unless @promote_to_organization_owner
organization = ::Organization.where(id: self.organization_id).first
raise "Trying to set organization owner when there's already one" unless organization.owner.nil?
user_organization = CartoDB::UserOrganization.new(organization_id, @cartodb_user.id)
user_organization.promote_user_to_admin
@cartodb_user.reload
rescue StandardError => e
handle_failure(e, mark_as_failure = true)
end
def load_common_data
@cartodb_user.load_common_data(@common_data_url) unless @common_data_url.nil?
rescue StandardError => e
handle_failure(e, mark_as_failure = false)
end
def create_in_central
cartodb_user.create_in_central
rescue StandardError => e
handle_failure(e, mark_as_failure = true)
end
def close_creation
clean_password
cartodb_user.notify_new_organization_user unless has_valid_invitation?
cartodb_user.organization.notify_if_disk_quota_limit_reached if cartodb_user.organization && !cartodb_user.viewer?
cartodb_user.organization.notify_if_seat_limit_reached if cartodb_user.organization && !cartodb_user.viewer?
CartoGearsApi::Events::EventManager.instance.notify(
CartoGearsApi::Events::UserCreationEvent.new(created_via, cartodb_user)
)
rescue StandardError => e
handle_failure(e, mark_as_failure = false)
end
def handle_failure(e, mark_as_failure)
CartoDB.notify_exception(e, { user_creation: self, mark_as_failure: mark_as_failure })
self.log.append("Error on state #{self.state}, mark_as_failure: #{mark_as_failure}. Error: #{e.message}")
self.log.append(e.backtrace.join("\n"))
process_failure if mark_as_failure
end
def process_failure
self.state = 'failure'
self.save
self.fail_user_creation
end
def clean_user
return unless cartodb_user && !cartodb_user.id.nil?
begin
cartodb_user.destroy
rescue StandardError => e
CartoDB.notify_exception(e, action: 'safe user destruction', user: cartodb_user)
begin
cartodb_user.delete
rescue StandardError => ee
CartoDB.notify_exception(ee, action: 'safe user deletion', user: cartodb_user)
end
end
end
def clean_password
self.crypted_password = ''
self.save
end
# INFO: state_machine needs guard methods to be instance methods
def sync_data_with_cartodb_central?
Cartodb::Central.sync_data_with_cartodb_central?
end
end