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

[RHCLOUD-36102] - Slow loading of data from roles endpoint perfo… #1354

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
710ff4b
[WIP][RHCLOUD-36102] - Slow loading of data from roles endpoint perfo…
EvanCasey13 Nov 29, 2024
3cd7d46
Roles filtering refactor
EvanCasey13 Dec 2, 2024
6849d7b
Roles endpoint filtering continued
EvanCasey13 Dec 3, 2024
cd1dbf0
Roles endpoint filtering - username query parameter handling
EvanCasey13 Dec 4, 2024
2c1caac
Roles endpoint - filtering addition complete
EvanCasey13 Dec 5, 2024
82f2c76
remove print statement from roles test
EvanCasey13 Dec 5, 2024
a221512
Roles endpoint filtering performance done
EvanCasey13 Dec 6, 2024
a49dc74
Pagination readded to roles endpoint
EvanCasey13 Dec 9, 2024
0c72049
Added prefetch_related to base_queryset for access and ext_relations
EvanCasey13 Dec 10, 2024
023ff1b
lint fix
EvanCasey13 Dec 10, 2024
6816159
Removed group_in and group_in_counts from role serializer, will add d…
EvanCasey13 Dec 11, 2024
3bc8ed4
added dynamic annotation and changed role serializer to groups_in field
EvanCasey13 Dec 19, 2024
4e6b58f
Changed to use get_role_queryset from querysets to handle scope/admin…
EvanCasey13 Jan 9, 2025
3d75490
Merge branch 'master' into role_endpoint_performance_improvements
EvanCasey13 Jan 9, 2025
7cce7af
role query reduction from 33 to 24 queries
EvanCasey13 Jan 14, 2025
eeb98de
reduction from to 6 queries for a request for roles endpoint from 33 …
EvanCasey13 Jan 14, 2025
b5a9c74
Merge branch 'master' into role_endpoint_performance_improvements
EvanCasey13 Jan 14, 2025
b8ef8fe
remove redundant default limit for common pagination#
EvanCasey13 Jan 14, 2025
0d9f7e5
Merge branch 'master' into role_endpoint_performance_improvements
EvanCasey13 Jan 30, 2025
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
4 changes: 4 additions & 0 deletions rbac/management/role/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class RoleSerializer(serializers.ModelSerializer):
access = AccessSerializer(many=True)
policyCount = serializers.IntegerField(read_only=True)
accessCount = serializers.IntegerField(read_only=True)
groups_in_count = serializers.IntegerField(read_only=True)
groups_in = serializers.ListField(read_only=True)
applications = serializers.SerializerMethodField()
system = serializers.BooleanField(read_only=True)
platform_default = serializers.BooleanField(read_only=True)
Expand All @@ -113,6 +115,8 @@ class Meta:
"access",
"policyCount",
"accessCount",
"groups_in_count",
"groups_in",
"applications",
"system",
"platform_default",
Expand Down
215 changes: 211 additions & 4 deletions rbac/management/role/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.db.models import F, Q
from django.db.models.aggregates import Count
from django.http import Http404
from django.utils.translation import gettext as _
Expand All @@ -34,6 +34,8 @@
from management.models import AuditLog, Permission
from management.notifications.notification_handlers import role_obj_change_notification_handler
from management.permissions import RoleAccessPermission
from management.principal.model import Principal
from management.principal.proxy import PrincipalProxy
from management.querysets import get_role_queryset, user_has_perm
from management.relation_replicator.relation_replicator import DualWriteException, ReplicationEventType
from management.role.relation_api_dual_write_handler import (
Expand All @@ -46,9 +48,10 @@
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response

from api.models import Tenant
from api.common.pagination import StandardResultsSetPagination
from api.models import Tenant, User
from rbac.env import ENVIRONMENT
from .model import Role
from .model import ExtTenant, Role
from .serializer import RoleSerializer

TESTING_APP = os.getenv("TESTING_APPLICATION")
Expand Down Expand Up @@ -141,6 +144,7 @@ class RoleViewSet(
filterset_class = RoleFilter
ordering_fields = ("name", "display_name", "modified", "policyCount")
ordering = ("name",)
default_limit = StandardResultsSetPagination.default_limit
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope thats redundant removed now, was from my testing of filtering and pagination stuff


def get_queryset(self):
"""Obtain queryset for requesting user based on access and action."""
Expand Down Expand Up @@ -324,7 +328,210 @@ def list(self, request, *args, **kwargs):
}

"""
return super().list(request=request, args=args, kwargs=kwargs)
public_tenant = Tenant.objects.get(tenant_name="public")
EvanCasey13 marked this conversation as resolved.
Show resolved Hide resolved
# Filtering
query_params = {
"external_tenant": request.query_params.get("external_tenant", None),
"system": request.query_params.get("system", None),
"application": request.query_params.get("application", None),
"display_name": request.query_params.get("display_name", None),
"name_match": request.query_params.get("name_match", None),
"permission": request.query_params.get("permission", None),
"name": request.query_params.get("name", None),
"platform_default": request.query_params.get("platform_default", None),
"admin_default": request.query_params.get("admin_default", None),
"username": request.query_params.get("username", None),
"limit": request.query_params.get("limit", 10),
"offset": request.query_params.get("offset", 0),
"add_fields": request.query_params.get("add_fields", ""),
}
add_fields = query_params["add_fields"]
add_fields_split = add_fields.split(",")

base_queryset = (
Role.objects.prefetch_related("access", "ext_relation")
.filter(tenant__in=[request.tenant, public_tenant])
.annotate(
policyCount=Count("policies", distinct=True),
accessCount=Count("access", distinct=True),
)
)

# Dynamic annotations
if "groups_in_count" in add_fields_split:
EvanCasey13 marked this conversation as resolved.
Show resolved Hide resolved
base_queryset = base_queryset.annotate(groups_in_count=Count("policies__group"))
elif "groups_in" in add_fields_split:
base_queryset = base_queryset.annotate(groups_in=F("policies__group__name"))
else:
base_queryset = base_queryset

filters = []
limit = query_params["limit"]
offset = int(query_params["offset"])
path = request.path

previous_offset = 0
if offset - limit > 0:
previous_offset = offset - limit

if query_params:
filtered_queryset = base_queryset
system_value = str(query_params["system"]).lower()

# Check if "add_fields" is a valid field
additional_fields = {"access", "groups_in", "groups_in_count"}
if query_params["add_fields"]:
split_fields = query_params["add_fields"].split(",")
invalid_field = [field for field in split_fields if field not in additional_fields]
if invalid_field:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": "Invalid additional field passed in query",
"source": "add_fields invalid parameter",
"status": status.HTTP_400_BAD_REQUEST,
}
]
},
)

# Username filter
if query_params["username"]:
princ = Principal.objects.filter(username=query_params["username"]).first()
if not princ:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": "Principal not found for this username",
"source": "Invalid username query parameter",
"status": status.HTTP_400_BAD_REQUEST,
}
]
},
)

proxy = PrincipalProxy
results = proxy.request_filtered_principals(
query_params["username"],
org_id=request.user.org_id,
)
results_exist = results["data"]
if results_exist:
principal = results["data"][0]

# Convert from principal to User Model to add to request object
org_id = principal.get("org_id")
user_id = principal.get("user_id")
username = principal.get("username")
account_number = principal.get("account_number")
is_active = principal.get("is_active")

# Map principal fields to User fields
user = User()
user.org_id = org_id
user.user_id = user_id
user.username = username
user.account = account_number
user.is_active = is_active
request.user_from_query = user

# External tenant filter
if query_params["external_tenant"]:
ext_tenant = ExtTenant.objects.get(name=query_params["external_tenant"])
if ext_tenant:
filters.append(Q(ext_relation__ext_tenant=ext_tenant))
# System value filter
if system_value == "false":
filters.append(Q(system=False))
elif system_value == "true":
filters.append(Q(system=True))
# Application filter
if query_params["application"]:
applications = query_params["application"].split(",")

external_tenant = ExtTenant.objects.filter(name=query_params["application"]).first()

filtered_queryset = filtered_queryset.filter(access__permission__application__in=applications)
# If a external tenant exists with the name passed to application query parameter
# return the roles for that external tenant
if external_tenant:
ext_tenant = ExtTenant.objects.get(name=query_params["application"])
filtered_queryset = base_queryset.filter(ext_relation__ext_tenant=ext_tenant).annotate(
external_tenant=F("ext_relation__ext_tenant__name")
)
# Display_name & name_match filter
if query_params["display_name"]:
if query_params["name_match"] == "partial":
filters.append(Q(display_name__contains=query_params["display_name"]))
elif query_params["name_match"] == "exact":
filters.append(Q(display_name__exact=query_params["display_name"]))
elif not query_params["name_match"]:
filters.append(Q(display_name__contains=query_params["display_name"]))
else:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": "Invalid name match value provided",
"source": "name_match query parameter",
"status": status.HTTP_400_BAD_REQUEST,
}
]
},
)
# name & name_match filter
if query_params["name"]:
if query_params["name_match"] == "partial":
filters.append(Q(name__contains=query_params["name"]))
elif query_params["name_match"] == "exact":
filters.append(Q(name__exact=query_params["name"]))
elif not query_params["name_match"]:
filters.append(Q(name__contains=query_params["name"]))
else:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": "Invalid name match value provided",
"source": "name_match query parameter",
"status": status.HTTP_400_BAD_REQUEST,
}
]
},
)
# Permission filter
if query_params["permission"]:
permissions = query_params["permission"].split(",")
filters.append(Q(access__permission__permission__in=permissions))

# Apply the filters
filtered_queryset = filtered_queryset.filter(*filters)

# Serialize the queryset for response
serializer = RoleSerializer(filtered_queryset, many=True, context={"request": request})

# Metadata
meta = {}
if query_params.get("username"):
meta["count"] = sum(len(group.roles()) for group in princ.group.all())
else:
meta["count"] = filtered_queryset.count()

# Pagination
links = {
"first": f"{path}?limit={limit}&offset=0",
"next": f"{path}?limit={limit}&offset={offset + limit}",
"previous": f"{path}?limit={limit}&offset={previous_offset}",
"last": f"{path}?limit={limit}&offset={meta['count'] - limit if (meta['count'] - limit) >= 0 else 0}",
}

return Response({"meta": meta, "links": links, "data": serializer.data})

def retrieve(self, request, *args, **kwargs):
"""Get a role.
Expand Down
1 change: 1 addition & 0 deletions tests/internal/integration/test_integration_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def test_roles_for_org(self, mock_request):
**self.request.META,
follow=True,
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get("meta").get("count"), 1)

Expand Down
10 changes: 7 additions & 3 deletions tests/management/role/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def setUp(self):
"display_name",
"system",
"created",
"access",
"policyCount",
"accessCount",
"modified",
Expand Down Expand Up @@ -663,6 +664,7 @@ def test_read_role_list_success(self):
self.assertIsNotNone(iterRole.get("name"))
# fields displayed are same as defined
self.assertEqual(self.display_fields, set(iterRole.keys()))

if iterRole.get("name") == role_name:
self.assertEqual(iterRole.get("accessCount"), 2)
role = iterRole
Expand All @@ -674,6 +676,7 @@ def test_get_role_by_application_single(self):
url = "{}?application={}".format(URL, "app")
client = APIClient()
response = client.get(url, **self.headers)

self.assertEqual(response.data.get("meta").get("count"), 1)
self.assertEqual(response.data.get("data")[0].get("name"), self.defRole.name)

Expand All @@ -682,6 +685,7 @@ def test_get_role_by_application_using_ext_tenant(self):
url = "{}?application={}".format(URL, "foo")
client = APIClient()
response = client.get(url, **self.headers)

self.assertEqual(response.data.get("meta").get("count"), 1)
self.assertEqual(response.data.get("data")[0].get("name"), self.defRole.name)

Expand Down Expand Up @@ -1112,6 +1116,7 @@ def test_list_role_with_groups_in_fields_for_admin_scope_success(self):

response_data = response.data.get("data")
for iterRole in response_data:
print(iterRole.get("groups_in"))
EvanCasey13 marked this conversation as resolved.
Show resolved Hide resolved
# fields displayed are same as defined, groupsInCount is added
self.assertEqual(new_display_fields, set(iterRole.keys()))
self.assertIsNotNone(iterRole.get("groups_in")[0]["name"])
Expand Down Expand Up @@ -1183,7 +1188,6 @@ def test_list_role_with_additional_fields_username_success(self, mock_request):
url = "{}?add_fields={},{}&username={}".format(URL, field_1, field_2, self.principal.username)
client = APIClient()
response = client.get(url, **self.headers)

self.assertEqual(len(response.data.get("data")), 5)

role = response.data.get("data")[0]
Expand All @@ -1205,6 +1209,7 @@ def test_list_role_with_additional_fields_principal_success(self):
self.assertEqual(len(response.data.get("data")), 5)

role = response.data.get("data")[0]
print(role)
self.assertEqual(new_display_fields, set(role.keys()))
self.assertEqual(role["groups_in_count"], 1)

Expand Down Expand Up @@ -1732,7 +1737,6 @@ def test_external_tenant_filter(self):
response = client.get(URL, **self.headers)

self.assertEqual(len(response.data.get("data")), 5)

url = f"{URL}?external_tenant=foo"
client = APIClient()
response = client.get(url, **self.headers)
Expand All @@ -1751,7 +1755,6 @@ def test_list_role_admin_platform_default_groups(self):
url = f"{URL}?display_name=platform_admin_default_display&add_fields=groups_in_count%2Cgroups_in"
client = APIClient()
response = client.get(url, **self.headers)

self.assertEqual(len(response.data.get("data")), 1)
role = response.data.get("data")[0]
self.assertEqual(role.get("groups_in_count"), 2)
Expand Down Expand Up @@ -1954,6 +1957,7 @@ def test_list_roles_with_User_Access_Admin_success(self):
response = client.get(url, **self.headers_user_based_principal)
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_count = self.system_roles_count + self.non_system_roles_count + 1

self.assertEqual(len(response.data.get("data")), expected_count)

response = client.get(url, **self.headers_service_account_principal)
Expand Down
Loading