88//! .password("s3cr3t")
99//! .database("analytics")
1010//! .max_connections(20)
11- //! .build();
11+ //! .build()?;
12+ //! # Ok::<(), nodedb_types::error::NodeDbError>(())
1213//! ```
1314
1415use std:: time:: Duration ;
1516
17+ use nodedb_types:: error:: { NodeDbError , NodeDbResult } ;
1618use nodedb_types:: protocol:: AuthMethod ;
1719
1820use super :: client:: NativeClient ;
@@ -99,55 +101,162 @@ impl ConnectionBuilder {
99101
100102 /// Build the `NativeClient`.
101103 ///
102- /// Falls back to sensible defaults for any unset option.
103- pub fn build ( self ) -> NativeClient {
104+ /// The connection address falls back to `127.0.0.1:6433` when unset —
105+ /// that is a convenience default, not a security decision. Identity is
106+ /// the one option with no default: trust auth is passwordless, so a
107+ /// defaulted username would silently authenticate as whatever that
108+ /// default happened to be (privilege escalation by omission). Callers
109+ /// must supply either [`api_key`](Self::api_key) (which carries its own
110+ /// identity via the token) or [`username`](Self::username) — do not
111+ /// reintroduce a fallback for either. Never add a default for `addr`
112+ /// beyond the existing one either; keep the two concerns separate.
113+ ///
114+ /// # Errors
115+ ///
116+ /// Returns an error if neither `api_key` nor `username` was set.
117+ pub fn build ( self ) -> NodeDbResult < NativeClient > {
104118 let addr = self . addr . unwrap_or_else ( || "127.0.0.1:6433" . to_string ( ) ) ;
119+ let auth = resolve_auth ( self . username , self . password , self . api_key ) ?;
105120
106- let auth = if let Some ( token) = self . api_key {
107- AuthMethod :: ApiKey { token }
108- } else if let Some ( password) = self . password {
109- let username = self . username . unwrap_or_else ( || "admin" . to_string ( ) ) ;
110- AuthMethod :: Password { username, password }
111- } else {
112- let username = self . username . unwrap_or_else ( || "admin" . to_string ( ) ) ;
113- AuthMethod :: Trust { username }
114- } ;
115-
116- let default_config = PoolConfig :: default ( ) ;
121+ // `PoolConfig::new` already carries the identity (`auth`, always
122+ // built above — never omitted) plus this crate's tuning defaults;
123+ // only override the tuning fields the caller actually set.
124+ let default_config = PoolConfig :: new ( addr, auth) ;
117125
118126 let config = PoolConfig {
119- addr,
120- auth,
121127 database : self . database ,
122128 max_size : self . max_connections . unwrap_or ( default_config. max_size ) ,
123129 connect_timeout : self
124130 . connect_timeout
125131 . unwrap_or ( default_config. connect_timeout ) ,
126132 idle_timeout : self . idle_timeout . unwrap_or ( default_config. idle_timeout ) ,
127133 tls : self . tls . unwrap_or_default ( ) ,
134+ ..default_config
128135 } ;
129136
130- NativeClient :: new ( config)
137+ Ok ( NativeClient :: new ( config) )
138+ }
139+ }
140+
141+ /// Resolve the builder's optional identity fields into a required
142+ /// [`AuthMethod`].
143+ ///
144+ /// This is a disjunction, not three independent defaults: an `api_key`
145+ /// carries its own identity via the token, so it alone is sufficient. The
146+ /// trust and password branches have no such built-in identity, so they
147+ /// require an explicit `username` — there is no fallback for either.
148+ fn resolve_auth (
149+ username : Option < String > ,
150+ password : Option < String > ,
151+ api_key : Option < String > ,
152+ ) -> NodeDbResult < AuthMethod > {
153+ if let Some ( token) = api_key {
154+ return Ok ( AuthMethod :: ApiKey { token } ) ;
131155 }
156+ let username = username. ok_or_else ( || {
157+ NodeDbError :: config (
158+ "no authentication identity configured: call .username(...) or .api_key(...)" ,
159+ )
160+ } ) ?;
161+ Ok ( match password {
162+ Some ( password) => AuthMethod :: Password { username, password } ,
163+ None => AuthMethod :: Trust { username } ,
164+ } )
132165}
133166
134167#[ cfg( test) ]
135168mod tests {
136169 use super :: * ;
137170
138171 #[ test]
139- fn builder_defaults ( ) {
140- let client = ConnectionBuilder :: new ( "127.0.0.1:6433" ) . build ( ) ;
141- let _ = client; // just verify it compiles
172+ fn resolve_auth_with_username_carries_it_into_trust ( ) {
173+ let auth = resolve_auth ( Some ( "alice" . to_string ( ) ) , None , None ) . expect ( "username given" ) ;
174+ match auth {
175+ AuthMethod :: Trust { username } => assert_eq ! ( username, "alice" ) ,
176+ other => panic ! ( "expected AuthMethod::Trust, got {other:?}" ) ,
177+ }
178+ }
179+
180+ #[ test]
181+ fn resolve_auth_with_username_and_password_carries_it_into_password ( ) {
182+ let auth = resolve_auth ( Some ( "bob" . to_string ( ) ) , Some ( "secret" . to_string ( ) ) , None )
183+ . expect ( "username given" ) ;
184+ match auth {
185+ AuthMethod :: Password { username, password } => {
186+ assert_eq ! ( username, "bob" ) ;
187+ assert_eq ! ( password, "secret" ) ;
188+ }
189+ other => panic ! ( "expected AuthMethod::Password, got {other:?}" ) ,
190+ }
191+ }
192+
193+ #[ test]
194+ fn resolve_auth_with_api_key_succeeds_without_username ( ) {
195+ let auth = resolve_auth ( None , None , Some ( "token-123" . to_string ( ) ) ) . expect ( "api_key given" ) ;
196+ assert ! ( matches!( auth, AuthMethod :: ApiKey { token } if token == "token-123" ) ) ;
197+ }
198+
199+ #[ test]
200+ fn resolve_auth_with_no_identity_errors ( ) {
201+ // Regression lock: no username, no api_key — must fail, never
202+ // silently authenticate as some default identity.
203+ let err = resolve_auth ( None , None , None ) . expect_err ( "must reject a missing identity" ) ;
204+ assert ! ( err. message( ) . contains( "authentication identity" ) ) ;
205+ }
206+
207+ #[ test]
208+ fn resolve_auth_with_password_but_no_username_errors ( ) {
209+ let err = resolve_auth ( None , Some ( "secret" . to_string ( ) ) , None )
210+ . expect_err ( "must reject password auth without a username" ) ;
211+ assert ! ( err. message( ) . contains( "authentication identity" ) ) ;
212+ }
213+
214+ #[ test]
215+ fn builder_with_username_succeeds ( ) {
216+ let client = ConnectionBuilder :: new ( "127.0.0.1:6433" )
217+ . username ( "alice" )
218+ . build ( ) ;
219+ assert ! ( client. is_ok( ) ) ;
220+ }
221+
222+ #[ test]
223+ fn builder_with_api_key_succeeds_without_username ( ) {
224+ let client = ConnectionBuilder :: new ( "127.0.0.1:6433" )
225+ . api_key ( "token-123" )
226+ . build ( ) ;
227+ assert ! ( client. is_ok( ) ) ;
228+ }
229+
230+ #[ test]
231+ fn builder_with_no_identity_errors ( ) {
232+ // Regression lock: no username, no api_key — must fail, never
233+ // silently authenticate as some default identity.
234+ // `NativeClient` is not `Debug` (it owns a live connection pool), so
235+ // the Ok payload is discarded before `expect_err`.
236+ let err = ConnectionBuilder :: new ( "127.0.0.1:6433" )
237+ . build ( )
238+ . map ( |_| ( ) )
239+ . expect_err ( "build() must reject a missing identity" ) ;
240+ assert ! ( err. message( ) . contains( "authentication identity" ) ) ;
241+ }
242+
243+ #[ test]
244+ fn builder_password_without_username_errors ( ) {
245+ let err = ConnectionBuilder :: new ( "127.0.0.1:6433" )
246+ . password ( "secret" )
247+ . build ( )
248+ . map ( |_| ( ) )
249+ . expect_err ( "build() must reject password auth without a username" ) ;
250+ assert ! ( err. message( ) . contains( "authentication identity" ) ) ;
142251 }
143252
144253 #[ test]
145254 fn builder_with_database ( ) {
146- // Smoke test: verify the builder accepts a database name without panic.
147255 let _client = ConnectionBuilder :: new ( "127.0.0.1:6433" )
148256 . username ( "alice" )
149257 . database ( "analytics" )
150- . build ( ) ;
258+ . build ( )
259+ . expect ( "username was supplied, build() must succeed" ) ;
151260 }
152261
153262 #[ test]
@@ -156,6 +265,7 @@ mod tests {
156265 . username ( "bob" )
157266 . password ( "secret" )
158267 . database ( "prod" )
159- . build ( ) ;
268+ . build ( )
269+ . expect ( "username was supplied, build() must succeed" ) ;
160270 }
161271}
0 commit comments