Skip to content

Commit d42359d

Browse files
committed
Implement permission and value checks to prevent unnecessary overwrites in recursive field processing
1 parent 66aab5d commit d42359d

File tree

4 files changed

+4435
-3
lines changed

4 files changed

+4435
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package uk.gov.hmcts.ccd.domain.service.common;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.node.ArrayNode;
5+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
6+
import com.fasterxml.jackson.databind.node.NullNode;
7+
import com.fasterxml.jackson.databind.node.ObjectNode;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.stereotype.Service;
10+
import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile;
11+
import uk.gov.hmcts.ccd.domain.model.definition.AccessControlList;
12+
import uk.gov.hmcts.ccd.domain.model.definition.CaseFieldDefinition;
13+
import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition;
14+
import uk.gov.hmcts.ccd.domain.model.definition.FieldTypeDefinition;
15+
import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException;
16+
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import java.util.Set;
22+
import java.util.stream.StreamSupport;
23+
24+
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.extractAccessProfileNames;
25+
26+
@Slf4j
27+
@Service
28+
public class RestrictedFieldProcessor {
29+
30+
private final CaseAccessService caseAccessService;
31+
32+
public RestrictedFieldProcessor(CaseAccessService caseAccessService) {
33+
this.caseAccessService = caseAccessService;
34+
}
35+
36+
public Map<String, JsonNode> filterRestrictedFields(final CaseTypeDefinition caseTypeDefinition,
37+
final Map<String, JsonNode> sanitisedData,
38+
final Map<String, JsonNode> existingData,
39+
final String caseReference) {
40+
Set<AccessProfile> accessProfiles = caseAccessService.getAccessProfilesByCaseReference(caseReference);
41+
if (accessProfiles == null || accessProfiles.isEmpty()) {
42+
throw new ValidationException("Cannot find user roles for the user");
43+
}
44+
45+
final Set<String> accessProfileNames = extractAccessProfileNames(accessProfiles);
46+
final Map<String, JsonNode> mergedData = new HashMap<>(sanitisedData);
47+
48+
sanitisedData.forEach((key, sanitizedValue) -> {
49+
JsonNode existingValue = existingData.get(key);
50+
51+
if (existingValue != null) {
52+
CaseFieldDefinition rootFieldDefinition = caseTypeDefinition.getCaseFieldDefinitions()
53+
.stream()
54+
.filter(field -> field.getId().equals(key))
55+
.findFirst()
56+
.orElse(null);
57+
58+
if (rootFieldDefinition != null && rootFieldDefinition.isCompoundFieldType()) {
59+
JsonNode updatedValue = processSubFieldsRecursively(
60+
rootFieldDefinition,
61+
sanitizedValue,
62+
existingValue,
63+
accessProfileNames
64+
);
65+
66+
mergedData.put(key, updatedValue);
67+
}
68+
}
69+
});
70+
71+
return mergedData;
72+
}
73+
74+
private JsonNode processSubFieldsRecursively(CaseFieldDefinition parentFieldDefinition,
75+
JsonNode sanitizedNode,
76+
JsonNode existingNode,
77+
Set<String> accessProfileNames) {
78+
if (existingNode == null) {
79+
return sanitizedNode;
80+
}
81+
82+
if (existingNode.isArray()) {
83+
return processCollectionFields(parentFieldDefinition, sanitizedNode, existingNode, accessProfileNames);
84+
}
85+
86+
if (!existingNode.isObject()) {
87+
return sanitizedNode;
88+
}
89+
90+
ObjectNode sanitizedObjectNode = sanitizedNode != null && sanitizedNode.isObject()
91+
? (ObjectNode) sanitizedNode.deepCopy()
92+
: JsonNodeFactory.instance.objectNode();
93+
94+
ObjectNode existingObjectNode = (ObjectNode) existingNode;
95+
96+
existingObjectNode.fieldNames().forEachRemaining(fieldName -> {
97+
JsonNode existingSubField = existingObjectNode.get(fieldName);
98+
JsonNode sanitizedSubField = sanitizedObjectNode.get(fieldName);
99+
100+
CaseFieldDefinition subFieldDefinition = getFieldDefinition(fieldName, parentFieldDefinition);
101+
102+
if (sanitizedSubField == null) {
103+
log.debug("Missing field '{}' under '{}'.", fieldName, parentFieldDefinition.getId());
104+
105+
if (isCreateWithoutReadAllowed(subFieldDefinition.getAccessControlLists(), accessProfileNames)) {
106+
log.info("Adding missing field '{}' under '{}'.", fieldName, parentFieldDefinition.getId());
107+
sanitizedObjectNode.set(fieldName, existingSubField);
108+
}
109+
} else {
110+
sanitizedObjectNode.set(fieldName, processSubFieldsRecursively(
111+
subFieldDefinition,
112+
sanitizedSubField,
113+
existingSubField,
114+
accessProfileNames));
115+
}
116+
});
117+
118+
return sanitizedObjectNode;
119+
}
120+
121+
private JsonNode processCollectionFields(CaseFieldDefinition subFieldDefinition,
122+
JsonNode sanitizedArrayNode,
123+
JsonNode existingArrayNode,
124+
Set<String> accessProfileNames) {
125+
126+
ArrayNode sanitizedArray = sanitizedArrayNode != null && sanitizedArrayNode.isArray()
127+
? (ArrayNode) sanitizedArrayNode.deepCopy()
128+
: JsonNodeFactory.instance.arrayNode();
129+
130+
ArrayNode existingArray = (ArrayNode) existingArrayNode;
131+
132+
for (JsonNode existingItem : existingArray) {
133+
JsonNode existingItemId = existingItem.get("id");
134+
135+
Optional<JsonNode> matchingNewItem = StreamSupport.stream(sanitizedArray.spliterator(), false)
136+
.filter(newItem -> !isNullId(newItem) && newItem.get("id").equals(existingItemId))
137+
.findFirst();
138+
139+
if (matchingNewItem.isEmpty()) {
140+
log.debug("Missing collection item with ID '{}' under '{}'.", existingItemId,
141+
subFieldDefinition.getId());
142+
143+
if (isCreateWithoutReadAllowed(subFieldDefinition.getAccessControlLists(), accessProfileNames)) {
144+
log.info("Adding missing collection item with ID '{}' under '{}'.", existingItemId,
145+
subFieldDefinition.getId());
146+
sanitizedArray.add(existingItem);
147+
}
148+
} else {
149+
JsonNode newValueField = matchingNewItem.get().get("value");
150+
JsonNode existingValueField = existingItem.get("value");
151+
152+
if (existingValueField != null) {
153+
JsonNode processedValueField;
154+
155+
if (existingValueField.isObject()) {
156+
processedValueField = processSubFieldsRecursively(subFieldDefinition,
157+
newValueField,
158+
existingValueField,
159+
accessProfileNames);
160+
} else {
161+
processedValueField = processSimpleValueField(
162+
subFieldDefinition, newValueField, existingValueField, accessProfileNames);
163+
}
164+
165+
((ObjectNode) matchingNewItem.get()).set("value", processedValueField);
166+
}
167+
}
168+
}
169+
170+
return sanitizedArray;
171+
}
172+
173+
private JsonNode processSimpleValueField(CaseFieldDefinition subFieldDefinition, JsonNode newValueField,
174+
JsonNode existingValueField,
175+
Set<String> accessProfileNames) {
176+
if (newValueField == null) {
177+
log.debug("Missing value field under '{}'.", subFieldDefinition.getId());
178+
179+
if (isCreateWithoutReadAllowed(subFieldDefinition.getAccessControlLists(), accessProfileNames)) {
180+
log.info("Adding missing value field under '{}'.", subFieldDefinition.getId());
181+
return existingValueField;
182+
}
183+
}
184+
185+
return newValueField != null ? newValueField : existingValueField;
186+
}
187+
188+
private boolean isNullId(JsonNode newItem) {
189+
return newItem.get("id") == null
190+
|| newItem.get("id").equals(NullNode.getInstance())
191+
|| "null".equalsIgnoreCase(newItem.get("id").asText());
192+
}
193+
194+
private boolean isCreateWithoutReadAllowed(List<AccessControlList> fieldAccessControlLists,
195+
Set<String> accessProfileNames) {
196+
boolean hasReadPermission = fieldAccessControlLists
197+
.stream()
198+
.anyMatch(acl -> accessProfileNames.contains(acl.getAccessProfile())
199+
&& Boolean.TRUE.equals(acl.isRead()));
200+
201+
boolean hasCreatePermission = fieldAccessControlLists
202+
.stream()
203+
.anyMatch(acl -> accessProfileNames.contains(acl.getAccessProfile())
204+
&& Boolean.TRUE.equals(acl.isCreate()));
205+
206+
return !hasReadPermission && hasCreatePermission;
207+
}
208+
209+
private CaseFieldDefinition getFieldDefinition(String fieldName,
210+
CaseFieldDefinition parentFieldDefinition) {
211+
// Check if the parent field's definition contains subfields
212+
FieldTypeDefinition parentFieldType = parentFieldDefinition.getFieldTypeDefinition();
213+
214+
if (parentFieldType == null) {
215+
return parentFieldDefinition;
216+
}
217+
218+
if (parentFieldType.getComplexFields() != null && !parentFieldType.getComplexFields().isEmpty()) {
219+
return parentFieldType.getComplexFields()
220+
.stream()
221+
.filter(subField -> subField.getId().equals(fieldName))
222+
.findFirst()
223+
.orElse(parentFieldDefinition);
224+
}
225+
226+
if (parentFieldType.getCollectionFieldTypeDefinition() != null && !parentFieldType
227+
.getCollectionFieldTypeDefinition().getComplexFields().isEmpty()) {
228+
return parentFieldType.getCollectionFieldTypeDefinition().getComplexFields()
229+
.stream()
230+
.filter(subField -> subField.getId().equals(fieldName))
231+
.findFirst()
232+
.orElse(parentFieldDefinition);
233+
}
234+
235+
return parentFieldDefinition;
236+
}
237+
}

src/main/java/uk/gov/hmcts/ccd/domain/service/createevent/CreateCaseEventService.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import uk.gov.hmcts.ccd.domain.model.aggregated.IdamUser;
2020
import uk.gov.hmcts.ccd.domain.model.definition.CaseDetails;
2121
import uk.gov.hmcts.ccd.domain.model.definition.CaseEventDefinition;
22+
import uk.gov.hmcts.ccd.domain.model.definition.CaseFieldDefinition;
2223
import uk.gov.hmcts.ccd.domain.model.definition.CaseStateDefinition;
2324
import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition;
2425
import uk.gov.hmcts.ccd.domain.model.std.AuditEvent;
@@ -33,6 +34,7 @@
3334
import uk.gov.hmcts.ccd.domain.service.common.CaseService;
3435
import uk.gov.hmcts.ccd.domain.service.common.CaseTypeService;
3536
import uk.gov.hmcts.ccd.domain.service.common.EventTriggerService;
37+
import uk.gov.hmcts.ccd.domain.service.common.RestrictedFieldProcessor;
3638
import uk.gov.hmcts.ccd.domain.service.common.SecurityClassificationServiceImpl;
3739
import uk.gov.hmcts.ccd.domain.service.common.UIDService;
3840
import uk.gov.hmcts.ccd.domain.service.getcasedocument.CaseDocumentService;
@@ -97,6 +99,7 @@ public class CreateCaseEventService {
9799
private final CaseDocumentTimestampService caseDocumentTimestampService;
98100
private final ApplicationParams applicationParams;
99101
private final CaseAccessGroupUtils caseAccessGroupUtils;
102+
private final RestrictedFieldProcessor restrictedFieldProcessor;
100103

101104
@Inject
102105
public CreateCaseEventService(@Qualifier(CachedUserRepository.QUALIFIER) final UserRepository userRepository,
@@ -130,7 +133,8 @@ public CreateCaseEventService(@Qualifier(CachedUserRepository.QUALIFIER) final U
130133
final CaseLinkService caseLinkService,
131134
final ApplicationParams applicationParams,
132135
final CaseAccessGroupUtils caseAccessGroupUtils,
133-
final CaseDocumentTimestampService caseDocumentTimestampService) {
136+
final CaseDocumentTimestampService caseDocumentTimestampService,
137+
final RestrictedFieldProcessor restrictedFieldProcessor) {
134138

135139
this.userRepository = userRepository;
136140
this.caseDetailsRepository = caseDetailsRepository;
@@ -161,7 +165,7 @@ public CreateCaseEventService(@Qualifier(CachedUserRepository.QUALIFIER) final U
161165
this.applicationParams = applicationParams;
162166
this.caseAccessGroupUtils = caseAccessGroupUtils;
163167
this.caseDocumentTimestampService = caseDocumentTimestampService;
164-
168+
this.restrictedFieldProcessor = restrictedFieldProcessor;
165169
}
166170

167171
@Transactional(propagation = Propagation.REQUIRES_NEW)
@@ -436,7 +440,12 @@ CaseDetails mergeUpdatedFieldsToCaseDetails(final Map<String, JsonNode> data,
436440
final Map<String, JsonNode> sanitisedData = caseSanitiser.sanitise(caseTypeDefinition, nonNullData);
437441
final Map<String, JsonNode> caseData = new HashMap<>(Optional.ofNullable(caseDetails.getData())
438442
.orElse(emptyMap()));
439-
caseData.putAll(sanitisedData);
443+
444+
final Map<String, JsonNode> filteredData =
445+
restrictedFieldProcessor.filterRestrictedFields(caseTypeDefinition, sanitisedData, caseData,
446+
caseDetails.getReferenceAsString());
447+
448+
caseData.putAll(filteredData);
440449
clonedCaseDetails.setData(globalSearchProcessorService.populateGlobalSearchData(caseTypeDefinition,
441450
caseData));
442451

0 commit comments

Comments
 (0)