cartodb/app/models/organization.rb
2020-06-15 10:58:47 +08:00

536 lines
17 KiB
Ruby

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