diff --git a/backend/src/zango/api/platform/permissions/v1/views.py b/backend/src/zango/api/platform/permissions/v1/views.py index 5a9b38423..cad947f80 100644 --- a/backend/src/zango/api/platform/permissions/v1/views.py +++ b/backend/src/zango/api/platform/permissions/v1/views.py @@ -137,12 +137,20 @@ def get(self, request, *args, **kwargs): return get_api_response(success, response, status) - def put(self, request, *args, **kwargs): + def put(self, request, app_uuid, *args, **kwargs): try: obj = self.get_obj(**kwargs) serializer = PolicySerializer(instance=obj, data=request.data, partial=True) if serializer.is_valid(): serializer.save(**{"roles": request.data.getlist("roles", [])}) + + tenant = TenantModel.objects.get(uuid=app_uuid) + connection.set_tenant(tenant) + with connection.cursor() as c: + ws = Workspace(connection.tenant, request=None, as_systemuser=True) + ws.ready() + ws.sync_role_policies() + success = True status_code = 200 result = { diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index bef45dee2..911237e6e 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -323,7 +323,7 @@ def post(self, request, *args, **kwargs): connection.set_tenant(tenant) with connection.cursor() as c: ws = Workspace(connection.tenant, request=None, as_systemuser=True) - ws.sync_role_with_policies() + ws.sync_role_policies() else: success = False status_code = 400 @@ -383,7 +383,7 @@ def put(self, request, *args, **kwargs): connection.set_tenant(tenant) with connection.cursor() as c: ws = Workspace(connection.tenant, request=None, as_systemuser=True) - ws.sync_role_with_policies() + ws.sync_role_policies() else: success = False status_code = 400 @@ -397,6 +397,10 @@ def put(self, request, *args, **kwargs): result = {"message": error_message} except Exception as e: + import traceback + + traceback.print_exc() + success = False result = {"message": str(e)} status_code = 500 diff --git a/backend/src/zango/apps/dynamic_models/workspace/base.py b/backend/src/zango/apps/dynamic_models/workspace/base.py index e71fdb68b..5766e3d66 100644 --- a/backend/src/zango/apps/dynamic_models/workspace/base.py +++ b/backend/src/zango/apps/dynamic_models/workspace/base.py @@ -5,8 +5,9 @@ import os import re +from collections import defaultdict + from django.conf import settings -from django.contrib.postgres.aggregates import ArrayAgg from django.db import connection from zango.apps.appauth.models import UserRoleModel @@ -433,7 +434,7 @@ def sync_policies(self): existing_policies = list( PolicyModel.objects.filter(type="user").values_list("id", flat=True) ) - role_with_policies = {} + policy_roles = defaultdict(list) modules = self.get_all_module_paths() for module in modules: policy_file = f"{module}/policies.json" @@ -451,10 +452,6 @@ def sync_policies(self): policy = json.load(f) except json.decoder.JSONDecodeError as e: raise Exception(f"Error parsing {policy_file}: {e}") - for policy_dict in policy["policies"]: - roles = policy_dict.get("roles", []) - for role in roles: - role_with_policies[role] = [] for policy_details in policy["policies"]: if not isinstance(policy_details["statement"], dict): raise Exception( @@ -477,8 +474,7 @@ def sync_policies(self): raise Exception("Policy name already exists") existing_policies.remove(policy.id) roles = policy_details.get("roles", []) - for role in roles: - role_with_policies[role].append(policy.id) + policy_roles[policy.id].extend(roles) except Exception as e: raise Exception( f"Error creating policy {policy_details['name']} in {policy_path}: {e}" @@ -486,42 +482,33 @@ def sync_policies(self): for policy_id in existing_policies: PolicyModel.objects.get(id=policy_id).delete() - self.sync_policies_with_roles(role_with_policies) + self.sync_policies_with_roles(policy_roles) - def sync_policies_with_roles(self, role_with_policies): + def sync_policies_with_roles(self, policy_roles): """ mapping roles from policies.json to UserRoleModel """ - for role, policies in role_with_policies.items(): - user_role = UserRoleModel.objects.filter( - name=role, - ).first() - if user_role: - user_role.policies.set(policies) - - def sync_role_with_policies(self): - """ - mapping roles from UserRoleModel to policies.json - """ - all_policies = {} - policies_without_roles = list( - PolicyModel.objects.filter(type="user", role_policies__isnull=True).values( - "name", "description", "statement" - ) - ) - policies_with_roles = list( - PolicyModel.objects.filter(type="user", role_policies__isnull=False) - .values("name", "description", "statement") - .annotate(roles=ArrayAgg("role_policies__name", distinct=True)) - ) - all_policies["policies"] = policies_without_roles + policies_with_roles - modules = self.get_all_module_paths() - for module in modules: - policy_file = f"{module}/policies.json" - if os.path.isfile(policy_file): - model_module = ( - module.replace(str(settings.BASE_DIR) + "/", "") + "/policies" - ) - model_module = model_module.lstrip("/").replace("/", ".") - with open(policy_file, "w") as f: - f.write(json.dumps(all_policies, indent=4)) + for policy_id, roles in policy_roles.items(): + try: + policy = PolicyModel.objects.get(id=policy_id) + role_ids = [UserRoleModel.objects.get(name=role).id for role in roles] + policy.role_policies.set(role_ids) + except Exception as e: + raise Exception(f"Error adding roles to policy {policy.name}: {e}") + except UserRoleModel.DoesNotExist: + raise Exception("Role does not exist") + + def sync_role_policies(self): + for policy in PolicyModel.objects.all(): + if not policy.path: + continue + if policy.path and "packages" in policy.path: + continue + roles = policy.role_policies.all() + with open(f"{self.path}/{policy.path}/policies.json", "r") as f: + policies = json.load(f) + for policy_details in policies["policies"]: + if policy_details["name"] == policy.name: + policy_details["roles"] = [role.name for role in roles] + with open(f"{self.path}/{policy.path}/policies.json", "w") as f: + json.dump(policies, f, indent=4) diff --git a/backend/src/zango/tests/apps/permissions/policy_framework/test_policy_ip_permission/tests.py b/backend/src/zango/tests/apps/permissions/policy_framework/test_policy_ip_permission/tests.py index f48a2889a..6130bd420 100644 --- a/backend/src/zango/tests/apps/permissions/policy_framework/test_policy_ip_permission/tests.py +++ b/backend/src/zango/tests/apps/permissions/policy_framework/test_policy_ip_permission/tests.py @@ -25,23 +25,22 @@ def sync_policies(self): ws = Workspace(self.tenant, as_systemuser=True) ws.ready() ws.sync_policies() - + def test_role_ip_permissions(self): self.sync_policies() # delete the all ip view policy as we have to check for specific IPs. PolicyModel.objects.get(name="AllIPGetViewAccess").delete() + PolicyModel.objects.get(name="AllowFromAnywhere").delete() self.client = ZangoClient(self.tenant) - res = self.client.get("/customers/customer/",**{ - 'REMOTE_ADDR': '1.2.3.4' - }) - self.assertHTMLEqual(res.content.decode(), "

Hey! This is IP testing response

") + res = self.client.get("/customers/customer/", **{"REMOTE_ADDR": "1.2.3.4"}) + self.assertHTMLEqual( + res.content.decode(), "

Hey! This is IP testing response

" + ) # Permission denied for IP not listed in policies.json. - res = self.client.get("/customers/customer/",**{ - 'REMOTE_ADDR': '1.2.3.5' - }) + res = self.client.get("/customers/customer/", **{"REMOTE_ADDR": "1.2.3.5"}) self.assertEqual(res.status_code, 403) def test_cidr_ip_permissions(self): @@ -49,19 +48,18 @@ def test_cidr_ip_permissions(self): self.sync_policies() # delete the all ip view policy as we have to check for specific IPs. PolicyModel.objects.get(name="AllIPGetViewAccess").delete() + PolicyModel.objects.get(name="AllowFromAnywhere").delete() self.client = ZangoClient(self.tenant) - res = self.client.get("/customers/cidr/",**{ - 'REMOTE_ADDR': '10.0.0.2' - }) - self.assertHTMLEqual(res.content.decode(), "

Hey! This is CIDR IP testing response

") + res = self.client.get("/customers/cidr/", **{"REMOTE_ADDR": "10.0.0.2"}) + self.assertHTMLEqual( + res.content.decode(), "

Hey! This is CIDR IP testing response

" + ) # Permission denied for IP outside the range of "10.0.0.0/24" as listed in policies.json. - res = self.client.get("/customers/cidr/",**{ - 'REMOTE_ADDR': '10.0.1.252' - }) + res = self.client.get("/customers/cidr/", **{"REMOTE_ADDR": "10.0.1.252"}) self.assertEqual(res.status_code, 403) - + def test_all_ip_permissions(self): self.sync_policies() @@ -69,17 +67,17 @@ def test_all_ip_permissions(self): self.client = ZangoClient(self.tenant) # returns a response to all IPs. - res = self.client.get("/customers/all-ip/",**{ - 'REMOTE_ADDR': '1.2.3.9' - }) - self.assertHTMLEqual(res.content.decode(), "

Hey! This view can be viewed from all IPs.

") - - res = self.client.get("/customers/all-ip/",**{ - 'REMOTE_ADDR': '10.0.4.2' - }) - self.assertHTMLEqual(res.content.decode(), "

Hey! This view can be viewed from all IPs.

") - - res = self.client.get("/customers/all-ip/",**{ - 'REMOTE_ADDR': '10.0.3.1' - }) - self.assertHTMLEqual(res.content.decode(), "

Hey! This view can be viewed from all IPs.

") + res = self.client.get("/customers/all-ip/", **{"REMOTE_ADDR": "1.2.3.9"}) + self.assertHTMLEqual( + res.content.decode(), "

Hey! This view can be viewed from all IPs.

" + ) + + res = self.client.get("/customers/all-ip/", **{"REMOTE_ADDR": "10.0.4.2"}) + self.assertHTMLEqual( + res.content.decode(), "

Hey! This view can be viewed from all IPs.

" + ) + + res = self.client.get("/customers/all-ip/", **{"REMOTE_ADDR": "10.0.3.1"}) + self.assertHTMLEqual( + res.content.decode(), "

Hey! This view can be viewed from all IPs.

" + ) diff --git a/backend/src/zango/tests/apps/permissions/policy_framework/test_role_policy_mapping/tests.py b/backend/src/zango/tests/apps/permissions/policy_framework/test_role_policy_mapping/tests.py index 39d4d9ef8..8277a87e0 100644 --- a/backend/src/zango/tests/apps/permissions/policy_framework/test_role_policy_mapping/tests.py +++ b/backend/src/zango/tests/apps/permissions/policy_framework/test_role_policy_mapping/tests.py @@ -1,4 +1,4 @@ -import os +import os from django_tenants.utils import schema_context from zango.test.cases import ZangoAppBaseTestCase @@ -18,7 +18,7 @@ def get_test_module_path(self): return os.path.join( "apps/permissions/policy_framework/test_role_policy_mapping" ) - + @classmethod def sync_policies(self): with schema_context(self.tenant.schema_name): @@ -26,55 +26,57 @@ def sync_policies(self): ws.ready() ws.sync_policies() - def test_multi_role_with_one_policy_mapping(self): - + def test_multi_role_with_one_policy_mapping(self): + expected_role_names = ["test_role_1", "test_role_2"] expected_policy_name = "CustomerGetViewAccess" UserRoleModel.objects.create(name="test_role_1") UserRoleModel.objects.create(name="test_role_2") - + UserRoleModel.objects.create(name="dummy_role_1") + self.sync_policies() for role_name in expected_role_names: role = UserRoleModel.objects.filter(name=role_name).first() self.assertIsNotNone(role, f"Role '{role_name}' does not exist") - + policies = role.policies.all() - + self.assertTrue( policies.filter(name=expected_policy_name).exists(), - f"Role '{role_name}' does not have expected policy '{expected_policy_name}'" + f"Role '{role_name}' does not have expected policy '{expected_policy_name}'", ) policy = PolicyModel.objects.filter(name=expected_policy_name).first() self.assertIsNotNone(policy, f"Policy '{expected_policy_name}' does not exist") - roles = policy.role_policies.all() - + for role_name in expected_role_names: self.assertTrue( roles.filter(name=role_name).exists(), - f"Policy '{expected_policy_name}' is not associated with role '{role_name}'" + f"Policy '{expected_policy_name}' is not associated with role '{role_name}'", ) - + def test_one_role_with_multi_policy_mapping(self): expected_role_name = "dummy_role_1" expected_policy_names = ["RetailersGetViewAccess", "DummyGetViewAccess"] with schema_context(self.tenant.schema_name): + UserRoleModel.objects.create(name="test_role_1") + UserRoleModel.objects.create(name="test_role_2") UserRoleModel.objects.create(name=expected_role_name) self.sync_policies() - + role = UserRoleModel.objects.filter(name=expected_role_name).first() self.assertIsNotNone(role, f"Role '{expected_role_name}' does not exist") - + policies = role.policies.all() for policy_name in expected_policy_names: self.assertTrue( policies.filter(name=policy_name).exists(), - f"Role '{expected_role_name}' does not have expected policy '{policy_name}'" + f"Role '{expected_role_name}' does not have expected policy '{policy_name}'", ) for policy_name in expected_policy_names: @@ -84,5 +86,5 @@ def test_one_role_with_multi_policy_mapping(self): self.assertTrue( roles.filter(name=expected_role_name).exists(), - f"Policy '{policy_name}' is not associated with role '{expected_role_name}'" + f"Policy '{policy_name}' is not associated with role '{expected_role_name}'", )