Skip to content

Commit

Permalink
Fix unprivileged access to views in Hasura (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
droserasprout authored Jun 24, 2021
1 parent a14fc94 commit 405db3f
Showing 1 changed file with 36 additions and 38 deletions.
74 changes: 36 additions & 38 deletions src/dipdup/hasura.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
_logger = logging.getLogger(__name__)


class HasuraError(RuntimeError):
...


def _is_model_class(obj) -> bool:
"""Is subclass of tortoise.Model, but not the base class"""
return isinstance(obj, type) and issubclass(obj, Model) and obj != Model and not getattr(obj.Meta, 'abstract', False)
Expand Down Expand Up @@ -51,11 +55,11 @@ def _format_object_relationship(name: str, column: str) -> Dict[str, Any]:
}


def _format_select_permissions(columns: List[str]) -> Dict[str, Any]:
def _format_select_permissions() -> Dict[str, Any]:
return {
"role": "user",
"permission": {
"columns": sorted(columns),
"columns": "*",
"filter": {},
"allow_aggregations": True,
},
Expand Down Expand Up @@ -90,11 +94,13 @@ def _iter_models(*modules) -> Iterator[Tuple[str, Type[Model]]]:
yield app, model


async def generate_hasura_metadata(config: DipDupConfig) -> Dict[str, Any]:
async def generate_hasura_metadata(config: DipDupConfig, views: List[str]) -> Dict[str, Any]:
"""Generate metadata based on dapp models.
Includes tables and their relations (but not entities created during execution of snippets from `sql` package directory)
"""
if not isinstance(config.database, PostgresDatabaseConfig):
raise RuntimeError
_logger.info('Generating Hasura metadata')
metadata_tables = {}
model_tables = {}
Expand All @@ -105,24 +111,31 @@ async def generate_hasura_metadata(config: DipDupConfig) -> Dict[str, Any]:
for app, model in _iter_models(models, int_models):
table_name = model._meta.db_table or pascal_to_snake(model.__name__) # pylint: disable=protected-access
model_tables[f'{app}.{model.__name__}'] = table_name

table = _format_table(
metadata_tables[table_name] = _format_table(
name=table_name,
schema=config.database.schema_name if isinstance(config.database, PostgresDatabaseConfig) else 'public',
schema=config.database.schema_name,
)

for view in views:
metadata_tables[view] = _format_table(
name=view,
schema=config.database.schema_name,
)
metadata_tables[view]['select_permissions'].append(
_format_select_permissions(),
)
metadata_tables[table_name] = table

for app, model in _iter_models(models, int_models):
table_name = model_tables[f'{app}.{model.__name__}']

metadata_tables[table_name]['select_permissions'].append(
_format_select_permissions(list(model._meta.db_fields)),
_format_select_permissions(),
)

for field in model._meta.fields_map.values():
if isinstance(field, fields.relational.ForeignKeyFieldInstance):
if not isinstance(field.related_name, str):
raise Exception(f'`related_name` of `{field}` must be set')
raise HasuraError(f'`related_name` of `{field}` must be set')
related_table_name = model_tables[field.model_name]
metadata_tables[table_name]['object_relationships'].append(
_format_object_relationship(
Expand All @@ -135,7 +148,7 @@ async def generate_hasura_metadata(config: DipDupConfig) -> Dict[str, Any]:
related_name=field.related_name,
table=table_name,
column=field.model_field_name + '_id',
schema=config.database.schema_name if isinstance(config.database, PostgresDatabaseConfig) else 'public',
schema=config.database.schema_name,
)
)

Expand All @@ -152,7 +165,16 @@ async def configure_hasura(config: DipDupConfig):

_logger.info('Configuring Hasura')
url = config.hasura.url.rstrip("/")
hasura_metadata = await generate_hasura_metadata(config)
views = [
row[0]
for row in (
await get_connection(None).execute_query(
f"SELECT table_name FROM information_schema.views WHERE table_schema = '{config.database.schema_name}'"
)
)[1]
]

hasura_metadata = await generate_hasura_metadata(config, views)

async with aiohttp.ClientSession() as session:
_logger.info('Waiting for Hasura instance to be healthy')
Expand All @@ -163,8 +185,7 @@ async def configure_hasura(config: DipDupConfig):
break
await asyncio.sleep(1)
else:
_logger.error('Hasura instance not responding for 60 seconds')
return
raise HasuraError('Hasura instance not responding for 60 seconds')

headers = {}
if config.hasura.admin_secret:
Expand All @@ -178,7 +199,7 @@ async def configure_hasura(config: DipDupConfig):
data=json.dumps(
{
"type": "export_metadata",
"args": hasura_metadata,
"args": {},
},
),
headers=headers,
Expand All @@ -204,29 +225,6 @@ async def configure_hasura(config: DipDupConfig):
headers=headers,
)
if result.get('message') != 'success':
_logger.error('Can\'t configure Hasura instance: %s', result)
return

views = await get_connection(None).execute_query(
f"SELECT table_name FROM information_schema.views WHERE table_schema = '{config.database.schema_name}'"
)
for view in views[1]:
result = await http_request(
session,
'post',
url=f'{url}/v1/query',
data=json.dumps(
{
"type": "add_existing_table_or_view",
"args": {
"name": view[0],
"schema": config.database.schema_name,
},
},
),
headers=headers,
)
if result.get('message') != 'success' and result.get('code') != 'already-tracked':
_logger.error('Can\'t configure Hasura instance: %s', result)
raise HasuraError('Can\'t configure Hasura instance', result)

_logger.info('Hasura instance has been configured')

0 comments on commit 405db3f

Please sign in to comment.