[Fix] Connection Management

This commit is contained in:
Alex Emmet 2026-09-13 20:58:41 +02:00
commit 3f2ac18333
No known key found for this signature in database
122 changed files with 19970 additions and 5263 deletions

View file

@ -6,13 +6,17 @@ edition = "2024"
[dependencies]
async-trait = "0.1.89"
iota-ipc = { path = "../iota-ipc" }
iota-auth = { path = "../iota-auth" }
iota-connection = { path = "../iota-connection" }
iota-identity = { path = "../iota-identity" }
iota-logger = { path = "../iota-logger" }
iota-state = { path = "../iota-state" }
iota-storage = { path = "../iota-storage" }
iota-updater = { path = "../iota-updater" }
iota-util = { path = "../iota-util" }
omikron-connector = { path = "../omikron-connector" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a135d0f0c2b35147011905f8ee0fc37050f69a6c" }
other-iota = { path = "../other-iota" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "1f19a0d897c265d1e3f590a876f95e766ff99318" }
libc = "0.2"
sysinfo = "0.38.0"
serde_yaml = "0.9"

View file

@ -0,0 +1,451 @@
use async_trait::async_trait;
use iota_identity::{
AuthorityKind, LocalNodeIdentity, LocalUserDescriptor, LocalUserId, PrincipalHome,
};
use iota_storage::users::pending_operations::{
self, PendingUserOperation, PendingUserOperationKind, PendingUserOperationPhase,
};
use iota_storage::users::user_manager;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use omikron_connector::{OmikronClient, OmikronError};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreateUserRequest {
pub username: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LocalAccount {
pub user: LocalUserId,
pub username: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CredentialPreview {
pub user: LocalUserId,
pub username: String,
pub assigned_iota_id: Option<i64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReconcileResult {
pub remote_iota_id: Option<i64>,
pub released_locally: bool,
}
#[derive(Debug)]
pub enum AccountError {
InvalidRequest(String),
Unauthorized(String),
NotFound,
Conflict(String),
Timeout(String),
Unavailable(String),
Storage(String),
Internal(String),
}
#[async_trait]
pub trait AccountAuthority: Send + Sync {
async fn is_available(&self) -> bool;
async fn create_user(&self, request: CreateUserRequest) -> Result<LocalAccount, AccountError>;
async fn inspect_credential(
&self,
credential: &[u8],
) -> Result<CredentialPreview, AccountError>;
async fn attach_user(&self, credential: &[u8]) -> Result<LocalAccount, AccountError>;
async fn reconcile_user(&self, user: LocalUserId) -> Result<ReconcileResult, AccountError>;
async fn release_user(&self, user: LocalUserId) -> Result<(), AccountError>;
async fn delete_user(&self, user: LocalUserId, credential: &[u8]) -> Result<(), AccountError>;
async fn reconcile_managed_users(&self) -> Result<(), AccountError>;
}
pub struct OmegaAccountAuthority {
client: Arc<dyn OmikronClient>,
}
pub struct LocalIotaAccountAuthority {
identity: LocalNodeIdentity,
creation_lock: Mutex<()>,
}
impl LocalIotaAccountAuthority {
pub fn new(identity: LocalNodeIdentity) -> Self {
Self {
identity,
creation_lock: Mutex::new(()),
}
}
}
#[async_trait]
impl AccountAuthority for LocalIotaAccountAuthority {
async fn is_available(&self) -> bool {
true
}
async fn create_user(&self, request: CreateUserRequest) -> Result<LocalAccount, AccountError> {
if !valid_username(&request.username) {
return Err(AccountError::InvalidRequest("invalid username".into()));
}
let _creation = self
.creation_lock
.lock()
.map_err(|_| AccountError::Internal("local account lock is poisoned".into()))?;
if user_manager::get_user_by_username(&request.username)
.map_err(|error| AccountError::Storage(error.to_string()))?
.is_some()
{
return Err(AccountError::Conflict("username is unavailable".into()));
}
let user_id = user_manager::allocate_local_user_id(iota_util::tu::MAX_PROTOCOL_ID)
.map_err(|error| AccountError::Storage(error.to_string()))?;
let keyring = iota_util::crypto_helper::generate_keyring();
let public_key =
iota_util::crypto_helper::public_key_bundle_to_base64(&keyring.public_key_bundle());
if public_key.is_empty() {
return Err(AccountError::Internal(
"generated user public key could not be encoded".into(),
));
}
let credential = iota_util::tu::TuCredential {
user_id,
authority: iota_util::tu::TuAuthority::Iota(self.identity.node_id().clone()),
keyring,
};
iota_util::file_util::write_user_credential(
&request.username,
&credential.to_canonical_string(),
)
.map_err(|error| AccountError::Storage(error.to_string()))?;
let profile = iota_storage::users::user_profile::UserProfile::new(
user_id,
request.username.clone(),
None,
public_key.clone(),
None,
None,
);
if let Err(error) = user_manager::try_add_user(profile) {
let _ = iota_util::file_util::remove_user_credential(user_id, Some(&request.username));
return Err(AccountError::Storage(error.to_string()));
}
let principal = iota_storage::identity::SqlitePrincipalStore.ensure_local_principal(
self.identity.authority_id(),
AuthorityKind::Iota,
&LocalUserDescriptor {
id: LocalUserId(user_id),
username: request.username.clone(),
display_name: None,
public_key,
},
PrincipalHome::Iota(self.identity.node_id().clone()),
now_millis(),
);
if let Err(error) = principal {
let _ = user_manager::remove_user(user_id);
let _ = iota_util::file_util::remove_user_credential(user_id, Some(&request.username));
return Err(AccountError::Storage(error.to_string()));
}
Ok(LocalAccount {
user: LocalUserId(user_id),
username: request.username,
})
}
async fn inspect_credential(
&self,
credential: &[u8],
) -> Result<CredentialPreview, AccountError> {
let credential = parse_local_credential(credential, &self.identity)?;
let user = user_manager::get_user(credential.user_id)
.map_err(|error| AccountError::Storage(error.to_string()))?
.ok_or(AccountError::NotFound)?;
if user.public_key
!= iota_util::crypto_helper::public_key_bundle_to_base64(
&credential.public_key_bundle(),
)
{
return Err(AccountError::Unauthorized(
"credential key does not match local account".into(),
));
}
Ok(CredentialPreview {
user: LocalUserId(user.user_id),
username: user.username,
assigned_iota_id: None,
})
}
async fn attach_user(&self, credential: &[u8]) -> Result<LocalAccount, AccountError> {
let preview = self.inspect_credential(credential).await?;
Ok(LocalAccount {
user: preview.user,
username: preview.username,
})
}
async fn reconcile_user(&self, user: LocalUserId) -> Result<ReconcileResult, AccountError> {
if user_manager::get_user(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))?
.is_none()
{
return Err(AccountError::NotFound);
}
Ok(ReconcileResult {
remote_iota_id: None,
released_locally: false,
})
}
async fn release_user(&self, _: LocalUserId) -> Result<(), AccountError> {
Err(AccountError::InvalidRequest(
"local Iota accounts cannot be released to another authority".into(),
))
}
async fn delete_user(&self, user: LocalUserId, credential: &[u8]) -> Result<(), AccountError> {
let parsed = parse_local_credential(credential, &self.identity)?;
if parsed.user_id != user.0 {
return Err(AccountError::InvalidRequest(
"credential user ID does not match deletion target".into(),
));
}
self.inspect_credential(credential).await?;
user_manager::purge_user_data(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))?;
iota_storage::identity::SqlitePrincipalStore
.retire_local_principal(user, now_millis())
.map_err(|error| AccountError::Storage(error.to_string()))?;
user_manager::remove_user(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))?;
iota_util::file_util::remove_user_credential(user.0, None)
.map_err(|error| AccountError::Storage(error.to_string()))
}
async fn reconcile_managed_users(&self) -> Result<(), AccountError> {
Ok(())
}
}
fn parse_local_credential(
credential: &[u8],
identity: &LocalNodeIdentity,
) -> Result<iota_util::tu::TuCredential, AccountError> {
let credential = std::str::from_utf8(credential)
.map_err(|_| AccountError::InvalidRequest("credential is not UTF-8".into()))?;
let credential = iota_util::tu::TuCredential::parse(credential)
.map_err(|error| AccountError::InvalidRequest(error.to_string()))?;
if credential
.principal()
.map_err(|error| AccountError::InvalidRequest(error.to_string()))?
.authority
!= *identity.authority_id()
{
return Err(AccountError::Unauthorized(
"credential belongs to another authority".into(),
));
}
Ok(credential)
}
fn valid_username(username: &str) -> bool {
!username.is_empty()
&& username.len() <= 15
&& username
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
}
impl OmegaAccountAuthority {
pub fn new(client: Arc<dyn OmikronClient>) -> Self {
Self { client }
}
}
#[async_trait]
impl AccountAuthority for OmegaAccountAuthority {
async fn is_available(&self) -> bool {
self.client.is_connected().await
}
async fn create_user(&self, request: CreateUserRequest) -> Result<LocalAccount, AccountError> {
omikron_connector::user_ops::create_user(self.client.as_ref(), &request.username)
.await
.map(|user| LocalAccount {
user: LocalUserId(user.user_id),
username: user.username,
})
.map_err(map_create_error)
}
async fn inspect_credential(
&self,
credential: &[u8],
) -> Result<CredentialPreview, AccountError> {
let credential = std::str::from_utf8(credential)
.map_err(|_| AccountError::InvalidRequest("credential is not UTF-8".into()))?;
omikron_connector::user_ops::inspect_tu_credential(self.client.as_ref(), credential)
.await
.map(|preview| CredentialPreview {
user: LocalUserId(preview.user_id),
username: preview.username,
assigned_iota_id: preview.assigned_iota_id,
})
.map_err(map_lifecycle_error)
}
async fn attach_user(&self, credential: &[u8]) -> Result<LocalAccount, AccountError> {
let credential = std::str::from_utf8(credential)
.map_err(|_| AccountError::InvalidRequest("credential is not UTF-8".into()))?;
omikron_connector::user_ops::attach_user_from_tu(self.client.as_ref(), credential)
.await
.map(|user| LocalAccount {
user: LocalUserId(user.user_id),
username: user.username,
})
.map_err(map_lifecycle_error)
}
async fn reconcile_user(&self, user: LocalUserId) -> Result<ReconcileResult, AccountError> {
let residency = user_manager::get_residency_by_id(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))?
.ok_or(AccountError::NotFound)?;
let remote_iota_id =
omikron_connector::user_ops::get_remote_user_assignment(self.client.as_ref(), user.0)
.await
.map_err(map_lifecycle_error)?;
let local_iota_id = iota_storage::util::config_util::CONFIG
.load()
.iota_id
.and_then(|id| i64::try_from(id).ok());
let released_locally = residency.state == user_manager::LocalUserState::Managed
&& remote_iota_id != local_iota_id;
if released_locally {
user_manager::finalize_local_release(user.0, Some(&residency.username))
.map_err(|error| AccountError::Storage(error.to_string()))?;
}
Ok(ReconcileResult {
remote_iota_id,
released_locally,
})
}
async fn release_user(&self, user: LocalUserId) -> Result<(), AccountError> {
let profile = user_manager::get_user(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))?
.ok_or(AccountError::NotFound)?;
pending_operations::upsert(&PendingUserOperation {
user_id: user.0,
operation: PendingUserOperationKind::Release,
username: profile.username,
public_key: None,
private_key_hash: None,
reset_token: None,
registration_token: None,
phase: PendingUserOperationPhase::Prepared,
created_at: now_millis(),
})
.map_err(|error| AccountError::Storage(error.to_string()))?;
let request = CommunicationValue::new(CommunicationType::ReleaseUserFromIota)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user.0.into()));
match self
.client
.await_response(&request, Duration::from_secs(20))
.await
{
Ok(response) if response.is_type(CommunicationType::Success) => {
user_manager::release_user(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))?;
pending_operations::remove(user.0)
.map_err(|error| AccountError::Storage(error.to_string()))
}
Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => {
let _ = pending_operations::remove(user.0);
Err(AccountError::Unauthorized(
"release was not authorized".into(),
))
}
Ok(_) => {
let _ = pending_operations::remove(user.0);
Err(AccountError::Conflict("release was rejected".into()))
}
Err(OmikronError::Timeout(message)) => Err(AccountError::Timeout(message)),
Err(error) => Err(AccountError::Unavailable(error.to_string())),
}
}
async fn delete_user(&self, user: LocalUserId, credential: &[u8]) -> Result<(), AccountError> {
let credential = std::str::from_utf8(credential)
.map_err(|_| AccountError::InvalidRequest("credential is not UTF-8".into()))?;
omikron_connector::user_ops::complete_delete_user_with_tu(
self.client.as_ref(),
credential,
user.0,
)
.await
.map_err(map_lifecycle_error)
}
async fn reconcile_managed_users(&self) -> Result<(), AccountError> {
omikron_connector::user_ops::reconcile_managed_users(self.client.as_ref()).await;
Ok(())
}
}
fn map_create_error(error: omikron_connector::user_ops::CreateUserError) -> AccountError {
use omikron_connector::user_ops::CreateUserError;
match error {
CreateUserError::InvalidUsername => AccountError::InvalidRequest("invalid username".into()),
CreateUserError::Transport(OmikronError::Timeout(message)) => {
AccountError::Timeout(message)
}
CreateUserError::Transport(error) => AccountError::Unavailable(error.to_string()),
CreateUserError::RemoteRejected => AccountError::Conflict("username is unavailable".into()),
CreateUserError::LocalFinalizationPending { user_id } => {
AccountError::Storage(format!("local finalization is pending for user {user_id}"))
}
CreateUserError::LocalPersistence(message) => AccountError::Storage(message),
CreateUserError::InvalidResponse => {
AccountError::Internal("Omega returned an invalid create-user response".into())
}
}
}
fn map_lifecycle_error(error: omikron_connector::user_ops::LifecycleUserError) -> AccountError {
use omikron_connector::user_ops::LifecycleUserError;
match error {
LifecycleUserError::InvalidCredential(message) => AccountError::Unauthorized(message),
LifecycleUserError::OmegaHostMismatch => {
AccountError::Unauthorized("credential belongs to another Omega".into())
}
LifecycleUserError::RemoteRejected => {
AccountError::Unauthorized("credential was rejected".into())
}
LifecycleUserError::Transport(OmikronError::Timeout(message)) => {
AccountError::Timeout(message)
}
LifecycleUserError::Transport(error) => AccountError::Unavailable(error.to_string()),
LifecycleUserError::LocalPersistence(message) => AccountError::Storage(message),
}
}
fn now_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(i64::MAX)
}

View file

@ -9,9 +9,7 @@ use iota_ipc::{
UserOperationSummary, UserReconcileResult, UserSummary,
};
use iota_logger::{log, log_command};
use iota_storage::users::pending_operations::{
self, PendingUserOperation, PendingUserOperationKind, PendingUserOperationPhase,
};
use iota_storage::users::pending_operations::{self, PendingUserOperationKind};
use iota_storage::users::user_manager;
use iota_storage::util::config_util::{self};
use iota_util::mtp_compat::OptionalDataValueExt;
@ -19,7 +17,9 @@ use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::accounts::{AccountError, CreateUserRequest};
use crate::daemon_state::{ShutdownReason, StartupPhase};
use iota_identity::LocalUserId;
pub use iota_ipc::IpcRole;
@ -172,7 +172,7 @@ impl CommandRouter {
{
return ResponseResult::Error(IpcErrorCode::Unauthorized);
}
let needs_omikron = matches!(
let needs_account_authority = matches!(
request,
LocalRequest::CreateUser { .. }
| LocalRequest::InspectTuCredential { .. }
@ -180,12 +180,17 @@ impl CommandRouter {
| LocalRequest::ReconcileUser { .. }
| LocalRequest::ReleaseUser { .. }
| LocalRequest::CompleteDeleteUser { .. }
| LocalRequest::CreateInvitation {
authority: InvitationAuthority::Omega,
..
}
);
if needs_omikron && !self.services.omikron.is_connected().await {
let needs_centralized_provider = matches!(
request,
LocalRequest::CreateInvitation {
authority: InvitationAuthority::Omega,
..
}
);
if (needs_account_authority && !self.services.accounts.is_available().await)
|| (needs_centralized_provider && self.services.centralized.is_none())
{
return ResponseResult::Error(
if self.runtime.current_startup_phase() != StartupPhase::Ready {
IpcErrorCode::NotReady
@ -287,6 +292,9 @@ impl CommandRouter {
if lifetime_seconds == 0 || lifetime_seconds > 7 * 24 * 60 * 60 {
return ResponseResult::Error(IpcErrorCode::InvalidRequest);
}
let Some(omikron) = self.services.omikron() else {
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
};
let password_protected = password.is_some();
let mut request = CommunicationValue::new(CommunicationType::CreateUserInvitation)
.add_typed_default(
@ -309,9 +317,7 @@ impl CommandRouter {
DataValue::Str(label.clone()),
);
}
let response = match self
.services
.omikron
let response = match omikron
.await_response(&request, Duration::from_secs(20))
.await
{
@ -387,9 +393,10 @@ impl CommandRouter {
}
LocalRequest::ListInvitations { authority } => {
if authority != Some(InvitationAuthority::Iota)
&& self.services.omikron.is_connected().await
&& let Some(omikron) = self.services.omikron()
&& omikron.is_connected().await
{
match self.services.omikron.sync_omega_invitations().await {
match omikron.sync_omega_invitations().await {
Ok(()) => {}
Err(omikron_connector::OmikronError::Storage(_)) => {
return ResponseResult::Error(IpcErrorCode::StorageFailure);
@ -487,11 +494,10 @@ impl CommandRouter {
if !changed {
return ResponseResult::Error(IpcErrorCode::Conflict);
}
if self.services.omikron.is_connected().await {
self.services
.omikron
.flush_pending_invitation_actions()
.await;
if let Some(omikron) = self.services.omikron()
&& omikron.is_connected().await
{
omikron.flush_pending_invitation_actions().await;
}
let invitation =
match iota_storage::users::invitations::list()
@ -541,40 +547,41 @@ impl CommandRouter {
}))
}
LocalRequest::CreateUser { username } => {
match omikron_connector::user_ops::create_user(
self.services.omikron.as_ref(),
&username,
)
.await
match self
.services
.accounts
.create_user(CreateUserRequest { username })
.await
{
Ok(user) => ResponseResult::Ok(ResponsePayload::UserCreated {
user_id: user.user_id,
user_id: user.user.0,
username: user.username,
}),
Err(error) => {
log!("User creation failed: {error:?}");
match error {
omikron_connector::user_ops::CreateUserError::InvalidUsername => {
AccountError::InvalidRequest(_) => {
ResponseResult::Error(IpcErrorCode::InvalidRequest)
}
omikron_connector::user_ops::CreateUserError::Transport(
omikron_connector::OmikronError::Timeout(_),
) => ResponseResult::Error(IpcErrorCode::Timeout),
omikron_connector::user_ops::CreateUserError::Transport(_) => {
AccountError::Timeout(_) => {
ResponseResult::Error(IpcErrorCode::Timeout)
}
AccountError::Unavailable(_) => {
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
}
omikron_connector::user_ops::CreateUserError::RemoteRejected => {
AccountError::Conflict(_) => {
ResponseResult::Error(IpcErrorCode::Conflict)
}
omikron_connector::user_ops::CreateUserError::LocalFinalizationPending { .. } => {
AccountError::Storage(_) => {
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
omikron_connector::user_ops::CreateUserError::LocalPersistence(_) => {
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
omikron_connector::user_ops::CreateUserError::InvalidResponse => {
AccountError::Internal(_) => {
ResponseResult::Error(IpcErrorCode::InternalFailure)
}
AccountError::Unauthorized(_) => {
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
AccountError::NotFound => ResponseResult::Error(IpcErrorCode::NotFound),
}
}
}
@ -594,14 +601,14 @@ impl CommandRouter {
}
},
LocalRequest::AttachUserFromTu { credential } => {
match omikron_connector::user_ops::attach_user_from_tu(
self.services.omikron.as_ref(),
&credential.0,
)
.await
match self
.services
.accounts
.attach_user(credential.0.as_bytes())
.await
{
Ok(user) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Added {} ({}) to this Iota", user.username, user.user_id),
message: format!("Added {} ({}) to this Iota", user.username, user.user.0),
}),
Err(error) => {
log!("Credential attach failed: {error:?}");
@ -610,15 +617,15 @@ impl CommandRouter {
}
}
LocalRequest::InspectTuCredential { credential } => {
match omikron_connector::user_ops::inspect_tu_credential(
self.services.omikron.as_ref(),
&credential.0,
)
.await
match self
.services
.accounts
.inspect_credential(credential.0.as_bytes())
.await
{
Ok(preview) => ResponseResult::Ok(ResponsePayload::TuCredentialPreview(
iota_ipc::TuCredentialPreview {
user_id: preview.user_id,
user_id: preview.user.0,
username: preview.username,
assigned_iota_id: preview.assigned_iota_id,
},
@ -635,32 +642,20 @@ impl CommandRouter {
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let omega_iota_id = match omikron_connector::user_ops::get_remote_user_assignment(
self.services.omikron.as_ref(),
user_id,
)
.await
let reconciliation = match self
.services
.accounts
.reconcile_user(LocalUserId(user_id))
.await
{
Ok(assignment) => assignment,
Ok(result) => result,
Err(error) => {
log!("User reconciliation failed for {user_id}: {error:?}");
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
}
};
let local_iota_id = config_util::CONFIG
.load()
.iota_id
.and_then(|id| i64::try_from(id).ok());
let action = if residency.state == user_manager::LocalUserState::Managed
&& omega_iota_id != local_iota_id
{
match user_manager::finalize_local_release(user_id, Some(&residency.username)) {
Ok(()) => ReconcileAction::ReleasedLocally,
Err(error) => {
log!("User reconciliation cleanup failed for {user_id}: {error}");
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}
let action = if reconciliation.released_locally {
ReconcileAction::ReleasedLocally
} else {
ReconcileAction::None
};
@ -672,7 +667,7 @@ impl CommandRouter {
iota_ipc::LocalUserState::Released
}
},
omega_iota_id,
omega_iota_id: reconciliation.remote_iota_id,
action,
}))
}
@ -787,12 +782,11 @@ impl CommandRouter {
let Ok(contents) = contents else {
return ResponseResult::Error(IpcErrorCode::Unauthorized);
};
match omikron_connector::user_ops::complete_delete_user_with_tu(
self.services.omikron.as_ref(),
&contents,
user_id,
)
.await
match self
.services
.accounts
.delete_user(LocalUserId(user_id), contents.as_bytes())
.await
{
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Deleted Tensamin account {user_id}"),
@ -805,76 +799,41 @@ impl CommandRouter {
}
LocalRequest::RemoveUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest),
LocalRequest::ReleaseUser { user_id } => {
let user = match user_manager::get_user(user_id) {
Ok(user) => user,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let Some(user) = user else {
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)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
match self
.services
.omikron
.await_response(&request, Duration::from_secs(20))
.accounts
.release_user(LocalUserId(user_id))
.await
{
Ok(response) if response.is_type(CommunicationType::Success) => {
match user_manager::release_user(user_id) {
Ok(()) if pending_operations::remove(user_id).is_ok() => {
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!(
"Released user {user_id}; hosted data was retained"
),
})
}
Ok(()) => ResponseResult::Error(IpcErrorCode::StorageFailure),
Err(error) => {
log!(
"Remote release succeeded but local cleanup failed for {user_id}: {error}"
);
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
}
}
Ok(response) if response.is_type(CommunicationType::ErrorNotAuthenticated) => {
let _ = pending_operations::remove(user_id);
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Released user {user_id}; hosted data was retained"),
}),
Err(AccountError::NotFound) => ResponseResult::Error(IpcErrorCode::NotFound),
Err(AccountError::Unauthorized(_)) => {
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
Ok(_) => {
let _ = pending_operations::remove(user_id);
ResponseResult::Error(IpcErrorCode::Conflict)
Err(AccountError::Conflict(_)) => ResponseResult::Error(IpcErrorCode::Conflict),
Err(AccountError::Timeout(_)) => ResponseResult::Error(IpcErrorCode::Timeout),
Err(AccountError::Unavailable(_)) => {
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
}
Err(omikron_connector::OmikronError::Timeout(_)) => {
ResponseResult::Error(IpcErrorCode::Timeout)
Err(AccountError::Storage(_)) => {
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
Err(_) => ResponseResult::Error(IpcErrorCode::InternalFailure),
}
}
LocalRequest::ReconnectOmikron => match self.services.omikron.reconnect().await {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Reconnected to Omikron server".into(),
}),
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
LocalRequest::ReconnectOmikron => match self.services.omikron() {
Some(omikron) => match omikron.reconnect().await {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Reconnected to Omikron server".into(),
}),
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
},
None => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
},
LocalRequest::RotateIotaIdentity => {
match self.services.omikron.rotate_identity().await {
LocalRequest::RotateIotaIdentity => match self.services.omikron() {
Some(omikron) => match omikron.rotate_identity().await {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "New identity registered with Omikron".into(),
}),
@ -882,8 +841,9 @@ impl CommandRouter {
log!("Iota identity rotation failed: {}", error);
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
}
}
}
},
None => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
},
LocalRequest::RequestProcessExit { intent } => {
if matches!(intent, ExitIntent::Restart)
&& !matches!(
@ -938,7 +898,10 @@ impl CommandRouter {
Err(_) => ResponseResult::Error(IpcErrorCode::InvalidRequest),
},
LocalRequest::GetOmikronStatus => {
let connected = self.services.omikron.is_connected().await;
let connected = match self.services.omikron() {
Some(omikron) => omikron.is_connected().await,
None => false,
};
let iota_id = config_util::CONFIG.load().iota_id;
ResponseResult::Ok(ResponsePayload::OmikronStatus(OmikronStatusResponse {
connected,

View file

@ -1,3 +1,4 @@
pub mod accounts;
pub mod command_router;
pub mod daemon_state;
pub mod deployment;
@ -7,8 +8,9 @@ pub mod log_buffer;
pub mod services;
pub mod task_registry;
pub use accounts::{AccountAuthority, LocalIotaAccountAuthority, OmegaAccountAuthority};
pub use command_router::{CommandRouter, IpcRole, PeerContext};
pub use daemon_state::{DaemonRuntime, ShutdownReason, StartupPhase};
pub use ipc_server::IpcServer;
pub use services::DaemonServices;
pub use services::{DaemonServiceComponents, DaemonServices};
pub use task_registry::TaskRegistry;

View file

@ -1,72 +1,524 @@
use async_trait::async_trait;
use iota_auth::{
ForeignPrincipalAuthenticator, HostedSessionRegistrar, IotaPeerAuthenticator, SessionManager,
};
use iota_connection::connection_handler::{PeerRouter, RouteDestination, RouteError, RouteOutcome};
use iota_connection::relay_service::{
LegacyRelayDecoder, LegacyRelayIdentity, RelayNodeIdentity, RelayService,
};
use iota_identity::{
AuthorityId, AuthorityKind, AuthorityLocator, CompositeIdentityResolver, IdentityError,
IdentityResolver, IotaNodeId, LocalUserStore, NodeDirectory, NodeIdentityResolver,
PrincipalHome, PrincipalId, PrincipalStore, ResolutionContext, ResolvedNodeIdentity,
ResolvedPrincipal, UserAddress,
};
use mtp::codec::CommunicationValue;
use omikron_connector::{OmikronClient, OmikronConnection, OmikronError};
use omikron_connector::{OmikronClient, OmikronConnection};
use std::sync::Arc;
use std::time::Duration;
use crate::accounts::{
AccountAuthority, AccountError, CreateUserRequest, CredentialPreview, LocalAccount,
LocalIotaAccountAuthority, OmegaAccountAuthority, ReconcileResult,
};
#[derive(Default)]
pub struct UserService;
#[derive(Default)]
pub struct ConfigService;
pub struct DaemonServices {
pub struct CentralizedServices {
pub omikron: Arc<dyn OmikronClient>,
}
pub struct AuthService {
pub hosted_accounts: Arc<HostedSessionRegistrar>,
pub foreign_principals: Arc<ForeignPrincipalAuthenticator>,
pub iota_peers: Arc<IotaPeerAuthenticator>,
}
pub struct DaemonServiceComponents {
pub accounts: Arc<dyn AccountAuthority>,
pub identities: Arc<dyn IdentityResolver>,
pub principals: Arc<dyn PrincipalStore>,
pub local_users: Arc<dyn LocalUserStore>,
pub node_identities: Arc<dyn NodeIdentityResolver>,
pub relay: Arc<RelayService>,
pub router: Arc<dyn PeerRouter>,
pub direct_router: Option<Arc<other_iota::DirectPeerRouter>>,
pub relay_routers: Option<Arc<other_iota::relay_router::RelayRouterSet>>,
pub centralized: Option<Arc<CentralizedServices>>,
}
pub struct DaemonServices {
pub accounts: Arc<dyn AccountAuthority>,
pub identities: Arc<dyn IdentityResolver>,
pub principals: Arc<dyn PrincipalStore>,
pub local_users: Arc<dyn LocalUserStore>,
pub relay: Arc<RelayService>,
pub router: Arc<dyn PeerRouter>,
pub direct_router: Option<Arc<other_iota::DirectPeerRouter>>,
pub relay_routers: Option<Arc<other_iota::relay_router::RelayRouterSet>>,
pub sessions: Arc<SessionManager>,
pub auth: Arc<AuthService>,
pub centralized: Option<Arc<CentralizedServices>>,
pub users: Arc<UserService>,
pub config: Arc<ConfigService>,
pub active: bool,
}
impl DaemonServices {
pub fn new(omikron: Arc<OmikronConnection>) -> Arc<Self> {
pub fn standalone(
identity: iota_identity::LocalNodeIdentity,
) -> Result<Arc<Self>, IdentityError> {
let principals = Arc::new(iota_storage::identity::SqlitePrincipalStore);
let local_users: Arc<dyn LocalUserStore> =
Arc::new(iota_storage::identity::SqliteLocalUserStore);
let local_identity: Arc<dyn IdentityResolver> =
Arc::new(iota_storage::identity::LocalIdentityResolver::new(
identity.authority_id().clone(),
AuthorityKind::Iota,
PrincipalHome::Iota(identity.node_id().clone()),
local_users.clone(),
principals.clone(),
));
let nodes = Arc::new(iota_storage::node_directory::SqliteNodeDirectory);
let node_directory: Arc<dyn NodeDirectory> = nodes.clone();
let federation_client: Arc<dyn other_iota::FederationIdentityClient> =
Arc::new(other_iota::HttpFederationIdentityClient::default());
let remote_iota: Arc<dyn IdentityResolver> = Arc::new(other_iota::RemoteIotaResolver::new(
federation_client.clone(),
principals.clone(),
node_directory.clone(),
));
let remote_omega: Arc<dyn IdentityResolver> =
Arc::new(other_iota::RemoteOmegaResolver::new(
federation_client,
principals.clone(),
node_directory,
));
let identities: Arc<dyn IdentityResolver> =
Arc::new(CompositeIdentityResolver::new(vec![
local_identity,
remote_iota,
remote_omega,
])?);
let direct_router = Arc::new(other_iota::DirectPeerRouter::default());
let relay_routers = Arc::new(other_iota::relay_router::RelayRouterSet::default());
let router: Arc<dyn PeerRouter> =
Arc::new(other_iota::relay_router::DirectThenRelayRouter::new(
direct_router.clone(),
relay_routers.clone(),
));
let relay_identity: Arc<dyn RelayNodeIdentity> =
Arc::new(StandaloneRelayIdentity(identity.clone()));
let relay = Arc::new(
RelayService::new(local_users.clone(), router.clone(), None)
.with_federation(identities.clone(), relay_identity),
);
Ok(Self::compose(DaemonServiceComponents {
accounts: Arc::new(LocalIotaAccountAuthority::new(identity.clone())),
identities,
principals,
local_users,
node_identities: nodes,
relay,
router,
direct_router: Some(direct_router),
relay_routers: Some(relay_routers),
centralized: None,
}))
}
pub fn new(omikron: Arc<OmikronConnection>) -> Result<Arc<Self>, IdentityError> {
let client: Arc<dyn OmikronClient> = omikron.clone();
let authority =
AuthorityId::omega_legacy(&omikron_connector::omega_discovery::omega_host())
.map_err(|error| IdentityError::InvalidIdentifier(error.to_string()))?;
iota_storage::identity::SqlitePrincipalStore.migrate_legacy_omega_authority(&authority)?;
let router: Arc<dyn PeerRouter> = Arc::new(
omikron_connector::router::OmikronPeerRouter::new(client.clone(), authority.clone()),
);
let services = Self::centralized(
client,
Some(router),
Some(omikron.session_manager()),
Some(omikron.clone()),
);
let _ = omikron.install_relay_service(services.relay.clone());
Ok(services)
}
pub fn with_centralized_client(client: Arc<dyn OmikronClient>) -> Arc<Self> {
let inactive = Arc::new(InactiveServices);
Self::centralized(client, None, None, Some(inactive))
}
fn centralized(
client: Arc<dyn OmikronClient>,
router: Option<Arc<dyn PeerRouter>>,
sessions: Option<Arc<SessionManager>>,
node_identity: Option<Arc<dyn LegacyRelayIdentity>>,
) -> Arc<Self> {
let principals: Arc<dyn PrincipalStore> =
Arc::new(iota_storage::identity::SqlitePrincipalStore);
let local_users: Arc<dyn LocalUserStore> =
Arc::new(iota_storage::identity::SqliteLocalUserStore);
let omega_host = omikron_connector::omega_discovery::omega_host();
let authority = AuthorityId::omega_legacy(&omega_host)
.expect("the configured Omega authority ID is valid");
let locator = AuthorityLocator::new(omega_host)
.expect("the configured Omega host is a valid authority locator");
let local_identity: Arc<dyn IdentityResolver> =
Arc::new(iota_storage::identity::LocalIdentityResolver::new(
authority.clone(),
AuthorityKind::Omega,
PrincipalHome::Omega(locator.clone()),
local_users.clone(),
Arc::new(iota_storage::identity::SqlitePrincipalStore),
));
let omega_identity: Arc<dyn IdentityResolver> =
Arc::new(omikron_connector::identity::OmegaIdentityResolver::new(
client.clone(),
authority.clone(),
locator,
principals.clone(),
));
let identities: Arc<dyn IdentityResolver> = Arc::new(
CompositeIdentityResolver::new(vec![local_identity, omega_identity])
.expect("the identity resolver chain is not empty"),
);
let router: Arc<dyn PeerRouter> = router.unwrap_or_else(|| {
Arc::new(omikron_connector::router::OmikronPeerRouter::new(
client.clone(),
authority.clone(),
))
});
let legacy_decoder = node_identity.map(|node_identity| {
Arc::new(LegacyRelayDecoder::new(
identities.clone(),
node_identity,
authority,
))
});
let relay = Arc::new(RelayService::new(
local_users.clone(),
router.clone(),
legacy_decoder,
));
Self::compose_with_sessions(
DaemonServiceComponents {
accounts: Arc::new(OmegaAccountAuthority::new(client.clone())),
identities,
principals,
local_users,
node_identities: Arc::new(InactiveNodeIdentityResolver),
relay,
router,
direct_router: None,
relay_routers: None,
centralized: Some(Arc::new(CentralizedServices { omikron: client })),
},
sessions.unwrap_or_else(|| Arc::new(SessionManager::default())),
)
}
pub fn compose(components: DaemonServiceComponents) -> Arc<Self> {
Self::compose_with_sessions(components, Arc::new(SessionManager::default()))
}
fn compose_with_sessions(
components: DaemonServiceComponents,
sessions: Arc<SessionManager>,
) -> Arc<Self> {
let auth = Arc::new(AuthService {
hosted_accounts: Arc::new(HostedSessionRegistrar::new(sessions.clone())),
foreign_principals: Arc::new(ForeignPrincipalAuthenticator::new(
components.identities.clone(),
sessions.clone(),
)),
iota_peers: Arc::new(IotaPeerAuthenticator::new(
components.node_identities.clone(),
)),
});
Arc::new(Self {
omikron,
accounts: components.accounts,
identities: components.identities,
principals: components.principals,
local_users: components.local_users,
relay: components.relay,
router: components.router,
direct_router: components.direct_router,
relay_routers: components.relay_routers,
sessions,
auth,
centralized: components.centralized,
users: Arc::new(UserService),
config: Arc::new(ConfigService),
active: true,
})
}
/// Services used while the daemon is awaiting terms acceptance. They can
/// never initiate a connection; the command router exposes status only.
pub fn inactive() -> Arc<Self> {
let identities: Arc<dyn IdentityResolver> = Arc::new(InactiveIdentityResolver);
let sessions = Arc::new(SessionManager::default());
let auth = Arc::new(AuthService {
hosted_accounts: Arc::new(HostedSessionRegistrar::new(sessions.clone())),
foreign_principals: Arc::new(ForeignPrincipalAuthenticator::new(
identities.clone(),
sessions.clone(),
)),
iota_peers: Arc::new(IotaPeerAuthenticator::new(Arc::new(
InactiveNodeIdentityResolver,
))),
});
let inactive = Arc::new(InactiveServices);
Arc::new(Self {
omikron: Arc::new(InactiveOmikron),
accounts: inactive.clone(),
identities: identities.clone(),
principals: inactive.clone(),
local_users: inactive.clone(),
relay: Arc::new(RelayService::new(inactive.clone(), inactive.clone(), None)),
router: inactive,
direct_router: None,
relay_routers: None,
sessions,
auth,
centralized: None,
users: Arc::new(UserService),
config: Arc::new(ConfigService),
active: false,
})
}
pub fn omikron(&self) -> Option<&Arc<dyn OmikronClient>> {
self.centralized.as_ref().map(|services| &services.omikron)
}
}
struct InactiveOmikron;
struct InactiveIdentityResolver;
struct InactiveNodeIdentityResolver;
struct StandaloneRelayIdentity(iota_identity::LocalNodeIdentity);
#[async_trait]
impl OmikronClient for InactiveOmikron {
async fn send_message(&self, _: &CommunicationValue) -> Result<(), OmikronError> {
Err(OmikronError::Disconnected(
"terms have not been accepted".into(),
))
impl RelayNodeIdentity for StandaloneRelayIdentity {
async fn keyring(&self) -> Option<Arc<mtp::crypto::Keyring>> {
Some(self.0.keyring())
}
async fn await_response(
fn node_id(&self) -> Option<IotaNodeId> {
Some(self.0.node_id().clone())
}
}
#[async_trait]
impl NodeIdentityResolver for InactiveNodeIdentityResolver {
async fn resolve_node(&self, _: &IotaNodeId) -> Result<ResolvedNodeIdentity, IdentityError> {
Err(IdentityError::Unavailable(
"node identity resolution is unavailable".into(),
))
}
}
#[async_trait]
impl IdentityResolver for InactiveIdentityResolver {
async fn resolve_address(
&self,
_: &CommunicationValue,
_: Duration,
) -> Result<CommunicationValue, OmikronError> {
Err(OmikronError::Disconnected(
"terms have not been accepted".into(),
))
_: &UserAddress,
_: &ResolutionContext,
) -> Result<ResolvedPrincipal, IdentityError> {
Err(IdentityError::Unavailable("daemon is inactive".into()))
}
async fn reconnect(&self) -> Result<(), OmikronError> {
Err(OmikronError::Disconnected(
"terms have not been accepted".into(),
))
async fn resolve_principal(&self, _: &PrincipalId) -> Result<ResolvedPrincipal, IdentityError> {
Err(IdentityError::Unavailable("daemon is inactive".into()))
}
async fn rotate_identity(&self) -> Result<(), OmikronError> {
Err(OmikronError::Disconnected(
"terms have not been accepted".into(),
))
async fn signing_keys(
&self,
_: &PrincipalId,
_: &ResolutionContext,
) -> Result<Vec<iota_identity::PublicKeyBundle>, IdentityError> {
Err(IdentityError::Unavailable("daemon is inactive".into()))
}
async fn is_connected(&self) -> bool {
}
struct InactiveServices;
#[async_trait]
impl AccountAuthority for InactiveServices {
async fn is_available(&self) -> bool {
false
}
async fn create_user(&self, _: CreateUserRequest) -> Result<LocalAccount, AccountError> {
Err(inactive_account())
}
async fn inspect_credential(&self, _: &[u8]) -> Result<CredentialPreview, AccountError> {
Err(inactive_account())
}
async fn attach_user(&self, _: &[u8]) -> Result<LocalAccount, AccountError> {
Err(inactive_account())
}
async fn reconcile_user(
&self,
_: iota_identity::LocalUserId,
) -> Result<ReconcileResult, AccountError> {
Err(inactive_account())
}
async fn release_user(&self, _: iota_identity::LocalUserId) -> Result<(), AccountError> {
Err(inactive_account())
}
async fn delete_user(
&self,
_: iota_identity::LocalUserId,
_: &[u8],
) -> Result<(), AccountError> {
Err(inactive_account())
}
async fn reconcile_managed_users(&self) -> Result<(), AccountError> {
Err(inactive_account())
}
}
fn inactive_account() -> AccountError {
AccountError::Unavailable("daemon is inactive".into())
}
impl LocalUserStore for InactiveServices {
fn get_local_user(
&self,
_: iota_identity::LocalUserId,
) -> Result<Option<iota_identity::LocalUserDescriptor>, IdentityError> {
Ok(None)
}
fn get_local_user_by_username(
&self,
_: &str,
) -> Result<Option<iota_identity::LocalUserDescriptor>, IdentityError> {
Ok(None)
}
fn is_hosted_here(&self, _: iota_identity::LocalUserId) -> Result<bool, IdentityError> {
Ok(false)
}
fn local_user_for_principal(
&self,
_: iota_identity::PrincipalHandle,
) -> Result<Option<iota_identity::LocalUserId>, IdentityError> {
Ok(None)
}
fn principal_for_local_user(
&self,
_: iota_identity::LocalUserId,
) -> Result<Option<iota_identity::PrincipalHandle>, IdentityError> {
Ok(None)
}
}
impl PrincipalStore for InactiveServices {
fn get_principal(
&self,
_: iota_identity::PrincipalHandle,
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
Ok(None)
}
fn get_by_canonical_id(
&self,
_: &PrincipalId,
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
Ok(None)
}
fn get_by_username(
&self,
_: &AuthorityId,
_: &str,
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
Ok(None)
}
fn upsert_remote_descriptor(
&self,
_: &iota_identity::VerifiedPrincipalDescriptor,
) -> Result<iota_identity::PrincipalHandle, IdentityError> {
Err(IdentityError::Unavailable("daemon is inactive".into()))
}
fn signing_keys(
&self,
_: &PrincipalId,
) -> Result<Vec<iota_identity::PublicKeyBundle>, IdentityError> {
Err(IdentityError::NotFound)
}
}
#[async_trait]
impl PeerRouter for InactiveServices {
async fn route(
&self,
destination: &RouteDestination,
_: CommunicationValue,
) -> Result<RouteOutcome, RouteError> {
match destination {
RouteDestination::Iota(node) => Err(RouteError::NoRoute(node.clone())),
RouteDestination::LegacyOmegaIota { .. } => Ok(RouteOutcome::Retryable {
reason: "daemon is inactive".into(),
}),
}
}
}
#[async_trait]
impl RelayNodeIdentity for InactiveServices {
async fn keyring(&self) -> Option<Arc<mtp::crypto::Keyring>> {
None
}
fn node_id(&self) -> Option<IotaNodeId> {
None
}
}
impl LegacyRelayIdentity for InactiveServices {
fn legacy_iota_id(&self) -> Option<u64> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn active_services_can_be_composed_without_omikron() {
let adapters = Arc::new(InactiveServices);
let identities: Arc<dyn IdentityResolver> = Arc::new(InactiveIdentityResolver);
let services = DaemonServices::compose(DaemonServiceComponents {
accounts: adapters.clone(),
identities,
principals: adapters.clone(),
local_users: adapters.clone(),
node_identities: Arc::new(InactiveNodeIdentityResolver),
relay: Arc::new(RelayService::new(adapters.clone(), adapters.clone(), None)),
router: adapters,
direct_router: None,
relay_routers: None,
centralized: None,
});
assert!(services.active);
assert!(services.centralized.is_none());
}
}

View file

@ -59,12 +59,7 @@ async fn reconnect_uses_the_injected_client() {
let fake = Arc::new(FakeOmikron {
reconnects: AtomicUsize::new(0),
});
let services = Arc::new(DaemonServices {
omikron: fake.clone(),
users: Default::default(),
config: Default::default(),
active: true,
});
let services = DaemonServices::with_centralized_client(fake.clone());
let router = CommandRouter::new(
Arc::new(DaemonRuntime::new()),
services,
@ -85,12 +80,7 @@ async fn identity_rotation_is_available_while_omikron_is_offline() {
let fake = Arc::new(FakeOmikron {
reconnects: AtomicUsize::new(0),
});
let services = Arc::new(DaemonServices {
omikron: fake.clone(),
users: Default::default(),
config: Default::default(),
active: true,
});
let services = DaemonServices::with_centralized_client(fake.clone());
let router = CommandRouter::new(
Arc::new(DaemonRuntime::new()),
services,
@ -111,12 +101,7 @@ async fn read_role_cannot_execute_an_administrative_request() {
let fake = Arc::new(FakeOmikron {
reconnects: AtomicUsize::new(0),
});
let services = Arc::new(DaemonServices {
omikron: fake.clone(),
users: Default::default(),
config: Default::default(),
active: true,
});
let services = DaemonServices::with_centralized_client(fake.clone());
let router = CommandRouter::new(
Arc::new(DaemonRuntime::new()),
services,

View file

@ -182,12 +182,7 @@ impl OmikronClient for TestOmikron {
}
fn active_services() -> Arc<DaemonServices> {
Arc::new(DaemonServices {
omikron: Arc::new(TestOmikron),
users: Default::default(),
config: Default::default(),
active: true,
})
DaemonServices::with_centralized_client(Arc::new(TestOmikron))
}
#[tokio::test]

View file

@ -0,0 +1,57 @@
use iota_daemon_lib::accounts::{AccountAuthority, CreateUserRequest, LocalIotaAccountAuthority};
use iota_identity::{LocalNodeIdentity, LocalUserId, PrincipalStore};
use iota_storage::identity::SqlitePrincipalStore;
#[tokio::test]
async fn standalone_accounts_survive_identity_reload_and_do_not_reuse_deleted_ids() {
let root = tempfile::tempdir().unwrap();
let storage = root.path().join("storage");
iota_util::file_util::configure_storage_directory(storage);
iota_storage::util::db::initialize_database().unwrap();
let identity_path = root.path().join("identity/iota.mk");
let identity = LocalNodeIdentity::load_or_create(&identity_path, None).unwrap();
let authority = LocalIotaAccountAuthority::new(identity.clone());
let alice = authority
.create_user(CreateUserRequest {
username: "alice".into(),
})
.await
.unwrap();
let alice_principal = SqlitePrincipalStore
.principal_for_local_user(alice.user)
.unwrap()
.and_then(|handle| SqlitePrincipalStore.get_principal(handle).unwrap())
.unwrap();
let credential = iota_util::file_util::read_user_credential("alice")
.unwrap()
.unwrap();
let restarted_identity = LocalNodeIdentity::load_or_create(&identity_path, None).unwrap();
assert_eq!(restarted_identity.node_id(), identity.node_id());
let restarted = LocalIotaAccountAuthority::new(restarted_identity);
let restored_principal = SqlitePrincipalStore
.principal_for_local_user(LocalUserId(alice.user.0))
.unwrap()
.and_then(|handle| SqlitePrincipalStore.get_principal(handle).unwrap())
.unwrap();
assert_eq!(restored_principal.principal, alice_principal.principal);
restarted
.delete_user(alice.user, credential.as_bytes())
.await
.unwrap();
let bob = restarted
.create_user(CreateUserRequest {
username: "bob".into(),
})
.await
.unwrap();
assert!(bob.user.0 > alice.user.0);
assert!(
SqlitePrincipalStore
.get_by_canonical_id(&alice_principal.principal)
.unwrap()
.is_some()
);
}