Create new OAuth scope to read/write all datasets

pull/15905/head
Alberto Miedes Garcés 4 years ago committed by VictorVelarde
parent 7468e610a7
commit c1b2c126dd

@ -329,7 +329,12 @@ SPEC_HELPER_MIN_SPECS = \
spec/lib/carto/styles/presenters/cartocss_spec.rb \
spec/lib/carto/forms_definition_spec.rb \
spec/lib/carto/form_spec.rb \
spec/lib/carto/oauth_provider/scopes/all_datasets_scope_spec.rb \
spec/lib/carto/oauth_provider/scopes/apis_scope_spec.rb \
spec/lib/carto/oauth_provider/scopes/dataservices_scope_spec.rb \
spec/lib/carto/oauth_provider/scopes/datasets_scope_spec.rb \
spec/lib/carto/oauth_provider/scopes/scopes_spec.rb \
spec/lib/carto/oauth_provider/scopes/user_scope_spec.rb \
spec/models/carto/legend_spec.rb \
spec/requests/carto/api/legends_controller_spec.rb \
spec/lib/carto/legend_definition_validator_spec.rb \

@ -18,6 +18,7 @@ Development
* Adapt default Rails mail logs to JSON format [#15894](https://github.com/CartoDB/cartodb/pull/15894)
* Fix export of Google Sheet files larger than 10MB [#15903](https://github.com/CartoDB/cartodb/pull/15903)
* Adding `builder_url` to `api/v4/me` endpoint [#15904](https://github.com/CartoDB/cartodb/pull/15904)
* Create OAuth scope for reading/writing all datasets [#15884](https://github.com/CartoDB/cartodb/pull/15884)
4.42.0 (2020-09-28)
-------------------

@ -21,9 +21,11 @@ module Carto
VALID_ORDER_PARAMS = %i(name).freeze
def index
tables = @user.in_database[select_tables_query].all
db_service = @user.carto_user.db_service
tables = db_service.tables_granted(@query_params)
result = enrich_tables(tables)
total = @user.in_database[count_tables_query].first[:count]
total = db_service.tables_granted_count
render_paged(result, total)
end
@ -39,6 +41,7 @@ module Carto
VALID_ORDER_PARAMS, default_order: 'name', default_order_direction: 'asc'
)
@offset = (@page - 1) * @per_page
@query_params = { order: @order, direction: @direction, limit: @per_page, offset: @offset }
end
def check_permissions
@ -47,12 +50,12 @@ module Carto
end
def enrich_tables(tables)
table_names = tables.map { |table| table[:name] }
table_names = tables.map(&:name)
visualizations = table_visualizations(table_names)
tables.map do |table|
viz = visualizations.find { |visualization| visualization[:name] == table[:name] }
viz = visualizations.find { |visualization| visualization[:name] == table.name }
extra_fields = viz || default_extra_fields
table.merge(extra_fields)
table.to_h.merge(extra_fields)
end
end
@ -82,36 +85,6 @@ module Carto
end
end
def select_tables_query
%{
SELECT * FROM (#{query}) AS q
ORDER BY #{@order} #{@direction}
LIMIT #{@per_page}
OFFSET #{@offset}
}.squish
end
def count_tables_query
%{
SELECT COUNT(*) FROM (#{query}) AS q
}.squish
end
def query
roles_in = @user.db_service.all_user_roles.join("','")
%{
SELECT table_schema, table_name as name,
string_agg(CASE privilege_type WHEN 'SELECT' THEN 'r' ELSE 'w' END,
'' order by privilege_type) as mode
FROM information_schema.role_table_grants
WHERE grantee IN ('#{roles_in}')
AND table_schema not in ('cartodb', 'aggregation')
AND grantor!='postgres'
AND privilege_type in ('SELECT', 'UPDATE')
GROUP BY table_schema,  table_name
}.squish
end
def render_paged(result, total)
enriched_response = paged_result(
result: result,

@ -1,5 +1,6 @@
require_dependency 'carto/oauth_provider/scopes/scopes'
require_dependency 'carto/oauth_provider/errors'
require_dependency 'carto/oauth_provider/scopes/scopes_validator'
module Carto
class OauthAppUser < ActiveRecord::Base

@ -144,6 +144,46 @@ module Carto
results.map { |row| [row['object_name'], row['granted_permissions'].split(',')] }.to_h
end
# Returns all tables the user has access to, including shared ones and excluding internal tables.
# The result has this format:
# [<OpenStruct table_schema='public', name='table_name', mode='rw'>]
def tables_granted(params = {})
table = table_grants
table.project('*')
table.order("#{params[:order]} #{params[:direction]}") if params[:order]
table.take(params[:limit]) if params[:limit]
table.skip(params[:offset]) if params[:offset]
@user.in_database.execute(table.to_sql).map { |t| OpenStruct.new(t) }
end
def tables_granted_count
@user.in_database
.execute(table_grants.project('COUNT(*)').to_sql)
.first['count'].to_i
end
def table_grants
table = Arel::Table.new('information_schema.role_table_grants')
query_sql = table.project(Arel.sql(%{
table_schema,
table_name AS name,
STRING_AGG(
CASE privilege_type
WHEN 'SELECT' THEN 'r'
ELSE 'w'
END,
'' ORDER BY privilege_type
) AS mode
})).where(Arel.sql(%{
grantee IN ('#{all_user_roles.join("','")}') AND
table_schema NOT IN ('cartodb', 'aggregation') AND
grantor != 'postgres' AND
privilege_type IN ('SELECT', 'UPDATE')
})).group(Arel.sql('table_schema, table_name')).to_sql
Arel::SelectManager.new(Arel::Table.engine, Arel.sql("(#{query_sql}) AS q"))
end
def exists_role?(rolname)
query = %{
SELECT 1

@ -0,0 +1,82 @@
module Carto
module OauthProvider
module Scopes
class AllDatasetsScope < DefaultScope
READ_PERMISSIONS = ['select'].freeze
WRITE_PERMISSIONS = ['insert', 'update', 'delete'].freeze
PERMISSIONS = {
r: READ_PERMISSIONS,
rw: READ_PERMISSIONS + WRITE_PERMISSIONS
}.with_indifferent_access.freeze
DESCRIPTIONS = {
r: 'All datasets (read access)',
rw: 'All datasets (read/write access)'
}.with_indifferent_access.freeze
EXCLUDED_INTERNAL_TABLES = %w(
geography_columns
geometry_columns
raster_columns
raster_overviews
spatial_ref_sys
).freeze
attr_reader :description, :permission_key, :permission
def self.is_a?(scope)
scope =~ /^datasets:(?:rw|r):\*$/
end
def self.valid_scopes(scopes)
scopes.select { |scope| AllDatasetsScope.is_a?(scope) }
end
def initialize(scope)
@permission_key = scope.split(':').second.to_sym
@permission = PERMISSIONS[permission_key]
@description = DESCRIPTIONS[permission_key]
@grant_key = :tables
super('database', permission_key, CATEGORY_DATASETS, description)
end
def name
"datasets:#{permission_key}:*"
end
def add_to_api_key_grants(grants, user)
ensure_includes_apis(grants, ['maps', 'sql'])
database_section = grant_section(grants)
granted_tables = user.db_service.tables_granted
return unless granted_tables
granted_tables.each do |table|
database_section[@grant_key] << {
name: table.name,
permissions: combined_permissions(table.mode.to_sym),
schema: table.table_schema
}
end
ensure_grant_section(grants, database_section)
end
private
def combined_permissions(table_mode)
if permission_key == table_mode
PERMISSIONS[@permission_key]
elsif [permission_key, table_mode] == [:rw, :r] || [permission_key, table_mode] == [:r, :rw]
PERMISSIONS[:r]
else
[]
end
end
end
end
end
end

@ -44,11 +44,19 @@ module Carto
SUPPORTED_SCOPES = (SCOPES.map(&:name) - [SCOPE_DEFAULT]).freeze
def self.invalid_scopes(scopes)
scopes - SUPPORTED_SCOPES - DatasetsScope.valid_scopes(scopes) - SchemasScope.valid_scopes(scopes)
scopes -
SUPPORTED_SCOPES -
DatasetsScope.valid_scopes(scopes) -
SchemasScope.valid_scopes(scopes) -
AllDatasetsScope.valid_scopes(scopes)
end
def self.invalid_scopes_and_tables(scopes, user)
scopes - SUPPORTED_SCOPES - DatasetsScope.valid_scopes_with_table(scopes, user) - SchemasScope.valid_scopes_with_schema(scopes, user)
scopes -
SUPPORTED_SCOPES -
DatasetsScope.valid_scopes_with_table(scopes, user) -
SchemasScope.valid_scopes_with_schema(scopes, user) -
AllDatasetsScope.valid_scopes(scopes)
end
def self.build(scope)
@ -56,6 +64,8 @@ module Carto
if !result
if DatasetsScope.is_a?(scope)
result = DatasetsScope.new(scope)
elsif AllDatasetsScope.is_a?(scope)
result = AllDatasetsScope.new(scope)
elsif SchemasScope.is_a?(scope)
result = SchemasScope.new(scope)
end

@ -0,0 +1,156 @@
require 'spec_helper_min'
require './spec/support/factories/organizations'
require_dependency 'carto/oauth_provider/scopes/scopes'
describe Carto::OauthProvider::Scopes::AllDatasetsScope do
include CartoDB::Factories
include Carto::Factories::Visualizations
include TableSharing
let(:r_scope_string) { 'datasets:r:*' }
let(:rw_scope_string) { 'datasets:rw:*' }
let(:invalid_scope_string) { 'datasets:rx:*' }
let(:scope_string) { r_scope_string }
let(:scope) { described_class.new(scope_string) }
let(:user) { create(:valid_user).carto_user }
let!(:organization) { OrganizationFactory.new.create_organization_with_users.carto_organization }
let!(:org_admin) { organization.owner }
let(:organization_user) { organization.users.where.not(id: org_admin.id).first }
let(:tables_grants) { grants.find { |grant| grant[:type] == 'database' }[:tables] }
describe '::is_a?' do
subject { described_class.is_a?(scope_string) }
context 'when scope matches' do
it { should be_true }
end
context 'when scope does not match' do
let(:scope_string) { invalid_scope_string }
it { should be_false }
end
end
describe '::valid_scopes' do
subject(:valid_scopes) { described_class.valid_scopes([r_scope_string, rw_scope_string, invalid_scope_string]) }
it 'returns only the valid scopes' do
expect(valid_scopes).to include(r_scope_string)
expect(valid_scopes).to include(rw_scope_string)
expect(valid_scopes).not_to include(invalid_scope_string)
end
end
describe '#name' do
subject(:name) { scope.name }
context 'for read permissions' do
it 'returns the scope name' do
expect(name).to eq('datasets:r:*')
end
end
context 'for read-write permissions' do
let(:scope_string) { rw_scope_string }
it 'returns the scope name' do
expect(name).to eq('datasets:rw:*')
end
end
end
describe '#add_to_api_key_grants' do
let!(:user_table) { create_full_visualization(user)[2] }
let(:grants) { [{ type: 'apis', apis: [] }] }
let(:granted_permissions) { tables_grants.find { |t| t[:name] == user_table.name }[:permissions] }
let(:granted_tables) { tables_grants.map { |table| table[:name] } }
context 'when granting read-only permissions' do
before { scope.add_to_api_key_grants(grants, user) }
it 'only grants read permissions' do
expect(granted_permissions).to include('select')
expect(granted_permissions).not_to include('insert')
expect(granted_permissions).not_to include('update')
expect(granted_permissions).not_to include('delete')
end
end
context 'when granting read-write permissions' do
let(:scope_string) { rw_scope_string }
before { scope.add_to_api_key_grants(grants, user) }
it 'grants read-write permissions' do
expect(granted_permissions).to include('select')
expect(granted_permissions).to include('insert')
expect(granted_permissions).to include('update')
expect(granted_permissions).to include('delete')
end
end
context 'when user belongs to organization' do
let(:user) { organization_user }
let!(:other_user_table) { create_full_visualization(org_admin)[2] }
before do
user.update!(organization: organization)
scope.add_to_api_key_grants(grants, user)
end
it 'only grants permissions to user datasets when in an organization' do
expect(granted_tables).to include(user_table.name)
expect(granted_tables).not_to include(other_user_table.name)
end
end
context 'when user has access to other shared datasets' do
let(:user) { organization_user }
let(:full_visualization_objects) { create_full_visualization(org_admin) }
let(:other_table) { full_visualization_objects[1] }
let!(:other_user_table) { full_visualization_objects[2] }
let(:granted_permissions) { tables_grants.find { |t| t[:name] == other_user_table.name }[:permissions] }
context 'with read-only access' do
before do
share_table_with_user(other_table, user)
scope.add_to_api_key_grants(grants, user)
end
it 'only grants read permissions' do
expect(granted_tables).to include(other_user_table.name)
expect(granted_permissions).to include('select')
expect(granted_permissions).not_to include('insert')
expect(granted_permissions).not_to include('update')
expect(granted_permissions).not_to include('delete')
end
end
context 'with read-write access' do
let(:scope_string) { rw_scope_string }
before do
share_table_with_user(other_table, user, access: Carto::Permission::ACCESS_READWRITE)
scope.add_to_api_key_grants(grants, user)
end
it 'grants read-write permissions' do
expect(granted_tables).to include(other_user_table.name)
expect(granted_permissions).to include('select')
expect(granted_permissions).to include('insert')
expect(granted_permissions).to include('update')
expect(granted_permissions).to include('delete')
end
end
end
it 'does not grant permissions over internal tables' do
scope.add_to_api_key_grants(grants, user)
expect(granted_tables).not_to include('spatial_ref_sys')
end
end
end

@ -0,0 +1,16 @@
require 'spec_helper_min'
require_dependency 'carto/oauth_provider/scopes/scopes'
require_relative '../../../../factories/organizations_contexts'
describe Carto::OauthProvider::Scopes::ApisScope do
include_context 'organization with users helper'
describe '#add_to_api_key_grants' do
it 'adds apis scope with do subset' do
scope = Carto::OauthProvider::Scopes::ApisScope.new('do', 'Data Observatory API')
grants = [{ type: 'apis', apis: [] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(eq([{ type: 'apis', apis: ['do'] }]))
end
end
end

@ -0,0 +1,23 @@
require 'spec_helper_min'
require_dependency 'carto/oauth_provider/scopes/scopes'
require_relative '../../../../factories/organizations_contexts'
describe Carto::OauthProvider::Scopes::DataservicesScope do
include_context 'organization with users helper'
describe '#add_to_api_key_grants' do
let(:scope) { Carto::OauthProvider::Scopes::DataservicesScope.new('geocoding', 'GC') }
it 'adds SQL api and dataservice' do
grants = [{ type: 'apis', apis: [] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(eq([{ type: 'apis', apis: ['sql'] }, { type: 'dataservices', services: ['geocoding'] }]))
end
it 'does not add duplicate SQL api' do
grants = [{ type: 'apis', apis: ['sql'] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(include(type: 'apis', apis: ['sql']))
end
end
end

@ -0,0 +1,119 @@
require 'spec_helper_min'
require_dependency 'carto/oauth_provider/scopes/scopes'
require_relative '../../../../factories/organizations_contexts'
describe Carto::OauthProvider::Scopes::DatasetsScope do
include_context 'organization with users helper'
describe '#add_to_api_key_grants' do
let(:full_dataset_scope) { Carto::OauthProvider::Scopes::DatasetsScope.new('datasets:rw:untitled_table') }
let(:read_dataset_scope) { Carto::OauthProvider::Scopes::DatasetsScope.new('datasets:r:untitled_table') }
let(:schema_scope) { Carto::OauthProvider::Scopes::SchemasScope.new('schemas:c') }
let(:dataset_metadata_scope) { Carto::OauthProvider::Scopes::DatasetsMetadataScope.new('datasets:metadata') }
let(:full_table_grants) do
[
{
apis: [
'maps',
'sql'
],
type: 'apis'
},
{
tables: [
{
name: 'untitled_table',
permissions: [
'select',
'insert',
'update',
'delete'
],
schema: 'wadus'
}
],
schemas: [
{
name: 'wadus',
permissions: [
'create'
]
}
],
type: 'database'
}
]
end
let(:read_table_grants) do
[
{
apis: [
'maps',
'sql'
],
type: 'apis'
},
{
tables: [
{
name: 'untitled_table',
permissions: [
'select'
],
schema: 'wadus'
}
],
type: 'database'
}
]
end
let(:metadata_grants) do
[
{
apis: [
'sql'
],
type: 'apis'
},
{
table_metadata: [],
type: 'database'
}
]
end
before(:all) do
@user = mock
@user.stubs(:database_schema).returns('wadus')
end
it 'adds full access permissions' do
grants = [{ type: 'apis', apis: [] }]
full_dataset_scope.add_to_api_key_grants(grants, @user)
schema_scope.add_to_api_key_grants(grants, @user)
expect(grants).to(eq(full_table_grants))
end
it 'does not add write permissions' do
grants = [{ type: 'apis', apis: [] }]
read_dataset_scope.add_to_api_key_grants(grants, @user)
expect(grants).to(eq(read_table_grants))
end
it 'adds metadata permissions' do
grants = [{ type: 'apis', apis: [] }]
dataset_metadata_scope.add_to_api_key_grants(grants, @user)
expect(grants).to(eq(metadata_grants))
end
it 'does add full access permissions and metadata' do
grants = [{ type: 'apis', apis: [] }]
dataset_metadata_scope.add_to_api_key_grants(grants, @user)
full_dataset_scope.add_to_api_key_grants(grants, @user)
expect(grants[1]).to have_key(:table_metadata)
expect(grants[1]).to have_key(:tables)
end
end
end

@ -301,160 +301,6 @@ describe Carto::OauthProvider::Scopes do
end
end
describe Carto::OauthProvider::Scopes::DataservicesScope do
describe '#add_to_api_key_grants' do
let(:scope) { Carto::OauthProvider::Scopes::DataservicesScope.new('geocoding', 'GC') }
it 'adds SQL api and dataservice' do
grants = [{ type: 'apis', apis: [] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(eq([{ type: 'apis', apis: ['sql'] }, { type: 'dataservices', services: ['geocoding'] }]))
end
it 'does not add duplicate SQL api' do
grants = [{ type: 'apis', apis: ['sql'] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(include(type: 'apis', apis: ['sql']))
end
end
end
describe Carto::OauthProvider::Scopes::UserScope do
describe '#add_to_api_key_grants' do
it 'adds user scope with profile subset' do
scope = Carto::OauthProvider::Scopes::UserScope.new('profile', 'User public profile')
grants = [{ type: 'apis', apis: [] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(eq([{ type: 'apis', apis: [] }, { type: 'user', data: ['profile'] }]))
end
end
end
describe Carto::OauthProvider::Scopes::ApisScope do
describe '#add_to_api_key_grants' do
it 'adds apis scope with do subset' do
scope = Carto::OauthProvider::Scopes::ApisScope.new('do', 'Data Observatory API')
grants = [{ type: 'apis', apis: [] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(eq([{ type: 'apis', apis: ['do'] }]))
end
end
end
describe Carto::OauthProvider::Scopes::DatasetsScope do
describe '#add_to_api_key_grants' do
let(:full_dataset_scope) { Carto::OauthProvider::Scopes::DatasetsScope.new('datasets:rw:untitled_table') }
let(:read_dataset_scope) { Carto::OauthProvider::Scopes::DatasetsScope.new('datasets:r:untitled_table') }
let(:schema_scope) { Carto::OauthProvider::Scopes::SchemasScope.new('schemas:c') }
let(:dataset_metadata_scope) { Carto::OauthProvider::Scopes::DatasetsMetadataScope.new('datasets:metadata') }
let(:full_table_grants) do
[
{
apis: [
'maps',
'sql'
],
type: 'apis'
},
{
tables: [
{
name: 'untitled_table',
permissions: [
'select',
'insert',
'update',
'delete'
],
schema: 'wadus'
}
],
schemas: [
{
name: 'wadus',
permissions: [
'create'
]
}
],
type: 'database'
}
]
end
let(:read_table_grants) do
[
{
apis: [
'maps',
'sql'
],
type: 'apis'
},
{
tables: [
{
name: 'untitled_table',
permissions: [
'select'
],
schema: 'wadus'
}
],
type: 'database'
}
]
end
let(:metadata_grants) do
[
{
apis: [
'sql'
],
type: 'apis'
},
{
table_metadata: [],
type: 'database'
}
]
end
before(:all) do
@user = mock
@user.stubs(:database_schema).returns('wadus')
end
it 'adds full access permissions' do
grants = [{ type: 'apis', apis: [] }]
full_dataset_scope.add_to_api_key_grants(grants, @user)
schema_scope.add_to_api_key_grants(grants, @user)
expect(grants).to(eq(full_table_grants))
end
it 'does not add write permissions' do
grants = [{ type: 'apis', apis: [] }]
read_dataset_scope.add_to_api_key_grants(grants, @user)
expect(grants).to(eq(read_table_grants))
end
it 'adds metadata permissions' do
grants = [{ type: 'apis', apis: [] }]
dataset_metadata_scope.add_to_api_key_grants(grants, @user)
expect(grants).to(eq(metadata_grants))
end
it 'does add full access permissions and metadata' do
grants = [{ type: 'apis', apis: [] }]
dataset_metadata_scope.add_to_api_key_grants(grants, @user)
full_dataset_scope.add_to_api_key_grants(grants, @user)
expect(grants[1]).to have_key(:table_metadata)
expect(grants[1]).to have_key(:tables)
end
end
end
describe '#subtract_scopes' do
it 'r - r' do
scopes1 = ['datasets:r:schema.table']

@ -0,0 +1,16 @@
require 'spec_helper_min'
require_dependency 'carto/oauth_provider/scopes/scopes'
require_relative '../../../../factories/organizations_contexts'
describe Carto::OauthProvider::Scopes::UserScope do
include_context 'organization with users helper'
describe '#add_to_api_key_grants' do
it 'adds user scope with profile subset' do
scope = Carto::OauthProvider::Scopes::UserScope.new('profile', 'User public profile')
grants = [{ type: 'apis', apis: [] }]
scope.add_to_api_key_grants(grants, nil)
expect(grants).to(eq([{ type: 'apis', apis: [] }, { type: 'user', data: ['profile'] }]))
end
end
end
Loading…
Cancel
Save