Skip to content

Commit 340426c

Browse files
authored
#2560 fix: ensure consistent exception propagation in ImmutableDocument during lazy loading (#2562)
1 parent 9c8ae17 commit 340426c

File tree

2 files changed

+245
-1
lines changed

2 files changed

+245
-1
lines changed

engine/src/main/java/com/arcadedb/database/ImmutableDocument.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ public Object get(final String propertyName) {
6161
if (propertyName == null)
6262
return null;
6363

64+
checkForLazyLoading();
6465
try {
65-
checkForLazyLoading();
6666
return database.getSerializer()
6767
.deserializeProperty(database, buffer, new EmbeddedModifierProperty(this, propertyName), propertyName, rid);
6868
} catch (Exception e) {
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.database;
20+
21+
import com.arcadedb.TestHelper;
22+
import com.arcadedb.event.AfterRecordReadListener;
23+
import com.arcadedb.schema.DocumentType;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.util.function.Consumer;
27+
import java.util.stream.Stream;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
31+
32+
/**
33+
* Test to verify the fix for issue #2560 - ImmutableDocument.checkForLazyLoading() consistency.
34+
* <p>
35+
* This test verifies that after the fix, the get() method now behaves consistently with other methods
36+
* (has(), modify(), toJSON(), toMap(), getPropertyNames()) by properly propagating exceptions thrown
37+
* during lazy loading instead of catching them and returning null.
38+
* <p>
39+
* The fix ensures that permission checking workflows that depend on exceptions being thrown during
40+
* lazy loading now work correctly with the get() method.
41+
*/
42+
public class ImmutableDocumentLazyLoadingInconsistencyTest extends TestHelper {
43+
44+
/**
45+
* Custom RuntimeException to simulate permission denied scenarios.
46+
*/
47+
public static class PermissionDeniedException extends RuntimeException {
48+
public PermissionDeniedException(String message) {
49+
super(message);
50+
}
51+
}
52+
53+
/**
54+
* Test listener that simulates a security check that denies access.
55+
*/
56+
public static class SecurityCheckListener implements AfterRecordReadListener {
57+
private final boolean shouldDenyAccess;
58+
private final String propertyName;
59+
60+
public SecurityCheckListener(boolean shouldDenyAccess, String propertyName) {
61+
this.shouldDenyAccess = shouldDenyAccess;
62+
this.propertyName = propertyName;
63+
}
64+
65+
@Override
66+
public Record onAfterRead(Record record) {
67+
if (shouldDenyAccess) {
68+
throw new PermissionDeniedException("Access denied to property: " + propertyName);
69+
}
70+
return record;
71+
}
72+
}
73+
74+
@Override
75+
public void beginTest() {
76+
database.transaction(() -> {
77+
final DocumentType type = database.getSchema().createDocumentType("SecurityTest");
78+
type.createProperty("publicProperty", com.arcadedb.schema.Type.STRING);
79+
type.createProperty("secretProperty", com.arcadedb.schema.Type.STRING);
80+
});
81+
}
82+
83+
@Test
84+
public void testLazyLoadingConsistencyWithSecurityException() {
85+
database.transaction(() -> {
86+
// Create a document with both public and secret properties
87+
final MutableDocument doc = database.newDocument("SecurityTest");
88+
doc.set("publicProperty", "public_value");
89+
doc.set("secretProperty", "secret_value");
90+
doc.save();
91+
92+
final RID documentRid = doc.getIdentity();
93+
94+
// Register a security listener that denies access when reading records
95+
final SecurityCheckListener securityListener = new SecurityCheckListener(true, "secretProperty");
96+
database.getEvents().registerListener(securityListener);
97+
98+
try {
99+
// Test with an ImmutableDocument created by reading from DB (lazy loading will occur on first access)
100+
database.commit();
101+
database.begin();
102+
103+
// Get the document as ImmutableDocument (it will have buffer=null initially)
104+
final ImmutableDocument immutableDoc = (ImmutableDocument) database.lookupByRID(documentRid, false);
105+
106+
// Test all methods that should trigger lazy loading and p ropagate the exception
107+
Stream.<Consumer<ImmutableDocument>>of(
108+
d -> d.has("secretProperty"),
109+
ImmutableDocument::modify,
110+
ImmutableDocument::toJSON,
111+
ImmutableDocument::toMap,
112+
ImmutableDocument::getPropertyNames,
113+
d -> d.get("secretProperty")
114+
).forEach(action -> {
115+
// Get a new instance for each test to ensure lazy loading is triggered
116+
final ImmutableDocument freshDoc = (ImmutableDocument) database.lookupByRID(documentRid, false);
117+
assertThatThrownBy(() -> action.accept(freshDoc))
118+
.isInstanceOf(PermissionDeniedException.class)
119+
.hasMessage("Access denied to property: secretProperty");
120+
});
121+
122+
} finally {
123+
// Clean up the security listener
124+
database.getEvents().unregisterListener(securityListener);
125+
}
126+
});
127+
}
128+
129+
@Test
130+
public void testLazyLoadingConsistencyWithoutSecurityException() {
131+
database.transaction(() -> {
132+
// Create a document
133+
final MutableDocument doc = database.newDocument("SecurityTest");
134+
doc.set("publicProperty", "public_value");
135+
doc.set("secretProperty", "secret_value");
136+
doc.save();
137+
138+
final RID documentRid = doc.getIdentity();
139+
140+
// Register a security listener that allows access
141+
final SecurityCheckListener securityListener = new SecurityCheckListener(false, "secretProperty");
142+
database.getEvents().registerListener(securityListener);
143+
144+
try {
145+
database.commit();
146+
database.begin();
147+
148+
// All methods should work normally when security check passes
149+
final ImmutableDocument immutableDoc1 = (ImmutableDocument) database.lookupByRID(documentRid, false);
150+
assertThat(immutableDoc1.has("secretProperty")).isTrue();
151+
152+
final ImmutableDocument immutableDoc2 = (ImmutableDocument) database.lookupByRID(documentRid, false);
153+
assertThat(immutableDoc2.get("secretProperty")).isEqualTo("secret_value");
154+
155+
final ImmutableDocument immutableDoc3 = (ImmutableDocument) database.lookupByRID(documentRid, false);
156+
assertThat(immutableDoc3.modify()).isNotNull();
157+
158+
final ImmutableDocument immutableDoc4 = (ImmutableDocument) database.lookupByRID(documentRid, false);
159+
assertThat(immutableDoc4.toJSON().has("secretProperty")).isTrue();
160+
161+
final ImmutableDocument immutableDoc5 = (ImmutableDocument) database.lookupByRID(documentRid, false);
162+
assertThat(immutableDoc5.toMap()).containsKey("secretProperty");
163+
164+
final ImmutableDocument immutableDoc6 = (ImmutableDocument) database.lookupByRID(documentRid, false);
165+
assertThat(immutableDoc6.getPropertyNames()).contains("secretProperty");
166+
167+
} finally {
168+
// Clean up the security listener
169+
database.getEvents().unregisterListener(securityListener);
170+
}
171+
});
172+
}
173+
174+
@Test
175+
public void testConsistentExceptionTypesInLazyLoading() {
176+
database.transaction(() -> {
177+
// Create a document
178+
final MutableDocument doc = database.newDocument("SecurityTest");
179+
doc.set("publicProperty", "public_value");
180+
doc.save();
181+
182+
final RID documentRid = doc.getIdentity();
183+
184+
// Test with different exception types
185+
final AfterRecordReadListener runtimeExceptionListener = new AfterRecordReadListener() {
186+
@Override
187+
public Record onAfterRead(Record record) {
188+
throw new RuntimeException("General runtime error");
189+
}
190+
};
191+
192+
final AfterRecordReadListener illegalStateListener = new AfterRecordReadListener() {
193+
@Override
194+
public Record onAfterRead(Record record) {
195+
throw new IllegalStateException("Invalid state for access");
196+
}
197+
};
198+
199+
// Test with RuntimeException
200+
database.getEvents().registerListener(runtimeExceptionListener);
201+
try {
202+
database.commit();
203+
database.begin();
204+
205+
final ImmutableDocument immutableDoc1 = (ImmutableDocument) database.lookupByRID(documentRid, false);
206+
// has() should propagate RuntimeException
207+
assertThatThrownBy(() -> immutableDoc1.has("publicProperty"))
208+
.isInstanceOf(RuntimeException.class)
209+
.hasMessage("General runtime error");
210+
211+
// get() now correctly propagates RuntimeException (FIXED)
212+
final ImmutableDocument immutableDoc2 = (ImmutableDocument) database.lookupByRID(documentRid, false);
213+
assertThatThrownBy(() -> immutableDoc2.get("publicProperty"))
214+
.isInstanceOf(RuntimeException.class)
215+
.hasMessage("General runtime error");
216+
217+
} finally {
218+
database.getEvents().unregisterListener(runtimeExceptionListener);
219+
}
220+
221+
// Test with IllegalStateException
222+
database.getEvents().registerListener(illegalStateListener);
223+
try {
224+
database.commit();
225+
database.begin();
226+
227+
final ImmutableDocument immutableDoc3 = (ImmutableDocument) database.lookupByRID(documentRid, false);
228+
// has() should propagate IllegalStateException
229+
assertThatThrownBy(() -> immutableDoc3.has("publicProperty"))
230+
.isInstanceOf(IllegalStateException.class)
231+
.hasMessage("Invalid state for access");
232+
233+
// get() now correctly propagates IllegalStateException (FIXED)
234+
final ImmutableDocument immutableDoc4 = (ImmutableDocument) database.lookupByRID(documentRid, false);
235+
assertThatThrownBy(() -> immutableDoc4.get("publicProperty"))
236+
.isInstanceOf(IllegalStateException.class)
237+
.hasMessage("Invalid state for access");
238+
239+
} finally {
240+
database.getEvents().unregisterListener(illegalStateListener);
241+
}
242+
});
243+
}
244+
}

0 commit comments

Comments
 (0)