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

Rework org perms again #5657

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
3 changes: 1 addition & 2 deletions temba/channels/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,8 +972,7 @@ def test_configuration(self):
config_url = reverse("channels.channel_configuration", args=[self.ex_channel.uuid])

# can't view configuration if not logged in
response = self.client.get(config_url)
self.assertLoginRedirect(response)
self.assertRequestDisallowed(config_url, [None, self.agent])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really gotta figure out a consistent behavior for when you hit an object specific URL for an object you don't have access to (but might by switching orgs). Here we're changing to a 404 from a login redirect.


self.login(self.admin)

Expand Down
3 changes: 1 addition & 2 deletions temba/contacts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,7 @@ def test_read(self, mr_mocks):

read_url = reverse("contacts.contact_read", args=[joe.uuid])

response = self.client.get(read_url)
self.assertLoginRedirect(response)
self.assertRequestDisallowed(read_url, [None, self.agent])

self.assertContentMenu(read_url, self.user, [])
self.assertContentMenu(read_url, self.editor, ["Edit", "Start Flow", "Open Ticket"])
Expand Down
2 changes: 1 addition & 1 deletion temba/orgs/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def derive_queryset(self, **kwargs):

# don't filter by org for staff users but let OrgObjPermsMixin provide a redirect
if not self.request.user.is_staff:
qs = qs.filter(org=self.request.org)
qs = qs.filter(org__users=self.request.user)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we were brave we could just not filter by org at all and trust OrgObjPerms to do its job


if hasattr(self.model, "is_active"):
qs = qs.filter(is_active=True)
Expand Down
67 changes: 34 additions & 33 deletions temba/orgs/views/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,54 +22,45 @@ class OrgPermsMixin:
def derive_org(self):
return self.request.org

def _check_org_perm(self, permission: str) -> tuple[bool, bool]:
def has_org_perm(self, permission: str) -> bool:
"""
Checks if the current user has the given permission for the current org and whether they have it by servicing.
Figures out if the current user has the given permission.
"""

org = self.derive_org()
user = self.request.user

# can't have an org perm without an org
if not org:
return False, False
return False

if user.is_anonymous:
return False, False

has_perm = user.has_org_perm(org, permission)

if not has_perm and user.is_staff:
return True, True

return has_perm, False
return False

def has_org_perm(self, permission: str) -> bool:
has_perm, by_servicing = self._check_org_perm(permission)
return has_perm
return user.is_staff or user.has_org_perm(org, permission)

def has_permission(self, request, *args, **kwargs) -> bool:
"""
Figures out if the current user has permissions for this view.
Figures out if the current user has the required permission for this view.
"""

self.kwargs = kwargs
self.args = args
self.request = request

has_perm, by_servicing = self._check_org_perm(self.permission)

# by default staff are only allowed to GET views when servicing
if self.readonly_servicing and by_servicing and request.method != "GET":
return False

return has_perm
return self.has_org_perm(self.permission)

def dispatch(self, request, *args, **kwargs):
# non admin authenticated users without orgs get the org chooser
org = self.derive_org()
user = self.request.user
if user.is_authenticated and not user.is_staff:
if not self.derive_org():

if org:
# when servicing, non-superuser staff can only GET
is_servicing = request.user.is_staff and not org.users.filter(id=request.user.id).exists()
if is_servicing and not request.user.is_superuser and request.method != "GET":
return HttpResponseForbidden()
else:
if user.is_authenticated and not user.is_staff:
return HttpResponseRedirect(reverse("orgs.org_choose"))

return super().dispatch(request, *args, **kwargs)
Expand All @@ -86,20 +77,30 @@ def get_object_org(self):
def has_permission(self, request, *args, **kwargs) -> bool:
has_perm = super().has_permission(request, *args, **kwargs)

# allow staff to GET any object because they'll be redirected on org mismatch
if self.request.user.is_staff and self.request.method == "GET":
# even without a current org, staff can switch to the object's org
if request.user.is_staff:
return True

return has_perm and self.request.org == self.get_object_org()
if not has_perm:
return False

obj_org = self.get_object_org()
is_this_org = self.request.org == obj_org

return has_perm and (is_this_org or obj_org.get_users().filter(id=request.user.id).exists())

def pre_process(self, request, *args, **kwargs):
org = self.get_object_org()

# staff users are redirected to service page if org doesn't match
if request.user.is_staff and self.request.org != org:
return HttpResponseRedirect(
f"{reverse('staff.org_service')}?next={quote_plus(request.path)}&other_org={org.id}"
)
if self.request.org != org:
if request.user.is_staff:
# staff users are redirected to service page if org doesn't match
return HttpResponseRedirect(
f"{reverse('staff.org_service')}?next={quote_plus(request.path)}&other_org={org.id}"
)
else:
# TODO implement view to let regular users switch orgs as well
return HttpResponseRedirect(reverse("orgs.org_choose"))

return super().pre_process(request, *args, **kwargs)

Expand Down
14 changes: 10 additions & 4 deletions temba/orgs/views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_has_permission(self):
self.assertEqual(200, self.client.get(create_url).status_code)

# staff still can't POST
self.assertLoginRedirect(self.client.post(create_url, {"name": "Sales"}))
self.assertEqual(403, self.client.post(create_url, {"name": "Sales"}).status_code)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericnewcomer ok so this is changing again.. but I think this is safer. You POST as staff, the request just fails. No redirecting to login and then maybe back and wondering what happened with the original form submission.


# but superuser can
self.customer_support.is_superuser = True
Expand All @@ -64,9 +64,10 @@ def test_has_permission(self):
self.assertEqual(200, self.client.get(create_url).status_code)
self.assertRedirect(self.client.post(create_url, {"name": "Support"}), "hide")

def test_org_obj_perms_mixin(self):
def test_obj_perms_mixin(self):
contact1 = self.create_contact("Bob", phone="+18001234567", org=self.org)
contact2 = self.create_contact("Zob", phone="+18001234567", org=self.org2)
self.org2.add_user(self.admin, OrgRole.ADMINISTRATOR)

contact1_url = reverse("contacts.contact_update", args=[contact1.id])
contact2_url = reverse("contacts.contact_update", args=[contact2.id])
Expand All @@ -80,11 +81,16 @@ def test_org_obj_perms_mixin(self):
self.assertLoginRedirect(self.client.get(contact1_url))
self.assertLoginRedirect(self.client.get(contact2_url))

# editor role does have access tho.. when the URL is for a group in their org
# editor does have access tho.. when the URL is for a contact in their org
self.login(self.editor)
self.assertEqual(200, self.client.get(contact1_url).status_code)
self.assertLoginRedirect(self.client.get(contact2_url))

# admin belongs to both orgs
self.login(self.admin, choose_org=self.org)
self.assertEqual(200, self.client.get(contact1_url).status_code)
self.assertRedirect(self.client.get(contact2_url), reverse("orgs.org_choose"))

# staff can't access without org
self.login(self.customer_support)
self.assertRedirect(self.client.get(contact1_url), "/staff/org/service/")
Expand All @@ -94,7 +100,7 @@ def test_org_obj_perms_mixin(self):
self.assertRedirect(self.client.get(contact2_url), "/staff/org/service/") # wrong org

# staff still can't POST
self.assertLoginRedirect(self.client.post(contact1_url, {"name": "Bob"}))
self.assertEqual(403, self.client.post(contact1_url, {"name": "Bob"}).status_code)
self.assertRedirect(self.client.get(contact2_url), "/staff/org/service/")


Expand Down
2 changes: 1 addition & 1 deletion temba/staff/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def test_service(self, mr_mocks):
response = self.client.post(
reverse("contacts.contact_create"), data={"name": "Ben Haggerty", "phone": "0788123123"}
)
self.assertLoginRedirect(response)
self.assertEqual(403, response.status_code)

# become super user
self.customer_support.is_superuser = True
Expand Down
Loading