Skip to content

Commit 6e2d309

Browse files
authored
Introduce Rust driver token-based authentication (#749)
## Usage and product changes We update the protocol version and introduce token-based authentication for all the available drivers. Now, instead of sending usernames and passwords for authentication purposes, authorization tokens are implicitly added to every network request to a TypeDB server. It enhances the authentication speed and security. These tokens are acquired as a result of driver instantiation and are renewed automatically. This feature does not require any user-side changes. Additionally, as a part of the HTTP client introduction work, we rename Concept Documents key style from snake_case to camelCase, which is a common convention for JSONs (it affects only value types: `value_type` -> `valueType`). ## Implementation Every network call adds tokens instead of user credentials and is repeated after a token renewal request if it receives an authentication error.
1 parent e216db8 commit 6e2d309

File tree

14 files changed

+135
-100
lines changed

14 files changed

+135
-100
lines changed

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dependencies/typedb/artifacts.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def typedb_artifact():
2525
artifact_name = "typedb-all-{platform}-{version}.{ext}",
2626
tag_source = deployment["artifact"]["release"]["download"],
2727
commit_source = deployment["artifact"]["snapshot"]["download"],
28-
commit = "588d742a36b24423b40eaf05d8013c0cd188425f"
28+
commit = "f98dba89712c7bc2a3418e8d65326af75c3bf150"
2929
)
3030

3131
#def typedb_cloud_artifact():

dependencies/typedb/repositories.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ def typedb_protocol():
2828
git_repository(
2929
name = "typedb_protocol",
3030
remote = "https://github.com/typedb/typedb-protocol",
31-
tag = "3.1.0", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_protocol
31+
commit = "9e46e089a005d6ca9f017ffb482337b9d5718695", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_protocol
3232
)
3333

3434
def typedb_behaviour():
3535
git_repository(
3636
name = "typedb_behaviour",
3737
remote = "https://github.com/typedb/typedb-behaviour",
38-
commit = "7c2df22e4378b32e53440df4a7b20d59f2ea0e28", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_behaviour
38+
commit = "57fc7216f4b0df05ce29044a1ba621e44bdd87e1", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_behaviour
3939
)

rust/Cargo.toml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
version = "1.13.0"
3232
default-features = false
3333

34-
[dev-dependencies.steps]
35-
path = "../rust/tests/behaviour/steps"
34+
[dev-dependencies.config]
35+
path = "../rust/tests/behaviour/config"
3636
features = []
3737
default-features = false
3838

39-
[dev-dependencies.config]
40-
path = "../rust/tests/behaviour/config"
39+
[dev-dependencies.steps]
40+
path = "../rust/tests/behaviour/steps"
4141
features = []
4242
default-features = false
4343

@@ -60,7 +60,7 @@
6060

6161
[dependencies.typedb-protocol]
6262
features = []
63-
rev = "7c7e24c43a59ac3e3b2c4bb17566eb9478099c21"
63+
rev = "9e46e089a005d6ca9f017ffb482337b9d5718695"
6464
git = "https://github.com/typedb/typedb-protocol"
6565
default-features = false
6666

@@ -79,16 +79,16 @@
7979
version = "0.3.31"
8080
default-features = false
8181

82-
[dependencies.uuid]
83-
features = ["default", "fast-rng", "rng", "serde", "std", "v4"]
84-
version = "1.15.1"
85-
default-features = false
86-
8782
[dependencies.itertools]
8883
features = ["default", "use_alloc", "use_std"]
8984
version = "0.10.5"
9085
default-features = false
9186

87+
[dependencies.uuid]
88+
features = ["default", "fast-rng", "rng", "serde", "std", "v4"]
89+
version = "1.15.1"
90+
default-features = false
91+
9292
[dependencies.prost]
9393
features = ["default", "derive", "prost-derive", "std"]
9494
version = "0.13.5"

rust/src/answer/concept_document.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ impl Leaf {
116116

117117
const KIND: Cow<'static, str> = Cow::Borrowed("kind");
118118
const LABEL: Cow<'static, str> = Cow::Borrowed("label");
119-
const VALUE_TYPE: Cow<'static, str> = Cow::Borrowed("value_type");
119+
const VALUE_TYPE: Cow<'static, str> = Cow::Borrowed("valueType");
120120

121121
fn json_type(kind: Kind, label: Cow<'static, str>) -> JSON {
122122
JSON::Object([(KIND, json_kind(kind)), (LABEL, JSON::String(label))].into())

rust/src/common/error.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::{collections::HashSet, error::Error as StdError, fmt};
2121

2222
use itertools::Itertools;
2323
use tonic::{Code, Status};
24-
use tonic_types::StatusExt;
24+
use tonic_types::{ErrorDetails, ErrorInfo, StatusExt};
2525

2626
use super::{address::Address, RequestID};
2727

@@ -150,7 +150,7 @@ error_messages! { ConnectionError
150150
15: "The replica is not the primary replica.",
151151
ClusterAllNodesFailed { errors: String } =
152152
16: "Attempted connecting to all TypeDB Cluster servers, but the following errors occurred: \n{errors}.",
153-
ClusterTokenCredentialInvalid =
153+
TokenCredentialInvalid =
154154
17: "Invalid token credentials.",
155155
EncryptionSettingsMismatch =
156156
18: "Unable to connect to TypeDB: possible encryption settings mismatch.",
@@ -275,6 +275,16 @@ impl Error {
275275
}
276276
}
277277

278+
fn try_extracting_connection_error(status: &Status, code: &str) -> Option<ConnectionError> {
279+
// TODO: We should probably catch more connection errors instead of wrapping them into
280+
// ServerErrors. However, the most valuable information even for connection is inside
281+
// stacktraces now.
282+
match code {
283+
"AUT2" | "AUT3" => Some(ConnectionError::TokenCredentialInvalid {}),
284+
_ => None,
285+
}
286+
}
287+
278288
fn from_message(message: &str) -> Self {
279289
// TODO: Consider converting some of the messages to connection errors
280290
Self::Other(message.to_owned())
@@ -352,9 +362,13 @@ impl From<Status> for Error {
352362
})
353363
} else if let Some(error_info) = details.error_info() {
354364
let code = error_info.reason.clone();
365+
if let Some(connection_error) = Self::try_extracting_connection_error(&status, &code) {
366+
return Self::Connection(connection_error);
367+
}
355368
let domain = error_info.domain.clone();
356369
let stack_trace =
357370
if let Some(debug_info) = details.debug_info() { debug_info.stack_entries.clone() } else { vec![] };
371+
358372
Self::Server(ServerError::new(code, domain, status.message().to_owned(), stack_trace))
359373
} else {
360374
Self::from_message(status.message())
@@ -364,7 +378,6 @@ impl From<Status> for Error {
364378
Self::parse_unavailable(status.message())
365379
} else if status.code() == Code::Unknown
366380
|| is_rst_stream(&status)
367-
|| status.code() == Code::InvalidArgument
368381
|| status.code() == Code::FailedPrecondition
369382
|| status.code() == Code::AlreadyExists
370383
{

rust/src/connection/message.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ use crate::{
3535
error::ServerError,
3636
info::UserInfo,
3737
user::User,
38-
Options, TransactionType,
38+
Credentials, Options, TransactionType,
3939
};
4040

4141
#[derive(Debug)]
4242
pub(super) enum Request {
43-
ConnectionOpen { driver_lang: String, driver_version: String },
43+
ConnectionOpen { driver_lang: String, driver_version: String, credentials: Credentials },
4444

4545
ServersAll,
4646

rust/src/connection/network/channel.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
* under the License.
1818
*/
1919

20-
use std::sync::Arc;
20+
use std::sync::{Arc, RwLock};
2121

2222
use tonic::{
2323
body::BoxBody,
2424
client::GrpcService,
25+
metadata::MetadataValue,
2526
service::{
2627
interceptor::{InterceptedService, ResponseFuture as InterceptorResponseFuture},
2728
Interceptor,
@@ -65,20 +66,33 @@ pub(super) fn open_callcred_channel(
6566
#[derive(Debug)]
6667
pub(super) struct CallCredentials {
6768
credentials: Credentials,
69+
token: RwLock<Option<String>>,
6870
}
6971

7072
impl CallCredentials {
7173
pub(super) fn new(credentials: Credentials) -> Self {
72-
Self { credentials }
74+
Self { credentials, token: RwLock::new(None) }
7375
}
7476

75-
pub(super) fn username(&self) -> &str {
76-
self.credentials.username()
77+
pub(super) fn credentials(&self) -> &Credentials {
78+
&self.credentials
79+
}
80+
81+
pub(super) fn set_token(&self, token: String) {
82+
*self.token.write().expect("Expected token write lock acquisition on set") = Some(token);
83+
}
84+
85+
pub(super) fn reset_token(&self) {
86+
*self.token.write().expect("Expected token write lock acquisition on reset") = None;
7787
}
7888

7989
pub(super) fn inject(&self, mut request: Request<()>) -> Request<()> {
80-
request.metadata_mut().insert("username", self.credentials.username().try_into().unwrap());
81-
request.metadata_mut().insert("password", self.credentials.password().try_into().unwrap());
90+
if let Some(token) = &*self.token.read().expect("Expected token read lock acquisition on inject") {
91+
request.metadata_mut().insert(
92+
"authorization",
93+
format!("Bearer {}", token).try_into().expect("Expected authorization header formatting"),
94+
);
95+
}
8296
request
8397
}
8498
}

rust/src/connection/network/proto/message.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
use itertools::Itertools;
2121
use typedb_protocol::{
22-
connection, database, database_manager, query::initial_res::Res, server_manager, transaction, user, user_manager,
23-
Version::Version,
22+
authentication, connection, database, database_manager, query::initial_res::Res, server_manager, transaction, user,
23+
user_manager, Version::Version,
2424
};
2525
use uuid::Uuid;
2626

@@ -32,14 +32,18 @@ use crate::{
3232
error::{ConnectionError, InternalError, ServerError},
3333
info::UserInfo,
3434
user::User,
35+
Credentials,
3536
};
3637

3738
impl TryIntoProto<connection::open::Req> for Request {
3839
fn try_into_proto(self) -> Result<connection::open::Req> {
3940
match self {
40-
Self::ConnectionOpen { driver_lang, driver_version } => {
41-
Ok(connection::open::Req { version: Version.into(), driver_lang, driver_version })
42-
}
41+
Self::ConnectionOpen { driver_lang, driver_version, credentials } => Ok(connection::open::Req {
42+
version: Version.into(),
43+
driver_lang,
44+
driver_version,
45+
authentication: Some(credentials.try_into_proto()?),
46+
}),
4347
other => Err(InternalError::UnexpectedRequestType { request_type: format!("{other:?}") }.into()),
4448
}
4549
}
@@ -225,14 +229,28 @@ impl TryIntoProto<user::delete::Req> for Request {
225229
}
226230
}
227231

232+
impl TryIntoProto<authentication::token::create::Req> for Credentials {
233+
fn try_into_proto(self) -> Result<authentication::token::create::Req> {
234+
Ok(authentication::token::create::Req {
235+
credentials: Some(authentication::token::create::req::Credentials::Password(
236+
authentication::token::create::req::Password {
237+
username: self.username().to_owned(),
238+
password: self.password().to_owned(),
239+
},
240+
)),
241+
})
242+
}
243+
}
244+
228245
impl TryFromProto<connection::open::Res> for Response {
229246
fn try_from_proto(proto: connection::open::Res) -> Result<Self> {
230247
let mut database_infos = Vec::new();
231-
for database_info_proto in proto.databases_all.unwrap().databases {
248+
for database_info_proto in proto.databases_all.expect("Expected databases data").databases {
232249
database_infos.push(DatabaseInfo::try_from_proto(database_info_proto)?);
233250
}
234251
Ok(Self::ConnectionOpen {
235-
connection_id: Uuid::from_slice(proto.connection_id.unwrap().id.as_slice()).unwrap(),
252+
connection_id: Uuid::from_slice(proto.connection_id.expect("Expected connection id").id.as_slice())
253+
.unwrap(),
236254
server_duration_millis: proto.server_duration_millis,
237255
databases: database_infos,
238256
})

rust/src/connection/network/stub.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ use tokio::sync::mpsc::{unbounded_channel as unbounded_async, UnboundedSender};
2525
use tokio_stream::wrappers::UnboundedReceiverStream;
2626
use tonic::{Response, Status, Streaming};
2727
use typedb_protocol::{
28-
connection, database, database_manager, server_manager, transaction, type_db_client::TypeDbClient as GRPC, user,
29-
user_manager,
28+
authentication, connection, database, database_manager, server_manager, transaction,
29+
type_db_client::TypeDbClient as GRPC, user, user_manager,
3030
};
3131

3232
use super::channel::{CallCredentials, GRPCChannel};
33-
use crate::common::{error::ConnectionError, Error, Result, StdResult};
33+
use crate::{
34+
common::{error::ConnectionError, Error, Result, StdResult},
35+
connection::network::proto::TryIntoProto,
36+
};
3437

3538
type TonicResult<T> = StdResult<Response<T>, Status>;
3639

@@ -45,15 +48,41 @@ impl<Channel: GRPCChannel> RPCStub<Channel> {
4548
Self { grpc: GRPC::new(channel), call_credentials }
4649
}
4750

48-
async fn call<F, R>(&mut self, call: F) -> Result<R>
51+
async fn call_with_auto_renew_token<F, R>(&mut self, call: F) -> Result<R>
4952
where
5053
for<'a> F: Fn(&'a mut Self) -> BoxFuture<'a, Result<R>>,
5154
{
52-
call(self).await
55+
match call(self).await {
56+
Err(Error::Connection(ConnectionError::TokenCredentialInvalid)) => {
57+
debug!("Request rejected because token credential was invalid. Renewing token and trying again...");
58+
self.renew_token().await?;
59+
call(self).await
60+
}
61+
res => res,
62+
}
63+
}
64+
65+
async fn renew_token(&mut self) -> Result {
66+
if let Some(call_credentials) = &self.call_credentials {
67+
trace!("Renewing token...");
68+
call_credentials.reset_token();
69+
let request = call_credentials.credentials().clone().try_into_proto()?;
70+
let token = self.grpc.authentication_token_create(request).await?.into_inner().token;
71+
call_credentials.set_token(token);
72+
trace!("Token renewed");
73+
}
74+
Ok(())
5375
}
5476

5577
pub(super) async fn connection_open(&mut self, req: connection::open::Req) -> Result<connection::open::Res> {
56-
self.single(|this| Box::pin(this.grpc.connection_open(req.clone()))).await
78+
let result = self.single(|this| Box::pin(this.grpc.connection_open(req.clone()))).await;
79+
if let Ok(response) = &result {
80+
if let Some(call_credentials) = &self.call_credentials {
81+
call_credentials
82+
.set_token(response.authentication.as_ref().expect("Expected authentication token").token.clone());
83+
}
84+
}
85+
result
5786
}
5887

5988
pub(super) async fn servers_all(&mut self, req: server_manager::all::Req) -> Result<server_manager::all::Res> {
@@ -107,7 +136,7 @@ impl<Channel: GRPCChannel> RPCStub<Channel> {
107136
&mut self,
108137
open_req: transaction::Req,
109138
) -> Result<(UnboundedSender<transaction::Client>, Streaming<transaction::Server>)> {
110-
self.call(|this| {
139+
self.call_with_auto_renew_token(|this| {
111140
let transaction_req = transaction::Client { reqs: vec![open_req.clone()] };
112141
Box::pin(async {
113142
let (sender, receiver) = unbounded_async();
@@ -154,6 +183,6 @@ impl<Channel: GRPCChannel> RPCStub<Channel> {
154183
for<'a> F: Fn(&'a mut Self) -> BoxFuture<'a, TonicResult<R>> + Send + Sync,
155184
R: 'static,
156185
{
157-
self.call(|this| Box::pin(call(this).map(|r| Ok(r?.into_inner())))).await
186+
self.call_with_auto_renew_token(|this| Box::pin(call(this).map(|r| Ok(r?.into_inner())))).await
158187
}
159188
}

0 commit comments

Comments
 (0)