Skip to content

Commit c7f0378

Browse files
fix: audit fields visibility for third parties (#4289)
* fix: audit fields visibility for third parties * chore: sanitize comments * Address the root cause issue 3rd party audit was not correctly initialized. Return default visibility if audit not initialized. --------- Co-authored-by: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
1 parent 7557db0 commit c7f0378

3 files changed

Lines changed: 38 additions & 65 deletions

File tree

backend/core/serializers.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2591,6 +2591,23 @@ def get_scores_definition(self, obj):
25912591
return sd["scale"]
25922592
return sd
25932593

2594+
field_visibility = serializers.SerializerMethodField()
2595+
2596+
def get_field_visibility(self, obj):
2597+
"""Return field_visibility with defaults applied.
2598+
2599+
If the stored map is empty (e.g. audits created before the field was
2600+
populated at creation time), fall back to the code defaults merged with
2601+
the framework template — the same values a newly created CA would get —
2602+
so the frontend always receives a complete map.
2603+
"""
2604+
fv = obj.field_visibility
2605+
if fv:
2606+
return fv
2607+
from core.utils import build_initial_field_visibility
2608+
2609+
return build_initial_field_visibility(obj.framework)
2610+
25942611
# Derived booleans, kept in the API for backwards compatibility. The actual
25952612
# storage is `field_visibility`; clients that want to change these should
25962613
# PATCH `field_visibility` directly.
@@ -3031,12 +3048,12 @@ def to_internal_value(self, data):
30313048
request = self.context.get("request")
30323049
if request and self.instance:
30333050
from core.utils import (
3034-
get_respondent_filtered_folder_ids,
3051+
get_auditee_filtered_folder_ids,
30353052
is_field_editable_by,
30363053
)
30373054

30383055
ca = self.instance.compliance_assessment
3039-
respondent_folders = get_respondent_filtered_folder_ids(request.user)
3056+
respondent_folders = get_auditee_filtered_folder_ids(request.user)
30403057
if respondent_folders and ca.folder_id in respondent_folders:
30413058
# Cascade through DEFAULT_VISIBILITY so default-hidden keys are
30423059
# stripped from a respondent's payload even when the CA has an
@@ -3075,9 +3092,9 @@ def validate(self, attrs):
30753092
# Assignment-level and field-level guards for respondent users (auditee or third-party)
30763093
request = self.context.get("request")
30773094
if request and self.instance and compliance_assessment:
3078-
from core.utils import get_respondent_filtered_folder_ids
3095+
from core.utils import get_auditee_filtered_folder_ids
30793096

3080-
respondent_folders = get_respondent_filtered_folder_ids(request.user)
3097+
respondent_folders = get_auditee_filtered_folder_ids(request.user)
30813098
if (
30823099
respondent_folders
30833100
and compliance_assessment.folder_id in respondent_folders

backend/core/utils.py

Lines changed: 12 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,25 +1269,12 @@ def build_questions_dict(node):
12691269

12701270

12711271
def _resolve_auditee_role_ids():
1272-
"""Resolve role IDs for auditee + higher roles via IAM snapshot cache."""
1273-
from iam.cache_builders import get_roles_state
1274-
1275-
role_id_by_name = get_roles_state().role_id_by_name
1276-
auditee_id = role_id_by_name.get(RoleCodename.AUDITEE.value)
1277-
higher_ids = frozenset(
1278-
role_id_by_name[rc.value]
1279-
for rc in (
1280-
RoleCodename.ANALYST,
1281-
RoleCodename.DOMAIN_MANAGER,
1282-
RoleCodename.ADMINISTRATOR,
1283-
)
1284-
if rc.value in role_id_by_name
1285-
)
1286-
return auditee_id, higher_ids
1287-
1272+
"""Resolve respondent role IDs (auditee + third-party respondent) and the
1273+
"higher" role IDs (analyst, domain-manager, administrator) via IAM cache.
12881274
1289-
def _resolve_respondent_role_ids():
1290-
"""Resolve role IDs for respondent roles (auditee + third-party respondent) + higher roles."""
1275+
Auditee and third-party respondent are synonymous here — both are
1276+
respondents on an audit and get the scoped, field-stripped view.
1277+
"""
12911278
from iam.cache_builders import get_roles_state
12921279

12931280
role_id_by_name = get_roles_state().role_id_by_name
@@ -1308,45 +1295,9 @@ def _resolve_respondent_role_ids():
13081295
return respondent_ids, higher_ids
13091296

13101297

1311-
def get_respondent_filtered_folder_ids(user) -> set:
1312-
"""Return folder IDs where *user* holds a respondent role (auditee or third-party
1313-
respondent) but NO higher role. Mirrors :func:`get_auditee_filtered_folder_ids`
1314-
but widens the role set so third-party respondents are also guarded.
1315-
"""
1316-
from iam.models import _iter_assignment_lites_for_user
1317-
from iam.cache_builders import (
1318-
get_folder_state,
1319-
iter_descendant_ids,
1320-
)
1321-
1322-
respondent_role_ids, higher_role_ids = _resolve_respondent_role_ids()
1323-
if not respondent_role_ids:
1324-
return set()
1325-
1326-
state = get_folder_state()
1327-
folder_roles: dict[UUID, set] = {}
1328-
1329-
for a in _iter_assignment_lites_for_user(user):
1330-
role_id = a.role_id
1331-
if role_id not in respondent_role_ids and role_id not in higher_role_ids:
1332-
continue
1333-
for pf_id in a.perimeter_folder_ids:
1334-
if a.is_recursive:
1335-
target_ids = iter_descendant_ids(state, pf_id, include_start=True)
1336-
else:
1337-
target_ids = (pf_id,)
1338-
for fid in target_ids:
1339-
folder_roles.setdefault(fid, set()).add(role_id)
1340-
1341-
return {
1342-
fid
1343-
for fid, role_ids in folder_roles.items()
1344-
if role_ids & respondent_role_ids and role_ids.isdisjoint(higher_role_ids)
1345-
}
1346-
1347-
13481298
def get_auditee_filtered_folder_ids(user) -> set:
1349-
"""Return folder IDs where *user* holds the auditee role but NO higher role.
1299+
"""Return folder IDs where *user* holds a respondent role (auditee or
1300+
third-party respondent) but NO higher role.
13501301
13511302
"Higher" means analyst, domain-manager or administrator — any role that
13521303
already grants full access to compliance data. For those folders the
@@ -1361,8 +1312,8 @@ def get_auditee_filtered_folder_ids(user) -> set:
13611312
iter_descendant_ids,
13621313
)
13631314

1364-
auditee_role_id, higher_role_ids = _resolve_auditee_role_ids()
1365-
if auditee_role_id is None:
1315+
respondent_role_ids, higher_role_ids = _resolve_auditee_role_ids()
1316+
if not respondent_role_ids:
13661317
return set()
13671318

13681319
state = get_folder_state()
@@ -1372,7 +1323,7 @@ def get_auditee_filtered_folder_ids(user) -> set:
13721323

13731324
for a in _iter_assignment_lites_for_user(user):
13741325
role_id = a.role_id
1375-
if role_id != auditee_role_id and role_id not in higher_role_ids:
1326+
if role_id not in respondent_role_ids and role_id not in higher_role_ids:
13761327
continue # irrelevant role
13771328

13781329
# expand perimeter folders
@@ -1385,11 +1336,11 @@ def get_auditee_filtered_folder_ids(user) -> set:
13851336
for fid in target_ids:
13861337
folder_roles.setdefault(fid, set()).add(role_id)
13871338

1388-
# Return only folders where user is auditee and has NO higher role
1339+
# Return only folders where user is a respondent and has NO higher role
13891340
return {
13901341
fid
13911342
for fid, role_ids in folder_roles.items()
1392-
if auditee_role_id in role_ids and role_ids.isdisjoint(higher_role_ids)
1343+
if role_ids & respondent_role_ids and role_ids.isdisjoint(higher_role_ids)
13931344
}
13941345

13951346

backend/tprm/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,18 @@ def _create_or_update_audit(self, instance, audit_data):
224224
]
225225
}
226226
)
227+
from core.utils import build_initial_field_visibility
228+
227229
audit = ComplianceAssessment.objects.create(
228230
name=locked.name,
229231
framework=audit_data["framework"],
230232
perimeter=locked.perimeter,
231233
selected_implementation_groups=audit_data[
232234
"selected_implementation_groups"
233235
],
236+
field_visibility=build_initial_field_visibility(
237+
audit_data["framework"]
238+
),
234239
)
235240

236241
enclave = Folder.objects.create(

0 commit comments

Comments
 (0)