Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix unprivileged access to views in Hasura #73

Merged
merged 4 commits into from
Jun 24, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')