@@ -137,4 +137,120 @@ void emptyTokenFileThrows(@TempDir final Path tmp) throws Exception {
137137 assertThatThrownBy (() -> new ClusterTokenProvider (config ).getClusterToken ())
138138 .isInstanceOf (ConfigurationException .class );
139139 }
140+
141+ // Issue #10462: a server given its root password as a file (the shipped StatefulSet mounts a Secret and passes only
142+ // arcadedb.server.rootPasswordPath) has a root password, so HA must derive its cluster token from that file too.
143+
144+ @ Test
145+ void tokenDerivedFromRootPasswordFile (@ TempDir final Path tmp ) throws Exception {
146+ final Path passwordFile = tmp .resolve ("rootPassword" );
147+ Files .writeString (passwordFile , "test-password" );
148+ final ContextConfiguration fromFile = new ContextConfiguration ();
149+ fromFile .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
150+ fromFile .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , passwordFile .toString ());
151+
152+ final ContextConfiguration inline = new ContextConfiguration ();
153+ inline .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
154+ inline .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD , "test-password" );
155+
156+ assertThat (new ClusterTokenProvider (fromFile ).getClusterToken ())
157+ .isEqualTo (new ClusterTokenProvider (inline ).getClusterToken ());
158+ }
159+
160+ @ Test
161+ void rootPasswordFileDropsOnlyTheTrailingNewlineLikeServerSecurity (@ TempDir final Path tmp ) throws Exception {
162+ // `kubectl create secret --from-file`, a here-doc or an editor append a newline that is not part of the password:
163+ // the token must match the one a node given the same password inline derives. Trailing spaces are part of the
164+ // password, exactly as ServerSecurity reads the same file into the root user.
165+ final Path passwordFile = tmp .resolve ("rootPassword" );
166+ Files .writeString (passwordFile , "test-password \r \n " );
167+ final ContextConfiguration fromFile = new ContextConfiguration ();
168+ fromFile .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
169+ fromFile .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , passwordFile .toString ());
170+
171+ final ContextConfiguration inline = new ContextConfiguration ();
172+ inline .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
173+ inline .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD , "test-password " );
174+
175+ assertThat (new ClusterTokenProvider (fromFile ).getClusterToken ())
176+ .isEqualTo (new ClusterTokenProvider (inline ).getClusterToken ());
177+ }
178+
179+ @ Test
180+ void inlineRootPasswordTakesPrecedenceOverRootPasswordFile (@ TempDir final Path tmp ) throws Exception {
181+ // Same precedence as ServerSecurity.askForRootPassword(): the file is read only when no inline password is set.
182+ final Path passwordFile = tmp .resolve ("rootPassword" );
183+ Files .writeString (passwordFile , "file-password" );
184+ final ContextConfiguration both = new ContextConfiguration ();
185+ both .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
186+ both .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD , "inline-password" );
187+ both .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , passwordFile .toString ());
188+
189+ final ContextConfiguration inline = new ContextConfiguration ();
190+ inline .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
191+ inline .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD , "inline-password" );
192+
193+ assertThat (new ClusterTokenProvider (both ).getClusterToken ())
194+ .isEqualTo (new ClusterTokenProvider (inline ).getClusterToken ());
195+ }
196+
197+ @ Test
198+ void explicitTokenPathTakesPrecedenceOverRootPasswordFile (@ TempDir final Path tmp ) throws Exception {
199+ final Path tokenFile = tmp .resolve ("clusterToken" );
200+ Files .writeString (tokenFile , "file-token\n " );
201+ final Path passwordFile = tmp .resolve ("rootPassword" );
202+ Files .writeString (passwordFile , "file-password" );
203+ final ContextConfiguration config = new ContextConfiguration ();
204+ config .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
205+ config .setValue (GlobalConfiguration .HA_CLUSTER_TOKEN_PATH , tokenFile .toString ());
206+ config .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , passwordFile .toString ());
207+ assertThat (new ClusterTokenProvider (config ).getClusterToken ()).isEqualTo ("file-token" );
208+ }
209+
210+ @ Test
211+ void unreadableRootPasswordFileThrowsNamingThePath () {
212+ final ContextConfiguration config = new ContextConfiguration ();
213+ config .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
214+ config .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , "/non/existent/rootPassword" );
215+ assertThatThrownBy (() -> new ClusterTokenProvider (config ).getClusterToken ())
216+ .isInstanceOf (ConfigurationException .class ).hasMessageContaining ("/non/existent/rootPassword" );
217+ }
218+
219+ @ Test
220+ void emptyRootPasswordFileThrows (@ TempDir final Path tmp ) throws Exception {
221+ final Path passwordFile = tmp .resolve ("rootPassword" );
222+ Files .writeString (passwordFile , "\n " );
223+ final ContextConfiguration config = new ContextConfiguration ();
224+ config .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
225+ config .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , passwordFile .toString ());
226+ assertThatThrownBy (() -> new ClusterTokenProvider (config ).getClusterToken ())
227+ .isInstanceOf (ConfigurationException .class ).hasMessageContaining ("empty" );
228+ }
229+
230+ @ Test
231+ void missingRootPasswordErrorNamesThePasswordFileSetting () {
232+ final ContextConfiguration config = new ContextConfiguration ();
233+ config .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
234+ assertThatThrownBy (() -> new ClusterTokenProvider (config ).getClusterToken ())
235+ .isInstanceOf (ConfigurationException .class )
236+ .hasMessageContaining (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH .getKey ())
237+ .hasMessageContaining (GlobalConfiguration .HA_CLUSTER_TOKEN_PATH .getKey ());
238+ }
239+
240+ @ Test
241+ void testHelperDerivesFromRootPasswordFile (@ TempDir final Path tmp ) throws Exception {
242+ final Path passwordFile = tmp .resolve ("rootPassword" );
243+ Files .writeString (passwordFile , "test-password\n " );
244+ final ContextConfiguration fromFile = new ContextConfiguration ();
245+ fromFile .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
246+ fromFile .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD_PATH , passwordFile .toString ());
247+ ClusterTokenProvider .initClusterTokenForTest (fromFile );
248+
249+ final ContextConfiguration inline = new ContextConfiguration ();
250+ inline .setValue (GlobalConfiguration .HA_CLUSTER_NAME , "test-cluster" );
251+ inline .setValue (GlobalConfiguration .SERVER_ROOT_PASSWORD , "test-password" );
252+
253+ assertThat (fromFile .getValueAsString (GlobalConfiguration .HA_CLUSTER_TOKEN ))
254+ .isEqualTo (new ClusterTokenProvider (inline ).getClusterToken ());
255+ }
140256}
0 commit comments