Compare commits

..
Author SHA1 Message Date
3e7a7b073e Update Rust crate rand_core to 0.10
Some checks failed
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
2026-08-29 14:02:37 +03:00
19 changed files with 341 additions and 1010 deletions

3
Cargo.lock generated
View file

@ -5017,10 +5017,7 @@ name = "web-ui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"iota-cli",
"iota-ipc",
"iota-logger", "iota-logger",
"iota-paths",
"iota-state", "iota-state",
"iota-storage", "iota-storage",
"iota-util", "iota-util",

View file

@ -280,6 +280,12 @@ impl ClientConnection {
return; return;
} }
if cv.is_type(CommunicationType::AddConversation) {
self.send_message(&message_handlers::handle_add_conversation(&cv))
.await;
return;
}
if cv.is_type(CommunicationType::AddCommunity) { if cv.is_type(CommunicationType::AddCommunity) {
self.send_message(&message_handlers::handle_add_community(&cv)) self.send_message(&message_handlers::handle_add_community(&cv))
.await; .await;
@ -410,9 +416,7 @@ impl ClientConnection {
return; return;
}; };
let Some(encrypted_challenge) = cv.get_data(DataType::Challenge).as_str() else { let Some(encrypted_challenge) = cv.get_data(DataType::Challenge).as_str() else { return };
return;
};
let solved = crypto_util::decrypt_challenge(encrypted_challenge, &keyring).ok(); let solved = crypto_util::decrypt_challenge(encrypted_challenge, &keyring).ok();

View file

@ -10,7 +10,6 @@ use mtp::codec::{
}; };
use crate::relay::VerifiedRelayContext; use crate::relay::VerifiedRelayContext;
use iota_storage::storage_error::StorageError;
#[derive(Debug)] #[derive(Debug)]
pub struct MessageMutation { pub struct MessageMutation {
@ -62,20 +61,6 @@ pub fn success_response(cv: &CommunicationValue) -> CommunicationValue {
error_response(cv, CommunicationType::Success) error_response(cv, CommunicationType::Success)
} }
fn add_conversation_for_user(
user_id: i64,
other_id: i64,
name: Option<&str>,
) -> Result<(), StorageError> {
let mut contact = get_user(user_id, other_id)?
.unwrap_or_else(|| iota_storage::users::contact::Contact::new(other_id));
if let Some(name) = name {
contact.user_name = Some(name.to_string());
}
contact.set_last_message_at(now_millis_i64());
mod_user(user_id, &contact)
}
fn relay_field<'a>( fn relay_field<'a>(
payload: &'a DataValue, payload: &'a DataValue,
data_type: DataType, data_type: DataType,
@ -157,11 +142,14 @@ pub fn apply_verified_relay_content(
match content.message_type.as_str() { match content.message_type.as_str() {
"MessageState" => { "MessageState" => {
let partner_id = let partner_id = relay_number(
relay_number(&content.content, DataType::ChatPartnerId, &context.type_map) &content.content,
.and_then(|value| i64::try_from(value).ok()) DataType::ChatPartnerId,
.filter(|id| *id == recipient_id) &context.type_map,
.ok_or_else(|| "Relay MessageState has an invalid ChatPartnerId".to_string())?; )
.and_then(|value| i64::try_from(value).ok())
.filter(|id| *id == recipient_id)
.ok_or_else(|| "Relay MessageState has an invalid ChatPartnerId".to_string())?;
let relay_message_id = relay_string( let relay_message_id = relay_string(
&content.content, &content.content,
DataType::RelayMessageId, DataType::RelayMessageId,
@ -171,10 +159,14 @@ pub fn apply_verified_relay_content(
let event_at = relay_number(&content.content, DataType::EventAt, &context.type_map) let event_at = relay_number(&content.content, DataType::EventAt, &context.type_map)
.and_then(|value| i64::try_from(value).ok()) .and_then(|value| i64::try_from(value).ok())
.ok_or_else(|| "Relay MessageState is missing EventAt".to_string())?; .ok_or_else(|| "Relay MessageState is missing EventAt".to_string())?;
let state = relay_string(&content.content, DataType::MessageState, &context.type_map) let state = relay_string(
.map(MessageState::from_str) &content.content,
.filter(|state| matches!(state, MessageState::Received | MessageState::Read)) DataType::MessageState,
.ok_or_else(|| "Relay MessageState has an invalid state".to_string())?; &context.type_map,
)
.map(MessageState::from_str)
.filter(|state| matches!(state, MessageState::Received | MessageState::Read))
.ok_or_else(|| "Relay MessageState has an invalid state".to_string())?;
chat_files::record_message_receipt( chat_files::record_message_receipt(
storage_owner, storage_owner,
recipient_id, recipient_id,
@ -200,28 +192,22 @@ pub fn apply_verified_relay_content(
.unwrap_or_default(); .unwrap_or_default();
let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map) let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map)
.and_then(|value| i64::try_from(value).ok()); .and_then(|value| i64::try_from(value).ok());
if relay_string( let relay_message_id = relay_string(
&content.content, &content.content,
DataType::RelayMessageId, DataType::RelayMessageId,
&context.type_map, &context.type_map,
) )
.is_some_and(|relay_message_id| relay_message_id != context.message_id) .ok_or_else(|| "Relay MessageSend is missing RelayMessageId".to_string())?;
{ if relay_message_id != context.message_id {
return Err( return Err("Relay MessageSend identity does not match its protected message ID".into());
"Relay MessageSend identity does not match its protected message ID".into(),
);
} }
chat_files::add_message(chat_files::NewMessage { chat_files::add_message(chat_files::NewMessage {
relay_signer_id: sender_id, relay_signer_id: sender_id,
relay_message_id: &context.message_id, relay_message_id,
authored_at: created_at, authored_at: created_at,
send_time, send_time,
storage_owner, storage_owner,
external_user: if sent_by_self { external_user: if sent_by_self { recipient_id } else { sender_id },
recipient_id
} else {
sender_id
},
sent_by_self, sent_by_self,
content: message, content: message,
height, height,
@ -270,7 +256,7 @@ pub fn apply_verified_relay_content(
.ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?; .ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?;
let recipient = recipients let recipient = recipients
.into_iter() .into_iter()
.find(|value| value.user_id == storage_owner.to_string()) .find(|value| value.user_id == context.final_recipient_id.to_string())
.ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?; .ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?;
let chat_id = data_string(&frame, DataType::ChatId) let chat_id = data_string(&frame, DataType::ChatId)
.ok_or_else(|| "Relay SetChatSecret is missing ChatId".to_string())?; .ok_or_else(|| "Relay SetChatSecret is missing ChatId".to_string())?;
@ -281,7 +267,7 @@ pub fn apply_verified_relay_content(
let wrapping_scheme = data_string(&frame, DataType::WrappingScheme) let wrapping_scheme = data_string(&frame, DataType::WrappingScheme)
.ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?; .ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?;
e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret { e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret {
user_id: storage_owner.to_string(), user_id: context.final_recipient_id.to_string(),
chat_id, chat_id,
secret_id, secret_id,
version, version,
@ -293,29 +279,6 @@ pub fn apply_verified_relay_content(
}) })
.map_err(|error| error.to_string()) .map_err(|error| error.to_string())
} }
"AddConversation" => {
let other_id =
relay_number(&content.content, DataType::ChatPartnerId, &context.type_map)
.and_then(|value| i64::try_from(value).ok())
.filter(|id| *id > 0)
.ok_or_else(|| {
"Relay AddConversation has an invalid ChatPartnerId".to_string()
})?;
let user_id = storage_owner;
if user_id <= 0 {
return Err("Relay AddConversation has an invalid storage owner".into());
}
add_conversation_for_user(
user_id,
other_id,
relay_string(
&content.content,
DataType::ChatPartnerName,
&context.type_map,
),
)
.map_err(|error| format!("AddConversation persistence failed: {error}"))
}
_ => Ok(()), _ => Ok(()),
} }
} }
@ -406,7 +369,10 @@ fn stored_message_fields(
DataType::SendTime, DataType::SendTime,
DataValue::SignedNumber(message.message_time as i128), DataValue::SignedNumber(message.message_time as i128),
), ),
(DataType::Content, DataValue::Str(message.content.clone())), (
DataType::Content,
DataValue::Str(message.content.clone()),
),
( (
DataType::MessageState, DataType::MessageState,
DataValue::Str(message.message_state.clone()), DataValue::Str(message.message_state.clone()),
@ -432,19 +398,10 @@ fn stored_message_fields(
} }
for (data_type, timestamp) in [ for (data_type, timestamp) in [
(DataType::AuthoredAt, message.authored_at), (DataType::AuthoredAt, message.authored_at),
( (DataType::OriginIotaReceivedAt, message.origin_iota_received_at),
DataType::OriginIotaReceivedAt, (DataType::DestinationIotaReceivedAt, message.destination_iota_received_at),
message.origin_iota_received_at,
),
(
DataType::DestinationIotaReceivedAt,
message.destination_iota_received_at,
),
(DataType::ClientReceivedAt, message.client_received_at), (DataType::ClientReceivedAt, message.client_received_at),
( (DataType::ClientReceivedRecordedAt, message.client_received_recorded_at),
DataType::ClientReceivedRecordedAt,
message.client_received_recorded_at,
),
(DataType::ReadAt, message.read_at), (DataType::ReadAt, message.read_at),
(DataType::ReadRecordedAt, message.read_recorded_at), (DataType::ReadRecordedAt, message.read_recorded_at),
] { ] {
@ -586,11 +543,7 @@ pub fn handle_create_app(cv: &CommunicationValue) -> CommunicationValue {
.to_string(); .to_string();
if !app_identifier.is_empty() && !app_public_key.is_empty() { if !app_identifier.is_empty() && !app_public_key.is_empty() {
let user = match iota_storage::users::user_manager::get_user(sender_id) { if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) {
Ok(user) => user,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
};
if let Some(mut user) = user {
if !user.trusted_apps.contains_key(&app_identifier) { if !user.trusted_apps.contains_key(&app_identifier) {
user.trusted_apps.insert(app_identifier, app_public_key); user.trusted_apps.insert(app_identifier, app_public_key);
iota_storage::users::user_manager::update_user(user); iota_storage::users::user_manager::update_user(user);
@ -615,11 +568,7 @@ pub fn handle_delete_app(cv: &CommunicationValue) -> CommunicationValue {
.to_string(); .to_string();
if !app_identifier.is_empty() { if !app_identifier.is_empty() {
let user = match iota_storage::users::user_manager::get_user(sender_id) { if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) {
Ok(user) => user,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
};
if let Some(mut user) = user {
if user.trusted_apps.contains_key(&app_identifier) { if user.trusted_apps.contains_key(&app_identifier) {
user.trusted_apps.remove(&app_identifier); user.trusted_apps.remove(&app_identifier);
iota_storage::users::user_manager::update_user(user); iota_storage::users::user_manager::update_user(user);
@ -663,12 +612,12 @@ fn contact_value(
typed_container(fields) typed_container(fields)
} }
fn current_contact_ids(user_id: i64) -> Result<DataValue, StorageError> { fn current_contact_ids(user_id: i64) -> DataValue {
Ok(contact_ids_value( contact_ids_value(
chats_util::get_users(user_id)? chats_util::get_users(user_id)
.into_iter() .into_iter()
.map(|contact| contact.user_id), .map(|contact| contact.user_id),
)) )
} }
fn contact_ids_value(ids: impl IntoIterator<Item = i64>) -> DataValue { fn contact_ids_value(ids: impl IntoIterator<Item = i64>) -> DataValue {
@ -765,10 +714,7 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
Err(_) => return sync_error(cv), Err(_) => return sync_error(cv),
}; };
( (
match chats_util::get_users(user_id) { chats_util::get_users(user_id),
Ok(contacts) => contacts,
Err(_) => return sync_error(cv),
},
chat_files::get_all_messages(user_id), chat_files::get_all_messages(user_id),
settings, settings,
Vec::new(), Vec::new(),
@ -785,10 +731,7 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
Err(_) => return sync_error(cv), Err(_) => return sync_error(cv),
}; };
( (
match chats_util::get_users_by_ids(user_id, &delta.contact_upserts) { chats_util::get_users_by_ids(user_id, &delta.contact_upserts),
Ok(contacts) => contacts,
Err(_) => return sync_error(cv),
},
chat_files::get_messages_by_ids(user_id, &delta.message_upserts), chat_files::get_messages_by_ids(user_id, &delta.message_upserts),
settings, settings,
delta.deleted_message_ids, delta.deleted_message_ids,
@ -803,10 +746,7 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
Err(_) => return sync_error(cv), Err(_) => return sync_error(cv),
}; };
( (
match chats_util::get_users(user_id) { chats_util::get_users(user_id),
Ok(contacts) => contacts,
Err(_) => return sync_error(cv),
},
chat_files::get_all_messages(user_id), chat_files::get_all_messages(user_id),
settings, settings,
Vec::new(), Vec::new(),
@ -821,10 +761,6 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
.iter() .iter()
.map(|message| stored_message_value(message, user_id, message.external_user)) .map(|message| stored_message_value(message, user_id, message.external_user))
.collect(); .collect();
let contact_ids = match current_contact_ids(user_id) {
Ok(contact_ids) => contact_ids,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
};
CommunicationValue::new(CommunicationType::ClientStateSync) CommunicationValue::new(CommunicationType::ClientStateSync)
.with_request_id(cv) .with_request_id(cv)
.with_receiver(sender_wire_id(user_id)) .with_receiver(sender_wire_id(user_id))
@ -886,7 +822,7 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
.collect(), .collect(),
), ),
) )
.add_typed_default(DataType::UserIds, contact_ids) .add_typed_default(DataType::UserIds, current_contact_ids(user_id))
.add_typed_default(DataType::Calls, DataValue::Array(Vec::new())) .add_typed_default(DataType::Calls, DataValue::Array(Vec::new()))
} }
@ -1012,10 +948,7 @@ pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue {
let Ok(user_id_i64) = i64::try_from(user_id) else { let Ok(user_id_i64) = i64::try_from(user_id) else {
return error_response(cv, CommunicationType::ErrorInvalidData); return error_response(cv, CommunicationType::ErrorInvalidData);
}; };
let users = match chats_util::get_users(user_id_i64) { let users = chats_util::get_users(user_id_i64);
Ok(users) => users,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
};
let mut user_array = Vec::new(); let mut user_array = Vec::new();
for user in users { for user in users {
let mut container = Vec::new(); let mut container = Vec::new();
@ -1037,6 +970,41 @@ pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue {
.add_typed_default(DataType::UserIds, DataValue::Array(user_array)) .add_typed_default(DataType::UserIds, DataValue::Array(user_array))
} }
pub fn handle_add_conversation(cv: &CommunicationValue) -> CommunicationValue {
let user_id = match cv.require_sender() {
Ok(user_id) => user_id,
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
};
let Ok(user_id_i64) = i64::try_from(user_id) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let session_id = match data_i64(cv, DataType::SessionId) {
Some(id) if id > 0 => id,
_ => return sync_error(cv),
};
let other_id = match data_i64(cv, DataType::ChatPartnerId) {
Some(id) if id > 0 => id,
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
};
let mut contact = get_user(user_id_i64, other_id)
.unwrap_or(iota_storage::users::contact::Contact::new(other_id));
if let Some(name) = cv.get_data(DataType::ChatPartnerName).as_str() {
contact.user_name = Some(name.to_string());
}
contact.set_last_message_at(now_millis_i64());
mod_user(user_id_i64, &contact);
CommunicationValue::new(CommunicationType::AddConversation)
.with_request_id(cv)
.with_receiver(user_id)
.add_typed_default(
DataType::SessionId,
DataValue::SignedNumber(session_id as i128),
)
.add_typed_default(DataType::UserIds, current_contact_ids(user_id_i64))
}
pub fn handle_add_community(cv: &CommunicationValue) -> CommunicationValue { pub fn handle_add_community(cv: &CommunicationValue) -> CommunicationValue {
let sender_id = match required_sender_id(cv) { let sender_id = match required_sender_id(cv) {
Ok(sender_id) => sender_id, Ok(sender_id) => sender_id,

View file

@ -7,14 +7,11 @@ use iota_ipc::{
TaskSummary, UpdateStatusResponse, UserDetailResponse, UserSummary, TaskSummary, UpdateStatusResponse, UserDetailResponse, UserSummary,
}; };
use iota_logger::{log, log_command}; use iota_logger::{log, log_command};
use iota_storage::users::pending_operations::{
self, PendingUserOperation, PendingUserOperationKind, PendingUserOperationPhase,
};
use iota_storage::users::user_manager; use iota_storage::users::user_manager;
use iota_storage::util::config_util::{self}; use iota_storage::util::config_util::{self};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::Duration;
use crate::daemon_state::{ShutdownReason, StartupPhase}; use crate::daemon_state::{ShutdownReason, StartupPhase};
@ -29,13 +26,6 @@ pub struct PeerContext {
const MAX_LOG_ENTRIES_PER_RESPONSE: usize = 512; const MAX_LOG_ENTRIES_PER_RESPONSE: usize = 512;
fn now_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
fn bounded_log_entries(mut entries: Vec<LogEntry>) -> Vec<LogEntry> { fn bounded_log_entries(mut entries: Vec<LogEntry>) -> Vec<LogEntry> {
entries.truncate(MAX_LOG_ENTRIES_PER_RESPONSE); entries.truncate(MAX_LOG_ENTRIES_PER_RESPONSE);
while !entries.is_empty() { while !entries.is_empty() {
@ -162,38 +152,32 @@ impl CommandRouter {
ResponseResult::Ok(ResponsePayload::Tasks(tasks)) ResponseResult::Ok(ResponsePayload::Tasks(tasks))
} }
LocalRequest::ListUsers => { LocalRequest::ListUsers => {
let users = user_manager::get_residency() let users: Vec<UserSummary> = user_manager::get_residency()
.into_iter() .into_iter()
.map(|user| { .map(|user| UserSummary {
let profile = user_manager::get_user(user.user_id)?; credential_present: user.state == user_manager::LocalUserState::Managed
Ok(UserSummary { && user_manager::get_user(user.user_id).is_some_and(|profile| {
credential_present: user.state == user_manager::LocalUserState::Managed iota_util::file_util::read_user_credential_with_legacy(
&& profile.is_some_and(|profile| { user.user_id,
iota_util::file_util::read_user_credential_with_legacy( &profile.username,
user.user_id, )
&profile.username, .ok()
) .flatten()
.ok() .is_some()
.flatten() }),
.is_some() user_id: user.user_id,
}), username: user.username,
user_id: user.user_id, state: match user.state {
username: user.username, user_manager::LocalUserState::Managed => {
state: match user.state { iota_ipc::LocalUserState::Managed
user_manager::LocalUserState::Managed => { }
iota_ipc::LocalUserState::Managed user_manager::LocalUserState::Released => {
} iota_ipc::LocalUserState::Released
user_manager::LocalUserState::Released => { }
iota_ipc::LocalUserState::Released },
} data_present: user.data_present,
},
data_present: user.data_present,
})
}) })
.collect::<Result<Vec<_>, iota_storage::storage_error::StorageError>>(); .collect();
let Ok(users) = users else {
return ResponseResult::Error(IpcErrorCode::StorageFailure);
};
ResponseResult::Ok(ResponsePayload::Users(users)) ResponseResult::Ok(ResponsePayload::Users(users))
} }
LocalRequest::CreateUser { username } => { LocalRequest::CreateUser { username } => {
@ -222,9 +206,6 @@ impl CommandRouter {
omikron_connector::user_ops::CreateUserError::RemoteRejected => { omikron_connector::user_ops::CreateUserError::RemoteRejected => {
ResponseResult::Error(IpcErrorCode::Conflict) ResponseResult::Error(IpcErrorCode::Conflict)
} }
omikron_connector::user_ops::CreateUserError::LocalFinalizationPending { .. } => {
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
omikron_connector::user_ops::CreateUserError::LocalPersistence(_) => { omikron_connector::user_ops::CreateUserError::LocalPersistence(_) => {
ResponseResult::Error(IpcErrorCode::StorageFailure) ResponseResult::Error(IpcErrorCode::StorageFailure)
} }
@ -266,8 +247,7 @@ impl CommandRouter {
let contents = match credential { let contents = match credential {
Some(value) => Ok(value.0), Some(value) => Ok(value.0),
None => user_manager::get_user(user_id) None => user_manager::get_user(user_id)
.map_err(|_| ()) .ok_or(())
.and_then(|user| user.ok_or(()))
.and_then(|user| { .and_then(|user| {
iota_util::file_util::read_user_credential_with_legacy( iota_util::file_util::read_user_credential_with_legacy(
user_id, user_id,
@ -298,27 +278,8 @@ impl CommandRouter {
} }
LocalRequest::RemoveUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), LocalRequest::RemoveUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest),
LocalRequest::ReleaseUser { user_id } => { LocalRequest::ReleaseUser { user_id } => {
let user = match user_manager::get_user(user_id) { if user_manager::get_user(user_id).is_none() {
Ok(user) => user,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let Some(user) = user else {
return ResponseResult::Error(IpcErrorCode::NotFound); return ResponseResult::Error(IpcErrorCode::NotFound);
};
if pending_operations::upsert(&PendingUserOperation {
user_id,
operation: PendingUserOperationKind::Release,
username: user.username,
public_key: None,
private_key_hash: None,
reset_token: None,
registration_token: None,
phase: PendingUserOperationPhase::Prepared,
created_at: now_millis(),
})
.is_err()
{
return ResponseResult::Error(IpcErrorCode::StorageFailure);
} }
let request = CommunicationValue::new(CommunicationType::ReleaseUserFromIota) let request = CommunicationValue::new(CommunicationType::ReleaseUserFromIota)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())); .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
@ -330,14 +291,11 @@ impl CommandRouter {
{ {
Ok(response) if response.is_type(CommunicationType::Success) => { Ok(response) if response.is_type(CommunicationType::Success) => {
match user_manager::release_user(user_id) { match user_manager::release_user(user_id) {
Ok(()) if pending_operations::remove(user_id).is_ok() => { Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
ResponseResult::Ok(ResponsePayload::Acknowledged { message: format!(
message: format!( "Released user {user_id}; hosted data was retained"
"Released user {user_id}; hosted data was retained" ),
), }),
})
}
Ok(()) => ResponseResult::Error(IpcErrorCode::StorageFailure),
Err(error) => { Err(error) => {
log!( log!(
"Remote release succeeded but local cleanup failed for {user_id}: {error}" "Remote release succeeded but local cleanup failed for {user_id}: {error}"
@ -347,13 +305,9 @@ impl CommandRouter {
} }
} }
Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => { Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => {
let _ = pending_operations::remove(user_id);
ResponseResult::Error(IpcErrorCode::Unauthorized) ResponseResult::Error(IpcErrorCode::Unauthorized)
} }
Ok(_) => { Ok(_) => ResponseResult::Error(IpcErrorCode::Conflict),
let _ = pending_operations::remove(user_id);
ResponseResult::Error(IpcErrorCode::Conflict)
}
Err(omikron_connector::OmikronError::Timeout(_)) => { Err(omikron_connector::OmikronError::Timeout(_)) => {
ResponseResult::Error(IpcErrorCode::Timeout) ResponseResult::Error(IpcErrorCode::Timeout)
} }
@ -452,7 +406,7 @@ impl CommandRouter {
ResponseResult::Ok(ResponsePayload::Components(components)) ResponseResult::Ok(ResponsePayload::Components(components))
} }
LocalRequest::GetUser { user_id } => match user_manager::get_user(user_id) { LocalRequest::GetUser { user_id } => match user_manager::get_user(user_id) {
Ok(Some(user)) => { Some(user) => {
let credential_present = let credential_present =
iota_util::file_util::read_user_credential_with_legacy( iota_util::file_util::read_user_credential_with_legacy(
user_id, user_id,
@ -475,8 +429,7 @@ impl CommandRouter {
credential_present, credential_present,
})) }))
} }
Ok(None) => ResponseResult::Error(IpcErrorCode::NotFound), None => ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => ResponseResult::Error(IpcErrorCode::StorageFailure),
}, },
LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest),
LocalRequest::GetLogs { limit } => { LocalRequest::GetLogs { limit } => {

View file

@ -1,4 +1,3 @@
pub mod contact; pub mod contact;
pub mod pending_operations; pub mod user_manager;
pub mod user_manager;
pub mod user_profile; pub mod user_profile;

View file

@ -1,173 +0,0 @@
use crate::storage_error::StorageError;
use crate::util::db;
use rusqlite::params;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PendingUserOperationKind {
Create,
Attach,
Release,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PendingUserOperationPhase {
Prepared,
CredentialWritten,
RemoteCommitted,
LocalCommitted,
}
impl PendingUserOperationPhase {
fn as_str(self) -> &'static str {
match self {
Self::Prepared => "prepared",
Self::CredentialWritten => "credential_written",
Self::RemoteCommitted => "remote_committed",
Self::LocalCommitted => "local_committed",
}
}
fn parse(value: &str) -> Result<Self, StorageError> {
match value {
"prepared" => Ok(Self::Prepared),
"credential_written" => Ok(Self::CredentialWritten),
"remote_committed" => Ok(Self::RemoteCommitted),
"local_committed" => Ok(Self::LocalCommitted),
_ => Err(StorageError::Other(
"unknown pending user operation phase".into(),
)),
}
}
}
impl PendingUserOperationKind {
fn as_str(self) -> &'static str {
match self {
Self::Create => "create",
Self::Attach => "attach",
Self::Release => "release",
}
}
fn parse(value: &str) -> Result<Self, StorageError> {
match value {
"create" => Ok(Self::Create),
"attach" => Ok(Self::Attach),
"release" => Ok(Self::Release),
_ => Err(StorageError::Other("unknown pending user operation".into())),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingUserOperation {
pub user_id: i64,
pub operation: PendingUserOperationKind,
pub username: String,
pub public_key: Option<String>,
pub private_key_hash: Option<String>,
pub reset_token: Option<String>,
pub registration_token: Option<String>,
pub phase: PendingUserOperationPhase,
pub created_at: i64,
}
pub fn upsert(operation: &PendingUserOperation) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
tx.execute(
r#"
INSERT INTO pending_user_operations (
user_id, operation, username, public_key, private_key_hash,
reset_token, registration_token, phase, created_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
ON CONFLICT(user_id) DO UPDATE SET
operation = excluded.operation,
username = excluded.username,
public_key = excluded.public_key,
private_key_hash = excluded.private_key_hash,
reset_token = excluded.reset_token,
registration_token = excluded.registration_token,
phase = excluded.phase,
created_at = excluded.created_at
"#,
params![
operation.user_id,
operation.operation.as_str(),
operation.username,
operation.public_key,
operation.private_key_hash,
operation.reset_token,
operation.registration_token,
operation.phase.as_str(),
operation.created_at,
],
)?;
Ok(())
})
}
pub fn get_all() -> Result<Vec<PendingUserOperation>, StorageError> {
db::with_db(|conn| {
let mut statement = conn.prepare(
"SELECT user_id, operation, username, public_key, private_key_hash, reset_token, registration_token, phase, created_at FROM pending_user_operations ORDER BY created_at",
)?;
let rows = statement.query_map([], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, Option<String>>(3)?,
row.get::<_, Option<String>>(4)?,
row.get::<_, Option<String>>(5)?,
row.get::<_, Option<String>>(6)?,
row.get::<_, String>(7)?,
row.get::<_, i64>(8)?,
))
})?;
rows.map(|row| {
let (
user_id,
operation,
username,
public_key,
private_key_hash,
reset_token,
registration_token,
phase,
created_at,
) = row?;
Ok(PendingUserOperation {
user_id,
operation: PendingUserOperationKind::parse(&operation)?,
username,
public_key,
private_key_hash,
reset_token,
registration_token,
phase: PendingUserOperationPhase::parse(&phase)?,
created_at,
})
})
.collect()
})
}
pub fn update_phase(user_id: i64, phase: PendingUserOperationPhase) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
tx.execute(
"UPDATE pending_user_operations SET phase = ?1 WHERE user_id = ?2",
params![phase.as_str(), user_id],
)?;
Ok(())
})
}
pub fn remove(user_id: i64) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
tx.execute(
"DELETE FROM pending_user_operations WHERE user_id = ?1",
[user_id],
)?;
Ok(())
})
}

View file

@ -32,8 +32,8 @@ pub fn add_user(user: UserProfile) {
} }
pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::StorageError> { pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::StorageError> {
db::with_immediate_transaction(|tx| { db::with_db(|conn| {
tx.execute( conn.execute(
r#" r#"
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name) INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
@ -56,7 +56,7 @@ pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::Stora
)?; )?;
for (app_id, app_secret) in &user.trusted_apps { for (app_id, app_secret) in &user.trusted_apps {
tx.execute( conn.execute(
r#" r#"
INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret) INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret)
VALUES (?1, ?2, ?3) VALUES (?1, ?2, ?3)
@ -64,7 +64,7 @@ pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::Stora
params![user.user_id, app_id, app_secret], params![user.user_id, app_id, app_secret],
)?; )?;
} }
tx.execute( conn.execute(
r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at) r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, updated_at)
VALUES (?1, ?2, 'managed', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3) VALUES (?1, ?2, 'managed', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3)
ON CONFLICT(user_id) DO UPDATE SET username = excluded.username, lifecycle_state = 'managed', updated_at = excluded.updated_at"#, ON CONFLICT(user_id) DO UPDATE SET username = excluded.username, lifecycle_state = 'managed', updated_at = excluded.updated_at"#,
@ -93,7 +93,7 @@ pub fn get_user_by_username(username: &str) -> Option<UserProfile> {
private_key_hash: r.get(3)?, private_key_hash: r.get(3)?,
created_at: r.get(5)?, created_at: r.get(5)?,
reset_token: r.get(4)?, reset_token: r.get(4)?,
trusted_apps: std::collections::HashMap::new(), trusted_apps: load_trusted_apps(user_id),
}) })
}, },
) { ) {
@ -110,8 +110,8 @@ pub fn get_user_by_username(username: &str) -> Option<UserProfile> {
} }
} }
pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_error::StorageError> { pub fn get_user(user_id: i64) -> Option<UserProfile> {
let user = db::with_db(|conn| { match db::with_db(|conn| {
match conn.query_row( match conn.query_row(
"SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE user_id = ?1 LIMIT 1", "SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE user_id = ?1 LIMIT 1",
params![user_id], params![user_id],
@ -125,7 +125,7 @@ pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_erro
private_key_hash: r.get(3)?, private_key_hash: r.get(3)?,
created_at: r.get(5)?, created_at: r.get(5)?,
reset_token: r.get(4)?, reset_token: r.get(4)?,
trusted_apps: std::collections::HashMap::new(), trusted_apps: load_trusted_apps(user_id),
}) })
}, },
) { ) {
@ -133,12 +133,13 @@ pub fn get_user(user_id: i64) -> Result<Option<UserProfile>, crate::storage_erro
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
})?; }) {
user.map(|mut user| { Ok(opt) => opt,
user.trusted_apps = load_trusted_apps(user_id)?; Err(e) => {
Ok(user) eprintln!("Error querying user: {}", e);
}) None
.transpose() }
}
} }
pub fn get_users() -> Vec<UserProfile> { pub fn get_users() -> Vec<UserProfile> {
@ -176,7 +177,7 @@ pub fn get_users() -> Vec<UserProfile> {
for row in rows { for row in rows {
match row { match row {
Ok(mut user) => { Ok(mut user) => {
user.trusted_apps = load_trusted_apps(user.user_id)?; user.trusted_apps = load_trusted_apps(user.user_id);
out.push(user); out.push(user);
} }
Err(e) => eprintln!("Failed to read user row: {}", e), Err(e) => eprintln!("Failed to read user row: {}", e),
@ -192,10 +193,8 @@ pub fn get_users() -> Vec<UserProfile> {
} }
} }
fn load_trusted_apps( fn load_trusted_apps(user_id: i64) -> std::collections::HashMap<String, String> {
user_id: i64, match db::with_db(|conn| {
) -> Result<std::collections::HashMap<String, String>, crate::storage_error::StorageError> {
db::with_db(|conn| {
let mut stmt = let mut stmt =
conn.prepare("SELECT app_id, app_secret FROM trusted_apps WHERE user_id = ?1")?; conn.prepare("SELECT app_id, app_secret FROM trusted_apps WHERE user_id = ?1")?;
let rows = stmt.query_map(params![user_id], |r| { let rows = stmt.query_map(params![user_id], |r| {
@ -204,11 +203,18 @@ fn load_trusted_apps(
let mut map = std::collections::HashMap::new(); let mut map = std::collections::HashMap::new();
for row in rows { for row in rows {
let (key, value) = row?; if let Ok((k, v)) = row {
map.insert(key, value); map.insert(k, v);
}
} }
Ok(map) Ok(map)
}) }) {
Ok(m) => m,
Err(e) => {
eprintln!("Failed to load trusted apps: {}", e);
std::collections::HashMap::new()
}
}
} }
pub fn remove_user(user_id: i64) { pub fn remove_user(user_id: i64) {
@ -227,11 +233,9 @@ pub fn remove_user(user_id: i64) {
/// Remove only local management authority. Hosted content is intentionally /// Remove only local management authority. Hosted content is intentionally
/// retained and is indexed as released for a later purge operation. /// retained and is indexed as released for a later purge operation.
pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageError> { pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageError> {
let username = get_user(user_id)? let username = get_user(user_id).map(|user| user.username).ok_or_else(|| {
.map(|user| user.username) crate::storage_error::StorageError::Other("managed user was not found".into())
.ok_or_else(|| { })?;
crate::storage_error::StorageError::Other("managed user was not found".into())
})?;
db::with_db(|conn| { db::with_db(|conn| {
let tx = conn.unchecked_transaction()?; let tx = conn.unchecked_transaction()?;
tx.execute( tx.execute(
@ -248,7 +252,7 @@ pub fn release_user(user_id: i64) -> Result<(), crate::storage_error::StorageErr
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
})?; })?;
remove_user_credential(user_id, Some(&username)) remove_user_credential(user_id)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) .map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))
} }
@ -308,12 +312,6 @@ pub fn purge_user_data(user_id: i64) -> Result<(), crate::storage_error::Storage
/// Complete local erasure is idempotent and is the target for a durable /// Complete local erasure is idempotent and is the target for a durable
/// Omega-hosted erasure request after account deletion. /// Omega-hosted erasure request after account deletion.
pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::StorageError> { pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::StorageError> {
let username = get_user(user_id)?.map(|user| user.username).or_else(|| {
get_residency()
.into_iter()
.find(|entry| entry.user_id == user_id)
.map(|entry| entry.username)
});
purge_user_data(user_id)?; purge_user_data(user_id)?;
db::with_db(|conn| { db::with_db(|conn| {
conn.execute( conn.execute(
@ -327,7 +325,7 @@ pub fn erase_user_locally(user_id: i64) -> Result<(), crate::storage_error::Stor
)?; )?;
Ok(()) Ok(())
})?; })?;
remove_user_credential(user_id, username.as_deref()) remove_user_credential(user_id)
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string())) .map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))
} }

View file

@ -523,7 +523,7 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
let mut contact = crate::users::contact::Contact::new(external_user); let mut contact = crate::users::contact::Contact::new(external_user);
contact.set_last_message_at(send_time); contact.set_last_message_at(send_time);
crate::util::chats_util::mod_user(storage_owner, &contact)?; crate::util::chats_util::mod_user(storage_owner, &contact);
Ok(msg_id) Ok(msg_id)
} }
@ -546,17 +546,8 @@ pub fn change_message_state_by_relay_id(
return Ok(()); return Ok(());
}; };
let state = MessageState::from_str(&current).upgrade(new_state).as_str(); let state = MessageState::from_str(&current).upgrade(new_state).as_str();
tx.execute( tx.execute("UPDATE messages SET message_state = ?1 WHERE id = ?2", params![state, msg_id])?;
"UPDATE messages SET message_state = ?1 WHERE id = ?2", sync::record_event(&tx, storage_owner, EntityType::Message, msg_id, Operation::Upsert)?;
params![state, msg_id],
)?;
sync::record_event(
&tx,
storage_owner,
EntityType::Message,
msg_id,
Operation::Upsert,
)?;
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
}) })
@ -590,9 +581,7 @@ pub fn record_message_receipt(
return Err(StorageError::Other("message receipt target was not found".into())); return Err(StorageError::Other("message receipt target was not found".into()));
}; };
if external_user != receipt_signer_id { if external_user != receipt_signer_id {
return Err(StorageError::Other( return Err(StorageError::Other("message receipt signer is not the chat partner".into()));
"message receipt signer is not the chat partner".into(),
));
} }
tx.execute( tx.execute(
"INSERT OR IGNORE INTO message_receipts (storage_owner, target_signer_id, target_message_id, receipt_signer_id, receipt_message_id, receipt_type, event_at, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", "INSERT OR IGNORE INTO message_receipts (storage_owner, target_signer_id, target_message_id, receipt_signer_id, receipt_message_id, receipt_type, event_at, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
@ -603,11 +592,9 @@ pub fn record_message_receipt(
} else { } else {
("client_received_at", "client_received_recorded_at") ("client_received_at", "client_received_recorded_at")
}; };
let state = MessageState::from_str(&tx.query_row( let state = MessageState::from_str(
"SELECT message_state FROM messages WHERE id = ?1", &tx.query_row("SELECT message_state FROM messages WHERE id = ?1", [message_id], |row| row.get::<_, String>(0))?,
[message_id], )
|row| row.get::<_, String>(0),
)?)
.upgrade(MessageState::from_str(receipt_type)) .upgrade(MessageState::from_str(receipt_type))
.as_str() .as_str()
.to_string(); .to_string();
@ -615,13 +602,7 @@ pub fn record_message_receipt(
&format!("UPDATE messages SET {state_column} = COALESCE({state_column}, ?1), {recorded_column} = COALESCE({recorded_column}, ?2), message_state = ?3 WHERE id = ?4"), &format!("UPDATE messages SET {state_column} = COALESCE({state_column}, ?1), {recorded_column} = COALESCE({recorded_column}, ?2), message_state = ?3 WHERE id = ?4"),
params![event_at, recorded_at, state, message_id], params![event_at, recorded_at, state, message_id],
)?; )?;
sync::record_event( sync::record_event(&tx, storage_owner, EntityType::Message, message_id, Operation::Upsert)?;
&tx,
storage_owner,
EntityType::Message,
message_id,
Operation::Upsert,
)?;
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
}) })
@ -649,13 +630,7 @@ pub fn record_destination_iota_received(
"UPDATE messages SET destination_iota_received_at = COALESCE(destination_iota_received_at, ?1), message_state = CASE WHEN message_state = 'sending' THEN 'sent' ELSE message_state END WHERE id = ?2", "UPDATE messages SET destination_iota_received_at = COALESCE(destination_iota_received_at, ?1), message_state = CASE WHEN message_state = 'sending' THEN 'sent' ELSE message_state END WHERE id = ?2",
params![accepted_at, message_id], params![accepted_at, message_id],
)?; )?;
sync::record_event( sync::record_event(&tx, storage_owner, EntityType::Message, message_id, Operation::Upsert)?;
&tx,
storage_owner,
EntityType::Message,
message_id,
Operation::Upsert,
)?;
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
}) })

View file

@ -14,8 +14,9 @@ pub fn has_user(storage_owner: i64, user_id: i64) -> Result<bool, StorageError>
}) })
} }
pub fn mod_user(storage_owner: i64, contact: &Contact) -> Result<(), StorageError> { pub fn mod_user(storage_owner: i64, contact: &Contact) {
db::with_immediate_transaction(|tx| { if let Err(e) = db::with_db(|conn| {
let tx = conn.unchecked_transaction()?;
tx.execute( tx.execute(
r#" r#"
INSERT INTO contacts (storage_owner, user_id, user_name, last_message_at) INSERT INTO contacts (storage_owner, user_id, user_name, last_message_at)
@ -27,34 +28,37 @@ pub fn mod_user(storage_owner: i64, contact: &Contact) -> Result<(), StorageErro
params![ params![
storage_owner, storage_owner,
contact.user_id, contact.user_id,
contact.user_name, contact.user_name.clone(),
contact.last_message_at, contact.last_message_at,
], ],
)?; )?;
sync::record_event( sync::record_event(
tx, &tx,
storage_owner, storage_owner,
EntityType::Contact, EntityType::Contact,
contact.user_id, contact.user_id,
Operation::Upsert, Operation::Upsert,
)?; )?;
tx.commit()?;
Ok(()) Ok(())
}) }) {
eprintln!("Failed to mod_user: {}", e);
}
} }
pub fn get_users_by_ids(storage_owner: i64, ids: &[i64]) -> Result<Vec<Contact>, StorageError> { pub fn get_users_by_ids(storage_owner: i64, ids: &[i64]) -> Vec<Contact> {
if ids.is_empty() { if ids.is_empty() {
return Ok(Vec::new()); return Vec::new();
} }
let wanted: std::collections::HashSet<i64> = ids.iter().copied().collect(); let wanted: std::collections::HashSet<i64> = ids.iter().copied().collect();
Ok(get_users(storage_owner)? get_users(storage_owner)
.into_iter() .into_iter()
.filter(|contact| wanted.contains(&contact.user_id)) .filter(|contact| wanted.contains(&contact.user_id))
.collect()) .collect()
} }
pub fn get_user(storage_owner: i64, user_id: i64) -> Result<Option<Contact>, StorageError> { pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
db::with_db(|conn| { match db::with_db(|conn| {
match conn.query_row( match conn.query_row(
r#" r#"
SELECT user_id, user_name, last_message_at SELECT user_id, user_name, last_message_at
@ -75,11 +79,17 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Result<Option<Contact>, Sto
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
}) }) {
Ok(opt) => opt,
Err(e) => {
eprintln!("Error querying user in get_user: {}", e);
None
}
}
} }
pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> { pub fn get_users(storage_owner: i64) -> Vec<Contact> {
db::with_db(|conn| { match db::with_db(|conn| {
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
r#" r#"
SELECT user_id, user_name, last_message_at SELECT user_id, user_name, last_message_at
@ -102,8 +112,17 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
let mut out = Vec::new(); let mut out = Vec::new();
for row in rows { for row in rows {
out.push(row?); match row {
Ok(contact) => out.push(contact),
Err(e) => eprintln!("Failed to read contact row: {}", e),
}
} }
Ok(out) Ok(out)
}) }) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to query contacts in get_users: {}", e);
Vec::new()
}
}
} }

View file

@ -421,15 +421,9 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
("relay_message_id", "relay_message_id TEXT"), ("relay_message_id", "relay_message_id TEXT"),
("authored_at", "authored_at INTEGER"), ("authored_at", "authored_at INTEGER"),
("origin_iota_received_at", "origin_iota_received_at INTEGER"), ("origin_iota_received_at", "origin_iota_received_at INTEGER"),
( ("destination_iota_received_at", "destination_iota_received_at INTEGER"),
"destination_iota_received_at",
"destination_iota_received_at INTEGER",
),
("client_received_at", "client_received_at INTEGER"), ("client_received_at", "client_received_at INTEGER"),
( ("client_received_recorded_at", "client_received_recorded_at INTEGER"),
"client_received_recorded_at",
"client_received_recorded_at INTEGER",
),
("read_at", "read_at INTEGER"), ("read_at", "read_at INTEGER"),
("read_recorded_at", "read_recorded_at INTEGER"), ("read_recorded_at", "read_recorded_at INTEGER"),
] { ] {
@ -495,38 +489,6 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
)?; )?;
} }
if current_version < 13 {
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS pending_user_operations (
user_id INTEGER PRIMARY KEY,
operation TEXT NOT NULL
CHECK (operation IN ('create', 'attach', 'release')),
username TEXT NOT NULL,
public_key TEXT,
private_key_hash TEXT,
reset_token TEXT,
registration_token TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pending_user_operations_operation
ON pending_user_operations (operation, created_at);
PRAGMA user_version = 13;
"#,
)?;
}
if current_version < 14 {
conn.execute_batch(
r#"
ALTER TABLE pending_user_operations
ADD COLUMN phase TEXT NOT NULL DEFAULT 'prepared'
CHECK (phase IN ('prepared', 'credential_written', 'remote_committed', 'local_committed'));
PRAGMA user_version = 14;
"#,
)?;
}
Ok(()) Ok(())
} }
@ -596,7 +558,7 @@ mod tests {
run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 13); assert_eq!(version, 12);
for column in ["height", "reply_to", "edited_count", "deleted_by_external"] { for column in ["height", "reply_to", "edited_count", "deleted_by_external"] {
let mut statement = let mut statement =
conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?;
@ -615,7 +577,7 @@ mod tests {
run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?;
run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 13); assert_eq!(version, 12);
for table in [ for table in [
"sync_heads", "sync_heads",
"sync_events", "sync_events",
@ -625,7 +587,6 @@ mod tests {
"pending_relays", "pending_relays",
"relay_inbox", "relay_inbox",
"synced_settings", "synced_settings",
"pending_user_operations",
] { ] {
let exists: i64 = conn.query_row( let exists: i64 = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1", "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
@ -650,17 +611,8 @@ mod tests {
run_migrations_on_connection(&conn)?; run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 13); assert_eq!(version, 12);
for column in [ for column in ["id", "user_id", "scope_type", "scope_key", "name", "payload", "revision", "deleted"] {
"id",
"user_id",
"scope_type",
"scope_key",
"name",
"payload",
"revision",
"deleted",
] {
let mut statement = let mut statement =
conn.prepare("SELECT 1 FROM pragma_table_info('synced_settings') WHERE name = ?1")?; conn.prepare("SELECT 1 FROM pragma_table_info('synced_settings') WHERE name = ?1")?;
assert!(statement.exists([column])?); assert!(statement.exists([column])?);

View file

@ -291,15 +291,30 @@ mod tests {
) )
.unwrap(); .unwrap();
let transaction = connection.unchecked_transaction().unwrap(); let transaction = connection.unchecked_transaction().unwrap();
let message = let message = super::record_event(
super::record_event(&transaction, 1, EntityType::Message, 10, Operation::Upsert) &transaction,
.unwrap(); 1,
let setting = EntityType::Message,
super::record_event(&transaction, 1, EntityType::Setting, 11, Operation::Upsert) 10,
.unwrap(); Operation::Upsert,
let contact = )
super::record_event(&transaction, 1, EntityType::Contact, 12, Operation::Upsert) .unwrap();
.unwrap(); let setting = super::record_event(
&transaction,
1,
EntityType::Setting,
11,
Operation::Upsert,
)
.unwrap();
let contact = super::record_event(
&transaction,
1,
EntityType::Contact,
12,
Operation::Upsert,
)
.unwrap();
transaction.commit().unwrap(); transaction.commit().unwrap();
assert_eq!((message, setting, contact), (1, 2, 3)); assert_eq!((message, setting, contact), (1, 2, 3));

View file

@ -1,7 +1,7 @@
use crate::storage_error::StorageError; use crate::storage_error::StorageError;
use crate::util::db; use crate::util::db;
use crate::util::sync::{self, EntityType, Operation}; use crate::util::sync::{self, EntityType, Operation};
use rusqlite::{Connection, OptionalExtension, Row, Transaction, params}; use rusqlite::{params, Connection, OptionalExtension, Row, Transaction};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingScope { pub enum SettingScope {
@ -401,8 +401,8 @@ pub(crate) fn delete_scope_in_tx(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
SettingScope, delete_in_tx, get_from_connection, is_valid_name, delete_in_tx, get_from_connection, is_valid_name, list_by_ids_from_connection,
list_by_ids_from_connection, list_from_connection, set_in_tx, list_from_connection, set_in_tx, SettingScope,
}; };
use rusqlite::Connection; use rusqlite::Connection;
@ -618,11 +618,9 @@ mod tests {
assert!(deleted.changed); assert!(deleted.changed);
assert_eq!(journal_operation, "delete"); assert_eq!(journal_operation, "delete");
assert!(list_from_connection(&connection, 1).unwrap().is_empty()); assert!(list_from_connection(&connection, 1).unwrap().is_empty());
assert!( assert!(list_by_ids_from_connection(&connection, 1, &[stored.id])
list_by_ids_from_connection(&connection, 1, &[stored.id]) .unwrap()
.unwrap() .is_empty());
.is_empty()
);
} }
#[test] #[test]

View file

@ -43,35 +43,14 @@ pub fn delete_user_directory(user_id: i64) -> io::Result<()> {
fs::remove_dir_all(user_dir) fs::remove_dir_all(user_dir)
} }
fn credential_filename(username: &str) -> io::Result<String> { pub fn credential_path(user_id: i64) -> PathBuf {
if username.is_empty() storage_directory()
|| username.chars().any(char::is_control)
|| username.contains(['/', '\\'])
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"unsafe credential owner name",
));
}
Ok(format!("{username}.tu"))
}
pub fn credential_path(username: &str) -> io::Result<PathBuf> {
credential_path_in(&storage_directory(), username)
}
fn credential_path_in(root: &Path, username: &str) -> io::Result<PathBuf> {
Ok(root
.join("credentials") .join("credentials")
.join(credential_filename(username)?)) .join(format!("{user_id}.tu"))
} }
fn legacy_credential_path(user_id: i64) -> io::Result<PathBuf> { pub fn read_user_credential(user_id: i64) -> io::Result<Option<String>> {
storage_file("credentials", format!("{user_id}.tu")) let path = credential_path(user_id);
}
pub fn read_user_credential(username: &str) -> io::Result<Option<String>> {
let path = credential_path(username)?;
match fs::read_to_string(path) { match fs::read_to_string(path) {
Ok(value) => Ok(Some(value)), Ok(value) => Ok(Some(value)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
@ -79,66 +58,55 @@ pub fn read_user_credential(username: &str) -> io::Result<Option<String>> {
} }
} }
/* Resolve a credential by account id while using the owner's name for the /// Resolve a credential by immutable account id. A valid legacy
* canonical filename. Older ID-based and root-level files are migrated when /// `<username>.tu` is migrated atomically the first time it is encountered.
* they are encountered. */
pub fn read_user_credential_with_legacy( pub fn read_user_credential_with_legacy(
user_id: i64, user_id: i64,
username: &str, username: &str,
) -> io::Result<Option<String>> { ) -> io::Result<Option<String>> {
let canonical_path = credential_path(username)?; if let Some(credential) = read_user_credential(user_id)? {
if let Some(credential) = read_user_credential(username)? {
return Ok(Some(credential)); return Ok(Some(credential));
} }
let legacy_paths = [ let legacy = storage_file("", format!("{username}.tu"))?;
legacy_credential_path(user_id)?, let credential = match fs::read_to_string(&legacy) {
storage_file("", credential_filename(username)?)?, Ok(value) => value,
]; Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
for legacy_path in legacy_paths { Err(error) => return Err(error),
let credential = match fs::read_to_string(&legacy_path) { };
Ok(value) => value, let parsed = crate::tu::TuCredential::parse(&credential)
Err(error) if error.kind() == io::ErrorKind::NotFound => continue, .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
Err(error) => return Err(error), if parsed.user_id != user_id {
}; return Err(io::Error::new(
let parsed = crate::tu::TuCredential::parse(&credential) io::ErrorKind::InvalidData,
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; "legacy credential user id mismatch",
if parsed.user_id != user_id { ));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"legacy credential user id mismatch",
));
}
write_user_credential(username, &parsed.to_canonical_string())?;
if legacy_path != canonical_path {
fs::remove_file(legacy_path)?;
}
return Ok(Some(parsed.to_canonical_string()));
} }
Ok(None) write_user_credential(user_id, &parsed.to_canonical_string())?;
fs::remove_file(legacy)?;
Ok(Some(parsed.to_canonical_string()))
} }
pub fn write_user_credential(username: &str, credential: &str) -> io::Result<()> { pub fn write_user_credential(user_id: i64, credential: &str) -> io::Result<()> {
let path = credential_path(username)?; let path = credential_path(user_id);
crate::atomic_file::replace_private(&path, credential.as_bytes(), 0) let parent = path.parent().expect("credential path has parent");
} fs::create_dir_all(parent)?;
let temporary = parent.join(format!(".{user_id}.tu.tmp"));
pub fn remove_user_credential(user_id: i64, username: Option<&str>) -> io::Result<()> { fs::write(&temporary, credential)?;
let mut paths = vec![legacy_credential_path(user_id)?]; if let Err(error) = fs::rename(&temporary, &path) {
if let Some(username) = username { let _ = fs::remove_file(&temporary);
paths.push(credential_path(username)?); return Err(error);
paths.push(storage_file("", credential_filename(username)?)?);
}
for path in paths {
match fs::remove_file(path) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
} }
Ok(()) Ok(())
} }
pub fn remove_user_credential(user_id: i64) -> io::Result<()> {
match fs::remove_file(credential_path(user_id)) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> { pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
let file_path = storage_file(path, name)?; let file_path = storage_file(path, name)?;
@ -487,22 +455,3 @@ pub async fn download_and_extract_zip(url: &str, as_name: &str) {
println!("Downloaded and extracted ZIP file successfully."); println!("Downloaded and extracted ZIP file successfully.");
} }
} }
#[cfg(test)]
mod tests {
use super::credential_path_in;
use std::path::Path;
#[test]
fn credential_path_uses_owner_name() {
let path = credential_path_in(Path::new("/tmp/iota"), "alice").unwrap();
assert!(path.ends_with("credentials/alice.tu"));
assert!(!path.ends_with("credentials/42.tu"));
}
#[test]
fn credential_path_rejects_unsafe_owner_name() {
assert!(credential_path_in(Path::new("/tmp/iota"), "../alice").is_err());
assert!(credential_path_in(Path::new("/tmp/iota"), "alice/bob").is_err());
}
}

View file

@ -1,6 +1,8 @@
/* Strict parsing and storage-independent handling of user credentials. A //! Strict parsing and storage-independent handling of user credentials.
* `.tu` file is identified by the account ID in its contents, while storage //!
* names the file after its owner's username. */ //! A `.tu` file is deliberately identified by the account id embedded in its
//! contents. Its filename is presentation data owned by the CLI, never an
//! account authority.
use crate::crypto_helper::{keyring_from_base64, keyring_to_base64}; use crate::crypto_helper::{keyring_from_base64, keyring_to_base64};
use mtp::crypto::{Keyring, PublicKeyBundle}; use mtp::crypto::{Keyring, PublicKeyBundle};

View file

@ -754,9 +754,7 @@ impl OmikronConnection {
let signer_id_i64 = i64::try_from(signer_id).map_err(|_| { let signer_id_i64 = i64::try_from(signer_id).map_err(|_| {
RelayValidationError::KeyLookup("signer ID exceeds local storage range".into()) RelayValidationError::KeyLookup("signer ID exceeds local storage range".into())
})?; })?;
let local_user = iota_storage::users::user_manager::get_user(signer_id_i64) if let Some(user) = iota_storage::users::user_manager::get_user(signer_id_i64) {
.map_err(|error| RelayValidationError::KeyLookup(error.to_string()))?;
if let Some(user) = local_user {
let key = iota_util::crypto_helper::public_key_bundle_from_base64(&user.public_key) let key = iota_util::crypto_helper::public_key_bundle_from_base64(&user.public_key)
.ok_or_else(|| { .ok_or_else(|| {
RelayValidationError::KeyLookup("stored user key is invalid".into()) RelayValidationError::KeyLookup("stored user key is invalid".into())
@ -790,10 +788,7 @@ impl OmikronConnection {
pub async fn hosting_iota_for_user(&self, user_id: u64) -> Result<u64, String> { pub async fn hosting_iota_for_user(&self, user_id: u64) -> Result<u64, String> {
let user_id_i64 = i64::try_from(user_id) let user_id_i64 = i64::try_from(user_id)
.map_err(|_| "user ID exceeds local storage range".to_string())?; .map_err(|_| "user ID exceeds local storage range".to_string())?;
if iota_storage::users::user_manager::get_user(user_id_i64) if iota_storage::users::user_manager::get_user(user_id_i64).is_some() {
.map_err(|error| error.to_string())?
.is_some()
{
return CONFIG return CONFIG
.load() .load()
.iota_id .iota_id
@ -906,54 +901,14 @@ impl OmikronConnection {
} }
}; };
let accepted_at = now_millis_i64(); let accepted_at = now_millis_i64();
let signer_id = match i64::try_from(verified.context.signer_id) { let signer_is_local = i64::try_from(verified.context.signer_id)
Ok(id) => id, .ok()
Err(_) => { .and_then(iota_storage::users::user_manager::get_user)
self.send_relay_response( .is_some();
Some(incoming_frame_id), let recipient_is_local = i64::try_from(verified.context.final_recipient_id)
CommunicationType::ErrorInvalidData, .ok()
) .and_then(iota_storage::users::user_manager::get_user)
.await; .is_some();
return;
}
};
let recipient_id = match i64::try_from(verified.context.final_recipient_id) {
Ok(id) => id,
Err(_) => {
self.send_relay_response(
Some(incoming_frame_id),
CommunicationType::ErrorInvalidData,
)
.await;
return;
}
};
let signer_is_local = match iota_storage::users::user_manager::get_user(signer_id) {
Ok(user) => user.is_some(),
Err(error) => {
log!(
"Relay locality lookup failed for signer {}: {}",
signer_id,
error
);
self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInternal)
.await;
return;
}
};
let recipient_is_local = match iota_storage::users::user_manager::get_user(recipient_id) {
Ok(user) => user.is_some(),
Err(error) => {
log!(
"Relay locality lookup failed for recipient {}: {}",
recipient_id,
error
);
self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInternal)
.await;
return;
}
};
if !signer_is_local && !recipient_is_local { if !signer_is_local && !recipient_is_local {
log!( log!(
"Rejecting Relay with no local origin or destination: signer {}, recipient {}", "Rejecting Relay with no local origin or destination: signer {}, recipient {}",
@ -1036,7 +991,7 @@ impl OmikronConnection {
let content = match open_verified_relay_content( let content = match open_verified_relay_content(
&verified, &verified,
&[&keyring], &[&keyring],
verified.context.final_recipient_id, verified.context.signer_id,
) { ) {
Ok(value) => value, Ok(value) => value,
Err(error) => { Err(error) => {
@ -1531,6 +1486,7 @@ impl OmikronConnection {
dispatch!(MessageGet, handle_message_get); dispatch!(MessageGet, handle_message_get);
dispatch!(MessagesGet, handle_messages_get); dispatch!(MessagesGet, handle_messages_get);
dispatch!(GetChats, handle_get_chats); dispatch!(GetChats, handle_get_chats);
dispatch!(AddConversation, handle_add_conversation);
dispatch!(AddCommunity, handle_add_community); dispatch!(AddCommunity, handle_add_community);
dispatch!(GetCommunities, handle_get_communities); dispatch!(GetCommunities, handle_get_communities);
dispatch!(RemoveCommunity, handle_remove_community); dispatch!(RemoveCommunity, handle_remove_community);
@ -1612,16 +1568,7 @@ impl OmikronConnection {
}; };
let mut trusted = false; let mut trusted = false;
let user = match iota_storage::users::user_manager::get_user(user_id) { if let Some(user) = iota_storage::users::user_manager::get_user(user_id) {
Ok(user) => user,
Err(_) => {
let _ = self
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
.await;
return;
}
};
if let Some(user) = user {
if let Some(pub_k) = user.trusted_apps.get(&app_identifier) { if let Some(pub_k) = user.trusted_apps.get(&app_identifier) {
if pub_k == &app_public_key { if pub_k == &app_public_key {
trusted = true; trusted = true;
@ -1914,17 +1861,7 @@ impl OmikronConnection {
&mutation, &mutation,
vec![(DataType::Content, DataValue::Str(content.to_string()))], vec![(DataType::Content, DataValue::Str(content.to_string()))],
); );
let partner_is_local = if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some()
match iota_storage::users::user_manager::get_user(mutation.partner_id) {
Ok(user) => user.is_some(),
Err(_) => {
let _ = self
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
.await;
return;
}
};
if partner_is_local
&& chat_files::apply_remote_edit( && chat_files::apply_remote_edit(
mutation.partner_id, mutation.partner_id,
mutation.sender_id, mutation.sender_id,
@ -1986,17 +1923,7 @@ impl OmikronConnection {
(DataType::Accepted, DataValue::Bool(add)), (DataType::Accepted, DataValue::Bool(add)),
], ],
); );
let partner_is_local = if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some() {
match iota_storage::users::user_manager::get_user(mutation.partner_id) {
Ok(user) => user.is_some(),
Err(_) => {
let _ = self
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
.await;
return;
}
};
if partner_is_local {
let result = if add { let result = if add {
chat_files::add_reaction( chat_files::add_reaction(
mutation.partner_id, mutation.partner_id,
@ -2039,16 +1966,7 @@ impl OmikronConnection {
Some(sender_id) => sender_id, Some(sender_id) => sender_id,
None => return, None => return,
}; };
let sender_is_local = match iota_storage::users::user_manager::get_user(sender_id) { if iota_storage::users::user_manager::get_user(sender_id).is_none() {
Ok(user) => user.is_some(),
Err(_) => {
let _ = self
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
.await;
return;
}
};
if !sender_is_local {
self.persist_and_deliver_remote_delete(cv).await; self.persist_and_deliver_remote_delete(cv).await;
return; return;
} }
@ -2070,17 +1988,7 @@ impl OmikronConnection {
&mutation, &mutation,
Vec::new(), Vec::new(),
); );
let partner_is_local = if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some()
match iota_storage::users::user_manager::get_user(mutation.partner_id) {
Ok(user) => user.is_some(),
Err(_) => {
let _ = self
.send_message(&error_response(cv, CommunicationType::ErrorInternal))
.await;
return;
}
};
if partner_is_local
&& chat_files::apply_remote_delete( && chat_files::apply_remote_delete(
mutation.partner_id, mutation.partner_id,
mutation.sender_id, mutation.sender_id,
@ -2116,6 +2024,12 @@ impl OmikronConnection {
.await; .await;
} }
async fn handle_add_conversation(self: Arc<Self>, cv: &CommunicationValue) {
let _ = self
.send_message(&message_handlers::handle_add_conversation(cv))
.await;
}
async fn handle_add_community(self: Arc<Self>, cv: &CommunicationValue) { async fn handle_add_community(self: Arc<Self>, cv: &CommunicationValue) {
let _ = self let _ = self
.send_message(&message_handlers::handle_add_community(cv)) .send_message(&message_handlers::handle_add_community(cv))

View file

@ -1,19 +1,16 @@
use base64::{Engine as _, engine::general_purpose::STANDARD}; use base64::{Engine as _, engine::general_purpose::STANDARD};
use iota_logger::{PrintType, log, log_cv, log_t}; use iota_logger::{PrintType, log, log_cv, log_t};
use iota_storage::users::pending_operations::{
self, PendingUserOperation, PendingUserOperationKind, PendingUserOperationPhase,
};
use iota_storage::users::user_manager::try_add_user; use iota_storage::users::user_manager::try_add_user;
use iota_storage::users::user_profile::UserProfile; use iota_storage::users::user_profile::UserProfile;
use iota_storage::util::config_util::CONFIG; use iota_storage::util::config_util::CONFIG;
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64}; use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
use iota_util::file_util::{remove_user_credential, write_user_credential}; use iota_util::file_util::write_user_credential;
use iota_util::mtp_compat::OptionalDataValueExt; use iota_util::mtp_compat::OptionalDataValueExt;
use iota_util::tu::TuCredential; use iota_util::tu::TuCredential;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme}; use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme};
use rand_core::{OsRng, RngCore}; use rand_core::{OsRng, RngCore};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::Duration;
use crate::OmikronClient; use crate::OmikronClient;
use crate::omega_discovery; use crate::omega_discovery;
@ -24,7 +21,6 @@ pub enum CreateUserError {
Transport(crate::OmikronError), Transport(crate::OmikronError),
InvalidResponse, InvalidResponse,
RemoteRejected, RemoteRejected,
LocalFinalizationPending { user_id: i64 },
LocalPersistence(String), LocalPersistence(String),
} }
@ -169,6 +165,15 @@ pub async fn attach_user_from_tu(
let credential = TuCredential::parse(contents) let credential = TuCredential::parse(contents)
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?; .map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
let (username, public_key) = inspect_credential_account(connection, &credential).await?; let (username, public_key) = inspect_credential_account(connection, &credential).await?;
credential_proof(
connection,
&credential,
CommunicationType::AttachUserBegin,
CommunicationType::AttachUserChallenge,
CommunicationType::AttachUserComplete,
b"tensamin:user-attach:v1\0",
)
.await?;
let profile = UserProfile::new( let profile = UserProfile::new(
credential.user_id, credential.user_id,
username, username,
@ -177,40 +182,10 @@ pub async fn attach_user_from_tu(
hex_hash(contents), hex_hash(contents),
String::new(), String::new(),
); );
write_user_credential(&profile.username, &credential.to_canonical_string()) write_user_credential(profile.user_id, &credential.to_canonical_string())
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; .map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
pending_operations::upsert(&PendingUserOperation {
user_id: profile.user_id,
operation: PendingUserOperationKind::Attach,
username: profile.username.clone(),
public_key: Some(profile.public_key.clone()),
private_key_hash: Some(profile.private_key_hash.clone()),
reset_token: Some(profile.reset_token.clone()),
registration_token: None,
phase: PendingUserOperationPhase::Prepared,
created_at: now_millis(),
})
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
if let Err(error) = credential_proof(
connection,
&credential,
CommunicationType::AttachUserBegin,
CommunicationType::AttachUserChallenge,
CommunicationType::AttachUserComplete,
b"tensamin:user-attach:v1\0",
)
.await
{
if matches!(error, LifecycleUserError::RemoteRejected) {
let _ = pending_operations::remove(profile.user_id);
let _ = remove_user_credential(profile.user_id, Some(&profile.username));
}
return Err(error);
}
try_add_user(profile.clone()) try_add_user(profile.clone())
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?; .map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
pending_operations::remove(profile.user_id)
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
Ok(profile) Ok(profile)
} }
@ -244,83 +219,6 @@ pub async fn reconcile_managed_users(connection: &dyn OmikronClient) {
let Ok(local_iota_id) = configured_iota_id() else { let Ok(local_iota_id) = configured_iota_id() else {
return; return;
}; };
let pending = match pending_operations::get_all() {
Ok(pending) => pending,
Err(error) => {
log!("Pending user operation reconciliation could not read storage: {error}");
return;
}
};
for operation in pending {
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
DataType::UserId,
DataValue::SignedNumber(operation.user_id.into()),
);
let response = connection
.await_response(&request, Duration::from_secs(10))
.await
.ok();
let remote_iota_id = response.as_ref().and_then(|response| {
response
.get_data(DataType::IotaId)
.as_signed_number()
.and_then(|value| i64::try_from(value).ok())
});
let remote_matches = response.as_ref().is_some_and(|response| {
response.is_type(CommunicationType::GetUserData)
&& remote_iota_id == Some(local_iota_id)
&& response.get_data(DataType::Username).as_str() == Some(&operation.username)
&& response.get_data(DataType::PublicKey).as_str()
== operation.public_key.as_deref()
});
let completion_retried = matches!(operation.operation, PendingUserOperationKind::Create)
&& matches!(
operation.phase,
PendingUserOperationPhase::Prepared | PendingUserOperationPhase::CredentialWritten
)
&& !remote_matches
&& complete_pending_create(connection, &operation).await;
match operation.operation {
PendingUserOperationKind::Create | PendingUserOperationKind::Attach
if remote_matches || completion_retried =>
{
let credential_present = iota_util::file_util::read_user_credential_with_legacy(
operation.user_id,
&operation.username,
)
.ok()
.flatten()
.is_some();
if !credential_present {
log!(
"Pending user {} has no credential; leaving it unresolved",
operation.user_id
);
continue;
}
let Some(public_key) = operation.public_key else {
continue;
};
let profile = UserProfile::new(
operation.user_id,
operation.username,
None,
public_key,
operation.private_key_hash.unwrap_or_default(),
operation.reset_token.unwrap_or_default(),
);
if try_add_user(profile).is_ok() {
let _ = pending_operations::remove(operation.user_id);
}
}
PendingUserOperationKind::Release if remote_iota_id != Some(local_iota_id) => {
if iota_storage::users::user_manager::release_user(operation.user_id).is_ok() {
let _ = pending_operations::remove(operation.user_id);
}
}
_ => {}
}
}
for user in iota_storage::users::user_manager::get_users() { for user in iota_storage::users::user_manager::get_users() {
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
DataType::UserId, DataType::UserId,
@ -342,86 +240,11 @@ pub async fn reconcile_managed_users(connection: &dyn OmikronClient) {
} }
} }
/*
* Retry completion only while the locally persisted operation still owns a
* valid registration lease. Omega treats an exact repeat as idempotent, which
* repairs an interrupted request without allocating another user ID.
*/
async fn complete_pending_create(
connection: &dyn OmikronClient,
operation: &PendingUserOperation,
) -> bool {
let Some(public_key) = operation.public_key.as_ref() else {
return false;
};
let Some(reset_token) = operation.reset_token.as_ref() else {
return false;
};
let Some(registration_token) = operation.registration_token.as_ref() else {
return false;
};
let request = CommunicationValue::new(CommunicationType::CompleteRegisterUser)
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(operation.user_id.into()),
)
.add_typed_default(
DataType::Username,
DataValue::Str(operation.username.clone()),
)
.add_typed_default(DataType::PublicKey, DataValue::Str(public_key.clone()))
.add_typed_default(DataType::ResetToken, DataValue::Str(reset_token.clone()))
.add_typed_default(
DataType::RegisterId,
DataValue::Str(registration_token.clone()),
);
match connection
.await_response(&request, Duration::from_secs(20))
.await
{
Ok(response) if response.is_type(CommunicationType::Success) => {
if let Err(error) = pending_operations::update_phase(
operation.user_id,
PendingUserOperationPhase::RemoteCommitted,
) {
log!(
"Pending user {} completed remotely but could not update its phase: {error}",
operation.user_id
);
}
true
}
Ok(response) => {
log!(
"Pending user {} registration retry was rejected with {}",
operation.user_id,
response.get_type()
);
false
}
Err(error) => {
log!(
"Pending user {} registration retry failed: {error}",
operation.user_id
);
false
}
}
}
fn valid_username(username: &str) -> bool { fn valid_username(username: &str) -> bool {
!username.is_empty() !username.is_empty()
&& username.len() <= 15 && username.chars().count() <= 15
&& username && !username.chars().any(char::is_control)
.bytes() && !username.contains(['/', '\\'])
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
}
fn now_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
} }
async fn request_user_id(connection: &dyn OmikronClient) -> Result<(i64, String), CreateUserError> { async fn request_user_id(connection: &dyn OmikronClient) -> Result<(i64, String), CreateUserError> {
@ -495,28 +318,6 @@ pub async fn create_user(
private_key_hash, private_key_hash,
reset_token.clone(), reset_token.clone(),
); );
let credential = format!(
"{}@{}::{}",
user_id,
omega_discovery::omega_host(),
keyring_b64
);
pending_operations::upsert(&PendingUserOperation {
user_id,
operation: PendingUserOperationKind::Create,
username: user_profile.username.clone(),
public_key: Some(user_profile.public_key.clone()),
private_key_hash: Some(user_profile.private_key_hash.clone()),
reset_token: Some(user_profile.reset_token.clone()),
registration_token: Some(registration_token.clone()),
phase: PendingUserOperationPhase::Prepared,
created_at: now_millis(),
})
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
write_user_credential(username, &credential)
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
pending_operations::update_phase(user_id, PendingUserOperationPhase::CredentialWritten)
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
let communication_value = CommunicationValue::new(CommunicationType::CompleteRegisterUser) let communication_value = CommunicationValue::new(CommunicationType::CompleteRegisterUser)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
@ -536,8 +337,6 @@ pub async fn create_user(
Ok(response) => { Ok(response) => {
log_cv!(PrintType::Omega, response); log_cv!(PrintType::Omega, response);
if !response.is_type(CommunicationType::Success) { if !response.is_type(CommunicationType::Success) {
let _ = pending_operations::remove(user_id);
let _ = remove_user_credential(user_id, Some(username));
return Err(CreateUserError::RemoteRejected); return Err(CreateUserError::RemoteRejected);
} }
} }
@ -556,15 +355,20 @@ pub async fn create_user(
} }
} }
} }
pending_operations::update_phase(user_id, PendingUserOperationPhase::RemoteCommitted)
.map_err(|_| CreateUserError::LocalFinalizationPending { user_id })?;
try_add_user(user_profile.clone())
.map_err(|_| CreateUserError::LocalFinalizationPending { user_id })?;
pending_operations::update_phase(user_id, PendingUserOperationPhase::LocalCommitted)
.map_err(|_| CreateUserError::LocalFinalizationPending { user_id })?;
pending_operations::remove(user_id)
.map_err(|_| CreateUserError::LocalFinalizationPending { user_id })?;
log!("Created User"); log!("Created User");
write_user_credential(
user_id,
&format!(
"{}@{}::{}",
user_id,
omega_discovery::omega_host(),
keyring_b64
),
)
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
try_add_user(user_profile.clone())
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
Ok(user_profile) Ok(user_profile)
} }
@ -612,12 +416,10 @@ mod tests {
#[test] #[test]
fn validates_usernames_before_remote_registration() { fn validates_usernames_before_remote_registration() {
assert!(valid_username("alice")); assert!(valid_username("alice"));
assert!(valid_username("abc123def456ghi")); assert!(valid_username("fifteen_char_ok"));
assert!(!valid_username("")); assert!(!valid_username(""));
assert!(!valid_username("sixteen_chars_bad")); assert!(!valid_username("sixteen_chars_bad"));
assert!(!valid_username("path/name")); assert!(!valid_username("path/name"));
assert!(!valid_username("upperCase"));
assert!(!valid_username("underscore_name"));
assert!(!valid_username("line\nbreak")); assert!(!valid_username("line\nbreak"));
} }

View file

@ -8,9 +8,6 @@ iota-storage = { path = "../iota-storage" }
iota-state = { path = "../iota-state" } iota-state = { path = "../iota-state" }
iota-util = { path = "../iota-util" } iota-util = { path = "../iota-util" }
iota-logger = { path = "../iota-logger" } iota-logger = { path = "../iota-logger" }
iota-cli = { path = "../iota-cli" }
iota-ipc = { path = "../iota-ipc" }
iota-paths = { path = "../iota-paths" }
actix-web = { version = "4", features = ["rustls-0_23"] } actix-web = { version = "4", features = ["rustls-0_23"] }
rustls = { version = "0.23.37", features = ["aws-lc-rs"] } rustls = { version = "0.23.37", features = ["aws-lc-rs"] }

View file

@ -1,6 +1,5 @@
use crate::server::is_local_network;
use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::{HttpRequest, HttpResponse, Responder, web};
use iota_ipc::{IpcErrorCode, LocalRequest, ResponsePayload, ResponseResult};
use iota_paths::{Scope, socket_path};
use iota_state::DaemonState; use iota_state::DaemonState;
use iota_storage::util::config_util::{CONFIG, modify_config}; use iota_storage::util::config_util::{CONFIG, modify_config};
use serde_json::{Value, json}; use serde_json::{Value, json};
@ -154,27 +153,11 @@ async fn users_add(
_ => return error(), _ => return error(),
}; };
let client = match iota_cli::ipc_client::IpcClient::connect(socket_path(Scope::User)).await { // The legacy web API is intentionally quarantined until it can use the
Ok(client) => client, // daemon's authenticated command/service boundary. It must not create a
Err(_) => return HttpResponse::ServiceUnavailable().json(json!({ "status": "not_ready" })), // second connector or mutate daemon storage directly.
}; let _ = username;
match client error()
.send_request(LocalRequest::CreateUser {
username: username.to_string(),
})
.await
{
Ok(ResponseResult::Ok(ResponsePayload::UserCreated { user_id, username })) => {
HttpResponse::Created().json(json!({
"uuid": user_id,
"username": username,
"has_tu": true,
}))
}
Ok(ResponseResult::Ok(_)) => HttpResponse::Created().json(json!({ "status": "created" })),
Ok(ResponseResult::Error(code)) => ipc_error_response(code),
Err(_) => HttpResponse::GatewayTimeout().json(json!({ "status": "timeout" })),
}
} }
async fn shutdown( async fn shutdown(
@ -217,26 +200,6 @@ fn error() -> HttpResponse {
HttpResponse::BadRequest().json(json!({ "type": "error" })) HttpResponse::BadRequest().json(json!({ "type": "error" }))
} }
fn ipc_error_response(code: IpcErrorCode) -> HttpResponse {
let status = match code {
IpcErrorCode::InvalidRequest => actix_web::http::StatusCode::BAD_REQUEST,
IpcErrorCode::Conflict => actix_web::http::StatusCode::CONFLICT,
IpcErrorCode::NotReady | IpcErrorCode::OmikronUnavailable => {
actix_web::http::StatusCode::SERVICE_UNAVAILABLE
}
IpcErrorCode::Timeout => actix_web::http::StatusCode::GATEWAY_TIMEOUT,
IpcErrorCode::Unauthorized => actix_web::http::StatusCode::FORBIDDEN,
IpcErrorCode::StorageFailure | IpcErrorCode::InternalFailure => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
IpcErrorCode::NotFound => actix_web::http::StatusCode::NOT_FOUND,
IpcErrorCode::UnsupportedVersion | IpcErrorCode::Disconnected | IpcErrorCode::Cancelled => {
actix_web::http::StatusCode::SERVICE_UNAVAILABLE
}
};
HttpResponse::build(status).json(json!({ "status": code.to_string() }))
}
fn user_id(payload: &Value) -> Result<i64, HttpResponse> { fn user_id(payload: &Value) -> Result<i64, HttpResponse> {
payload payload
.get("uuid") .get("uuid")
@ -245,8 +208,7 @@ fn user_id(payload: &Value) -> Result<i64, HttpResponse> {
} }
fn is_allowed(addr: SocketAddr, ssl: bool) -> bool { fn is_allowed(addr: SocketAddr, ssl: bool) -> bool {
let _ = ssl; is_local_network(addr.ip()) || ssl
addr.ip().is_loopback()
} }
fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool { fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool {

View file

@ -19,7 +19,7 @@ use tokio::sync::oneshot;
pub async fn start(port: u16, state: Arc<DaemonState>) -> bool { pub async fn start(port: u16, state: Arc<DaemonState>) -> bool {
let (tx, rx) = oneshot::channel::<ServerHandle>(); let (tx, rx) = oneshot::channel::<ServerHandle>();
let bind_addr = std::env::var("BIND_ADDRESS").unwrap_or_else(|_| "127.0.0.1".to_string()); let bind_addr = std::env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".to_string());
let bind_ip: std::net::IpAddr = bind_addr.parse().expect("Invalid BIND_ADDRESS"); let bind_ip: std::net::IpAddr = bind_addr.parse().expect("Invalid BIND_ADDRESS");
let server_state = state.clone(); let server_state = state.clone();