[Fix] Connection Management
This commit is contained in:
parent
9e9e3597da
commit
3f2ac18333
122 changed files with 19970 additions and 5263 deletions
|
|
@ -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"
|
||||
|
|
|
|||
451
iota-daemon-lib/src/accounts.rs
Normal file
451
iota-daemon-lib/src/accounts.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
57
iota-daemon-lib/tests/local_authority.rs
Normal file
57
iota-daemon-lib/tests/local_authority.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue