-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Nest query ACL to dropdowns #3544
Conversation
}); | ||
}; | ||
|
||
if (this.props.parentQueryId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't feel right to have this logic here and pass around the parentQueryId
. How about we have a dropdownValues
function on the Query
instance. When it's a new query it will use the unsafe API and when it's not, it won't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and pass this function into this component)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gosh, at first it felt like a "d'oh! how didn't I do this before?", but digging into this, I kinda feel that while putting this function on Query
feels like a more natural place for this logic, it will probably look uglier because of the multiple layers of function signature that will have to carry this function across in order to get to QueryBasedParameterInput
. (that is - QueryBasedParameterInput
props, ParameterValueInput
props, parameter-value-input
directive, parameters
directive and query
page)
Let me know which you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it the same amount of effort to pass around the Query object as passing around the parentQueryId? What's the difference?
Also, what happens on dashboard level parameters? We will pick one of the queries to serve as a parent? 🤔
I wonder if there is another way to approach this.
redash/handlers/queries.py
Outdated
@@ -171,6 +171,12 @@ def get(self): | |||
|
|||
return response | |||
|
|||
def require_access_to_dropdown_queries(user, query_def): | |||
parameters = query_def.get('options', {}).get('parameters', []) | |||
dropdown_queries = [models.Query.get_by_id(p["queryId"]) for p in parameters if p["type"] == "query"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will execute N queries. Better to rewrite in a way that will execute a single query.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was actually using query.in_
before (which is a single query), but it didn't fail on queries outside the range (and that led to a situation in which you could have specified a non-existing query id, which may exist later on). Given that most queries have a very small number dropdowns, I felt its best not to handle it and keep the code clean.
I guess I could find another way of writing it in a single query, I just didn't see it as that important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. We can verify that all the ids passed in were returned in the database response?
While this is generally not a big deal, these things tend to accumulate. Considering this will run on every query save, it can be an annoyance. If the fix isn't that complex, worth handling.
Also, please add a test case that verifies you can't pass in non existing query ids.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
b4fca74 does the trick. Let me know if you find it better or worse.
redash/handlers/queries.py
Outdated
dropdown_queries = [models.Query.get_by_id(p["queryId"]) for p in parameters if p["type"] == "query"] | ||
|
||
for query in dropdown_queries: | ||
require_access(query.data_source.groups, user, view_only) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will trigger N+1 queries for data sources and groups. :| Not sure if there is a pretty way around it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also handled in b4fca74
@@ -310,6 +317,7 @@ def post(self, query_id): | |||
query_def = request.get_json(force=True) | |||
|
|||
require_object_modify_permission(query, self.current_user) | |||
require_access_to_dropdown_queries(self.current_user, query_def) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should do this only if there was a change to the dropdown queries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also handled in b4fca74
redash/utils/parameterized_query.py
Outdated
@@ -16,19 +16,22 @@ def _pluck_name_and_value(default_column, row): | |||
return {"name": row[name_column], "value": row[value_column]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(unrelated to this line)
I think this class should move into redash.models
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
}] | ||
} | ||
|
||
rv = self.make_request('post', '/api/queries/{0}'.format(my_query.id), data={'options': options}, user=self.factory.user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth adding similar test for creation of a query, because it's a different code path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redash/handlers/queries.py
Outdated
dropdown_query_ids = [str(p['queryId']) for p in parameters if p['type'] == 'query'] | ||
|
||
if dropdown_query_ids: | ||
groups = models.db.session.execute('select group_id, view_only from queries join data_source_groups on queries.data_source_id = data_source_groups.data_source_id where queries.id in ({})'.format(','.join(dropdown_query_ids))).fetchall() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The concept is definitely better than previously: we run a single query and don't have to do N+1 queries twice. 👌
But the implementation has a few issues --
- All direct queries should be in
redash.models
. Outside ofredash.models
we should use an API exposed by the models. - By building a query on your own, you open this to SQL injections. I'm pretty sure
session.execute
can take a parameterized query and if not there is a similar API that can do this. - Why not use SQLAlchemy's models to build this query? I know it's more effort at first due to the learning curve, but it's more consistent with the rest of the code and has other benefits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Would you feel better with this hanging off a
Query
class method? - You're right, should have used params.
- I saw this coming, but decided to push a quicker fix to hear your thoughts, specifically because
If the fix isn't that complex, worth handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Yes. Something like
Query.get_groups_for_query_ids
? - 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish there wasn't so much nesting but at least it's just a primitive value.
@kravets-levko maybe we can utilize global state here?
@ranbena I doubt global state is the solution here as we’re not dealing with a global thing. There are a few possible approaches here, but after review with @rauchy to go with the current implementation until we remove Angular from the upper layers (Parameters component and the query and dashboard pages). Then we can revisit with a better approach. |
if (this.props.queryId === queryId) { | ||
this.setState({ options, loading: false }); | ||
const options = await this.props.parameter.loadDropdownValues(); | ||
if (this.props.queryId === queryId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I think that async/await are amazing, in this case they're hiding very important thing. This condition is needed because while we load dropdown values, component may receive new queryId - so we should drop outdated results. With .then(...)
it's very clear that we have async operation; but with await
this fragment looks linear, and this condition looks confusing (but it's really important). That's my opinion. @arikfr, @ranbena - WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just adding a comment // stale queryId check
would make it clear for both patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that's the case, I'd favor await
over Promises. Not sure why we haven't begun embracing await
s elsewhere in the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why we haven't begun embracing awaits elsewhere in the code?
We try to leave things for future refactors 😎
but with await this fragment looks linear, and this condition looks confusing
But await
itself signals that this is async operation. Maybe it's just confusing because we're less used to using it?
Let's give it a try and see how goes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@arikfr I'm not against of using async
/await
- just wanted to say that we need somehow indicate non-obvious/confusing code (like in this case). Comment is 100% enough. Also, @rauchy - can you please check that babel does not transpile async
/await
- since we don't need to support IE, we can use that syntax natively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we care if the transpiled version is using async/await natively? Feels meaningless to me but I may be missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took care of it #3609
ba7859d
to
6ad4424
Compare
* change API to /api/queries/:id/dropdowns/:dropdown_id * extract property * split to 2 different dropdown endpoints and implement the second * make access control optional for dropdowns (assuming it is verified at a different level) * add test cases for /api/queries/:id/dropdowns/:id * use new /dropdowns endpoint in frontend * require access to dropdown queries when creating or updating parent queries * rename Query resource dropdown endpoints * check access to dropdown query associations in one fugly query * move ParameterizedQuery to models folder * add dropdown association tests to query creation * move group by query ids query into models.Query * use bound parameters for groups query * format groups query * use new associatedDropdowns endpoint in dashboards * pass down parameter and let it return dropdown options. Go Levko! * change API to /api/queries/:id/dropdowns/:dropdown_id * split to 2 different dropdown endpoints and implement the second * use new /dropdowns endpoint in frontend * pass down parameter and let it return dropdown options. Go Levko! * fix bad rebase * add comment to clarify the purpose of checking the queryId
* change API to /api/queries/:id/dropdowns/:dropdown_id * extract property * split to 2 different dropdown endpoints and implement the second * make access control optional for dropdowns (assuming it is verified at a different level) * add test cases for /api/queries/:id/dropdowns/:id * use new /dropdowns endpoint in frontend * require access to dropdown queries when creating or updating parent queries * rename Query resource dropdown endpoints * check access to dropdown query associations in one fugly query * move ParameterizedQuery to models folder * add dropdown association tests to query creation * move group by query ids query into models.Query * use bound parameters for groups query * format groups query * use new associatedDropdowns endpoint in dashboards * pass down parameter and let it return dropdown options. Go Levko! * change API to /api/queries/:id/dropdowns/:dropdown_id * split to 2 different dropdown endpoints and implement the second * use new /dropdowns endpoint in frontend * pass down parameter and let it return dropdown options. Go Levko! * fix bad rebase * add comment to clarify the purpose of checking the queryId
What type of PR is this? (check all applicable)
Description
Dropdown queries are currently available through
/api/queries/:id/dropdown
. This works fine for users who create queries themselves and share it within the same group, but once such a dropdown query is to be accessed by someone else (for example, anonymous embed users), then theapi_key
used for the share wouldn't provide access to the dropdown query.This PR adds another dropdown endpoint (
/api/queries/:parent_id/dropdowns/:id
) which will check access to the parent query (the parent query is the query which uses values from the dropdown as parameters), and if the user has access to the parent query, he will have access to all associated dropdown queries. The reasoning behind creating a second endpoint for this scenario is that it requires a parent query, but when you are in the process of creating a query, it is not yet persisted so there is no parent query to inherit access from.Related Tickets & Documents
Addresses #3467
Mobile & Desktop Screenshots/Recordings (if there are UI changes)