require_relative '../controllers/carto/api/group_presenter' require_relative './organization/organization_decorator' require_relative '../helpers/data_services_metrics_helper' require_relative './permission' require_dependency 'carto/helpers/auth_token_generator' require_dependency 'common/organization_common' class Organization < Sequel::Model class OrganizationWithoutOwner < StandardError attr_reader :organization def initialize(organization) @organization = organization super "Organization #{organization.name} has no owner" end end include CartoDB::OrganizationDecorator include Concerns::CartodbCentralSynchronizable include DataServicesMetricsHelper include Carto::AuthTokenGenerator include SequelFormCompatibility include Carto::OrganizationSoftLimits Organization.raise_on_save_failure = true self.strict_param_setting = false # @param id String (uuid) # @param seats String # @param quota_in_bytes Integer # @param created_at Timestamp # @param updated_at Timestamp # @param name String # @param avatar_url String # @param website String # @param description String # @param display_name String # @param discus_shortname String # @param twitter_username String # @param location String # @param geocoding_quota Integer # @param map_view_quota Integer # @param geocoding_block_price Integer # @param map_view_block_price Integer one_to_many :users one_to_many :groups one_to_many :assets many_to_one :owner, class_name: '::User', key: 'owner_id' plugin :serialization, :json, :auth_saml_configuration plugin :validation_helpers DEFAULT_GEOCODING_QUOTA = 0 DEFAULT_HERE_ISOLINES_QUOTA = 0 DEFAULT_OBS_SNAPSHOT_QUOTA = 0 DEFAULT_OBS_GENERAL_QUOTA = 0 DEFAULT_MAPZEN_ROUTING_QUOTA = nil def default_password_expiration_in_d Cartodb.get_config(:passwords, 'expiration_in_d') end def validate super validates_presence [:name, :quota_in_bytes, :seats] validates_unique :name validates_format (/\A[a-z0-9\-]+\z/), :name, message: 'must only contain lowercase letters, numbers & hyphens' validates_integer :default_quota_in_bytes, :allow_nil => true validates_integer :geocoding_quota, allow_nil: false, message: 'geocoding_quota cannot be nil' validates_integer :here_isolines_quota, allow_nil: false, message: 'here_isolines_quota cannot be nil' validates_integer :obs_snapshot_quota, allow_nil: false, message: 'obs_snapshot_quota cannot be nil' validates_integer :obs_general_quota, allow_nil: false, message: 'obs_general_quota cannot be nil' validate_password_expiration_in_d if default_quota_in_bytes errors.add(:default_quota_in_bytes, 'Default quota must be positive') if default_quota_in_bytes <= 0 end errors.add(:name, 'cannot exist as user') if name_exists_in_users? if whitelisted_email_domains.present? && !auth_enabled? errors.add(:whitelisted_email_domains, 'enable at least one auth. system or clear whitelisted email domains') end errors.add(:seats, 'cannot be less than the number of builders') if seats && remaining_seats < 0 errors.add(:viewer_seats, 'cannot be less than the number of viewers') if viewer_seats && remaining_viewer_seats < 0 end def validate_password_expiration_in_d valid = password_expiration_in_d.blank? || password_expiration_in_d > 0 && password_expiration_in_d < 366 errors.add(:password_expiration_in_d, 'must be greater than 0 and lower than 366') unless valid end def validate_for_signup(errors, user) validate_seats(user, errors) if !valid_disk_quota?(user.quota_in_bytes.to_i) errors.add(:quota_in_bytes, "not enough disk quota") end end def validate_seats(user, errors) if user.builder? && !valid_builder_seats?([user]) errors.add(:organization, "not enough seats") end if user.viewer? && remaining_viewer_seats(excluded_users: [user]) <= 0 errors.add(:organization, "not enough viewer seats") end end def valid_disk_quota?(quota = default_quota_in_bytes) unassigned_quota >= quota end def valid_builder_seats?(users = []) remaining_seats(excluded_users: users) > 0 end def before_validation self.geocoding_quota ||= DEFAULT_GEOCODING_QUOTA self.here_isolines_quota ||= DEFAULT_HERE_ISOLINES_QUOTA self.obs_snapshot_quota ||= DEFAULT_OBS_SNAPSHOT_QUOTA self.obs_general_quota ||= DEFAULT_OBS_GENERAL_QUOTA self.mapzen_routing_quota ||= DEFAULT_MAPZEN_ROUTING_QUOTA end # Just to make code more uniform with user.database_schema def database_schema self.name end def before_save super @geocoding_quota_modified = changed_columns.include?(:geocoding_quota) @here_isolines_quota_modified = changed_columns.include?(:here_isolines_quota) @obs_snapshot_quota_modified = changed_columns.include?(:obs_snapshot_quota) @obs_general_quota_modified = changed_columns.include?(:obs_general_quota) @mapzen_routing_quota_modified = changed_columns.include?(:mapzen_routing_quota) self.updated_at = Time.now raise errors.join('; ') unless valid? end def before_destroy return false unless destroy_assets destroy_groups end def after_create super save_metadata end def after_save super save_metadata end def after_destroy super destroy_metadata end # INFO: replacement for destroy because destroying owner triggers # organization destroy def destroy_cascade(delete_in_central: false) # This remains commented because we consider that enabling this for users at SaaS is unnecessary and risky. # Nevertheless, code remains, _just in case_. More info at https://github.com/CartoDB/cartodb/issues/12049 # Central branch: 1764-Allow_updating_inactive_users # Central asks for usage information before deleting, so organization must be first deleted there # Corollary: you need multithreading for organization to work if you run Central # self.delete_in_central if delete_in_central destroy_groups destroy_non_owner_users if owner owner.destroy_cascade else destroy end end def destroy_non_owner_users non_owner_users.each do |user| user.ensure_nonviewer user.shared_entities.map(&:entity).uniq.each(&:delete) user.destroy_cascade end end def non_owner_users users.select { |u| owner && u.id != owner.id } end ## # SLOW! Checks redis data (geocoding and isolines) for every user in every organization # delta: get organizations who are also this percentage below their limit. # example: 0.20 will get all organizations at 80% of their map view limit # def self.overquota(delta = 0) Organization.all.select do |o| begin limit = o.geocoding_quota.to_i - (o.geocoding_quota.to_i * delta) over_geocodings = o.get_geocoding_calls > limit limit = o.here_isolines_quota.to_i - (o.here_isolines_quota.to_i * delta) over_here_isolines = o.get_here_isolines_calls > limit limit = o.obs_snapshot_quota.to_i - (o.obs_snapshot_quota.to_i * delta) over_obs_snapshot = o.get_obs_snapshot_calls > limit limit = o.obs_general_quota.to_i - (o.obs_general_quota.to_i * delta) over_obs_general = o.get_obs_general_calls > limit limit = o.twitter_datasource_quota.to_i - (o.twitter_datasource_quota.to_i * delta) over_twitter_imports = o.get_twitter_imports_count > limit limit = o.mapzen_routing_quota.to_i - (o.mapzen_routing_quota.to_i * delta) over_mapzen_routing = o.get_mapzen_routing_calls > limit over_geocodings || over_twitter_imports || over_here_isolines || over_obs_snapshot || over_obs_general || over_mapzen_routing rescue OrganizationWithoutOwner => error # Avoid aborting because of inconistent organizations; just omit them CartoDB::Logger.error( message: 'Skipping organization without owner in overquota report', organization: name, exception: error ) false end end end def get_api_calls(options = {}) users.map{ |u| u.get_api_calls(options).sum }.sum end def get_geocoding_calls(options = {}) require_organization_owner_presence! date_from, date_to = quota_dates(options) get_organization_geocoding_data(self, date_from, date_to) end def get_here_isolines_calls(options = {}) date_from, date_to = quota_dates(options) get_organization_here_isolines_data(self, date_from, date_to) end def get_obs_snapshot_calls(options = {}) date_from, date_to = quota_dates(options) get_organization_obs_snapshot_data(self, date_from, date_to) end def get_obs_general_calls(options = {}) date_from, date_to = quota_dates(options) get_organization_obs_general_data(self, date_from, date_to) end def get_twitter_imports_count(options = {}) date_from, date_to = quota_dates(options) SearchTweet.get_twitter_imports_count(users_dataset.join(:search_tweets, :user_id => :id), date_from, date_to) end def get_mapzen_routing_calls(options = {}) date_from, date_to = quota_dates(options) get_organization_mapzen_routing_data(self, date_from, date_to) end def remaining_geocoding_quota remaining = geocoding_quota - get_geocoding_calls (remaining > 0 ? remaining : 0) end def remaining_here_isolines_quota remaining = here_isolines_quota - get_here_isolines_calls (remaining > 0 ? remaining : 0) end def remaining_obs_snapshot_quota remaining = obs_snapshot_quota - get_obs_snapshot_calls (remaining > 0 ? remaining : 0) end def remaining_obs_general_quota remaining = obs_general_quota - get_obs_general_calls (remaining > 0 ? remaining : 0) end def remaining_twitter_quota remaining = twitter_datasource_quota - get_twitter_imports_count (remaining > 0 ? remaining : 0) end def remaining_mapzen_routing_quota remaining = mapzen_routing_quota.to_i - get_mapzen_routing_calls (remaining > 0 ? remaining : 0) end def db_size_in_bytes users.map(&:db_size_in_bytes).sum.to_i end def assigned_quota users_dataset.sum(:quota_in_bytes).to_i end def unassigned_quota quota_in_bytes - assigned_quota end def to_poro { created_at: created_at, description: description, discus_shortname: discus_shortname, display_name: display_name, id: id, name: name, owner: { id: owner ? owner.id : nil, username: owner ? owner.username : nil, avatar_url: owner ? owner.avatar_url : nil, email: owner ? owner.email : nil, groups: owner && owner.groups ? owner.groups.map { |g| Carto::Api::GroupPresenter.new(g).to_poro } : [] }, admins: users.select(&:org_admin).map { |u| { id: u.id } }, quota_in_bytes: quota_in_bytes, unassigned_quota: unassigned_quota, geocoding_quota: geocoding_quota, map_view_quota: map_view_quota, twitter_datasource_quota: twitter_datasource_quota, map_view_block_price: map_view_block_price, geocoding_block_price: geocoding_block_price, here_isolines_quota: here_isolines_quota, here_isolines_block_price: here_isolines_block_price, obs_snapshot_quota: obs_snapshot_quota, obs_snapshot_block_price: obs_snapshot_block_price, obs_general_quota: obs_general_quota, obs_general_block_price: obs_general_block_price, geocoder_provider: geocoder_provider, isolines_provider: isolines_provider, routing_provider: routing_provider, mapzen_routing_quota: mapzen_routing_quota, mapzen_routing_block_price: mapzen_routing_block_price, seats: seats, twitter_username: twitter_username, location: twitter_username, updated_at: updated_at, website: website, admin_email: admin_email, avatar_url: avatar_url, user_count: users.count, password_expiration_in_d: password_expiration_in_d } end def tags(type, exclude_shared=true) users.map { |u| u.tags(exclude_shared, type) }.flatten end def public_vis_by_type(type, page_num, items_per_page, tags, order = 'updated_at', version = nil) CartoDB::Visualization::Collection.new.fetch( user_id: self.users.map(&:id), type: type, privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC, page: page_num, per_page: items_per_page, tags: tags, order: order, o: { updated_at: :desc }, version: version ) end def signup_page_enabled whitelisted_email_domains.present? && auth_enabled? end def auth_enabled? auth_username_password_enabled || auth_google_enabled || auth_github_enabled || auth_saml_enabled? end def total_seats seats + viewer_seats end def remaining_seats(excluded_users: []) seats - assigned_seats(excluded_users: excluded_users) end def remaining_viewer_seats(excluded_users: []) viewer_seats - assigned_viewer_seats(excluded_users: excluded_users) end def assigned_seats(excluded_users: []) builder_users.count { |u| !excluded_users.map(&:id).include?(u.id) } end def assigned_viewer_seats(excluded_users: []) viewer_users.count { |u| !excluded_users.map(&:id).include?(u.id) } end def builder_users (users || []).select(&:builder?) end def viewer_users (users || []).select(&:viewer?) end def admin?(user) user.belongs_to_organization?(self) && user.organization_admin? end def notify_if_disk_quota_limit_reached ::Resque.enqueue(::Resque::OrganizationJobs::Mail::DiskQuotaLimitReached, id) if disk_quota_limit_reached? end def notify_if_seat_limit_reached ::Resque.enqueue(::Resque::OrganizationJobs::Mail::SeatLimitReached, id) if seat_limit_reached? end def database_name owner ? owner.database_name : nil end def revoke_cdb_conf_access return unless users users.map { |user| user.db_service.revoke_cdb_conf_access } end def name_to_display display_name.nil? ? name : display_name end # create the key that is used in redis def key "rails:orgs:#{name}" end # save orgs basic metadata to redis for other services (node sql api, geocoder api, etc) # to use def save_metadata $users_metadata.HMSET key, 'id', id, 'geocoding_quota', geocoding_quota, 'here_isolines_quota', here_isolines_quota, 'obs_snapshot_quota', obs_snapshot_quota, 'obs_general_quota', obs_general_quota, 'mapzen_routing_quota', mapzen_routing_quota, 'google_maps_client_id', google_maps_key, 'google_maps_api_key', google_maps_private_key, 'period_end_date', period_end_date, 'geocoder_provider', geocoder_provider, 'isolines_provider', isolines_provider, 'routing_provider', routing_provider end def destroy_metadata $users_metadata.DEL key end def require_organization_owner_presence! if owner.nil? raise Organization::OrganizationWithoutOwner.new(self) end end def max_import_file_size owner ? owner.max_import_file_size : ::User::DEFAULT_MAX_IMPORT_FILE_SIZE end def max_import_table_row_count owner ? owner.max_import_table_row_count : ::User::DEFAULT_MAX_IMPORT_TABLE_ROW_COUNT end def max_concurrent_import_count owner ? owner.max_concurrent_import_count : ::User::DEFAULT_MAX_CONCURRENT_IMPORT_COUNT end def max_layers owner ? owner.max_layers : ::User::DEFAULT_MAX_LAYERS end def auth_saml_enabled? auth_saml_configuration.present? end private def destroy_assets assets.map { |asset| Carto::Asset.find(asset.id) }.map(&:destroy).all? end def destroy_groups return unless groups groups.map { |g| Carto::Group.find(g.id).destroy_group_with_extension } reload end # Returns true if disk quota won't allow new signups with existing defaults def disk_quota_limit_reached? unassigned_quota < default_quota_in_bytes end # Returns true if seat limit will be reached with new user def seat_limit_reached? (remaining_seats - 1) < 1 end def quota_dates(options) date_to = (options[:to] ? options[:to].to_date : Date.today) date_from = (options[:from] ? options[:from].to_date : last_billing_cycle) return date_from, date_to end def last_billing_cycle owner ? owner.last_billing_cycle : Date.today end def period_end_date owner ? owner.period_end_date : nil end def public_vis_count_by_type(type) CartoDB::Visualization::Collection.new.fetch( user_id: self.users.map(&:id), type: type, privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC, per_page: CartoDB::Visualization::Collection::ALL_RECORDS ).count end def name_exists_in_users? !::User.where(username: self.name).first.nil? end end