1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// SPDX-License-Identifier: BUSL-1.1
//! Native-protocol auth handshake: authenticates the client, assembles the
//! three-level (global/database/tenant) admission permit, and builds the
//! auth response.
use nodedb_types::protocol::{NativeResponse, RequestFields};
use crate::control::server::admission::ConnectionPermit;
use super::NativeSession;
use super::dispatch;
impl NativeSession {
/// Handle authentication request.
pub(super) async fn handle_auth(&mut self, seq: u64, fields: &RequestFields) -> NativeResponse {
// Re-authentication is not supported on the native protocol. Once a
// session has assembled its three-level admission permit, the identity
// is fixed for the connection's lifetime — allowing re-auth would let
// a client silently swap to a different (database, tenant) scope while
// still holding the original scope's connection slots.
if self.identity.is_some() || self.connection_permit.is_some() {
return NativeResponse::error(
seq,
"0A000",
"already authenticated; reconnect to switch identity",
);
}
let auth = match fields {
RequestFields::Text(f) => match &f.auth {
Some(a) => a,
None => {
return NativeResponse::error(seq, "28000", "missing 'auth' field");
}
},
_ => {
return NativeResponse::error(seq, "0A000", "unsupported request fields variant");
}
};
match dispatch::handle_auth(
&self.state,
&self.auth_mode,
auth,
&self.peer_addr.to_string(),
)
.await
{
Ok((identity, warning)) => {
// Phase 2 admission: acquire per-database and per-tenant permits
// now that we know the identity. The database scope is the
// identity's default database (or DEFAULT if none is set).
let db_id = identity
.default_database
.unwrap_or(nodedb_types::DatabaseId::DEFAULT);
let tenant_id = identity.tenant_id;
let db_permit = match self.admission_registry.try_acquire_database(db_id) {
Ok(p) => p,
Err(e) => {
return NativeResponse::error(
seq,
nodedb_types::error::sqlstate::QUOTA_EXCEEDED,
format!("{e}"),
);
}
};
let tenant_permit =
match self.admission_registry.try_acquire_tenant(db_id, tenant_id) {
Ok(p) => p,
Err(e) => {
// db_permit is dropped here, releasing the DB slot.
drop(db_permit);
return NativeResponse::error(
seq,
nodedb_types::error::sqlstate::QUOTA_EXCEEDED,
format!("{e}"),
);
}
};
// Assemble the three-level permit. The global slot moves from
// `global_permit` into the `ConnectionPermit`. The re-auth
// guard at the top of this function ensures `global_permit`
// is still `Some` here — it is initialized at construction
// and only consumed on the auth path.
let Some(global) = self.global_permit.take() else {
// Release the freshly acquired Phase 2 permits so we
// don't leak slots into the per-DB / per-tenant pools.
drop(tenant_permit);
drop(db_permit);
return NativeResponse::error(
seq,
"XX000",
"internal error: global admission permit missing during auth assembly",
);
};
self.connection_permit = Some(ConnectionPermit {
global,
database: db_permit,
tenant: tenant_permit,
db_id,
tenant_id,
});
let mut resp = NativeResponse::auth_ok(
seq,
identity.username.clone(),
identity.tenant_id.as_u64(),
);
if let Some(w) = warning {
resp.warnings.push(w);
}
self.auth_context = Some(super::super::super::session_auth::build_auth_context(
&identity,
));
self.identity = Some(identity);
resp
}
// A transient login rate-limit is distinct from a credential
// failure: it maps to TOO_MANY_CONNECTIONS (53300), which clients
// recognise as retryable, and carries a distinct message. Every
// other auth error (wrong password, lockout, unknown user) stays
// collapsed into the generic invalid-password 28P01 so none can be
// distinguished from the others.
Err(e @ crate::Error::RateExceeded { .. }) => NativeResponse::error(
seq,
nodedb_types::error::sqlstate::TOO_MANY_CONNECTIONS,
format!("{e}"),
),
Err(e) => NativeResponse::error(seq, "28P01", format!("{e}")),
}
}
}