require 'securerandom' require_dependency 'carto/password_validator' require_dependency 'carto/strong_password_strategy' require_dependency 'carto/standard_password_strategy' require_dependency 'dummy_password_generator' # This class is quite coupled to UserCreation. module CartoDB class UserAccountCreator include DummyPasswordGenerator PARAM_USERNAME = :username PARAM_EMAIL = :email PARAM_PASSWORD = :password # For user creations from orgs PARAM_SOFT_GEOCODING_LIMIT = :soft_geocoding_limit PARAM_SOFT_HERE_ISOLINES_LIMIT = :soft_here_isolines_limit PARAM_SOFT_OBS_SNAPSHOT_LIMIT = :soft_obs_snapshot_limit PARAM_SOFT_OBS_GENERAL_LIMIT = :soft_obs_general_limit PARAM_SOFT_TWITTER_DATASOURCE_LIMIT = :soft_twitter_datasource_limit PARAM_SOFT_MAPZEN_ROUTING_LIMIT = :soft_mapzen_routing_limit PARAM_QUOTA_IN_BYTES = :quota_in_bytes PARAM_VIEWER = :viewer PARAM_ORG_ADMIN = :org_admin def initialize(created_via) @built = false @organization = nil @google_user_data = nil @user = ::User.new @user_params = {} @custom_errors = {} @created_via = created_via @force_password_change = false end def with_username(value) with_param(PARAM_USERNAME, value) end def with_email(value) with_param(PARAM_EMAIL, value) end def with_password(value) with_param(PARAM_PASSWORD, value) end def with_soft_geocoding_limit(value) with_param(PARAM_SOFT_GEOCODING_LIMIT, value) end def with_soft_here_isolines_limit(value) with_param(PARAM_SOFT_HERE_ISOLINES_LIMIT, value) end def with_soft_obs_snapshot_limit(value) with_param(PARAM_SOFT_OBS_SNAPSHOT_LIMIT, value) end def with_soft_obs_general_limit(value) with_param(PARAM_SOFT_OBS_GENERAL_LIMIT, value) end def with_soft_twitter_datasource_limit(value) with_param(PARAM_SOFT_TWITTER_DATASOURCE_LIMIT, value) end def with_soft_mapzen_routing_limit(value) with_param(PARAM_SOFT_MAPZEN_ROUTING_LIMIT, value) end def with_quota_in_bytes(value) with_param(PARAM_QUOTA_IN_BYTES, value) end def with_viewer(value) with_param(PARAM_VIEWER, value) end def with_org_admin(value) with_param(PARAM_ORG_ADMIN, value) end def with_force_password_change @built = false @force_password_change = true end def with_organization(organization, viewer: false) @built = false @organization = organization @user = ::User.new_with_organization(organization, viewer: viewer) self end def with_invitation_token(invitation_token) @invitation_token = invitation_token self end def with_email_only(email) with_email(email) with_username(self.class.email_to_username(email)) with_password(SecureRandom.hex) self end # Transforms an email address (e.g. firstname.lastname@example.com) into a string # which can serve as a subdomain. # firstname.lastname@example.com -> firstname-lastname # Replaces all non-allowable characters with # hyphens. This could potentially result in collisions between two specially- # constructed names (e.g. John Smith-Bob and Bob-John Smith). # We're ignoring this for now since this type of email is unlikely to come up. def self.email_to_username(email) email.strip.split('@')[0].gsub(/[^A-Za-z0-9-]/, '-').downcase end def user @user end def with_oauth_api(oauth_api) @built = false @oauth_api = oauth_api self end def valid_creation?(creator_user) valid? && @user.valid_creation?(creator_user) end def valid? build if @organization if @organization.owner.nil? if !promote_to_organization_owner? @custom_errors[:organization] = ["Organization owner is not set. Administrator must login first."] end else validate_organization_soft_limits end password_validator = if requires_strong_password_validation? Carto::PasswordValidator.new(Carto::StrongPasswordStrategy.new) else Carto::PasswordValidator.new(Carto::StandardPasswordStrategy.new) end password = @user_params[PARAM_PASSWORD] || @user.password password_errors = password_validator.validate(password, password, @user) unless password_errors.empty? @custom_errors[:password] = [password_validator.formatted_error_message(password_errors)] end end if @force_password_change && @user.password_expiration_in_d.nil? @custom_errors[:force_password_change] = ['Cannot be set if password expiration is not configured'] end @custom_errors[:oauth] = 'Invalid oauth' if @oauth_api && !@oauth_api.valid?(@user) @user.created_via = @created_via @user.valid? && @user.validate_credentials_not_taken_in_central && @custom_errors.empty? end def requires_strong_password_validation? @organization.strong_passwords_enabled && !generate_dummy_password? end def generate_dummy_password? @oauth_api || @google_user_data || VIAS_WITHOUT_PASSWORD.include?(@created_via) end VIAS_WITHOUT_PASSWORD = [ Carto::UserCreation::CREATED_VIA_LDAP, Carto::UserCreation::CREATED_VIA_SAML ].freeze def validation_errors @user.errors.merge!(@custom_errors) end def enqueue_creation(current_controller) user_creation = build_user_creation user_creation.save common_data_url = CartoDB::Visualization::CommonDataService.build_url(current_controller) ::Resque.enqueue(::Resque::UserJobs::Signup::NewUser, user_creation.id, common_data_url, promote_to_organization_owner?) { id: user_creation.id, username: user_creation.username } end def build_user_creation build user_creation = Carto::UserCreation.new_user_signup(@user, @created_via).with_invitation_token(@invitation_token) user_creation.viewer = true if user_creation.pertinent_invitation.try(:viewer?) user_creation end def build return if @built if generate_dummy_password? dummy_password = generate_dummy_password @user.password = dummy_password @user.password_confirmation = dummy_password end if @oauth_api @user.set(@oauth_api.user_params) @user.email = @user_params[PARAM_EMAIL] if @user_params[PARAM_EMAIL].present? else @user.email = @user_params[PARAM_EMAIL] @user.password = @user_params[PARAM_PASSWORD] if @user_params[PARAM_PASSWORD] @user.password_confirmation = @user_params[PARAM_PASSWORD] if @user_params[PARAM_PASSWORD] end @user.invitation_token = @invitation_token @user.username = @user_params[PARAM_USERNAME] if @user_params[PARAM_USERNAME] @user.soft_geocoding_limit = @user_params[PARAM_SOFT_GEOCODING_LIMIT] == 'true' @user.soft_here_isolines_limit = @user_params[PARAM_SOFT_HERE_ISOLINES_LIMIT] == 'true' @user.soft_obs_snapshot_limit = @user_params[PARAM_SOFT_OBS_SNAPSHOT_LIMIT] == 'true' @user.soft_obs_general_limit = @user_params[PARAM_SOFT_OBS_GENERAL_LIMIT] == 'true' @user.soft_twitter_datasource_limit = @user_params[PARAM_SOFT_TWITTER_DATASOURCE_LIMIT] == 'true' @user.soft_mapzen_routing_limit = @user_params[PARAM_SOFT_MAPZEN_ROUTING_LIMIT] == 'true' @user.quota_in_bytes = @user_params[PARAM_QUOTA_IN_BYTES] if @user_params[PARAM_QUOTA_IN_BYTES] @user.viewer = @user_params[PARAM_VIEWER] if @user_params[PARAM_VIEWER] @user.org_admin = @user_params[PARAM_ORG_ADMIN] if @user_params[PARAM_ORG_ADMIN] if @force_password_change && @user.password_expiration_in_d.present? @user.last_password_change_date = Date.today - @user.password_expiration_in_d - 1 end @built = true @user end private # This is coupled to OrganizationUserController soft limits validations. def validate_organization_soft_limits owner = @organization.owner if @user_params[PARAM_SOFT_GEOCODING_LIMIT] == 'true' && !owner.soft_geocoding_limit @custom_errors[:soft_geocoding_limit] = ["Owner can't assign soft geocoding limit"] end if @user_params[PARAM_SOFT_HERE_ISOLINES_LIMIT] == 'true' && !owner.soft_here_isolines_limit @custom_errors[:soft_here_isolines_limit] = ["Owner can't assign soft here isolines limit"] end if @user_params[PARAM_SOFT_OBS_SNAPSHOT_LIMIT] == 'true' && !owner.soft_obs_snapshot_limit @custom_errors[:soft_obs_snapshot_limit] = ["Owner can't assign soft data observatory snapshot limit"] end if @user_params[PARAM_SOFT_OBS_GENERAL_LIMIT] == 'true' && !owner.soft_obs_general_limit @custom_errors[:soft_obs_general_limit] = ["Owner can't assign soft data observatory general limit"] end if @user_params[PARAM_SOFT_TWITTER_DATASOURCE_LIMIT] == 'true' && !owner.soft_twitter_datasource_limit @custom_errors[:soft_twitter_datasource_limit] = ["Owner can't assign soft twitter datasource limit"] end if @user_params[PARAM_SOFT_MAPZEN_ROUTING_LIMIT] == 'true' && !owner.soft_mapzen_routing_limit @custom_errors[:soft_mapzen_routing_limit] = ["Owner can't assign soft mapzen routing limit"] end end def with_param(key, value) @built = false @user_params[key] = value self end def promote_to_organization_owner? # INFO: Custom installs convention: org owner always has `-admin` format !!(@organization && !@organization.owner_id && @user_params[PARAM_USERNAME] && @user_params[PARAM_USERNAME] == "#{@organization.name}-admin") end end end