Skip to content

Commit

Permalink
Merge pull request RedHatInsights#1366 from RedHatInsights/rhcloud-36…
Browse files Browse the repository at this point in the history
…577-rebootstrap

RHCLOUD-36577: Restrap the boot
  • Loading branch information
alechenninger authored Dec 4, 2024
2 parents f4f812a + 961f97f commit c516a64
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 34 deletions.
14 changes: 12 additions & 2 deletions rbac/internal/specs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,16 @@
"schema": {
"type": "string"
}
},
{
"name": "force",
"in": "query",
"required": false,
"description": "Whether or not to force replication to happen, even if the Tenant is already bootstrapped. Cannot be 'true' if replication is on, due to inconsistency risk.",
"schema": {
"default": false,
"type": "boolean"
}
}
],
"responses": {
Expand Down Expand Up @@ -1752,7 +1762,7 @@
}
}
},
"ServiceAccount": {
"ServiceAccount": {
"required": [
"clientId",
"username",
Expand Down Expand Up @@ -1983,4 +1993,4 @@
}
}
}
}
}
19 changes: 16 additions & 3 deletions rbac/internal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,20 +513,33 @@ def data_migration(request):
def bootstrap_tenant(request):
"""View method for bootstrapping a tenant.
POST /_private/api/utils/bootstrap_tenant/?org_id=12345
org_id is required,
POST /_private/api/utils/bootstrap_tenant/?org_id=12345&force=false
org_id:
(required) The org_id of the Tenant to bootstrap.
force:
Whether or not to force replication to happen, even if the Tenant is already bootstrapped.
Cannot be 'true' if replication is on, due to inconsistency risk.
"""
if request.method != "POST":
return HttpResponse('Invalid method, only "POST" is allowed.', status=405)
logger.info("Running bootstrap tenant.")

org_id = request.GET.get("org_id")
force = request.GET.get("force", "false").lower() == "true"
if not org_id:
return HttpResponse('Invalid request, must supply the "org_id" query parameter.', status=400)
if force and settings.REPLICATION_TO_RELATION_ENABLED:
return HttpResponse(
"Forcing replication is not allowed when replication is on, "
"due to race condition with default group customization.",
status=400,
)
with transaction.atomic():
tenant = get_object_or_404(Tenant, org_id=org_id)
bootstrap_service = V2TenantBootstrapService(OutboxReplicator())
bootstrap_service.bootstrap_tenant(tenant)
bootstrap_service.bootstrap_tenant(tenant, force=force)
return HttpResponse(f"Bootstrap tenant with org_id {org_id} finished.", status=200)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def _replicate(self):
self._replicator.replicate(
ReplicationEvent(
event_type=self.event_type,
info={"group_uuid": str(self.group.uuid)},
info={"group_uuid": str(self.group.uuid), "org_id": str(self.group.tenant.org_id)},
partition_key=PartitionKey.byEnvironment(),
remove=self.relations_to_remove,
add=self.relations_to_add,
Expand Down
91 changes: 63 additions & 28 deletions rbac/management/tenant_service/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ def new_bootstrapped_tenant(self, org_id: str, account_number: Optional[str] = N
tenant = Tenant.objects.create(org_id=org_id, account_id=account_number)
return self._bootstrap_tenant(tenant)

def bootstrap_tenant(self, tenant: Tenant) -> BootstrappedTenant:
"""Bootstrap an existing tenant."""
def bootstrap_tenant(self, tenant: Tenant, force: bool = False) -> BootstrappedTenant:
"""
Bootstrap an existing tenant.
If [force] is True, will re-bootstrap the tenant if already bootstrapped.
This does not change the RBAC data that already exists, but will replicate to Relations.
"""
try:
mapping = TenantMapping.objects.get(tenant=tenant)
if force:
self._replicate_bootstrap(tenant, mapping)
return BootstrappedTenant(tenant=tenant, mapping=mapping)
except TenantMapping.DoesNotExist:
return self._bootstrap_tenant(tenant)
Expand Down Expand Up @@ -99,7 +106,7 @@ def update_user(
self._replicator.replicate(
ReplicationEvent(
event_type=ReplicationEventType.EXTERNAL_USER_UPDATE,
info={"user_id": user_id},
info={"user_id": user_id, "org_id": user.org_id},
partition_key=PartitionKey.byEnvironment(),
add=tuples_to_add,
remove=tuples_to_remove,
Expand Down Expand Up @@ -211,7 +218,7 @@ def _disable_user_in_tenant(self, user: User):
self._replicator.replicate(
ReplicationEvent(
event_type=ReplicationEventType.EXTERNAL_USER_UPDATE,
info={"user_id": user_id},
info={"user_id": user_id, "org_id": user.org_id},
partition_key=PartitionKey.byEnvironment(),
remove=tuples_to_remove,
)
Expand Down Expand Up @@ -262,6 +269,27 @@ def _bootstrap_tenant(self, tenant: Tenant) -> BootstrappedTenant:

return BootstrappedTenant(tenant, mapping, default_workspace=default_workspace, root_workspace=root_workspace)

def _replicate_bootstrap(self, tenant: Tenant, mapping: TenantMapping):
"""Replicate the bootstrapping of a tenant."""
built_in_workspaces = Workspace.objects.filter(
tenant=tenant, type__in=[Workspace.Types.ROOT, Workspace.Types.DEFAULT]
)
root = next(ws for ws in built_in_workspaces if ws.type == Workspace.Types.ROOT)
default = next(ws for ws in built_in_workspaces if ws.type == Workspace.Types.DEFAULT)

relationships = []
relationships.extend(self._built_in_hierarchy_tuples(default.id, root.id, tenant.org_id))
relationships.extend(self._bootstrap_default_access(tenant, mapping, str(default.id)))

self._replicator.replicate(
ReplicationEvent(
event_type=ReplicationEventType.BOOTSTRAP_TENANT,
info={"org_id": tenant.org_id, "forced": True},
partition_key=PartitionKey.byEnvironment(),
add=relationships,
)
)

def _get_or_bootstrap_tenants(self, org_ids: set, ready: bool) -> list[BootstrappedTenant]:
"""Bootstrap list of tenants, used by import_bulk_users."""
# Fetch existing tenants
Expand Down Expand Up @@ -365,9 +393,34 @@ def _default_group_tuple_edits(self, user: User, mapping) -> tuple[list[Relation

return tuples_to_add, tuples_to_remove

def _create_default_relation_tuples(
def _built_in_hierarchy_tuples(self, default_workspace_id, root_workspace_id, org_id) -> List[Relationship]:
"""Create the tuples used to bootstrap the hierarchy of default->root->tenant->platform."""
tenant_id = f"{self._user_domain}/{org_id}"

return [
create_relationship(
("rbac", "workspace"),
str(default_workspace_id),
("rbac", "workspace"),
str(root_workspace_id),
"parent",
),
create_relationship(
("rbac", "workspace"), str(root_workspace_id), ("rbac", "tenant"), tenant_id, "parent"
),
# Include platform for tenant
create_relationship(("rbac", "tenant"), tenant_id, ("rbac", "platform"), settings.ENV_NAME, "platform"),
]

def _default_binding_tuples(
self, default_workspace_id, role_binding_uuid, default_role_uuid, default_group_uuid
):
) -> List[Relationship]:
"""
Create the tuples used to bootstrap default access for a Workspace.
Can be used for both default access and admin access as long as the correct arguments are provided.
Each of role binding, role, and group must refer to admin or default versions.
"""
return [
create_relationship(
("rbac", "workspace"),
Expand Down Expand Up @@ -429,7 +482,7 @@ def _bootstrap_default_access(
hasattr(tenant, "platform_default_groups") and tenant.platform_default_groups
):
tuples_to_add.extend(
self._create_default_relation_tuples(
self._default_binding_tuples(
default_workspace_id,
default_user_role_binding_uuid,
platform_default_role_uuid,
Expand All @@ -444,7 +497,7 @@ def _bootstrap_default_access(
# Admin role binding is not customizable
if admin_default_role_uuid:
tuples_to_add.extend(
self._create_default_relation_tuples(
self._default_binding_tuples(
default_workspace_id,
default_admin_role_binding_uuid,
admin_default_role_uuid,
Expand All @@ -467,26 +520,8 @@ def _built_in_workspaces(self, tenant: Tenant) -> tuple[Workspace, Workspace, li
root_workspace_id = root.id
default_workspace_id = default.id

tenant_id = f"{self._user_domain}/{tenant.org_id}"

relationships.extend(
[
create_relationship(
("rbac", "workspace"),
str(default_workspace_id),
("rbac", "workspace"),
str(root.id),
"parent",
),
create_relationship(
("rbac", "workspace"), str(root_workspace_id), ("rbac", "tenant"), tenant_id, "parent"
),
# Include platform for tenant
create_relationship(
("rbac", "tenant"), tenant_id, ("rbac", "platform"), settings.ENV_NAME, "platform"
),
]
)
relationships.extend(self._built_in_hierarchy_tuples(default_workspace_id, root_workspace_id, tenant.org_id))

return root, default, relationships

def _get_platform_default_policy_uuid(self) -> Optional[str]:
Expand Down
4 changes: 4 additions & 0 deletions rbac/migration_tool/in_memory_tuples.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ def __repr__(self):
"""Return a representation of the store."""
return f"InMemoryTuples({repr(self._tuples)})"

def __len__(self):
"""Return the number of tuples in the store."""
return len(self._tuples)


class TuplePredicate:
"""A predicate that can be used to filter relation tuples."""
Expand Down
60 changes: 60 additions & 0 deletions tests/internal/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
from management.tenant_service.v1 import V1TenantBootstrapService
from management.tenant_service.v2 import V2TenantBootstrapService
from management.workspace.model import Workspace
from migration_tool.in_memory_tuples import InMemoryRelationReplicator, InMemoryTuples
from rbac.settings import REPLICATION_TO_RELATION_ENABLED
from tests.identity_request import IdentityRequest
from tests.management.role.test_dual_write import RbacFixture

Expand Down Expand Up @@ -602,6 +604,64 @@ def test_bootstrapping_tenant(self):
Workspace.objects.filter(tenant=tenant, type=Workspace.Types.DEFAULT).exists()
self.assertTrue(getattr(tenant, "tenant_mapping"))

@patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate")
def test_bootstrapping_existing_tenant_without_force_does_nothing(self, replicate):
tuples = InMemoryTuples()
replicator = InMemoryRelationReplicator(tuples)
replicate.side_effect = replicator.replicate
fixture = RbacFixture(V2TenantBootstrapService(replicator))

org_id = "12345"

fixture.new_tenant(org_id)
tuples.clear()

response = self.client.post(
f"/_private/api/utils/bootstrap_tenant/?org_id={org_id}",
**self.request.META,
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(tuples), 0)

response = self.client.post(
f"/_private/api/utils/bootstrap_tenant/?org_id={org_id}&force=false",
**self.request.META,
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(tuples), 0)

@patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate")
@override_settings(REPLICATION_TO_RELATION_ENABLED=False)
def test_force_bootstrapping_tenant(self, replicate):
tuples = InMemoryTuples()
replicator = InMemoryRelationReplicator(tuples)
replicate.side_effect = replicator.replicate
fixture = RbacFixture(V2TenantBootstrapService(replicator))

org_id = "12345"

fixture.new_tenant(org_id)
tuples.clear()

response = self.client.post(
f"/_private/api/utils/bootstrap_tenant/?org_id={org_id}&force=true",
**self.request.META,
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(tuples), 9)

@override_settings(REPLICATION_TO_RELATION_ENABLED=True)
def test_cannot_force_bootstrapping_while_replication_enabled(self):
org_id = "12345"
response = self.client.post(
f"/_private/api/utils/bootstrap_tenant/?org_id={org_id}&force=true",
**self.request.META,
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_listing_migration_resources(self):
"""Test that we can list migration resources."""
org_id = "12345678"
Expand Down
36 changes: 36 additions & 0 deletions tests/management/tenant/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,42 @@ def test_bulk_import_updates_user_ids_on_principals_but_does_not_add_principals(
# Assert no extra principals created
self.assertEqual(4, Principal.objects.count())

def test_force_bootstrap_replicates_already_bootstrapped_unready_tenants(self):
bootstrapped = self.fixture.new_tenant(org_id="o1")
self.tuples.clear()

original_mapping = TenantMapping.objects.get(tenant=bootstrapped.tenant)
original_workspaces = list(Workspace.objects.filter(tenant=bootstrapped.tenant))

self.service.bootstrap_tenant(bootstrapped.tenant, force=True)

self.assertTenantBootstrapped("o1", existing=False)

new_mapping = TenantMapping.objects.get(tenant=bootstrapped.tenant)
new_workspaces = list(Workspace.objects.filter(tenant=bootstrapped.tenant))

self.assertEqual(original_mapping, new_mapping)
self.assertCountEqual(original_workspaces, new_workspaces)

def test_force_bootstrap_replicates_already_bootstrapped_ready_tenants(self):
bootstrapped = self.fixture.new_tenant(org_id="o1")
bootstrapped.tenant.ready = True
bootstrapped.tenant.save()
self.tuples.clear()

original_mapping = TenantMapping.objects.get(tenant=bootstrapped.tenant)
original_workspaces = list(Workspace.objects.filter(tenant=bootstrapped.tenant))

self.service.bootstrap_tenant(bootstrapped.tenant, force=True)

self.assertTenantBootstrapped("o1", existing=True)

new_mapping = TenantMapping.objects.get(tenant=bootstrapped.tenant)
new_workspaces = list(Workspace.objects.filter(tenant=bootstrapped.tenant))

self.assertEqual(original_mapping, new_mapping)
self.assertCountEqual(original_workspaces, new_workspaces)

def assertAddedToDefaultGroup(self, user_id: str, tenant_mapping: TenantMapping, and_admin_group: bool = False):
self.assertEqual(
1,
Expand Down

0 comments on commit c516a64

Please sign in to comment.