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

254 lines
8.2 KiB
Ruby

require_dependency 'carto/oauth_provider/scopes/scopes'
require_dependency 'carto/oauth_provider/errors'
module Carto
class OauthAppUser < ActiveRecord::Base
include OauthProvider::Scopes
belongs_to :user, inverse_of: :oauth_app_users
belongs_to :oauth_app, inverse_of: :oauth_app_users
belongs_to :api_key, inverse_of: :oauth_app_user
has_many :oauth_authorization_codes, inverse_of: :oauth_app_user, dependent: :destroy
has_many :oauth_access_tokens, inverse_of: :oauth_app_user, dependent: :destroy
has_many :oauth_refresh_tokens, inverse_of: :oauth_app_user, dependent: :destroy
validates :user, presence: true, uniqueness: { scope: :oauth_app }
validates :oauth_app, presence: true
validates :scopes, scopes: true
validate :validate_user_authorizable, on: :create
after_create :create_roles, :enable_schema_triggers, :ensure_role_grants, unless: :skip_role_setup
before_update :grant_dataset_role_privileges
after_destroy :reassign_owners, :drop_roles, :disable_schema_triggers, unless: :skip_role_setup
attr_accessor :skip_role_setup
def authorized?(requested_scopes)
OauthProvider::Scopes.subtract_scopes(requested_scopes, all_scopes, user.database_schema).empty?
end
def upgrade!(requested_scopes)
update!(scopes: all_scopes | requested_scopes)
end
def all_scopes
no_dataset_scopes + dataset_scopes
end
def revoke_permissions(table, revoked_permissions)
db_connection = user.in_database(as: :superuser)
Carto::TableAndFriends.apply(db_connection, table.database_schema, table.name) do |_s, _t, qualified_name|
query = %{
REVOKE #{revoked_permissions.join(', ')}
ON TABLE #{qualified_name}
FROM \"#{dataset_role_name}\"
}
db_connection.execute(query)
sequences_for_table(qualified_name).each do |seq|
db_connection.execute("REVOKE ALL ON SEQUENCE #{seq} FROM \"#{dataset_role_name}\"")
end
end
end
def create_roles
create_dataset_role
create_ownership_role
end
def create_dataset_role
db_run(create_dataset_role_query)
end
def create_ownership_role
db_run(create_ownership_role_query)
end
def create_dataset_role_query
"CREATE ROLE \"#{dataset_role_name}\" CREATEROLE"
end
def create_ownership_role_query
"CREATE ROLE \"#{ownership_role_name}\""
end
def ensure_role_grants
grant_dataset_role_privileges
grant_ownership_role_privileges
end
def grant_ownership_role_privileges
db_run("GRANT \"#{ownership_role_name}\" TO \"#{user.database_username}\"")
end
def grant_dataset_role_privileges
validate_scopes
invalid_scopes = []
DatasetsScope.valid_scopes(scopes).each do |scope|
dataset_scope = DatasetsScope.new(scope)
schema = dataset_scope.schema || user.database_schema
schema_table = Carto::TableAndFriends.qualified_table_name(schema, dataset_scope.table)
table_query = %{
GRANT #{dataset_scope.permission.join(',')}
ON TABLE #{schema_table}
TO \"#{dataset_role_name}\" WITH GRANT OPTION;
}
schema_query = %{
GRANT USAGE ON SCHEMA \"#{schema}\"
TO \"#{dataset_role_name}\";
}
begin
user.in_database(as: :superuser).execute(table_query)
user.in_database(as: :superuser).execute(schema_query)
sequences_for_table(schema_table).each do |seq|
seq_query = "GRANT USAGE, SELECT ON SEQUENCE #{seq} TO \"#{dataset_role_name}\""
user.in_database(as: :superuser).execute(seq_query)
end
rescue ActiveRecord::StatementInvalid => e
invalid_scopes << scope
CartoDB::Logger.error(message: 'Error granting permissions to dataset role', exception: e)
end
end
raise OauthProvider::Errors::InvalidScope.new(invalid_scopes) if invalid_scopes.any?
end
def ownership_role_name
"carto_oauth_app_o_#{id}"
end
def exists_ownership_role?
user.db_service.exists_role?(ownership_role_name)
end
private
def validate_user_authorizable
return unless oauth_app.try(:restricted?)
organization = user.organization
return errors.add(:user, 'is not part of an organization') unless user.organization
org_authorization = oauth_app.oauth_app_organizations.where(organization: organization).first
unless org_authorization
return errors.add(:user, 'is part of an organization which is not allowed access to this application')
end
errors.add(:user, 'does not have an available seat to use this application') unless org_authorization.open_seats?
end
def no_dataset_scopes
DatasetsScope.non_dataset_scopes(scopes)
end
def dataset_scopes
begin
results = user.db_service.all_tables_granted(dataset_role_name)
rescue ActiveRecord::StatementInvalid => e
CartoDB::Logger.error(message: 'Error getting scopes', exception: e)
raise OauthProvider::Errors::ServerError.new
end
scopes = []
results.each do |row|
permission = DatasetsScope.permission_from_db_to_scope(row['permission'])
unless permission.nil?
schema = row['schema'] != user.database_schema ? "#{row['schema']}." : ''
scopes << "datasets:#{permission}:#{schema}#{row['t']}"
end
end
scopes
end
def reassign_owners
roles = [dataset_role_name, ownership_role_name]
queries = roles.map do |role|
"REASSIGN OWNED BY \"#{role}\" TO \"#{user.database_username}\";"
end
db_run(queries.join, error_title: 'Error reassigning owners')
end
def drop_roles
roles = [dataset_role_name, ownership_role_name]
queries = roles.map do |role|
%{
DROP OWNED BY "#{role}";
DROP ROLE IF EXISTS "#{role}";
}
end
db_run(queries.join, error_title: 'Error dropping roles')
end
def enable_schema_triggers
return if user.organization_user? && oauth_users_in_organization > 1
user.db_service.create_oauth_reassign_ownership_event_trigger
rescue StandardError => e
CartoDB::Logger.error(
message: "Error enabling schema trigger",
exception: e,
user: self
)
end
def disable_schema_triggers
return if user.organization_user? && oauth_users_in_organization >= 1
user.db_service.drop_oauth_reassign_ownership_event_trigger
rescue StandardError => e
CartoDB::Logger.error(
message: "Error disabling schema trigger",
exception: e,
user: self
)
end
def validate_scopes
invalid_scopes = OauthProvider::Scopes.invalid_scopes_and_tables(scopes, user)
raise OauthProvider::Errors::InvalidScope.new(invalid_scopes) if invalid_scopes.present?
end
def sequences_for_table(schema_table)
query = %{
SELECT
n.nspname, quote_ident(c.relname) as relname
FROM
pg_depend d
JOIN pg_class c ON d.objid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE
d.refobjsubid > 0 AND
d.classid = 'pg_class'::regclass AND
c.relkind = 'S'::"char" AND
d.refobjid = ('#{schema_table}')::regclass
}
user.in_database.execute(query).map { |r| "\"#{r['nspname']}\".#{r['relname']}" }
end
def dataset_role_name
"carto_oauth_app_#{id}"
end
def db_run(query, connection = db_connection, error_title: 'Error running SQL command')
connection.execute(query)
rescue ActiveRecord::StatementInvalid => e
CartoDB::Logger.error(message: error_title, exception: e)
return if e.message =~ /OWNED BY/ # role might not exist becuase it has been already dropped
raise OauthProvider::Errors::ServerError.new("#{error_title}: #{e.message}")
end
def db_connection
@db_connection ||= user.in_database(as: :superuser)
end
def oauth_users_in_organization
return 0 unless user.organization_user?
Carto::OauthAppUser.joins(:user).where('organization_id = ?', user.organization_id).count
end
end
end