fix(server): restore online client approval UX with sdk management
This commit is contained in:
@@ -8,13 +8,19 @@ use arbiter_proto::{
|
||||
},
|
||||
transport::expect_message,
|
||||
};
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, update};
|
||||
use diesel::{
|
||||
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
|
||||
};
|
||||
use diesel_async::RunQueryDsl as _;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use kameo::error::SendError;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
actors::client::ClientConnection,
|
||||
actors::{
|
||||
client::ClientConnection,
|
||||
router::{self, RequestClientApproval},
|
||||
},
|
||||
db::{self, schema::program_client},
|
||||
};
|
||||
|
||||
@@ -34,14 +40,24 @@ pub enum Error {
|
||||
DatabaseOperationFailed,
|
||||
#[error("Invalid challenge solution")]
|
||||
InvalidChallengeSolution,
|
||||
#[error("Client not registered")]
|
||||
NotRegistered,
|
||||
#[error("Client approval request failed")]
|
||||
ApproveError(#[from] ApproveError),
|
||||
#[error("Internal error")]
|
||||
InternalError,
|
||||
#[error("Transport error")]
|
||||
Transport,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ApproveError {
|
||||
#[error("Internal error")]
|
||||
Internal,
|
||||
#[error("Client connection denied by user agents")]
|
||||
Denied,
|
||||
#[error("Upstream error: {0}")]
|
||||
Upstream(router::ApprovalError),
|
||||
}
|
||||
|
||||
/// Atomically reads and increments the nonce for a known client.
|
||||
/// Returns `None` if the pubkey is not registered.
|
||||
async fn get_nonce(
|
||||
@@ -84,6 +100,85 @@ async fn get_nonce(
|
||||
})
|
||||
}
|
||||
|
||||
async fn approve_new_client(
|
||||
actors: &crate::actors::GlobalActors,
|
||||
pubkey: VerifyingKey,
|
||||
) -> Result<(), Error> {
|
||||
let result = actors
|
||||
.router
|
||||
.ask(RequestClientApproval {
|
||||
client_pubkey: pubkey,
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(Error::ApproveError(ApproveError::Denied)),
|
||||
Err(SendError::HandlerError(e)) => {
|
||||
error!(error = ?e, "Approval upstream error");
|
||||
Err(Error::ApproveError(ApproveError::Upstream(e)))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = ?e, "Approval request to router failed");
|
||||
Err(Error::ApproveError(ApproveError::Internal))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum InsertClientResult {
|
||||
Inserted(i32),
|
||||
AlreadyExists,
|
||||
}
|
||||
|
||||
async fn insert_client(
|
||||
db: &db::DatabasePool,
|
||||
pubkey: &VerifyingKey,
|
||||
) -> Result<InsertClientResult, Error> {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i32;
|
||||
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::DatabasePoolUnavailable
|
||||
})?;
|
||||
|
||||
match insert_into(program_client::table)
|
||||
.values((
|
||||
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
||||
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
|
||||
program_client::created_at.eq(now),
|
||||
program_client::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(diesel::result::Error::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::UniqueViolation,
|
||||
_,
|
||||
)) => return Ok(InsertClientResult::AlreadyExists),
|
||||
Err(e) => {
|
||||
error!(error = ?e, "Failed to insert new client");
|
||||
return Err(Error::DatabaseOperationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
let client_id = program_client::table
|
||||
.filter(program_client::public_key.eq(pubkey.as_bytes().to_vec()))
|
||||
.order(program_client::id.desc())
|
||||
.select(program_client::id)
|
||||
.first::<i32>(&mut conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to load inserted client id");
|
||||
Error::DatabaseOperationFailed
|
||||
})?;
|
||||
|
||||
Ok(InsertClientResult::Inserted(client_id))
|
||||
}
|
||||
|
||||
async fn challenge_client(
|
||||
props: &mut ClientConnection,
|
||||
pubkey: VerifyingKey,
|
||||
@@ -134,7 +229,10 @@ async fn challenge_client(
|
||||
|
||||
fn connect_error_code(err: &Error) -> ConnectErrorCode {
|
||||
match err {
|
||||
Error::NotRegistered => ConnectErrorCode::ApprovalDenied,
|
||||
Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied,
|
||||
Error::ApproveError(ApproveError::Upstream(
|
||||
router::ApprovalError::NoUserAgentsConnected,
|
||||
)) => ConnectErrorCode::NoUserAgentsOnline,
|
||||
_ => ConnectErrorCode::Unknown,
|
||||
}
|
||||
}
|
||||
@@ -156,7 +254,16 @@ async fn authenticate(props: &mut ClientConnection) -> Result<(VerifyingKey, i32
|
||||
|
||||
let (client_id, nonce) = match get_nonce(&props.db, &pubkey).await? {
|
||||
Some((client_id, nonce)) => (client_id, nonce),
|
||||
None => return Err(Error::NotRegistered),
|
||||
None => {
|
||||
approve_new_client(&props.actors, pubkey).await?;
|
||||
match insert_client(&props.db, &pubkey).await? {
|
||||
InsertClientResult::Inserted(client_id) => (client_id, 0),
|
||||
InsertClientResult::AlreadyExists => match get_nonce(&props.db, &pubkey).await? {
|
||||
Some((client_id, nonce)) => (client_id, nonce),
|
||||
None => return Err(Error::InternalError),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
challenge_client(props, pubkey, nonce).await?;
|
||||
|
||||
Reference in New Issue
Block a user