Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
commit
4caa6bb3e9
33 changed files with 2028 additions and 445 deletions
|
|
@ -5,13 +5,13 @@ pub mod transport;
|
|||
pub use protocol::{
|
||||
ClientMessage, CommunitySummary, ComponentHealth, ComponentId, ComponentStatusResponse,
|
||||
ConfigResponse, ConnectionStatus, DaemonMessage, DaemonStatusResponse, DeploymentMode,
|
||||
ExitIntent, HealthStatus, HelloAck, IpcErrorCode, LifecycleEvent, LifecyclePhase, LocalRequest,
|
||||
LocalUserState, LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse,
|
||||
RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult, SecretString, StartupPhase,
|
||||
StateSnapshot, StatusResponse, SupervisorKind, TaskSummary, UpdateStatusResponse,
|
||||
UserDetailResponse, UserSummary,
|
||||
ExitIntent, HealthStatus, HelloAck, IpcErrorCode, IpcRole, LifecycleEvent, LifecyclePhase,
|
||||
LocalRequest, LocalUserState, LogEntriesResponse, LogEntry, MetricSample,
|
||||
OmikronStatusResponse, RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult,
|
||||
SecretString, StartupPhase, StateSnapshot, StatusResponse, SupervisorKind, TaskSummary,
|
||||
UpdateStatusResponse, UserDetailResponse, UserSummary,
|
||||
};
|
||||
pub use transport::{read_msg, write_msg};
|
||||
pub use transport::{MAX_MESSAGE_SIZE, read_msg, write_msg};
|
||||
|
||||
/// Current IPC protocol version.
|
||||
pub const PROTOCOL_VERSION: u16 = 2;
|
||||
|
|
|
|||
|
|
@ -97,6 +97,59 @@ pub enum LocalRequest {
|
|||
ListCommunities,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum IpcRole {
|
||||
Read,
|
||||
Operate,
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl IpcRole {
|
||||
pub fn allows(self, required: IpcRole) -> bool {
|
||||
matches!(
|
||||
(self, required),
|
||||
(IpcRole::Admin, _)
|
||||
| (IpcRole::Operate, IpcRole::Operate | IpcRole::Read)
|
||||
| (IpcRole::Read, IpcRole::Read)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalRequest {
|
||||
/// Return the minimum authenticated local role required to execute a
|
||||
/// request. New request variants must be assigned explicitly here.
|
||||
pub fn required_role(&self) -> IpcRole {
|
||||
match self {
|
||||
Self::GetStatus
|
||||
| Self::ListTasks
|
||||
| Self::ListUsers
|
||||
| Self::GetDaemonStatus
|
||||
| Self::GetOmikronStatus
|
||||
| Self::ListComponents
|
||||
| Self::GetUser { .. }
|
||||
| Self::GetLogs { .. }
|
||||
| Self::CheckUpdate
|
||||
| Self::ListCommunities => IpcRole::Read,
|
||||
|
||||
Self::ReconnectOmikron | Self::ReloadConfig => IpcRole::Operate,
|
||||
|
||||
Self::CreateUser { .. }
|
||||
| Self::AttachUserFromTu { .. }
|
||||
| Self::PurgeUserData { .. }
|
||||
| Self::ReleaseUser { .. }
|
||||
| Self::CompleteDeleteUser { .. }
|
||||
| Self::RemoveUser { .. }
|
||||
| Self::RotateIotaIdentity
|
||||
| Self::RequestProcessExit { .. }
|
||||
| Self::RestartDaemon
|
||||
| Self::StopDaemon
|
||||
| Self::GetConfig
|
||||
| Self::SetConfig { .. }
|
||||
| Self::ImportUser { .. } => IpcRole::Admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExitIntent {
|
||||
|
|
@ -296,7 +349,9 @@ impl std::fmt::Display for IpcErrorCode {
|
|||
Self::Disconnected => "the daemon connection was lost",
|
||||
Self::Timeout => "the daemon did not respond in time",
|
||||
Self::Cancelled => "the daemon cancelled the request",
|
||||
Self::Unauthorized => "the daemon denied this operation",
|
||||
Self::Unauthorized => {
|
||||
"the daemon denied this operation because the IPC account lacks the required role"
|
||||
}
|
||||
Self::InternalFailure => "the daemon encountered an internal failure",
|
||||
})
|
||||
}
|
||||
|
|
@ -317,6 +372,11 @@ mod error_tests {
|
|||
.to_string()
|
||||
.contains("InternalFailure")
|
||||
);
|
||||
assert!(
|
||||
IpcErrorCode::Unauthorized
|
||||
.to_string()
|
||||
.contains("required role")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ use serde::de::DeserializeOwned;
|
|||
use std::io::{Error, ErrorKind, Result};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
const MAX_MESSAGE_SIZE: usize = 1024 * 1024;
|
||||
/// Maximum encoded payload size for a single IPC frame.
|
||||
///
|
||||
/// This is a wire-level contract shared by both sides of the connection.
|
||||
pub const MAX_MESSAGE_SIZE: usize = 1024 * 1024;
|
||||
|
||||
/* Length-prefixing preserves message boundaries on a byte stream and bounds
|
||||
* allocations before JSON is deserialized. */
|
||||
|
|
@ -14,6 +17,12 @@ where
|
|||
{
|
||||
let payload =
|
||||
serde_json::to_vec(message).map_err(|error| Error::new(ErrorKind::InvalidData, error))?;
|
||||
if payload.len() > MAX_MESSAGE_SIZE {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"IPC message exceeds limit",
|
||||
));
|
||||
}
|
||||
let len = u32::try_from(payload.len())
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "IPC message is too large"))?;
|
||||
writer.write_u32(len).await?;
|
||||
|
|
@ -40,7 +49,7 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{read_msg, write_msg};
|
||||
use super::{MAX_MESSAGE_SIZE, read_msg, write_msg};
|
||||
use crate::protocol::{ClientMessage, LocalRequest, RequestEnvelope};
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -57,4 +66,31 @@ mod tests {
|
|||
let received: ClientMessage = read_msg(&mut reader).await.expect("read succeeds");
|
||||
assert!(matches!(received, ClientMessage::Request(req) if req.request_id == 4));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_rejects_message_above_frame_limit() {
|
||||
let (mut writer, _reader) = tokio::io::duplex(MAX_MESSAGE_SIZE + 16);
|
||||
let message = "x".repeat(MAX_MESSAGE_SIZE + 1);
|
||||
|
||||
let error = write_msg(&mut writer, &message)
|
||||
.await
|
||||
.expect_err("oversized payload must be rejected before framing");
|
||||
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
|
||||
assert!(error.to_string().contains("exceeds limit"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_rejects_frame_above_limit_before_allocating_payload() {
|
||||
let (mut writer, mut reader) = tokio::io::duplex(16);
|
||||
tokio::io::AsyncWriteExt::write_u32(&mut writer, (MAX_MESSAGE_SIZE + 1) as u32)
|
||||
.await
|
||||
.expect("length prefix write succeeds");
|
||||
|
||||
let error = read_msg::<_, ClientMessage>(&mut reader)
|
||||
.await
|
||||
.expect_err("oversized frame must be rejected");
|
||||
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue