[Add] Proper User managment
This commit is contained in:
parent
430c12e139
commit
b38b68ad96
38 changed files with 4331 additions and 1065 deletions
|
|
@ -4,16 +4,17 @@ pub mod transport;
|
|||
|
||||
pub use protocol::{
|
||||
ClientMessage, CommunitySummary, ComponentHealth, ComponentId, ComponentStatusResponse,
|
||||
ConfigResponse, ConnectionStatus, DaemonMessage, DaemonStatusResponse, DeploymentMode,
|
||||
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,
|
||||
ConfigResponse, ConnectionStatus, CredentialStatus, DaemonMessage, DaemonStatusResponse,
|
||||
DeploymentMode, ExitIntent, HealthStatus, HelloAck, IpcErrorCode, IpcRole, LifecycleEvent,
|
||||
LifecyclePhase, LocalRequest, LocalUserState, LogEntriesResponse, LogEntry, MetricSample,
|
||||
OmikronStatusResponse, ReconcileAction, RequestEnvelope, ResponseEnvelope, ResponsePayload,
|
||||
ResponseResult, SecretString, StartupPhase, StateSnapshot, StatusResponse, SupervisorKind,
|
||||
TaskSummary, TuCredentialPreview, UpdateStatusResponse, UserDetailResponse, UserDiagnostics,
|
||||
UserOperationKind, UserOperationSummary, UserReconcileResult, UserSummary,
|
||||
};
|
||||
pub use transport::{MAX_MESSAGE_SIZE, read_msg, write_msg};
|
||||
|
||||
/// Current IPC protocol version.
|
||||
pub const PROTOCOL_VERSION: u16 = 2;
|
||||
pub const PROTOCOL_VERSION: u16 = 4;
|
||||
/// Minimum protocol version this daemon understands.
|
||||
pub const MIN_PROTOCOL_VERSION: u16 = 2;
|
||||
|
|
|
|||
|
|
@ -48,9 +48,34 @@ pub enum LocalRequest {
|
|||
CreateUser {
|
||||
username: String,
|
||||
},
|
||||
InspectTuCredential {
|
||||
credential: SecretString,
|
||||
},
|
||||
AttachUserFromTu {
|
||||
credential: SecretString,
|
||||
},
|
||||
ReconcileUser {
|
||||
user_id: i64,
|
||||
},
|
||||
ForceDetachUser {
|
||||
user_id: i64,
|
||||
},
|
||||
ForgetReleasedUser {
|
||||
user_id: i64,
|
||||
},
|
||||
GetUserDiagnostics {
|
||||
user_id: i64,
|
||||
},
|
||||
RevokeTrustedApp {
|
||||
user_id: i64,
|
||||
app_id: String,
|
||||
},
|
||||
RevokeAllTrustedApps {
|
||||
user_id: i64,
|
||||
},
|
||||
ExportUserCredential {
|
||||
user_id: i64,
|
||||
},
|
||||
PurgeUserData {
|
||||
user_id: i64,
|
||||
},
|
||||
|
|
@ -127,6 +152,7 @@ impl LocalRequest {
|
|||
| Self::GetOmikronStatus
|
||||
| Self::ListComponents
|
||||
| Self::GetUser { .. }
|
||||
| Self::GetUserDiagnostics { .. }
|
||||
| Self::GetLogs { .. }
|
||||
| Self::CheckUpdate
|
||||
| Self::ListCommunities => IpcRole::Read,
|
||||
|
|
@ -134,7 +160,14 @@ impl LocalRequest {
|
|||
Self::ReconnectOmikron | Self::ReloadConfig => IpcRole::Operate,
|
||||
|
||||
Self::CreateUser { .. }
|
||||
| Self::InspectTuCredential { .. }
|
||||
| Self::AttachUserFromTu { .. }
|
||||
| Self::ReconcileUser { .. }
|
||||
| Self::ForceDetachUser { .. }
|
||||
| Self::ForgetReleasedUser { .. }
|
||||
| Self::RevokeTrustedApp { .. }
|
||||
| Self::RevokeAllTrustedApps { .. }
|
||||
| Self::ExportUserCredential { .. }
|
||||
| Self::PurgeUserData { .. }
|
||||
| Self::ReleaseUser { .. }
|
||||
| Self::CompleteDeleteUser { .. }
|
||||
|
|
@ -220,6 +253,14 @@ pub enum ResponsePayload {
|
|||
user_id: i64,
|
||||
username: String,
|
||||
},
|
||||
TuCredentialPreview(TuCredentialPreview),
|
||||
UserReconciled(UserReconcileResult),
|
||||
UserDiagnostics(UserDiagnostics),
|
||||
UserCredentialExport {
|
||||
user_id: i64,
|
||||
username: String,
|
||||
credential: SecretString,
|
||||
},
|
||||
/// Retained only for wire compatibility. New lifecycle code never emits it.
|
||||
UserRemoved {
|
||||
user_id: i64,
|
||||
|
|
@ -267,7 +308,7 @@ pub struct UserDetailResponse {
|
|||
pub trusted_apps: Vec<String>,
|
||||
pub state: LocalUserState,
|
||||
pub data_present: bool,
|
||||
pub credential_present: bool,
|
||||
pub credential_status: CredentialStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
|
@ -304,7 +345,66 @@ pub struct UserSummary {
|
|||
pub username: String,
|
||||
pub state: LocalUserState,
|
||||
pub data_present: bool,
|
||||
pub credential_present: bool,
|
||||
pub credential_status: CredentialStatus,
|
||||
#[serde(default)]
|
||||
pub pending_operation: Option<UserOperationSummary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UserOperationKind {
|
||||
Create,
|
||||
Attach,
|
||||
Release,
|
||||
Purge,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct UserOperationSummary {
|
||||
pub operation: UserOperationKind,
|
||||
pub phase: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CredentialStatus {
|
||||
LocalPresent,
|
||||
External,
|
||||
Missing,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct TuCredentialPreview {
|
||||
pub user_id: i64,
|
||||
pub username: String,
|
||||
pub assigned_iota_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct UserReconcileResult {
|
||||
pub user_id: i64,
|
||||
pub local_state: LocalUserState,
|
||||
pub omega_iota_id: Option<i64>,
|
||||
pub action: ReconcileAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReconcileAction {
|
||||
None,
|
||||
ReleasedLocally,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct UserDiagnostics {
|
||||
pub user_id: i64,
|
||||
pub username: String,
|
||||
pub local_state: LocalUserState,
|
||||
pub data_present: bool,
|
||||
pub credential_status: CredentialStatus,
|
||||
pub trusted_app_count: usize,
|
||||
pub pending_operation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
|
|
@ -359,6 +459,11 @@ impl std::fmt::Display for IpcErrorCode {
|
|||
|
||||
#[cfg(test)]
|
||||
mod error_tests {
|
||||
use super::{
|
||||
CredentialStatus, LocalUserState, ResponsePayload, SecretString, UserOperationKind,
|
||||
UserOperationSummary, UserSummary,
|
||||
};
|
||||
|
||||
use super::IpcErrorCode;
|
||||
|
||||
#[test]
|
||||
|
|
@ -378,6 +483,50 @@ mod error_tests {
|
|||
.contains("required role")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credential_export_debug_output_is_redacted() {
|
||||
let payload = ResponsePayload::UserCredentialExport {
|
||||
user_id: 42,
|
||||
username: "alice".into(),
|
||||
credential: SecretString("private-tu-contents".into()),
|
||||
};
|
||||
let output = format!("{payload:?}");
|
||||
|
||||
assert!(!output.contains("private-tu-contents"));
|
||||
assert!(output.contains("<redacted>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_summary_serializes_pending_operation_without_lifecycle_secrets() {
|
||||
let summary = UserSummary {
|
||||
user_id: 42,
|
||||
username: "alice".into(),
|
||||
state: LocalUserState::Managed,
|
||||
data_present: true,
|
||||
credential_status: CredentialStatus::LocalPresent,
|
||||
pending_operation: Some(UserOperationSummary {
|
||||
operation: UserOperationKind::Purge,
|
||||
phase: "database_purged".into(),
|
||||
}),
|
||||
};
|
||||
let encoded = serde_json::to_string(&summary).unwrap();
|
||||
|
||||
assert!(encoded.contains("purge"));
|
||||
assert!(encoded.contains("database_purged"));
|
||||
assert!(!encoded.contains("private_key"));
|
||||
assert!(!encoded.contains("reset_token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_summary_accepts_older_payload_without_pending_operation() {
|
||||
let summary: UserSummary = serde_json::from_str(
|
||||
r#"{"user_id":42,"username":"alice","state":"managed","data_present":true,"credential_status":"local_present"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(summary.pending_operation.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
|
|
|||
|
|
@ -5,9 +5,16 @@ pub const COMMANDS: &[&str] = &[
|
|||
"tasks",
|
||||
"users list",
|
||||
"users show ",
|
||||
"users add ",
|
||||
"users remove ",
|
||||
"users import ",
|
||||
"users add create ",
|
||||
"users hosting release ",
|
||||
"users data purge ",
|
||||
"users account delete ",
|
||||
"users repair reconcile ",
|
||||
"users repair diagnostics ",
|
||||
"users repair force-detach ",
|
||||
"users forget ",
|
||||
"users apps revoke ",
|
||||
"users apps revoke-all ",
|
||||
"omikron status",
|
||||
"reconnect",
|
||||
"identity rotate",
|
||||
|
|
@ -59,16 +66,47 @@ pub fn parse(line: &str) -> Option<LocalRequest> {
|
|||
let user_id = id_str.parse::<i64>().ok()?;
|
||||
Some(LocalRequest::GetUser { user_id })
|
||||
}
|
||||
["user" | "users", "add", username] => Some(LocalRequest::CreateUser {
|
||||
["user" | "users", "add", "create", username] => Some(LocalRequest::CreateUser {
|
||||
username: username.to_string(),
|
||||
}),
|
||||
["user" | "users", "remove", id_str] => {
|
||||
["user" | "users", "hosting", "release", id_str] => {
|
||||
let user_id = id_str.parse::<i64>().ok()?;
|
||||
Some(LocalRequest::RemoveUser { user_id })
|
||||
Some(LocalRequest::ReleaseUser { user_id })
|
||||
}
|
||||
["user" | "users", "import", username] => Some(LocalRequest::ImportUser {
|
||||
username: username.to_string(),
|
||||
["user" | "users", "data", "purge", id_str] => Some(LocalRequest::PurgeUserData {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
}),
|
||||
["user" | "users", "account", "delete", id_str] => Some(LocalRequest::CompleteDeleteUser {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
credential: None,
|
||||
}),
|
||||
["user" | "users", "repair", "reconcile", id_str] => Some(LocalRequest::ReconcileUser {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
}),
|
||||
["user" | "users", "repair", "diagnostics", id_str] => {
|
||||
Some(LocalRequest::GetUserDiagnostics {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
})
|
||||
}
|
||||
["user" | "users", "repair", "force-detach", id_str] => {
|
||||
Some(LocalRequest::ForceDetachUser {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
})
|
||||
}
|
||||
["user" | "users", "forget", id_str] => Some(LocalRequest::ForgetReleasedUser {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
}),
|
||||
["user" | "users", "apps", "revoke", id_str, app_id] => {
|
||||
Some(LocalRequest::RevokeTrustedApp {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
app_id: app_id.to_string(),
|
||||
})
|
||||
}
|
||||
["user" | "users", "apps", "revoke-all", id_str] => {
|
||||
Some(LocalRequest::RevokeAllTrustedApps {
|
||||
user_id: id_str.parse::<i64>().ok()?,
|
||||
})
|
||||
}
|
||||
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
|
||||
["regenerate", "keys"] | ["identity", "rotate"] => Some(LocalRequest::RotateIotaIdentity),
|
||||
["reload"] | ["restart"] => Some(LocalRequest::RequestProcessExit {
|
||||
|
|
@ -116,7 +154,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parses_user_add() {
|
||||
let req = parse("user add alice").unwrap();
|
||||
let req = parse("user add create alice").unwrap();
|
||||
match req {
|
||||
LocalRequest::CreateUser { username } => assert_eq!(username, "alice"),
|
||||
_ => panic!("expected CreateUser"),
|
||||
|
|
@ -127,12 +165,20 @@ mod tests {
|
|||
fn accepts_the_headless_cli_user_vocabulary() {
|
||||
assert!(matches!(parse("users list"), Some(LocalRequest::ListUsers)));
|
||||
assert!(matches!(
|
||||
parse("users add alice"),
|
||||
parse("users add create alice"),
|
||||
Some(LocalRequest::CreateUser { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
parse("users remove 42"),
|
||||
Some(LocalRequest::RemoveUser { user_id: 42 })
|
||||
parse("users hosting release 42"),
|
||||
Some(LocalRequest::ReleaseUser { user_id: 42 })
|
||||
));
|
||||
assert!(matches!(
|
||||
parse("users apps revoke 42 desktop"),
|
||||
Some(LocalRequest::RevokeTrustedApp { user_id: 42, .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
parse("users apps revoke-all 42"),
|
||||
Some(LocalRequest::RevokeAllTrustedApps { user_id: 42 })
|
||||
));
|
||||
assert!(matches!(
|
||||
parse("identity rotate"),
|
||||
|
|
@ -141,17 +187,18 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_remove_by_id() {
|
||||
let req = parse("user remove 42").unwrap();
|
||||
fn parses_user_release_by_id() {
|
||||
let req = parse("user hosting release 42").unwrap();
|
||||
match req {
|
||||
LocalRequest::RemoveUser { user_id } => assert_eq!(user_id, 42),
|
||||
_ => panic!("expected RemoveUser"),
|
||||
LocalRequest::ReleaseUser { user_id } => assert_eq!(user_id, 42),
|
||||
_ => panic!("expected ReleaseUser"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_remove_requires_numeric_id() {
|
||||
assert!(parse("user remove alice").is_none());
|
||||
fn generic_remove_is_not_routed() {
|
||||
assert!(parse("user remove 42").is_none());
|
||||
assert!(parse("users remove alice").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -295,12 +342,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parses_users_import() {
|
||||
let req = parse("users import alice").unwrap();
|
||||
match req {
|
||||
LocalRequest::ImportUser { username } => assert_eq!(username, "alice"),
|
||||
_ => panic!("expected ImportUser"),
|
||||
}
|
||||
fn deprecated_import_is_not_routed() {
|
||||
assert!(parse("users import alice").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -317,7 +360,15 @@ mod tests {
|
|||
#[test]
|
||||
fn completion_is_prefix_based_and_deterministic() {
|
||||
assert_eq!(completions("identity r"), vec!["identity rotate"]);
|
||||
assert_eq!(completions("/users a"), vec!["users add "]);
|
||||
assert_eq!(
|
||||
completions("/users a"),
|
||||
vec![
|
||||
"users add create ",
|
||||
"users account delete ",
|
||||
"users apps revoke ",
|
||||
"users apps revoke-all ",
|
||||
]
|
||||
);
|
||||
assert!(completions("definitely-unknown").is_empty());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue