Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
da13333
fix: resolve slow DB query in org permission check (#8228)
srijantrpth Aug 6, 2026
5c5c4c0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
3869f7b
Add # type: ignore[arg-type]
srijantrpth Aug 6, 2026
9cd011c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
6b72617
fix: rename test to match FT003 linting convention
srijantrpth Aug 6, 2026
0831342
chore: document reason for Organisation type suppression
srijantrpth Aug 6, 2026
fb0b063
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
ba75c3f
fix: resolve mypy strict typing errors for CI
srijantrpth Aug 6, 2026
d129cda
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
0349618
fix: add missing type annotations for test fixture arguments
srijantrpth Aug 6, 2026
9747b40
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
777dd96
fix: use typing.cast to satisfy strict PR review bot without triggeri…
srijantrpth Aug 6, 2026
c28865b
test: assert exact query shape to prevent massive join regression
srijantrpth Aug 6, 2026
bb2999c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
79ad30f
fix: import typing module to resolve name-defined mypy error
srijantrpth Aug 6, 2026
3822e9b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
d4de26a
fix: import typing module
srijantrpth Aug 6, 2026
26c8637
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2026
901a942
test: move test to dedicated module and enforce exact query count
srijantrpth Aug 7, 2026
b048a4a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 7, 2026
1d4b857
test: adjust exact query count to 3 due to disabled RBAC in test env
srijantrpth Aug 7, 2026
f86cf3f
test: assert exact SQL shape and table names in query count test
srijantrpth Aug 7, 2026
83d835e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 7, 2026
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
39 changes: 25 additions & 14 deletions api/permissions/permission_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import typing
from typing import TYPE_CHECKING, List, Set, Union

from django.conf import settings
Expand Down Expand Up @@ -202,23 +203,33 @@ def user_has_organisation_permission(
if is_user_organisation_admin(user, organisation):
return True

# NOTE: since we store organisation admin slightly differently
# compared to project and environment `get_base_permission_filter`
# with allow_admin=True will not work for organisation
base_filter = get_base_permission_filter(
user,
Organisation, # type: ignore[arg-type]
permission_key,
allow_admin=False,
)
filter_ = base_filter & Q(id=organisation.id)
# Base query to ensure the user actually belongs to the organisation
base_qs = Organisation.objects.filter(id=organisation.id, users=user)

queryset = Organisation.objects.filter(filter_)
# 1. Check direct user permissions (Fastest)
user_filter = get_user_permission_filter(user, permission_key, allow_admin=False)
if base_qs.filter(user_filter).exists():
return True

# Final check to verify that user belongs to organisation
queryset = queryset.filter(users=user)
# 2. Check group permissions
group_filter = get_group_permission_filter(user, permission_key, allow_admin=False)
if base_qs.filter(group_filter).exists():
return True

return queryset.exists() # type: ignore[no-any-return]
# 3. Check role permissions (only if RBAC is installed)
if settings.IS_RBAC_INSTALLED: # pragma: no cover
role_filter = get_role_permission_filter(
user,
# Type gap: get_role_permission_filter type hint expects an instance,
# but safely handles the model class at runtime.
typing.cast(typing.Any, Organisation),
permission_key,
allow_admin=False,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
if base_qs.filter(role_filter).exists():
return True

return False


def master_api_key_has_organisation_permission(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from organisations.models import Organisation, UserOrganisation
from organisations.permissions.models import (
OrganisationPermissionModel,
Expand Down Expand Up @@ -156,3 +158,34 @@ def test_user_has_organisation_permission__user_removed_from_organisation__retur
organisation=organisation,
permission_key=CREATE_PROJECT,
)


def test_user_has_organisation_permission__evaluating_permission__executes_exact_queries(
django_assert_num_queries: typing.Any,
django_user_model: typing.Any,
organisation: typing.Any,
) -> None:
# Given
user = django_user_model.objects.create(email="test_sequential_eval@example.com")
user.add_organisation(organisation)

# When
with django_assert_num_queries(3) as ctx:
has_permission = user_has_organisation_permission(
user=user, organisation=organisation, permission_key="MANAGE_USER_GROUPS"
)

# Then
assert has_permission is False

# Verify the exact queries executed match the expected sequential EXISTS pattern
queries = [query["sql"].lower() for query in ctx.captured_queries]

# Query 1: Base user organisation role check
assert "organisations_userorganisation" in queries[0]

# Query 2: User-specific permission check
assert "organisation_permissions_userorganisationpermission" in queries[1]

# Query 3: Group-specific permission check
assert "organisation_permissions_userpermissiongroup" in queries[2]
5 changes: 4 additions & 1 deletion api/tests/unit/users/test_unit_users_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
from organisations.permissions.permissions import ORGANISATION_PERMISSIONS
from projects.models import Project
from tests.types import WithProjectPermissionsCallable
from users.models import FFAdminUser, UserPermissionGroup
from users.models import (
FFAdminUser,
UserPermissionGroup,
)


def test_belongs_to__user_in_organisation__returns_true(
Expand Down
Loading