Skip to content

Commit 405db3f

Browse files
Fix unprivileged access to views in Hasura (#73)
1 parent a14fc94 commit 405db3f

File tree

1 file changed

+36
-38
lines changed

1 file changed

+36
-38
lines changed

src/dipdup/hasura.py

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
_logger = logging.getLogger(__name__)
1818

1919

20+
class HasuraError(RuntimeError):
21+
...
22+
23+
2024
def _is_model_class(obj) -> bool:
2125
"""Is subclass of tortoise.Model, but not the base class"""
2226
return isinstance(obj, type) and issubclass(obj, Model) and obj != Model and not getattr(obj.Meta, 'abstract', False)
@@ -51,11 +55,11 @@ def _format_object_relationship(name: str, column: str) -> Dict[str, Any]:
5155
}
5256

5357

54-
def _format_select_permissions(columns: List[str]) -> Dict[str, Any]:
58+
def _format_select_permissions() -> Dict[str, Any]:
5559
return {
5660
"role": "user",
5761
"permission": {
58-
"columns": sorted(columns),
62+
"columns": "*",
5963
"filter": {},
6064
"allow_aggregations": True,
6165
},
@@ -90,11 +94,13 @@ def _iter_models(*modules) -> Iterator[Tuple[str, Type[Model]]]:
9094
yield app, model
9195

9296

93-
async def generate_hasura_metadata(config: DipDupConfig) -> Dict[str, Any]:
97+
async def generate_hasura_metadata(config: DipDupConfig, views: List[str]) -> Dict[str, Any]:
9498
"""Generate metadata based on dapp models.
9599
96100
Includes tables and their relations (but not entities created during execution of snippets from `sql` package directory)
97101
"""
102+
if not isinstance(config.database, PostgresDatabaseConfig):
103+
raise RuntimeError
98104
_logger.info('Generating Hasura metadata')
99105
metadata_tables = {}
100106
model_tables = {}
@@ -105,24 +111,31 @@ async def generate_hasura_metadata(config: DipDupConfig) -> Dict[str, Any]:
105111
for app, model in _iter_models(models, int_models):
106112
table_name = model._meta.db_table or pascal_to_snake(model.__name__) # pylint: disable=protected-access
107113
model_tables[f'{app}.{model.__name__}'] = table_name
108-
109-
table = _format_table(
114+
metadata_tables[table_name] = _format_table(
110115
name=table_name,
111-
schema=config.database.schema_name if isinstance(config.database, PostgresDatabaseConfig) else 'public',
116+
schema=config.database.schema_name,
117+
)
118+
119+
for view in views:
120+
metadata_tables[view] = _format_table(
121+
name=view,
122+
schema=config.database.schema_name,
123+
)
124+
metadata_tables[view]['select_permissions'].append(
125+
_format_select_permissions(),
112126
)
113-
metadata_tables[table_name] = table
114127

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

118131
metadata_tables[table_name]['select_permissions'].append(
119-
_format_select_permissions(list(model._meta.db_fields)),
132+
_format_select_permissions(),
120133
)
121134

122135
for field in model._meta.fields_map.values():
123136
if isinstance(field, fields.relational.ForeignKeyFieldInstance):
124137
if not isinstance(field.related_name, str):
125-
raise Exception(f'`related_name` of `{field}` must be set')
138+
raise HasuraError(f'`related_name` of `{field}` must be set')
126139
related_table_name = model_tables[field.model_name]
127140
metadata_tables[table_name]['object_relationships'].append(
128141
_format_object_relationship(
@@ -135,7 +148,7 @@ async def generate_hasura_metadata(config: DipDupConfig) -> Dict[str, Any]:
135148
related_name=field.related_name,
136149
table=table_name,
137150
column=field.model_field_name + '_id',
138-
schema=config.database.schema_name if isinstance(config.database, PostgresDatabaseConfig) else 'public',
151+
schema=config.database.schema_name,
139152
)
140153
)
141154

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

153166
_logger.info('Configuring Hasura')
154167
url = config.hasura.url.rstrip("/")
155-
hasura_metadata = await generate_hasura_metadata(config)
168+
views = [
169+
row[0]
170+
for row in (
171+
await get_connection(None).execute_query(
172+
f"SELECT table_name FROM information_schema.views WHERE table_schema = '{config.database.schema_name}'"
173+
)
174+
)[1]
175+
]
176+
177+
hasura_metadata = await generate_hasura_metadata(config, views)
156178

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

169190
headers = {}
170191
if config.hasura.admin_secret:
@@ -178,7 +199,7 @@ async def configure_hasura(config: DipDupConfig):
178199
data=json.dumps(
179200
{
180201
"type": "export_metadata",
181-
"args": hasura_metadata,
202+
"args": {},
182203
},
183204
),
184205
headers=headers,
@@ -204,29 +225,6 @@ async def configure_hasura(config: DipDupConfig):
204225
headers=headers,
205226
)
206227
if result.get('message') != 'success':
207-
_logger.error('Can\'t configure Hasura instance: %s', result)
208-
return
209-
210-
views = await get_connection(None).execute_query(
211-
f"SELECT table_name FROM information_schema.views WHERE table_schema = '{config.database.schema_name}'"
212-
)
213-
for view in views[1]:
214-
result = await http_request(
215-
session,
216-
'post',
217-
url=f'{url}/v1/query',
218-
data=json.dumps(
219-
{
220-
"type": "add_existing_table_or_view",
221-
"args": {
222-
"name": view[0],
223-
"schema": config.database.schema_name,
224-
},
225-
},
226-
),
227-
headers=headers,
228-
)
229-
if result.get('message') != 'success' and result.get('code') != 'already-tracked':
230-
_logger.error('Can\'t configure Hasura instance: %s', result)
228+
raise HasuraError('Can\'t configure Hasura instance', result)
231229

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

0 commit comments

Comments
 (0)