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

Cache public tenant #1368

Open
wants to merge 4 commits into
base: master
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
5 changes: 5 additions & 0 deletions deploy/rbac-clowdapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,8 @@ objects:
value: ${V2_READ_ONLY_API_MODE}
- name: READ_ONLY_API_MODE
value: ${READ_ONLY_API_MODE}
- name: PUBLIC_TENANT_CACHE_LIFETIME
value: ${PUBLIC_TENANT_CACHE_LIFETIME}

jobs:
- name: tenant-org-id-populator
Expand Down Expand Up @@ -1205,3 +1207,6 @@ parameters:
- name: USER_ID_POPULATOR_BATCH_SIZE
description: The number of user records to process in each batch
value: '90'
- name: PUBLIC_TENANT_CACHE_LIFETIME
description: TTL for the public tenant cache in seconds
value: '86400'
2 changes: 1 addition & 1 deletion rbac/api/cross_access/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def validate_and_format_patch_input(self, request_data):

def format_roles(self, roles):
"""Format role list as expected for cross-account-request."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
for role_name in roles:
try:
role = Role.objects.get(tenant=public_tenant, display_name=role_name)
Expand Down
12 changes: 12 additions & 0 deletions rbac/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from django.db import models
from django.db.models import Q
from management.cache import PublicTenantCache

from api.cross_access.model import CrossAccountRequest # noqa: F401
from api.status.model import Status # noqa: F401
Expand All @@ -35,6 +36,15 @@ def modified_only(self):
.distinct()
)

def get_public_tenant(self):
"""Return the public tenant."""
cache = PublicTenantCache()
tenant = cache.get_tenant(Tenant.PUBLIC_TENANT_NAME)
if tenant is None:
tenant = self.get(tenant_name=Tenant.PUBLIC_TENANT_NAME)
cache.save_tenant(tenant)
return tenant


class Tenant(models.Model):
"""The model used to create a tenant schema."""
Expand All @@ -45,6 +55,8 @@ class Tenant(models.Model):
org_id = models.CharField(max_length=36, unique=True, default=None, db_index=True, null=True)
objects = TenantModifiedQuerySet.as_manager()

PUBLIC_TENANT_NAME = "public"

def __str__(self):
"""Get string representation of Tenant."""
return f"Tenant ({self.org_id})"
Expand Down
4 changes: 2 additions & 2 deletions rbac/internal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def invalid_default_admin_groups(request):
DELETE /_private/api/utils/invalid_default_admin_groups/
"""
logger.info(f"Invalid default admin groups: {request.method} {request.user.username}")
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
invalid_default_admin_groups_list = Group.objects.filter(
admin_default=True, system=False, platform_default=False
).exclude(tenant=public_tenant)
Expand Down Expand Up @@ -431,7 +431,7 @@ def role_removal(request):
)
role_name = escape(role_name)
# Add tenant public to prevent deletion of custom roles
role_obj = get_object_or_404(Role, name=role_name, tenant=Tenant.objects.get(tenant_name="public"))
role_obj = get_object_or_404(Role, name=role_name, tenant=Tenant.objects.get_public_tenant())
with transaction.atomic():
try:
logger.warning(f"Deleting role '{role_name}'. Requested by '{request.user.username}'")
Expand Down
14 changes: 14 additions & 0 deletions rbac/management/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ def delete_tenant(self, key):
super().delete_cached(key, "tenant")


class PublicTenantCache(TenantCache):
"""Redis-based caching of public tenant."""

def set_cache(self, pipe, key, item):
"""Override the method to set public tenant to cache."""
pipe.set(self.key_for(key), pickle.dumps(item))
pipe.expire(self.key_for(key), settings.PUBLIC_TENANT_CACHE_LIFETIME)
pipe.execute()

def save_tenant(self, tenant):
"""Write the public tenant to Redis."""
super().save(tenant.tenant_name, tenant, "tenant")


class AccessCache(BasicCache):
"""Redis-based caching of per-Principal per-app access policy.""" # noqa: D204

Expand Down
8 changes: 4 additions & 4 deletions rbac/management/group/definer.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

def seed_group() -> Tuple[Group, Group]:
"""Create or update default group."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
with transaction.atomic():
name = "Default access"
group_description = (
Expand Down Expand Up @@ -104,7 +104,7 @@ def clone_default_group_in_public_schema(group, tenant) -> Optional[Group]:
else:
group_uuid = uuid4()

public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
tenant_default_policy = group.policies.get(system=True)
group.name = "Custom default access"
group.system = False
Expand Down Expand Up @@ -148,7 +148,7 @@ def add_roles(group, roles_or_role_ids, tenant, user=None):
if system_policy_created:
logger.info(f"Created new system policy for tenant {tenant.org_id}.")

system_roles = roles.filter(tenant=Tenant.objects.get(tenant_name="public"))
system_roles = roles.filter(tenant=Tenant.objects.get_public_tenant())

# Custom roles are locked to prevent resources from being added/removed concurrently,
# in the case that the Roles had _no_ resources specified to begin with.
Expand Down Expand Up @@ -192,7 +192,7 @@ def remove_roles(group, roles_or_role_ids, tenant, user=None):
"""Process list of roles and remove them from the group."""
roles = _roles_by_query_or_ids(roles_or_role_ids)
group = Group.objects.get(name=group.name, tenant=tenant)
system_roles = roles.filter(tenant=Tenant.objects.get(tenant_name="public"))
system_roles = roles.filter(tenant=Tenant.objects.get_public_tenant())

# Custom roles are locked to prevent resources from being added/removed concurrently,
# in the case that the Roles had _no_ resources specified to begin with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,6 @@ def _default_binding(self, mapping: Optional[TenantMapping] = None) -> Relations

def _get_public_tenant(self) -> Tenant:
if self._public_tenant is None:
self._public_tenant = Tenant.objects.get(tenant_name="public")
self._public_tenant = Tenant.objects.get_public_tenant()
assert self._public_tenant is not None
return self._public_tenant
6 changes: 3 additions & 3 deletions rbac/management/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def _gather_group_querysets(request, args, kwargs, base_query: Optional[QuerySet
if scope != ORG_ID_SCOPE and not username:
return get_object_principal_queryset(request, scope, Group)

public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
default_group_set = Group.platform_default_set().filter(
tenant=request.tenant
) or Group.platform_default_set().filter(tenant=public_tenant)
Expand Down Expand Up @@ -155,7 +155,7 @@ def annotate_roles_with_counts(queryset):
def get_role_queryset(request) -> QuerySet:
"""Obtain the queryset for roles."""
scope = validate_and_get_key(request.query_params, SCOPE_KEY, VALID_SCOPES, ORG_ID_SCOPE)
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
base_query = annotate_roles_with_counts(Role.objects.prefetch_related("access")).filter(
tenant__in=[request.tenant, public_tenant]
)
Expand Down Expand Up @@ -292,7 +292,7 @@ def _filter_admin_default(request: Request, queryset: QuerySet):

# If the principal is an org admin, make sure they get any and all admin_default groups
if is_org_admin:
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
admin_default_group_set = Group.admin_default_set().filter(
tenant=request.tenant
) or Group.admin_default_set().filter(tenant=public_tenant)
Expand Down
4 changes: 2 additions & 2 deletions rbac/management/role/definer.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _add_ext_relation_if_it_exists(external_relation, role):

def _make_role(data, dual_write_handler, force_create_relationships=False):
"""Create the role object in the database."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
name = data.pop("name")
display_name = data.get("display_name", name)
access_list = data.get("access")
Expand Down Expand Up @@ -165,7 +165,7 @@ def seed_roles(force_create_relationships=False):

def seed_permissions():
"""Update or create defined permissions."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()

permission_directory = os.path.join(settings.BASE_DIR, "management", "role", "permissions")
permission_files = [
Expand Down
2 changes: 1 addition & 1 deletion rbac/management/role/relation_api_dual_write_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def _get_admin_default_policy_uuid(self) -> Optional[str]:

def _get_public_tenant(self) -> Tenant:
if self._public_tenant is None:
self._public_tenant = Tenant.objects.get(tenant_name="public")
self._public_tenant = Tenant.objects.get_public_tenant()
return self._public_tenant


Expand Down
2 changes: 1 addition & 1 deletion rbac/management/role/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def obtain_groups_in(obj, request):
else:
assigned_groups = filter_queryset_by_tenant(Group.objects.filter(policies__in=policy_ids), request.tenant)

public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()

platform_default_groups = Group.platform_default_set().filter(tenant=request.tenant).filter(
policies__in=policy_ids
Expand Down
4 changes: 2 additions & 2 deletions rbac/management/role/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def get_queryset(self):
# You would be able to remove `select_for_update` here,
# and instead rely on REPEATABLE READ's lost update detection to abort the tx.
# Nothing else should need to change.
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
base_query = Role.objects.filter(tenant__in=[self.request.tenant, public_tenant]).select_for_update()

# TODO: May be redundant with RolePermissions check but copied from querysets.py for safety
Expand Down Expand Up @@ -513,7 +513,7 @@ def perform_destroy(self, instance: Role):

Assumes concurrent updates are prevented (e.g. with atomic block and locks).
"""
if instance.tenant_id == Tenant.objects.get(tenant_name="public").id:
if instance.tenant_id == Tenant.objects.get_public_tenant().id:
key = "role"
message = "System roles cannot be deleted."
error = {key: [_(message)]}
Expand Down
2 changes: 1 addition & 1 deletion rbac/management/tenant_service/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,5 +548,5 @@ def _get_admin_default_policy_uuid(self) -> Optional[str]:

def _get_public_tenant(self) -> Tenant:
if self._public_tenant is None:
self._public_tenant = Tenant.objects.get(tenant_name="public")
self._public_tenant = Tenant.objects.get_public_tenant()
return self._public_tenant
2 changes: 1 addition & 1 deletion rbac/management/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def groups_for_principal(principal: Principal, tenant, **kwargs):
if principal.cross_account:
return set()
assigned_group_set = principal.group.all()
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()

# Only user principals should be able to get permissions from the default groups. For service accounts, customers
# need to explicitly add the service accounts to a group.
Expand Down
1 change: 1 addition & 0 deletions rbac/rbac/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@

ACCESS_CACHE_DB = 1
ACCESS_CACHE_LIFETIME = 10 * 60
PUBLIC_TENANT_CACHE_LIFETIME = ENVIRONMENT.get_value("PUBLIC_TENANT_CACHE_LIFETIME", default=86400)
ACCESS_CACHE_ENABLED = ENVIRONMENT.bool("ACCESS_CACHE_ENABLED", default=True)
ACCESS_CACHE_CONNECT_SIGNALS = ENVIRONMENT.bool("ACCESS_CACHE_CONNECT_SIGNALS", default=True)

Expand Down
2 changes: 1 addition & 1 deletion tests/api/cross_access/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def setUp(self):
"roles": ["role_1", "role_2"],
}

public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()

tenant_for_target_account = Tenant.objects.create(
tenant_name=f"acct{self.data4create['target_account']}",
Expand Down
40 changes: 40 additions & 0 deletions tests/api/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#
# Copyright 2024 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Test cases for api models."""

from django.test import TestCase

from api.models import Tenant


class TenantModelTests(TestCase):
"""Test the Tenant model."""

def setUp(self):
"""Set up the tenant model tests."""
super().setUp()
self.tenant = Tenant.objects.create(tenant_name="acct1234", org_id="1234")
self.public_tenant = Tenant.objects.get_public_tenant()

def tearDown(self):
"""Tear down tenant model tests."""
Tenant.objects.all().delete()

def test_get_public_tenant(self):
"""Test the tenant model manager method to get the public tenant."""
self.assertCountEqual(Tenant.objects.all(), [self.public_tenant, self.tenant])
self.assertEqual(Tenant.objects.get_public_tenant(), self.public_tenant)
2 changes: 1 addition & 1 deletion tests/api/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_migration_resource_deletion(self):
)
system_role = Role.objects.create(
name="role",
tenant=Tenant.objects.get(tenant_name="public"),
tenant=Tenant.objects.get_public_tenant(),
system=True,
)
BindingMapping.objects.create(
Expand Down
4 changes: 2 additions & 2 deletions tests/internal/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def setUp(self):
self.policy.save()
self.group.policies.add(self.policy)
self.group.save()
self.public_tenant = Tenant.objects.get(tenant_name="public")
self.public_tenant = Tenant.objects.get_public_tenant()

self._prior_logging_disable_level = logging.root.manager.disable
logging.disable(logging.NOTSET)
Expand Down Expand Up @@ -126,7 +126,7 @@ def test_delete_tenant_allowed_and_unmodified(self, mock):
@patch.object(Tenant, "delete")
def test_delete_tenant_no_schema(self, mock):
"""Test that we can delete a tenant with no schema."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
Group.objects.create(name="Custom Group", tenant=public_tenant)

tenant_no_schema = Tenant.objects.create(tenant_name="no_schema", org_id="1234")
Expand Down
2 changes: 1 addition & 1 deletion tests/management/access/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def setUp(self):
user.account = self.customer_data["account_id"]
user.org_id = self.customer_data["org_id"]
request.user = user
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()

self.access_data = {
"permission": "app:*:*",
Expand Down
2 changes: 1 addition & 1 deletion tests/management/group/test_definer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class GroupDefinerTests(IdentityRequest):
def setUp(self):
"""Set up the group definer tests."""
super().setUp()
self.public_tenant = Tenant.objects.get(tenant_name="public")
self.public_tenant = Tenant.objects.get_public_tenant()
seed_roles()
seed_group()

Expand Down
2 changes: 1 addition & 1 deletion tests/management/group/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def setUp(self):
test_request = test_request_context["request"]
self.test_headers = test_request.META

self.public_tenant = Tenant.objects.get(tenant_name="public")
self.public_tenant = Tenant.objects.get_public_tenant()
self.principal = Principal(username=self.user_data["username"], tenant=self.tenant, user_id="1")
self.principal.save()
self.principalB = Principal(username="mock_user", tenant=self.tenant, user_id="2")
Expand Down
8 changes: 4 additions & 4 deletions tests/management/principal/test_cleaner.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ def test_cleanup_same_principal_name_in_multiple_tenants(self, client_mock, prox
@override_settings(PRINCIPAL_CLEANUP_UPDATE_ENABLED_UMB=True)
def test_principal_creation_event_updates_existing_principal(self, client_mock, proxy_mock):
"""Test that we can run principal creation event."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
Group.objects.create(name="default", platform_default=True, tenant=public_tenant)
client_mock.canRead.side_effect = [True, False]
client_mock.receiveFrame.return_value = MagicMock(body=FRAME_BODY_CREATION)
Expand Down Expand Up @@ -518,7 +518,7 @@ def acquire_lock(ready: Event, close: Event):
client_mock.canRead.side_effect = [True, False]
client_mock.receiveFrame.return_value = MagicMock(body=FRAME_BODY_CREATION)

public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
Group.objects.create(name="default", platform_default=True, tenant=public_tenant)
tenant = Tenant.objects.get(org_id="17685860")
Principal.objects.create(tenant=tenant, username="principal-test")
Expand Down Expand Up @@ -600,7 +600,7 @@ def test_principal_creation_event_disabled(self, client_mock, proxy_mock):
@patch("management.principal.cleaner.UMB_CLIENT")
def test_principal_creation_event_does_not_create_principal(self, client_mock, proxy_mock):
"""Test that we can run principal creation event."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
Group.objects.create(name="default", platform_default=True, tenant=public_tenant)
client_mock.canRead.side_effect = [True, False]
client_mock.receiveFrame.return_value = MagicMock(body=FRAME_BODY_CREATION)
Expand Down Expand Up @@ -925,7 +925,7 @@ def setUp(self):
@override_settings(PRINCIPAL_CLEANUP_UPDATE_ENABLED_UMB=True)
def test_principal_creation_event_does_not_create_principal_nor_tenant(self, client_mock, proxy_mock):
"""Test that we can run principal creation event."""
public_tenant = Tenant.objects.get(tenant_name="public")
public_tenant = Tenant.objects.get_public_tenant()
Group.objects.create(name="default", platform_default=True, tenant=public_tenant)
client_mock.canRead.side_effect = [True, False]
client_mock.receiveFrame.return_value = MagicMock(body=FRAME_BODY_CREATION)
Expand Down
2 changes: 1 addition & 1 deletion tests/management/role/test_definer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class RoleDefinerTests(IdentityRequest):
def setUp(self):
"""Set up the role definer tests."""
super().setUp()
self.public_tenant = Tenant.objects.get(tenant_name="public")
self.public_tenant = Tenant.objects.get_public_tenant()

@patch("core.kafka.RBACProducer.send_kafka_message")
def test_role_create(self, send_kafka_message):
Expand Down
Loading
Loading