iota/iota-daemon-lib/src/command_router.rs
2026-09-10 23:14:25 +02:00

1122 lines
51 KiB
Rust

use crate::log_buffer::LogBuffer;
use crate::{DaemonRuntime, DaemonServices};
use iota_ipc::{
CommunitySummary, ComponentStatusResponse, ConfigResponse, DaemonMessage, ExitIntent,
InvitationAuthority, InvitationCreated, InvitationState, InvitationSummary, IpcErrorCode,
LocalRequest, LogEntriesResponse, LogEntry, MAX_MESSAGE_SIZE, OmikronStatusResponse,
ReconcileAction, ResponseEnvelope, ResponsePayload, ResponseResult, StatusResponse,
TaskSummary, UpdateStatusResponse, UserDetailResponse, UserDiagnostics, UserOperationKind,
UserOperationSummary, UserReconcileResult, UserSummary,
};
use iota_logger::{log, log_command};
use iota_storage::users::pending_operations::{
self, PendingUserOperation, PendingUserOperationKind, PendingUserOperationPhase,
};
use iota_storage::users::user_manager;
use iota_storage::util::config_util::{self};
use iota_util::mtp_compat::OptionalDataValueExt;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::daemon_state::{ShutdownReason, StartupPhase};
pub use iota_ipc::IpcRole;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PeerContext {
pub pid: i32,
pub uid: u32,
pub role: IpcRole,
}
const MAX_LOG_ENTRIES_PER_RESPONSE: usize = 512;
fn credential_status(
residency: &user_manager::UserResidency,
profile: Option<&iota_storage::users::user_profile::UserProfile>,
) -> iota_ipc::CredentialStatus {
match (residency.state, residency.credential_origin) {
(user_manager::LocalUserState::Released, _) => iota_ipc::CredentialStatus::None,
(_, user_manager::CredentialOrigin::External) => iota_ipc::CredentialStatus::External,
(_, user_manager::CredentialOrigin::Local)
if profile.is_some_and(|profile| {
iota_util::file_util::read_user_credential_with_legacy(
residency.user_id,
&profile.username,
)
.ok()
.flatten()
.is_some()
}) =>
{
iota_ipc::CredentialStatus::LocalPresent
}
_ => iota_ipc::CredentialStatus::Missing,
}
}
fn now_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
fn bounded_log_entries(mut entries: Vec<LogEntry>) -> Vec<LogEntry> {
entries.truncate(MAX_LOG_ENTRIES_PER_RESPONSE);
while !entries.is_empty() {
let response = DaemonMessage::Response(ResponseEnvelope {
request_id: u64::MAX,
result: ResponseResult::Ok(ResponsePayload::LogEntries(LogEntriesResponse {
entries: entries.clone(),
})),
});
let fits = serde_json::to_vec(&response)
.map(|encoded| encoded.len() <= MAX_MESSAGE_SIZE)
.unwrap_or(false);
if fits {
return entries;
}
entries.remove(0);
}
entries
}
#[derive(Clone)]
pub struct CommandRouter {
runtime: Arc<DaemonRuntime>,
services: Arc<DaemonServices>,
log_buffer: Arc<Mutex<LogBuffer>>,
}
impl CommandRouter {
pub fn new(
runtime: Arc<DaemonRuntime>,
services: Arc<DaemonServices>,
log_buffer: Arc<Mutex<LogBuffer>>,
) -> Self {
Self {
runtime,
services,
log_buffer,
}
}
pub async fn route(
&self,
peer: &PeerContext,
request_id: u64,
request: LocalRequest,
) -> ResponseEnvelope {
if !peer.role.allows(request.required_role()) {
log!(
"IPC authorization denied: pid={}, uid={}, role={:?}, request={:?}",
peer.pid,
peer.uid,
peer.role,
request
);
return ResponseEnvelope {
request_id,
result: ResponseResult::Error(IpcErrorCode::Unauthorized),
};
}
log_command!(
"pid={} uid={} role={:?} request={:?}",
peer.pid,
peer.uid,
peer.role,
request
);
let result = self.execute(request).await;
ResponseEnvelope { request_id, result }
}
async fn execute(&self, request: LocalRequest) -> ResponseResult {
if !self.services.active
&& !matches!(
request,
LocalRequest::GetStatus | LocalRequest::GetDaemonStatus
)
{
return ResponseResult::Error(IpcErrorCode::Unauthorized);
}
let needs_omikron = matches!(
request,
LocalRequest::CreateUser { .. }
| LocalRequest::InspectTuCredential { .. }
| LocalRequest::AttachUserFromTu { .. }
| LocalRequest::ReconcileUser { .. }
| LocalRequest::ReleaseUser { .. }
| LocalRequest::CompleteDeleteUser { .. }
| LocalRequest::CreateInvitation {
authority: InvitationAuthority::Omega,
..
}
);
if needs_omikron && !self.services.omikron.is_connected().await {
return ResponseResult::Error(
if self.runtime.current_startup_phase() != StartupPhase::Ready {
IpcErrorCode::NotReady
} else {
IpcErrorCode::OmikronUnavailable
},
);
}
match request {
LocalRequest::GetStatus => {
let phase = self.runtime.current_startup_phase();
let degraded = self.runtime.degraded_reason.borrow().clone();
let tasks: Vec<String> = self
.runtime
.state
.active_tasks
.iter()
.map(|task| task.to_string())
.collect();
ResponseResult::Ok(ResponsePayload::Status(StatusResponse {
phase: format!("{:?}", phase),
tasks: tasks.clone(),
degraded_reason: degraded,
}))
}
LocalRequest::ListTasks => {
let tasks: Vec<TaskSummary> = self
.runtime
.state
.active_tasks
.iter()
.map(|task| TaskSummary {
name: task.to_string(),
})
.collect();
ResponseResult::Ok(ResponsePayload::Tasks(tasks))
}
LocalRequest::ListUsers => {
let pending = match pending_operations::get_all() {
Ok(operations) => operations
.into_iter()
.map(|operation| {
let kind = match operation.operation {
PendingUserOperationKind::Create => UserOperationKind::Create,
PendingUserOperationKind::Attach => UserOperationKind::Attach,
PendingUserOperationKind::Release => UserOperationKind::Release,
PendingUserOperationKind::Purge => UserOperationKind::Purge,
};
(
operation.user_id,
UserOperationSummary {
operation: kind,
phase: operation.phase.as_str().into(),
},
)
})
.collect::<std::collections::HashMap<_, _>>(),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let users = match user_manager::get_residency() {
Ok(users) => users,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
}
.into_iter()
.map(|user| {
let user_id = user.user_id;
let profile = user_manager::get_user(user.user_id)?;
Ok(UserSummary {
credential_status: credential_status(&user, profile.as_ref()),
user_id,
username: user.username,
state: match user.state {
user_manager::LocalUserState::Managed => {
iota_ipc::LocalUserState::Managed
}
user_manager::LocalUserState::Released => {
iota_ipc::LocalUserState::Released
}
},
data_present: user.data_present,
pending_operation: pending.get(&user_id).cloned(),
})
})
.collect::<Result<Vec<_>, iota_storage::storage_error::StorageError>>();
let Ok(users) = users else {
return ResponseResult::Error(IpcErrorCode::StorageFailure);
};
ResponseResult::Ok(ResponsePayload::Users(users))
}
LocalRequest::CreateInvitation {
authority,
lifetime_seconds,
password,
label,
} => {
if authority == InvitationAuthority::Iota {
return ResponseResult::Error(IpcErrorCode::Unsupported);
}
if lifetime_seconds == 0 || lifetime_seconds > 7 * 24 * 60 * 60 {
return ResponseResult::Error(IpcErrorCode::InvalidRequest);
}
let password_protected = password.is_some();
let mut request = CommunicationValue::new(CommunicationType::CreateUserInvitation)
.add_typed_default(
DataType::InvitationAuthority,
DataValue::Str("omega".into()),
)
.add_typed_default(
DataType::InvitationLifetimeSeconds,
DataValue::SignedNumber(lifetime_seconds.into()),
);
if let Some(password) = password {
request = request.add_typed_default(
DataType::InvitationPassword,
DataValue::Str(password.0),
);
}
if let Some(label) = label.as_ref() {
request = request.add_typed_default(
DataType::InvitationLabel,
DataValue::Str(label.clone()),
);
}
let response = match self
.services
.omikron
.await_response(&request, Duration::from_secs(20))
.await
{
Ok(response) => response,
Err(omikron_connector::OmikronError::Timeout(_)) => {
return ResponseResult::Error(IpcErrorCode::Timeout);
}
Err(_) => return ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
};
let invitation_id = response
.get_data(DataType::InvitationId)
.as_signed_number()
.and_then(|value| i64::try_from(value).ok());
let raw_token = response
.get_data(DataType::InvitationToken)
.as_str()
.map(str::to_owned);
let expires_at = response
.get_data(DataType::InvitationExpiresAt)
.as_signed_number()
.and_then(|value| i64::try_from(value).ok());
let created_at = response
.get_data(DataType::InvitationCreatedAt)
.as_signed_number()
.and_then(|value| i64::try_from(value).ok());
let remote_revision = response
.get_data(DataType::InvitationRevision)
.as_signed_number()
.and_then(|value| i64::try_from(value).ok())
.unwrap_or(1);
let short_url = response
.get_data(DataType::Link)
.as_str()
.map(str::to_owned);
let Some((invitation_id, raw_token, created_at, expires_at)) = invitation_id
.zip(raw_token)
.zip(created_at)
.zip(expires_at)
.map(|(((id, token), created), expires)| (id, token, created, expires))
else {
return ResponseResult::Error(IpcErrorCode::InternalFailure);
};
let summary = iota_storage::users::invitations::InvitationSummary {
invitation_id,
authority: iota_storage::users::invitations::InvitationAuthority::Omega,
label,
password_protected,
created_at,
expires_at: Some(expires_at),
state: iota_storage::users::invitations::InvitationState::Pending,
remote_revision,
redeemed_user_id: None,
redeemed_at: None,
revoked_at: None,
pending_action: None,
pending_action_at: None,
last_synced_at: Some(now_millis()),
local_provisioned_user_id: None,
local_provisioned_at: None,
};
if iota_storage::users::invitations::insert(&summary, None, now_millis()).is_err() {
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
ResponseResult::Ok(ResponsePayload::InvitationCreated(InvitationCreated {
authority,
invitation_id,
raw_token: iota_ipc::SecretString(raw_token),
expires_at,
short_url,
}))
}
LocalRequest::ListInvitations { authority } => {
if authority != Some(InvitationAuthority::Iota)
&& self.services.omikron.is_connected().await
{
match self.services.omikron.sync_omega_invitations().await {
Ok(()) => {}
Err(omikron_connector::OmikronError::Storage(_)) => {
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
Err(omikron_connector::OmikronError::Timeout(_)) => {
return ResponseResult::Error(IpcErrorCode::Timeout);
}
Err(omikron_connector::OmikronError::Disconnected(_)) => {
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
}
Err(_) => return ResponseResult::Error(IpcErrorCode::InternalFailure),
}
}
if iota_storage::users::invitations::expire_pending(now_millis()).is_err() {
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
let invitations = match iota_storage::users::invitations::list() {
Ok(invitations) => invitations,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let mut summaries = Vec::new();
for invitation in invitations {
let invitation_authority = match invitation.authority {
iota_storage::users::invitations::InvitationAuthority::Omega => {
InvitationAuthority::Omega
}
iota_storage::users::invitations::InvitationAuthority::Iota => {
InvitationAuthority::Iota
}
};
if authority.is_some_and(|filter| filter != invitation_authority) {
continue;
}
let display_state = if invitation.pending_action
== Some(iota_storage::users::invitations::PendingAction::Revoke)
{
InvitationState::RevocationPending
} else if invitation.authority
== iota_storage::users::invitations::InvitationAuthority::Omega
&& invitation.state
== iota_storage::users::invitations::InvitationState::Pending
&& invitation
.expires_at
.is_some_and(|expires_at| expires_at <= now_millis())
{
InvitationState::Expired
} else {
match invitation.state {
iota_storage::users::invitations::InvitationState::Pending => {
InvitationState::Pending
}
iota_storage::users::invitations::InvitationState::Provisioning => {
InvitationState::Provisioning
}
iota_storage::users::invitations::InvitationState::Redeemed => {
InvitationState::Redeemed
}
iota_storage::users::invitations::InvitationState::Revoked => {
InvitationState::Revoked
}
iota_storage::users::invitations::InvitationState::Expired => {
InvitationState::Expired
}
}
};
summaries.push(InvitationSummary {
authority: invitation_authority,
invitation_id: invitation.invitation_id,
label: invitation.label,
password_protected: invitation.password_protected,
created_at: invitation.created_at,
expires_at: invitation.expires_at,
state: display_state,
redeemed_user_id: invitation.redeemed_user_id,
local_provisioned_user_id: invitation.local_provisioned_user_id,
local_provisioned_at: invitation.local_provisioned_at,
});
}
ResponseResult::Ok(ResponsePayload::Invitations(summaries))
}
LocalRequest::RevokeInvitation {
authority,
invitation_id,
} => {
if authority == InvitationAuthority::Iota {
return ResponseResult::Error(IpcErrorCode::Unsupported);
}
let changed = match iota_storage::users::invitations::mark_revoke_pending(
invitation_id,
now_millis(),
) {
Ok(changed) => changed,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
if !changed {
return ResponseResult::Error(IpcErrorCode::Conflict);
}
if self.services.omikron.is_connected().await {
self.services
.omikron
.flush_pending_invitation_actions()
.await;
}
let invitation =
match iota_storage::users::invitations::list()
.ok()
.and_then(|items| {
items
.into_iter()
.find(|item| item.invitation_id == invitation_id)
}) {
Some(invitation) => invitation,
None => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let state = if invitation.pending_action
== Some(iota_storage::users::invitations::PendingAction::Revoke)
{
InvitationState::RevocationPending
} else {
match invitation.state {
iota_storage::users::invitations::InvitationState::Pending => {
InvitationState::Pending
}
iota_storage::users::invitations::InvitationState::Provisioning => {
InvitationState::Provisioning
}
iota_storage::users::invitations::InvitationState::Redeemed => {
InvitationState::Redeemed
}
iota_storage::users::invitations::InvitationState::Revoked => {
InvitationState::Revoked
}
iota_storage::users::invitations::InvitationState::Expired => {
InvitationState::Expired
}
}
};
ResponseResult::Ok(ResponsePayload::InvitationUpdated(InvitationSummary {
authority: InvitationAuthority::Omega,
invitation_id,
label: invitation.label,
password_protected: invitation.password_protected,
created_at: invitation.created_at,
expires_at: invitation.expires_at,
state,
redeemed_user_id: invitation.redeemed_user_id,
local_provisioned_user_id: invitation.local_provisioned_user_id,
local_provisioned_at: invitation.local_provisioned_at,
}))
}
LocalRequest::CreateUser { username } => {
match omikron_connector::user_ops::create_user(
self.services.omikron.as_ref(),
&username,
)
.await
{
Ok(user) => ResponseResult::Ok(ResponsePayload::UserCreated {
user_id: user.user_id,
username: user.username,
}),
Err(error) => {
log!("User creation failed: {error:?}");
match error {
omikron_connector::user_ops::CreateUserError::InvalidUsername => {
ResponseResult::Error(IpcErrorCode::InvalidRequest)
}
omikron_connector::user_ops::CreateUserError::Transport(
omikron_connector::OmikronError::Timeout(_),
) => ResponseResult::Error(IpcErrorCode::Timeout),
omikron_connector::user_ops::CreateUserError::Transport(_) => {
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
}
omikron_connector::user_ops::CreateUserError::RemoteRejected => {
ResponseResult::Error(IpcErrorCode::Conflict)
}
omikron_connector::user_ops::CreateUserError::LocalFinalizationPending { .. } => {
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
omikron_connector::user_ops::CreateUserError::LocalPersistence(_) => {
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
omikron_connector::user_ops::CreateUserError::InvalidResponse => {
ResponseResult::Error(IpcErrorCode::InternalFailure)
}
}
}
}
}
LocalRequest::PurgeUserData { user_id } => match user_manager::purge_user_data(user_id)
{
Ok(()) => ResponseResult::Ok(ResponsePayload::UserDataPurged { user_id }),
Err(iota_storage::storage_error::StorageError::PendingRelayOwnershipUnknown) => {
log!(
"User data purge is waiting for pending relay ownership classification for {user_id}"
);
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
Err(error) => {
log!("User data purge failed for {user_id}: {error}");
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
},
LocalRequest::AttachUserFromTu { credential } => {
match omikron_connector::user_ops::attach_user_from_tu(
self.services.omikron.as_ref(),
&credential.0,
)
.await
{
Ok(user) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Added {} ({}) to this Iota", user.username, user.user_id),
}),
Err(error) => {
log!("Credential attach failed: {error:?}");
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
}
}
LocalRequest::InspectTuCredential { credential } => {
match omikron_connector::user_ops::inspect_tu_credential(
self.services.omikron.as_ref(),
&credential.0,
)
.await
{
Ok(preview) => ResponseResult::Ok(ResponsePayload::TuCredentialPreview(
iota_ipc::TuCredentialPreview {
user_id: preview.user_id,
username: preview.username,
assigned_iota_id: preview.assigned_iota_id,
},
)),
Err(error) => {
log!("Credential inspection failed: {error:?}");
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
}
}
LocalRequest::ReconcileUser { user_id } => {
let residency = match user_manager::get_residency_by_id(user_id) {
Ok(Some(residency)) => residency,
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
{
Ok(assignment) => assignment,
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);
}
}
} else {
ReconcileAction::None
};
ResponseResult::Ok(ResponsePayload::UserReconciled(UserReconcileResult {
user_id,
local_state: match residency.state {
user_manager::LocalUserState::Managed => iota_ipc::LocalUserState::Managed,
user_manager::LocalUserState::Released => {
iota_ipc::LocalUserState::Released
}
},
omega_iota_id,
action,
}))
}
LocalRequest::ForceDetachUser { user_id } => {
match user_manager::force_local_detach(user_id) {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!(
"Force-detached user {user_id} locally; Omega was not changed"
),
}),
Err(error) => {
log!("Force local detach failed for {user_id}: {error}");
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
}
}
LocalRequest::ForgetReleasedUser { user_id } => {
match user_manager::forget_released_user(user_id) {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Forgot released user residency {user_id}"),
}),
Err(error) => {
log!("Forget residency failed for {user_id}: {error}");
ResponseResult::Error(IpcErrorCode::Conflict)
}
}
}
LocalRequest::GetUserDiagnostics { user_id } => {
let residency = match user_manager::get_residency_by_id(user_id) {
Ok(Some(residency)) => residency,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let profile = match user_manager::get_user(user_id) {
Ok(profile) => profile,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let pending_operation = match pending_operations::get_all() {
Ok(operations) => operations
.into_iter()
.find(|operation| operation.user_id == user_id)
.map(|operation| {
format!(
"{}/{}",
operation.operation.as_str(),
operation.phase.as_str()
)
}),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let credential_status = credential_status(&residency, profile.as_ref());
ResponseResult::Ok(ResponsePayload::UserDiagnostics(UserDiagnostics {
user_id,
username: residency.username,
local_state: match residency.state {
user_manager::LocalUserState::Managed => iota_ipc::LocalUserState::Managed,
user_manager::LocalUserState::Released => {
iota_ipc::LocalUserState::Released
}
},
data_present: residency.data_present,
credential_status,
trusted_app_count: profile
.as_ref()
.map_or(0, |profile| profile.trusted_apps.len()),
pending_operation,
}))
}
LocalRequest::RevokeTrustedApp { user_id, app_id } => {
match user_manager::revoke_trusted_app(user_id, &app_id) {
Ok(true) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Revoked trusted application {app_id} for user {user_id}"),
}),
Ok(false) => ResponseResult::Error(IpcErrorCode::NotFound),
Err(error) => {
log!("Trusted application revocation failed for {user_id}: {error}");
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
}
}
LocalRequest::RevokeAllTrustedApps { user_id } => {
match user_manager::revoke_all_trusted_apps(user_id) {
Ok(removed) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!(
"Revoked {removed} trusted applications for user {user_id}"
),
}),
Err(error) => {
log!("Trusted application revocation failed for {user_id}: {error}");
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
}
}
LocalRequest::CompleteDeleteUser {
user_id,
credential,
} => {
let contents = match credential {
Some(value) => Ok(value.0),
None => user_manager::get_user(user_id)
.map_err(|_| ())
.and_then(|user| user.ok_or(()))
.and_then(|user| {
iota_util::file_util::read_user_credential_with_legacy(
user_id,
&user.username,
)
.map_err(|_| ())
})
.and_then(|value| value.ok_or(())),
};
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
{
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Deleted Tensamin account {user_id}"),
}),
Err(error) => {
log!("Credential deletion failed for {user_id}: {error:?}");
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
}
}
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))
.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);
ResponseResult::Error(IpcErrorCode::Unauthorized)
}
Ok(_) => {
let _ = pending_operations::remove(user_id);
ResponseResult::Error(IpcErrorCode::Conflict)
}
Err(omikron_connector::OmikronError::Timeout(_)) => {
ResponseResult::Error(IpcErrorCode::Timeout)
}
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
}
}
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::RotateIotaIdentity => {
match self.services.omikron.rotate_identity().await {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "New identity registered with Omikron".into(),
}),
Err(error) => {
log!("Iota identity rotation failed: {}", error);
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
}
}
}
LocalRequest::RequestProcessExit { intent } => {
if matches!(intent, ExitIntent::Restart)
&& !matches!(
crate::deployment::from_environment().supervisor,
iota_ipc::SupervisorKind::Systemd | iota_ipc::SupervisorKind::IotaUi
)
{
return ResponseResult::Error(IpcErrorCode::Conflict);
}
self.runtime.request_shutdown(match intent {
ExitIntent::Stop => ShutdownReason::Stop,
ExitIntent::Restart => ShutdownReason::Restart,
});
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "process exit accepted".into(),
})
}
LocalRequest::GetDaemonStatus => ResponseResult::Ok(ResponsePayload::DaemonStatus(
iota_ipc::DaemonStatusResponse {
formatted: format!("{:?}", self.runtime.snapshot()),
},
)),
LocalRequest::RestartDaemon => {
self.runtime.request_shutdown(ShutdownReason::Restart);
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Daemon restart requested".into(),
})
}
LocalRequest::StopDaemon => {
self.runtime.request_shutdown(ShutdownReason::Stop);
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Daemon shutdown requested".into(),
})
}
LocalRequest::GetConfig => {
let cfg = config_util::CONFIG.load();
let yaml = serde_yaml::to_string(&**cfg).unwrap_or_default();
ResponseResult::Ok(ResponsePayload::Config(ConfigResponse { yaml }))
}
LocalRequest::SetConfig { key, value } => {
match config_util::modify_config_value(&key, &value) {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Set {key} = {value}"),
}),
Err(_e) => ResponseResult::Error(IpcErrorCode::InvalidRequest),
}
}
LocalRequest::ReloadConfig => match config_util::load_config() {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Configuration reloaded".into(),
}),
Err(_) => ResponseResult::Error(IpcErrorCode::InvalidRequest),
},
LocalRequest::GetOmikronStatus => {
let connected = self.services.omikron.is_connected().await;
let iota_id = config_util::CONFIG.load().iota_id;
ResponseResult::Ok(ResponsePayload::OmikronStatus(OmikronStatusResponse {
connected,
iota_id,
}))
}
LocalRequest::ListComponents => {
let snapshot = self.runtime.snapshot();
let components: Vec<ComponentStatusResponse> = snapshot
.components
.into_iter()
.map(|(id, health)| ComponentStatusResponse {
id,
status: health.status,
message: health.message,
})
.collect();
ResponseResult::Ok(ResponsePayload::Components(components))
}
LocalRequest::GetUser { user_id } => {
let residency = match user_manager::get_residency_by_id(user_id) {
Ok(Some(residency)) => residency,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
match user_manager::get_user(user_id) {
Ok(Some(user)) => {
let credential_status = credential_status(&residency, Some(&user));
ResponseResult::Ok(ResponsePayload::UserDetail(UserDetailResponse {
user_id: user.user_id,
username: user.username,
display_name: user.display_name,
created_at: user.created_at,
trusted_apps: user.trusted_apps.keys().cloned().collect(),
state: iota_ipc::LocalUserState::Managed,
data_present: residency.data_present,
credential_status,
}))
}
Ok(None) if residency.state == user_manager::LocalUserState::Released => {
ResponseResult::Ok(ResponsePayload::UserDetail(UserDetailResponse {
user_id: residency.user_id,
username: residency.username,
display_name: None,
created_at: 0,
trusted_apps: Vec::new(),
state: iota_ipc::LocalUserState::Released,
data_present: residency.data_present,
credential_status: iota_ipc::CredentialStatus::None,
}))
}
Ok(None) => ResponseResult::Error(IpcErrorCode::StorageFailure),
Err(_) => ResponseResult::Error(IpcErrorCode::StorageFailure),
}
}
LocalRequest::ExportUserCredential { user_id } => {
let residency = match user_manager::get_residency_by_id(user_id) {
Ok(Some(residency)) => residency,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
if residency.state != user_manager::LocalUserState::Managed
|| residency.credential_origin != user_manager::CredentialOrigin::Local
{
return ResponseResult::Error(IpcErrorCode::Conflict);
}
let credential = match iota_util::file_util::read_user_credential_with_legacy(
user_id,
&residency.username,
) {
Ok(Some(credential)) => credential,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let parsed = match iota_util::tu::TuCredential::parse(&credential) {
Ok(parsed) if parsed.user_id == user_id => parsed,
Ok(_) | Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
ResponseResult::Ok(ResponsePayload::UserCredentialExport {
user_id,
username: residency.username,
credential: iota_ipc::SecretString(parsed.to_canonical_string()),
})
}
LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest),
LocalRequest::GetLogs { limit } => {
let entries = if let Ok(buf) = self.log_buffer.lock() {
bounded_log_entries(buf.recent(limit.min(MAX_LOG_ENTRIES_PER_RESPONSE)))
} else {
Vec::new()
};
ResponseResult::Ok(ResponsePayload::LogEntries(LogEntriesResponse { entries }))
}
LocalRequest::CheckUpdate => match iota_updater::check_update().await {
Ok(available) => {
ResponseResult::Ok(ResponsePayload::UpdateStatus(UpdateStatusResponse {
available,
}))
}
Err(_e) => ResponseResult::Error(IpcErrorCode::InternalFailure),
},
LocalRequest::ListCommunities => {
let iota_id = config_util::CONFIG.load().iota_id;
let Ok(iota_id) = iota_id.map(i64::try_from).unwrap_or(Ok(0)) else {
return ResponseResult::Ok(ResponsePayload::Communities(Vec::new()));
};
let stored =
match iota_storage::util::communities_util::CommunitiesUtil::get_communities(
iota_id,
) {
Ok(stored) => stored,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure),
};
let summaries: Vec<CommunitySummary> = stored
.into_iter()
.map(|c| CommunitySummary {
name: c.address,
title: c.title,
})
.collect();
ResponseResult::Ok(ResponsePayload::Communities(summaries))
}
}
}
}
#[cfg(test)]
mod tests {
use super::{IpcRole, LocalRequest, bounded_log_entries};
use iota_ipc::{ExitIntent, LogEntry, SecretString};
#[test]
fn every_request_has_an_explicit_role_policy() {
let requests = [
LocalRequest::GetStatus,
LocalRequest::ListTasks,
LocalRequest::ListUsers,
LocalRequest::CreateUser {
username: "alice".into(),
},
LocalRequest::AttachUserFromTu {
credential: SecretString("credential".into()),
},
LocalRequest::InspectTuCredential {
credential: SecretString("credential".into()),
},
LocalRequest::ReconcileUser { user_id: 1 },
LocalRequest::ForceDetachUser { user_id: 1 },
LocalRequest::ForgetReleasedUser { user_id: 1 },
LocalRequest::GetUserDiagnostics { user_id: 1 },
LocalRequest::RevokeTrustedApp {
user_id: 1,
app_id: "desktop".into(),
},
LocalRequest::RevokeAllTrustedApps { user_id: 1 },
LocalRequest::ExportUserCredential { user_id: 1 },
LocalRequest::PurgeUserData { user_id: 1 },
LocalRequest::ReleaseUser { user_id: 1 },
LocalRequest::CompleteDeleteUser {
user_id: 1,
credential: None,
},
LocalRequest::RemoveUser { user_id: 1 },
LocalRequest::ReconnectOmikron,
LocalRequest::RotateIotaIdentity,
LocalRequest::RequestProcessExit {
intent: ExitIntent::Stop,
},
LocalRequest::GetDaemonStatus,
LocalRequest::RestartDaemon,
LocalRequest::StopDaemon,
LocalRequest::GetConfig,
LocalRequest::SetConfig {
key: "port".into(),
value: "1984".into(),
},
LocalRequest::ReloadConfig,
LocalRequest::GetOmikronStatus,
LocalRequest::ListComponents,
LocalRequest::GetUser { user_id: 1 },
LocalRequest::ImportUser {
username: "alice".into(),
},
LocalRequest::GetLogs { limit: 10 },
LocalRequest::CheckUpdate,
LocalRequest::ListCommunities,
];
assert_eq!(requests.len(), 33);
for request in requests {
let required = request.required_role();
assert!(IpcRole::Admin.allows(required));
assert_eq!(
IpcRole::Operate.allows(required),
required != IpcRole::Admin
);
assert_eq!(IpcRole::Read.allows(required), required == IpcRole::Read);
}
}
#[test]
fn log_responses_drop_entries_that_cannot_fit_one_ipc_frame() {
let entries = vec![LogEntry {
timestamp_ms: 0,
sender: "test".into(),
message: "x".repeat(2 * 1024 * 1024),
is_error: false,
}];
assert!(bounded_log_entries(entries).is_empty());
}
}