246 lines
8.1 KiB
Ruby
246 lines
8.1 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
|
|
log_error(message: 'Error granting permissions to dataset role', exception: e, current_user: user)
|
|
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
|
|
log_error(message: 'Error getting scopes', exception: e, current_user: user)
|
|
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
|
|
log_error(message: 'Error enabling schema trigger', exception: e, current_user: user)
|
|
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
|
|
log_error(message: 'Error disabling schema trigger', exception: e, current_user: user)
|
|
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
|
|
log_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
|