|
7 | 7 | import pytz |
8 | 8 | from sqlalchemy import UniqueConstraint, and_, cast, distinct, func, or_ |
9 | 9 | from sqlalchemy.dialects.postgresql import ARRAY, DOUBLE_PRECISION, JSONB |
| 10 | +from flask_login import current_user |
10 | 11 | from sqlalchemy.event import listens_for |
11 | 12 | from sqlalchemy.ext.hybrid import hybrid_property |
12 | 13 | from sqlalchemy.orm import ( |
|
36 | 37 | gfk_type, |
37 | 38 | key_type, |
38 | 39 | primary_key, |
| 40 | + BaseQuery, |
39 | 41 | ) |
40 | 42 | from redash.models.changes import Change, ChangeTrackingMixin # noqa |
41 | 43 | from redash.models.mixins import BelongsToOrgMixin, TimestampMixin |
@@ -346,8 +348,11 @@ def unused(cls, days=7): |
346 | 348 | ) |
347 | 349 |
|
348 | 350 | @classmethod |
349 | | - def get_latest(cls, data_source, query, max_age=0): |
350 | | - query_hash = gen_query_hash(query) |
| 351 | + def get_latest(cls, data_source, query, max_age=0, is_hash=False): |
| 352 | + if is_hash: |
| 353 | + query_hash = query |
| 354 | + else: |
| 355 | + query_hash = gen_query_hash(query) |
351 | 356 |
|
352 | 357 | if max_age == -1 and settings.QUERY_RESULTS_EXPIRED_TTL_ENABLED: |
353 | 358 | max_age = settings.QUERY_RESULTS_EXPIRED_TTL |
@@ -391,6 +396,35 @@ def groups(self): |
391 | 396 | return self.data_source.groups |
392 | 397 |
|
393 | 398 |
|
| 399 | +@listens_for(BaseQuery, "before_compile", retval=True) |
| 400 | +def prefilter_query_results(query): |
| 401 | + """ |
| 402 | + Ensure that a user with a db_role defined can only see QueryResults that |
| 403 | + they themselves created. |
| 404 | +
|
| 405 | + This is to ensure that they don't see results that might include resources |
| 406 | + from accounts that shouldn't be visibile to them. Ideally, this would use |
| 407 | + `set role` and the RLS policy that is applied to the table, but without a |
| 408 | + "post-query" type event, that's not really feasible. |
| 409 | +
|
| 410 | + The RLS policy on the redash.query_results table still applies to the |
| 411 | + arbitrary query that the user executes, so that they can't issue a query |
| 412 | + directly against that table and get around this check. |
| 413 | + """ |
| 414 | + for desc in query.column_descriptions: |
| 415 | + if desc['type'] is QueryResult: |
| 416 | + db_role = getattr(current_user, "db_role", None) |
| 417 | + if not db_role: |
| 418 | + continue |
| 419 | + limit = query._limit |
| 420 | + offset = query._offset |
| 421 | + query = query.limit(None).offset(None) |
| 422 | + query.offset(None) |
| 423 | + query = query.filter(desc['entity'].db_role == db_role) |
| 424 | + query = query.limit(limit).offset(offset) |
| 425 | + return query |
| 426 | + |
| 427 | + |
394 | 428 | def should_schedule_next(previous_iteration, now, interval, time=None, day_of_week=None, failures=0): |
395 | 429 | # if previous_iteration is None, it means the query has never been run before |
396 | 430 | # so we should schedule it immediately |
|
0 commit comments