-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat(project-tree): rbac #30933
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
feat(project-tree): rbac #30933
Conversation
posthog/rbac/user_access_control.py
Outdated
|
||
# 3) Annotate the queryset with 'effective_access_level'. | ||
queryset = queryset.annotate( | ||
effective_access_level=Coalesce( |
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.
In the new access control system we aren't using effective_access_level
, any uses of it are legacy. In this case, I see why you did it because it's not the exact resource but a pointer to it. Just wanted to call that out.
posthog/rbac/user_access_control.py
Outdated
|
||
# 4) Exclude items that have 'none' (0) unless user is the creator | ||
# i.e. skip items where effective_access_level=0 AND created_by != user | ||
queryset = queryset.exclude(Q(effective_access_level=0) & ~Q(created_by=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.
I see why you're mapping these to integers for easy filtering, but we tried to avoid using numbers because adding new access levels would make this hard to change.
We have the notion of ordered_access_levels
, which was meant to solve this.
Could we instead annotate the access control enum and exclude where it's "none" to be more explicit?
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.
done
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.
PR Summary
This PR integrates RBAC into the file system API by filtering querysets based on user access control and updating outcomes for endpoints like "unfiled".
- Modified
/posthog/api/file_system.py
to apply RBAC filtering and prevent users from accessing unauthorized file system objects. - Introduced and utilized
filter_and_annotate_file_system_queryset
in/posthog/rbac/user_access_control.py
. - Updated tests in
/posthog/api/test/test_file_system.py
to assert RBAC restrictions and change the "unfiled" endpoint to return only a count. - Extended RBAC tests in
/posthog/rbac/test/test_user_access_control.py
to cover new file system scenarios.
4 file(s) reviewed, 2 comment(s)
Edit PR Review Bot Settings | Greptile
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.
Just comments, no blockers
FileSystem.objects.filter(team=self.team, path__startswith=f"{instance.path}/").delete() | ||
qs = FileSystem.objects.filter(team=self.team, path__startswith=f"{instance.path}/") | ||
if self.user_access_control: | ||
qs = self.user_access_control.filter_and_annotate_file_system_queryset(qs) |
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.
Interesting - what happens to things in it where they don't have access? This is just deleting the file system not the actual resource, right?
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, currently deletes are only at the filesystem level. It'll probably remain like this for a bit (as I'll be off for a week now), though the plan is to move it to the actual object level. If you wouldn't have access, those files would just linger on. Someone else with access will then see the folder with whatever is left.
@@ -444,6 +446,74 @@ def filter_queryset_by_access_level(self, queryset: QuerySet, include_all_if_adm | |||
|
|||
return queryset | |||
|
|||
def filter_and_annotate_file_system_queryset(self, queryset: QuerySet["FileSystem"]) -> QuerySet["FileSystem"]: |
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.
You're creative 😉
This looks good to me
@@ -138,12 +139,18 @@ def safely_get_queryset(self, queryset: QuerySet) -> QuerySet: | |||
if self.action == "list": | |||
queryset = queryset.order_by("path") | |||
|
|||
if self.user_access_control: | |||
queryset = self.user_access_control.filter_and_annotate_file_system_queryset(queryset) |
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.
These changes in the viewset look good but I do wonder if the access control filter should be moved into the model manager so you don't have to think about it. Something to ponder. I just worry if it needs to be used in every location then it could get forgotten.
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 do wonder the same. I just added a count_by_path
endpoint, and had to now merge with master and add the access control check manually.
I did think about making this check automatic, but decided against it because I wasn't 100% sure of the Django API here and of all the places to add it. I was afraid of accidentally missing something, and causing leakage tha way. I guess this is good enough for now?
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.
Yeah good for now but worth relooking at.
Bascially just add a custom manager to the FileSystem model that overwrites the queryset. The tricky part would be making sure the manager has access to the user access control which it might not be possible.
Quick psuedo code:
class FileSystemManager(RootTeamManager):
def get_queryset(self):
qs = super().get_queryset()
qs = self.user_access_control.filter_and_annotate_file_system_queryset(qs)
return qs
class FileSystem(models.Model):
objects = FileSystemManager()
...
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.
Thanks! 🙌
FileSystem.objects.filter(team=self.team, path__startswith=f"{instance.path}/").delete() | ||
qs = FileSystem.objects.filter(team=self.team, path__startswith=f"{instance.path}/") | ||
if self.user_access_control: | ||
qs = self.user_access_control.filter_and_annotate_file_system_queryset(qs) |
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, currently deletes are only at the filesystem level. It'll probably remain like this for a bit (as I'll be off for a week now), though the plan is to move it to the actual object level. If you wouldn't have access, those files would just linger on. Someone else with access will then see the folder with whatever is left.
@@ -138,12 +139,18 @@ def safely_get_queryset(self, queryset: QuerySet) -> QuerySet: | |||
if self.action == "list": | |||
queryset = queryset.order_by("path") | |||
|
|||
if self.user_access_control: | |||
queryset = self.user_access_control.filter_and_annotate_file_system_queryset(queryset) |
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 do wonder the same. I just added a count_by_path
endpoint, and had to now merge with master and add the access control check manually.
I did think about making this check automatic, but decided against it because I wasn't 100% sure of the Django API here and of all the places to add it. I was afraid of accidentally missing something, and causing leakage tha way. I guess this is good enough for now?
Problem
The file system doesn't support role based access controls.
Changes
Small caveat
Caveat: I understand currently not all types are implemented for access control, which includes hog functions... However if/when implemented, the
hog_function
resource types won't work out of the box. The file system stores the type as"hog_function/destination"
, whereas I assume access controls will just store"hog_function"
. We might want to limit access per hog function type... or we might want to refactor the file system to add a "subtype" field. I'm not sure, but I don't see anything really blocking here...How did you test this code?
Just with unit tests until now.