From 0549f4fa67a982fd1e75731245b02815778ccf7c Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 26 Mar 2024 08:58:18 +0100 Subject: [PATCH 01/19] initial commit of users service --- rust/Cargo.lock | 3 + rust/agama-lib/src/users.rs | 2 +- rust/agama-lib/src/users/client.rs | 1 + rust/agama-lib/src/users/settings.rs | 2 +- rust/agama-server/src/lib.rs | 1 + rust/agama-server/src/users.rs | 2 + rust/agama-server/src/users/web.rs | 116 +++++++++++++++++++++++++++ rust/agama-server/src/web.rs | 3 +- rust/agama-server/src/web/event.rs | 9 ++- 9 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 rust/agama-server/src/users.rs create mode 100644 rust/agama-server/src/users/web.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a1074e1097..aeac4854b6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1908,6 +1908,9 @@ name = "macaddr" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" +dependencies = [ + "serde", +] [[package]] name = "malloc_buf" diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index a7dc878639..9ee6a72b5a 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -1,7 +1,7 @@ //! Implements support for handling the users settings mod client; -mod proxies; +pub mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 437ff3f21d..6b2729a354 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -66,6 +66,7 @@ impl Settings for FirstUser { } /// D-Bus client for the users service +#[derive(Clone)] pub struct UsersClient<'a> { users_proxy: Users1Proxy<'a>, } diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index 39b31c1484..c902896156 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -17,7 +17,7 @@ pub struct UserSettings { /// First user settings /// /// Holds the settings for the first user. -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Settings, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct FirstUserSettings { /// First user's full name diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index 3cb0e78344..a483ef2e6f 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -4,5 +4,6 @@ pub mod manager; pub mod network; pub mod questions; pub mod software; +pub mod users; pub mod web; pub use web::service; diff --git a/rust/agama-server/src/users.rs b/rust/agama-server/src/users.rs new file mode 100644 index 0000000000..2356d1ef98 --- /dev/null +++ b/rust/agama-server/src/users.rs @@ -0,0 +1,2 @@ +pub mod web; +pub use web::{users_service, add_users_streams}; diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs new file mode 100644 index 0000000000..b5ad736d95 --- /dev/null +++ b/rust/agama-server/src/users/web.rs @@ -0,0 +1,116 @@ +//! +//! The module offers two public functions: +//! +//! * `users_service` which returns the Axum service. +//! * `users_stream` which offers an stream that emits the users events coming from D-Bus. + +use axum::{ + extract::State, + routing::{get, post, put}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, pin::Pin}; +use tokio_stream::{Stream, StreamExt, StreamMap}; +use crate::{ + error::Error, + web::{ + common::{issues_router, progress_router, service_status_router}, + Event, + }, +}; +use agama_lib::{ + connection, error::ServiceError, users::{ + proxies::Users1Proxy, FirstUserSettings, UsersClient + } +}; + +#[derive(Clone)] +struct UsersState<'a> { + users: UsersClient<'a>, +} + +/// Returns an stream that emits users related events coming from D-Bus. +/// +/// It emits the Event::RootPasswordChange, Event::RootSSHKeyChanged and Event::FirstUserChanged events. +/// +/// * `connection`: D-Bus connection to listen for events. +/// * `map`: stream map to which it adds streams +pub async fn add_users_streams( + dbus: zbus::Connection, + mut map: StreamMap<&str, Pin + Send>>>, +) -> Result + Send>>>, Error> { + map.insert("first_user", Box::pin(first_user_changed_stream(dbus.clone()).await?)); + map.insert("root_password", Box::pin(root_password_changed_stream(dbus.clone()).await?)); + map.insert("root_sshkey", Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?)); + Ok(map) +} + +async fn first_user_changed_stream(dbus: zbus::Connection, +) -> Result, Error> { + let proxy = Users1Proxy::new(&dbus).await?; + let stream = proxy + .receive_first_user_changed() + .await + .then(|change| async move { + if let Ok(user) = change.get().await { + let user_struct = FirstUserSettings { + full_name: Some(user.0), + user_name: Some(user.1), + password: Some(user.2), + autologin: Some(user.3), + }; + return Some(Event::FirstUserChanged(user_struct)); + } + None + }) + .filter_map(|e| e); + Ok(stream) +} + +async fn root_password_changed_stream(dbus: zbus::Connection, +) -> Result, Error> { + let proxy = Users1Proxy::new(&dbus).await?; + let stream = proxy + .receive_root_password_set_changed() + .await + .then(|change| async move { + if let Ok(is_set) = change.get().await { + return Some(Event::RootPasswordChanged { password_is_set: is_set }); + } + None + }) + .filter_map(|e| e); + Ok(stream) +} + +async fn root_ssh_key_changed_stream(dbus: zbus::Connection, +) -> Result, Error> { + let proxy = Users1Proxy::new(&dbus).await?; + let stream = proxy + .receive_root_sshkey_changed() + .await + .then(|change| async move { + if let Ok(key) = change.get().await { + let value = if key.is_empty() { + None + } else { + Some(key) + }; + return Some(Event::RootSSHKeyChanged { key: value }); + } + None + }) + .filter_map(|e| e); + Ok(stream) +} + +/// Sets up and returns the axum service for the software module. +pub async fn users_service(dbus: zbus::Connection) -> Result { + + let users = UsersClient::new(dbus).await?; + let state = UsersState { users }; + let router = Router::new() + .with_state(state); + Ok(router) +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index c87d219594..8278c67324 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -11,6 +11,7 @@ use crate::{ network::{web::network_service, NetworkManagerAdapter}, questions::web::{questions_service, questions_stream}, software::web::{software_service, software_stream}, + users::web::{users_service, add_users_streams}, web::common::{issues_stream, progress_stream, service_status_stream}, }; use axum::Router; @@ -105,7 +106,7 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res ) .await?, ); - + stream = add_users_streams(dbus.clone(), stream).await?; stream.insert("software", software_stream(dbus.clone()).await?); stream.insert( "software-status", diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 255886a1e4..73f2c82ce4 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,5 +1,5 @@ use crate::l10n::web::LocaleConfig; -use agama_lib::{manager::InstallationPhase, progress::Progress, software::SelectedBy}; +use agama_lib::{manager::InstallationPhase, progress::Progress, software::SelectedBy, users::FirstUserSettings}; use serde::Serialize; use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; @@ -21,6 +21,13 @@ pub enum Event { ProductChanged { id: String, }, + FirstUserChanged(FirstUserSettings), + RootPasswordChanged { + password_is_set: bool + }, + RootSSHKeyChanged { + key: Option, + }, PatternsChanged(HashMap), QuestionsChanged, InstallationPhaseChanged { From 5902ba9f7b9e54fc787a4738cc9c722951c5e232 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 26 Mar 2024 11:10:48 +0100 Subject: [PATCH 02/19] use tuple of streams instead of StreamMap --- rust/agama-server/src/users.rs | 2 +- rust/agama-server/src/users/web.rs | 26 ++++++++++++++------------ rust/agama-server/src/web.rs | 6 ++++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/rust/agama-server/src/users.rs b/rust/agama-server/src/users.rs index 2356d1ef98..76ddbc68ee 100644 --- a/rust/agama-server/src/users.rs +++ b/rust/agama-server/src/users.rs @@ -1,2 +1,2 @@ pub mod web; -pub use web::{users_service, add_users_streams}; +pub use web::{users_service, users_streams}; diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index b5ad736d95..ce5b652dce 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -10,6 +10,7 @@ use axum::{ Json, Router, }; use serde::{Deserialize, Serialize}; +use zbus::PropertyStream; use std::{collections::HashMap, pin::Pin}; use tokio_stream::{Stream, StreamExt, StreamMap}; use crate::{ @@ -30,24 +31,25 @@ struct UsersState<'a> { users: UsersClient<'a>, } -/// Returns an stream that emits users related events coming from D-Bus. +/// Returns streams that emits users related events coming from D-Bus. /// /// It emits the Event::RootPasswordChange, Event::RootSSHKeyChanged and Event::FirstUserChanged events. /// /// * `connection`: D-Bus connection to listen for events. -/// * `map`: stream map to which it adds streams -pub async fn add_users_streams( +pub async fn users_streams( dbus: zbus::Connection, - mut map: StreamMap<&str, Pin + Send>>>, -) -> Result + Send>>>, Error> { - map.insert("first_user", Box::pin(first_user_changed_stream(dbus.clone()).await?)); - map.insert("root_password", Box::pin(root_password_changed_stream(dbus.clone()).await?)); - map.insert("root_sshkey", Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?)); - Ok(map) +) -> Result + Send>>)>, Error> { + let result : Vec<(String, Pin + Send>>)> = vec![ + ("first_user".to_string(), Box::pin(first_user_changed_stream(dbus.clone()).await?)), + ("root_password".to_string(), Box::pin(root_password_changed_stream(dbus.clone()).await?)), + ("root_sshkey".to_string(), Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?)), + ]; + + Ok(result) } async fn first_user_changed_stream(dbus: zbus::Connection, -) -> Result, Error> { +) -> Result + Send, Error> { let proxy = Users1Proxy::new(&dbus).await?; let stream = proxy .receive_first_user_changed() @@ -69,7 +71,7 @@ async fn first_user_changed_stream(dbus: zbus::Connection, } async fn root_password_changed_stream(dbus: zbus::Connection, -) -> Result, Error> { +) -> Result + Send, Error> { let proxy = Users1Proxy::new(&dbus).await?; let stream = proxy .receive_root_password_set_changed() @@ -85,7 +87,7 @@ async fn root_password_changed_stream(dbus: zbus::Connection, } async fn root_ssh_key_changed_stream(dbus: zbus::Connection, -) -> Result, Error> { +) -> Result + Send, Error> { let proxy = Users1Proxy::new(&dbus).await?; let stream = proxy .receive_root_sshkey_changed() diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 8278c67324..ef17ecf7b1 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -11,7 +11,7 @@ use crate::{ network::{web::network_service, NetworkManagerAdapter}, questions::web::{questions_service, questions_stream}, software::web::{software_service, software_stream}, - users::web::{users_service, add_users_streams}, + users::web::{users_service, users_streams}, web::common::{issues_stream, progress_stream, service_status_stream}, }; use axum::Router; @@ -106,7 +106,9 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res ) .await?, ); - stream = add_users_streams(dbus.clone(), stream).await?; + for (id, user_stream) in users_streams(dbus.clone()).await? { + stream.insert(id.as_str(), user_stream); + } stream.insert("software", software_stream(dbus.clone()).await?); stream.insert( "software-status", From a9222b7c10c2150fdbc4a90edc599f52cbe45a69 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 26 Mar 2024 14:11:04 +0100 Subject: [PATCH 03/19] implement routes for first user --- rust/agama-lib/src/users/client.rs | 8 ++++++-- rust/agama-server/src/users/web.rs | 31 +++++++++++++++++++++++------- rust/agama-server/src/web.rs | 2 +- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 6b2729a354..9e6f917bbc 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -3,11 +3,11 @@ use super::proxies::{FirstUser as FirstUserFromDBus, Users1Proxy}; use crate::error::ServiceError; use agama_settings::{settings::Settings, SettingValue, SettingsError}; -use serde::Serialize; +use serde::{Serialize,Deserialize}; use zbus::Connection; /// Represents the settings for the first user -#[derive(Serialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct FirstUser { /// First user's full name pub full_name: String, @@ -122,4 +122,8 @@ impl<'a> UsersClient<'a> { ) .await } + + pub async fn remove_first_user(&self) -> zbus::Result { + Ok(self.users_proxy.remove_first_user().await? == 0) + } } diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index ce5b652dce..c7a3ac01af 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -6,7 +6,7 @@ use axum::{ extract::State, - routing::{get, post, put}, + routing::{delete, get, post, put}, Json, Router, }; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ use crate::{ }; use agama_lib::{ connection, error::ServiceError, users::{ - proxies::Users1Proxy, FirstUserSettings, UsersClient + proxies::Users1Proxy, FirstUser, FirstUserSettings, UsersClient } }; @@ -38,11 +38,14 @@ struct UsersState<'a> { /// * `connection`: D-Bus connection to listen for events. pub async fn users_streams( dbus: zbus::Connection, -) -> Result + Send>>)>, Error> { - let result : Vec<(String, Pin + Send>>)> = vec![ - ("first_user".to_string(), Box::pin(first_user_changed_stream(dbus.clone()).await?)), - ("root_password".to_string(), Box::pin(root_password_changed_stream(dbus.clone()).await?)), - ("root_sshkey".to_string(), Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?)), +) -> Result + Send>>)>, Error> { + const FIRST_USER_ID : &str = "first_user"; + const ROOT_PASSWORD_ID : &str = "root_password"; + const ROOT_SSHKEY_ID : &str = "root_sshkey"; + let result : Vec<(&str, Pin + Send>>)> = vec![ + (FIRST_USER_ID, Box::pin(first_user_changed_stream(dbus.clone()).await?)), + (ROOT_PASSWORD_ID, Box::pin(root_password_changed_stream(dbus.clone()).await?)), + (ROOT_SSHKEY_ID, Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?)), ]; Ok(result) @@ -113,6 +116,20 @@ pub async fn users_service(dbus: zbus::Connection) -> Result>) -> Result<(), Error> { + state.users.remove_first_user().await?; + Ok(()) +} + +async fn set_first_user( + State(state): State>, + Json(config) : Json + ) -> Result<(), Error> { + state.users.set_first_user(&config).await?; + Ok(()) +} \ No newline at end of file diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index ef17ecf7b1..4537924039 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -107,7 +107,7 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res .await?, ); for (id, user_stream) in users_streams(dbus.clone()).await? { - stream.insert(id.as_str(), user_stream); + stream.insert(id, user_stream); } stream.insert("software", software_stream(dbus.clone()).await?); stream.insert( From 547dfb27bb861bb05af13a8360aed96117090d4f Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 26 Mar 2024 14:56:30 +0100 Subject: [PATCH 04/19] add root password routes --- rust/agama-lib/src/users/client.rs | 4 ++++ rust/agama-server/src/users/web.rs | 35 ++++++++++++++++++++++-------- rust/agama-server/src/web.rs | 3 ++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 9e6f917bbc..923a61627c 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -92,6 +92,10 @@ impl<'a> UsersClient<'a> { Ok(self.users_proxy.set_root_password(value, encrypted).await?) } + pub async fn remove_root_password(&self) -> Result { + Ok(self.users_proxy.remove_root_password().await?) + } + /// Whether the root password is set or not pub async fn is_root_password(&self) -> Result { Ok(self.users_proxy.root_password_set().await?) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index c7a3ac01af..b33b6686ca 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -6,22 +6,19 @@ use axum::{ extract::State, - routing::{delete, get, post, put}, + routing::put, Json, Router, }; use serde::{Deserialize, Serialize}; -use zbus::PropertyStream; -use std::{collections::HashMap, pin::Pin}; -use tokio_stream::{Stream, StreamExt, StreamMap}; +use std::pin::Pin; +use tokio_stream::{Stream, StreamExt}; use crate::{ error::Error, - web::{ - common::{issues_router, progress_router, service_status_router}, - Event, - }, + web::Event + , }; use agama_lib::{ - connection, error::ServiceError, users::{ + error::ServiceError, users::{ proxies::Users1Proxy, FirstUser, FirstUserSettings, UsersClient } }; @@ -117,6 +114,7 @@ pub async fn users_service(dbus: zbus::Connection) -> Result Result<(), Error> { state.users.set_first_user(&config).await?; Ok(()) +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RootPasswordSettings { + pub value: String, + pub encrypted: bool, +} + +async fn remove_root_password(State(state): State>) -> Result<(), Error> { + state.users.remove_root_password().await?; + Ok(()) +} + +async fn set_root_password( + State(state): State>, + Json(config) : Json +) -> Result<(), Error> { +state.users.set_root_password(&config.value, config.encrypted).await?; +Ok(()) } \ No newline at end of file diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 4537924039..1e20241202 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -62,7 +62,8 @@ where "/network", network_service(dbus.clone(), network_adapter).await?, ) - .add_service("/questions", questions_service(dbus).await?) + .add_service("/questions", questions_service(dbus.clone()).await?) + .add_service("/users", users_service(dbus.clone()).await?) .with_config(config) .build(); Ok(router) From 20b72e7a9474f157b4c6f0d3354e9b2e6c390334 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 27 Mar 2024 10:53:00 +0100 Subject: [PATCH 05/19] add route for ssh key --- rust/agama-server/src/users/web.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index b33b6686ca..cef65ef2a6 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -115,6 +115,7 @@ pub async fn users_service(dbus: zbus::Connection) -> Result Result<(), Error> { state.users.set_root_password(&config.value, config.encrypted).await?; Ok(()) +} + +async fn remove_root_sshkey(State(state): State>) -> Result<(), Error> { + state.users.set_root_sshkey("").await?; + Ok(()) +} + +async fn set_root_sshkey( + State(state): State>, + Json(key) : Json +) -> Result<(), Error> { +state.users.set_root_sshkey(key.as_str()).await?; +Ok(()) } \ No newline at end of file From 9987a22af55373a81665f075b6afb5f9388c5c38 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 27 Mar 2024 13:02:03 +0100 Subject: [PATCH 06/19] add root route for users to get info --- rust/agama-lib/src/users/client.rs | 2 +- rust/agama-server/src/users/web.rs | 38 +++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 923a61627c..2c24e1e602 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -7,7 +7,7 @@ use serde::{Serialize,Deserialize}; use zbus::Connection; /// Represents the settings for the first user -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, Default)] pub struct FirstUser { /// First user's full name pub full_name: String, diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index cef65ef2a6..bc693d9795 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -6,7 +6,7 @@ use axum::{ extract::State, - routing::put, + routing::{get, put}, Json, Router, }; use serde::{Deserialize, Serialize}; @@ -113,9 +113,10 @@ pub async fn users_service(dbus: zbus::Connection) -> Result Result<(), Error> { state.users.set_root_sshkey(key.as_str()).await?; Ok(()) -} \ No newline at end of file +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RootInfo { + password: bool, + sshkey: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UsersInfo { + user: Option, + root: RootInfo, +} + +async fn get_info(State(state): State>) + -> Result, Error> { + let mut result = UsersInfo::default(); + let first_user = state.users.first_user().await?; + if first_user.user_name.is_empty() { + result.user = None; + } else { + result.user = Some(first_user); + } + result.root.password = state.users.is_root_password().await?; + let ssh_key = state.users.root_ssh_key().await?; + if ssh_key.is_empty() { + result.root.sshkey = None; + } else { + result.root.sshkey = Some(ssh_key); + } + Ok(Json(result)) +} From 13163847d3862c8887029da8f7595cf1e63135cc Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 27 Mar 2024 23:06:27 +0100 Subject: [PATCH 07/19] Add validation router and use it in users --- rust/agama-lib/src/proxies.rs | 11 +++ rust/agama-lib/src/users/client.rs | 4 +- rust/agama-server/src/users/web.rs | 134 +++++++++++++++++++--------- rust/agama-server/src/web/common.rs | 105 +++++++++++++++++++++- rust/agama-server/src/web/docs.rs | 11 +++ rust/agama-server/src/web/event.rs | 11 ++- 6 files changed, 231 insertions(+), 45 deletions(-) diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index 5a64753915..d33efa6921 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -178,3 +178,14 @@ trait Issues { #[dbus_proxy(property)] fn all(&self) -> zbus::Result>; } + +#[dbus_proxy(interface = "org.opensuse.Agama1.Validation", assume_defaults = true)] +trait Validation { + /// Errors property + #[dbus_proxy(property)] + fn errors(&self) -> zbus::Result>; + + /// Valid property + #[dbus_proxy(property)] + fn valid(&self) -> zbus::Result; +} diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 2c24e1e602..37a1badb16 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -3,11 +3,11 @@ use super::proxies::{FirstUser as FirstUserFromDBus, Users1Proxy}; use crate::error::ServiceError; use agama_settings::{settings::Settings, SettingValue, SettingsError}; -use serde::{Serialize,Deserialize}; +use serde::{Deserialize, Serialize}; use zbus::Connection; /// Represents the settings for the first user -#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, Default, utoipa::ToSchema)] pub struct FirstUser { /// First user's full name pub full_name: String, diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index bc693d9795..5b42b59c11 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -4,6 +4,17 @@ //! * `users_service` which returns the Axum service. //! * `users_stream` which offers an stream that emits the users events coming from D-Bus. +use crate::{ + error::Error, + web::{ + common::{service_status_router, validation_router}, + Event, + }, +}; +use agama_lib::{ + error::ServiceError, + users::{proxies::Users1Proxy, FirstUser, FirstUserSettings, UsersClient}, +}; use axum::{ extract::State, routing::{get, put}, @@ -12,16 +23,6 @@ use axum::{ use serde::{Deserialize, Serialize}; use std::pin::Pin; use tokio_stream::{Stream, StreamExt}; -use crate::{ - error::Error, - web::Event - , -}; -use agama_lib::{ - error::ServiceError, users::{ - proxies::Users1Proxy, FirstUser, FirstUserSettings, UsersClient - } -}; #[derive(Clone)] struct UsersState<'a> { @@ -36,19 +37,29 @@ struct UsersState<'a> { pub async fn users_streams( dbus: zbus::Connection, ) -> Result + Send>>)>, Error> { - const FIRST_USER_ID : &str = "first_user"; - const ROOT_PASSWORD_ID : &str = "root_password"; - const ROOT_SSHKEY_ID : &str = "root_sshkey"; - let result : Vec<(&str, Pin + Send>>)> = vec![ - (FIRST_USER_ID, Box::pin(first_user_changed_stream(dbus.clone()).await?)), - (ROOT_PASSWORD_ID, Box::pin(root_password_changed_stream(dbus.clone()).await?)), - (ROOT_SSHKEY_ID, Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?)), + const FIRST_USER_ID: &str = "first_user"; + const ROOT_PASSWORD_ID: &str = "root_password"; + const ROOT_SSHKEY_ID: &str = "root_sshkey"; + let result: Vec<(&str, Pin + Send>>)> = vec![ + ( + FIRST_USER_ID, + Box::pin(first_user_changed_stream(dbus.clone()).await?), + ), + ( + ROOT_PASSWORD_ID, + Box::pin(root_password_changed_stream(dbus.clone()).await?), + ), + ( + ROOT_SSHKEY_ID, + Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?), + ), ]; Ok(result) } -async fn first_user_changed_stream(dbus: zbus::Connection, +async fn first_user_changed_stream( + dbus: zbus::Connection, ) -> Result + Send, Error> { let proxy = Users1Proxy::new(&dbus).await?; let stream = proxy @@ -70,7 +81,8 @@ async fn first_user_changed_stream(dbus: zbus::Connection, Ok(stream) } -async fn root_password_changed_stream(dbus: zbus::Connection, +async fn root_password_changed_stream( + dbus: zbus::Connection, ) -> Result + Send, Error> { let proxy = Users1Proxy::new(&dbus).await?; let stream = proxy @@ -78,7 +90,9 @@ async fn root_password_changed_stream(dbus: zbus::Connection, .await .then(|change| async move { if let Ok(is_set) = change.get().await { - return Some(Event::RootPasswordChanged { password_is_set: is_set }); + return Some(Event::RootPasswordChanged { + password_is_set: is_set, + }); } None }) @@ -86,7 +100,8 @@ async fn root_password_changed_stream(dbus: zbus::Connection, Ok(stream) } -async fn root_ssh_key_changed_stream(dbus: zbus::Connection, +async fn root_ssh_key_changed_stream( + dbus: zbus::Connection, ) -> Result + Send, Error> { let proxy = Users1Proxy::new(&dbus).await?; let stream = proxy @@ -94,11 +109,7 @@ async fn root_ssh_key_changed_stream(dbus: zbus::Connection, .await .then(|change| async move { if let Ok(key) = change.get().await { - let value = if key.is_empty() { - None - } else { - Some(key) - }; + let value = if key.is_empty() { None } else { Some(key) }; return Some(Event::RootSSHKeyChanged { key: value }); } None @@ -109,27 +120,48 @@ async fn root_ssh_key_changed_stream(dbus: zbus::Connection, /// Sets up and returns the axum service for the software module. pub async fn users_service(dbus: zbus::Connection) -> Result { + const DBUS_SERVICE: &str = "org.opensuse.Agama.Users1"; + const DBUS_PATH: &str = "/org/opensuse/Agama/Users1"; - let users = UsersClient::new(dbus).await?; + let users = UsersClient::new(dbus.clone()).await?; let state = UsersState { users }; + let validation_router = validation_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; + let status_router = service_status_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; let router = Router::new() .route("/", get(get_info)) .route("/first_user", put(set_first_user).delete(remove_first_user)) - .route("/root_password", put(set_root_password).delete(remove_root_password)) - .route("/root_sshkey", put(set_root_sshkey).delete(remove_root_sshkey)) + .route( + "/root_password", + put(set_root_password).delete(remove_root_password), + ) + .route( + "/root_sshkey", + put(set_root_sshkey).delete(remove_root_sshkey), + ) + .merge(validation_router) + .merge(status_router) .with_state(state); Ok(router) } +/// Removes the first user settings +#[utoipa::path(delete, path = "/users/user", responses( + (status = 200, description = "Removes the first user"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] async fn remove_first_user(State(state): State>) -> Result<(), Error> { state.users.remove_first_user().await?; Ok(()) } +#[utoipa::path(put, path = "/users/user", responses( + (status = 200, description = "User values are set"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] async fn set_first_user( - State(state): State>, - Json(config) : Json - ) -> Result<(), Error> { + State(state): State>, + Json(config): Json, +) -> Result<(), Error> { state.users.set_first_user(&config).await?; Ok(()) } @@ -140,30 +172,49 @@ pub struct RootPasswordSettings { pub encrypted: bool, } +#[utoipa::path(delete, path = "/users/root_password", responses( + (status = 200, description = "Removes the root password"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] async fn remove_root_password(State(state): State>) -> Result<(), Error> { state.users.remove_root_password().await?; Ok(()) } +#[utoipa::path(put, path = "/users/root_password", responses( + (status = 200, description = "Root password is set"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] async fn set_root_password( State(state): State>, - Json(config) : Json + Json(config): Json, ) -> Result<(), Error> { -state.users.set_root_password(&config.value, config.encrypted).await?; -Ok(()) + state + .users + .set_root_password(&config.value, config.encrypted) + .await?; + Ok(()) } +#[utoipa::path(delete, path = "/users/root_sshkey", responses( + (status = 200, description = "Removes the root SSH key"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] async fn remove_root_sshkey(State(state): State>) -> Result<(), Error> { state.users.set_root_sshkey("").await?; Ok(()) } +#[utoipa::path(put, path = "/users/root_sshkey", responses( + (status = 200, description = "Root SSH Key is set"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] async fn set_root_sshkey( State(state): State>, - Json(key) : Json + Json(key): Json, ) -> Result<(), Error> { -state.users.set_root_sshkey(key.as_str()).await?; -Ok(()) + state.users.set_root_sshkey(key.as_str()).await?; + Ok(()) } #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] @@ -178,8 +229,11 @@ pub struct UsersInfo { root: RootInfo, } -async fn get_info(State(state): State>) - -> Result, Error> { +#[utoipa::path(put, path = "/users/", responses( + (status = 200, description = "Configuration for users including root and the first user", body = UsersInfo), + (status = 400, description = "The D-Bus service could not perform the action"), +))] +async fn get_info(State(state): State>) -> Result, Error> { let mut result = UsersInfo::default(); let first_user = state.users.first_user().await?; if first_user.user_name.is_empty() { diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index 891a292f31..9477f1ea70 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -5,7 +5,7 @@ use std::{pin::Pin, task::Poll}; use agama_lib::{ error::ServiceError, progress::Progress, - proxies::{IssuesProxy, ProgressProxy, ServiceStatusProxy}, + proxies::{IssuesProxy, ProgressProxy, ServiceStatusProxy, ValidationProxy}, }; use axum::{extract::State, routing::get, Json, Router}; use pin_project::pin_project; @@ -352,3 +352,106 @@ async fn build_issues_proxy<'a>( .await?; Ok(proxy) } + +/// Builds a router to the `org.opensuse.Agama1.Validation` interface of a given +/// D-Bus object. +/// +/// ```no_run +/// # use axum::{extract::State, routing::get, Json, Router}; +/// # use agama_lib::connection; +/// # use agama_server::web::common::validation_router; +/// # use tokio_test; +/// +/// # tokio_test::block_on(async { +/// async fn hello(state: State) {}; +/// +/// #[derive(Clone)] +/// struct HelloWorldState {}; +/// +/// let dbus = connection().await.unwrap(); +/// let issues_router = validation_router( +/// &dbus, "org.opensuse.HelloWorld", "/org/opensuse/hello" +/// ).await.unwrap(); +/// let router: Router = Router::new() +/// .route("/hello", get(hello)) +/// .merge(validation_router) +/// .with_state(HelloWorldState {}); +/// }); +/// ``` +/// +/// * `dbus`: D-Bus connection. +/// * `destination`: D-Bus service name. +/// * `path`: D-Bus object path. +pub async fn validation_router( + dbus: &zbus::Connection, + destination: &str, + path: &str, +) -> Result, ServiceError> { + let proxy = build_validation_proxy(dbus, destination, path).await?; + let state = ValidationState { proxy }; + Ok(Router::new().route("/", get(validation)).with_state(state)) +} + +#[derive(Clone, Serialize, utoipa::ToSchema)] +pub struct ValidationResult { + valid: bool, + errors: Vec, +} + +async fn validation( + State(state): State>, +) -> Result, Error> { + let validation = ValidationResult { + valid: state.proxy.valid().await?, + errors: state.proxy.errors().await?, + }; + Ok(Json(validation)) +} + +#[derive(Clone)] +struct ValidationState<'a> { + proxy: ValidationProxy<'a>, +} + +/// Builds a stream of the changes in the the `org.opensuse.Agama1.Issues` +/// interface of the given D-Bus object. +/// +/// * `dbus`: D-Bus connection. +/// * `destination`: D-Bus service name. +/// * `path`: D-Bus object path. +pub async fn validation_stream( + dbus: zbus::Connection, + destination: &'static str, + path: &'static str, +) -> Result + Send>>, Error> { + let proxy = build_validation_proxy(&dbus, destination, path).await?; + let stream = proxy + .receive_errors_changed() + .await + .then(move |change| async move { + if let Ok(errors) = change.get().await { + Some(Event::ValidationChanged { + service: destination.to_string(), + path: path.to_string(), + errors, + }) + } else { + None + } + }) + .filter_map(|e| e); + Ok(Box::pin(stream)) +} + +async fn build_validation_proxy<'a>( + dbus: &zbus::Connection, + destination: &str, + path: &str, +) -> Result, zbus::Error> { + let proxy = ValidationProxy::builder(dbus) + .destination(destination.to_string())? + .path(path.to_string())? + .build() + .await?; + Ok(proxy) +} diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index fdacf6cc86..341cbb2979 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -20,6 +20,13 @@ use utoipa::OpenApi; crate::manager::web::installer_status, crate::questions::web::list_questions, crate::questions::web::answer, + crate::users::web::get_info, + crate::users::web::set_first_user, + crate::users::web::remove_first_user, + crate::users::web::set_root_password, + crate::users::web::remove_root_password, + crate::users::web::set_root_sshkey, + crate::users::web::remove_root_sshkey, super::http::ping, ), components( @@ -44,6 +51,10 @@ use utoipa::OpenApi; schemas(crate::questions::web::Answer), schemas(crate::questions::web::GenericAnswer), schemas(crate::questions::web::PasswordAnswer), + schemas(agama_lib::users::FirstUser), + schemas(crate::users::web::RootPasswordSettings), + schemas(crate::users::web::RootInfo), + schemas(crate::users::web::UsersInfo), schemas(super::http::PingResponse), ) )] diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 73f2c82ce4..067e61f8f2 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,5 +1,7 @@ use crate::l10n::web::LocaleConfig; -use agama_lib::{manager::InstallationPhase, progress::Progress, software::SelectedBy, users::FirstUserSettings}; +use agama_lib::{ + manager::InstallationPhase, progress::Progress, software::SelectedBy, users::FirstUserSettings, +}; use serde::Serialize; use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; @@ -23,7 +25,7 @@ pub enum Event { }, FirstUserChanged(FirstUserSettings), RootPasswordChanged { - password_is_set: bool + password_is_set: bool, }, RootSSHKeyChanged { key: Option, @@ -45,6 +47,11 @@ pub enum Event { path: String, issues: Vec, }, + ValidationChanged { + service: String, + path: String, + errors: Vec, + }, } pub type EventsSender = Sender; From 7abc0b9f66bf2551ed30b2a17c9cc1a6914b9de9 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Sun, 31 Mar 2024 00:05:30 +0100 Subject: [PATCH 08/19] adapt users js code (WIP) --- rust/agama-server/src/users/web.rs | 5 +- web/src/client/http.js | 17 +++++ web/src/client/mixins.js | 24 +++---- web/src/client/users.js | 72 ++++++++------------ web/src/components/overview/OverviewPage.jsx | 1 + 5 files changed, 61 insertions(+), 58 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 5b42b59c11..e1f0205cfe 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -128,8 +128,9 @@ pub async fn users_service(dbus: zbus::Connection) -> Result} Server response. + */ + async delete(url) { + const response = await fetch(`${this.baseUrl}/${url}`, { + method: "DELETE", + }); + + try { + return await response.json(); + } catch (e) { + console.warn("Expecting a JSON response", e); + return response.status === 200; + } + } + /** * Registers a handler for a given type of events. * diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index c7a9b28b7d..88ca2f2ec4 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -21,11 +21,6 @@ // @ts-check -const ISSUES_IFACE = "org.opensuse.Agama1.Issues"; -const STATUS_IFACE = "org.opensuse.Agama1.ServiceStatus"; -const PROGRESS_IFACE = "org.opensuse.Agama1.Progress"; -const VALIDATION_IFACE = "org.opensuse.Agama1.Validation"; - /** * @typedef {new(...args: any[]) => T} GConstructor * @template {object} T @@ -245,11 +240,12 @@ const createError = (message) => { /** * Extends the given class with methods to get validation errors over D-Bus - * @param {string} object_path - object_path + * @template {!WithHTTPClient} T * @param {T} superclass - superclass to extend - * @template {!WithDBusClient} T + * @param {string} validation_path - status resource path (e.g., "/manager/status"). + * @param {string} service_name - service name (e.g., "org.opensuse.Agama.Manager1"). */ -const WithValidation = (superclass, object_path) => class extends superclass { +const WithValidation = (superclass, validation_path, service_name) => class extends superclass { /** * Returns the validation errors * @@ -259,12 +255,12 @@ const WithValidation = (superclass, object_path) => class extends superclass { let errors; try { - errors = await this.client.getProperty(object_path, VALIDATION_IFACE, "Errors"); + errors = await this.client.get(validation_path); } catch (error) { - console.error(`Could not get validation errors for ${object_path}`, error); + console.error(`Could not get validation errors for ${validation_path}`, error); } - return errors.map(createError); + return errors.errors.map(createError); } /** @@ -274,8 +270,10 @@ const WithValidation = (superclass, object_path) => class extends superclass { * @return {import ("./dbus").RemoveFn} function to disable the callback */ onValidationChange(handler) { - return this.client.onObjectChanged(object_path, VALIDATION_IFACE, () => { - this.getValidationErrors().then(handler); + return this.client.onEvent("ValidationChange", ({ service, errors }) => { + if (service === service_name) { + handler(errors); + } }); } }; diff --git a/web/src/client/users.js b/web/src/client/users.js index 2a2b46588e..8a0315050d 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -21,12 +21,9 @@ // @ts-check -import DBusClient from "./dbus"; import { WithValidation } from "./mixins"; -const USERS_SERVICE = "org.opensuse.Agama.Manager1"; -const USERS_IFACE = "org.opensuse.Agama.Users1"; -const USERS_PATH = "/org/opensuse/Agama/Users1"; +const USERS_PATH = "/users/info"; // TODO: it should be /users/ when routing in rs is solved /** * @typedef {object} UserResult @@ -56,10 +53,10 @@ const USERS_PATH = "/org/opensuse/Agama/Users1"; */ class UsersBaseClient { /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. + * @param {import("./http").HTTPClient} client - HTTP client. */ - constructor(address = undefined) { - this.client = new DBusClient(USERS_SERVICE, address); + constructor(client) { + this.client = client; } /** @@ -68,8 +65,12 @@ class UsersBaseClient { * @return {Promise} */ async getUser() { - const proxy = await this.client.proxy(USERS_IFACE); - const [fullName, userName, password, autologin] = proxy.FirstUser; + const proxy = await this.client.get(USERS_PATH); + if (proxy.user === undefined) { + return { fullName: "", userName: "", password: "", autologin: false }; + } + + const [fullName, userName, password, autologin] = proxy.user; return { fullName, userName, password, autologin }; } @@ -79,8 +80,8 @@ class UsersBaseClient { * @return {Promise} */ async isRootPasswordSet() { - const proxy = await this.client.proxy(USERS_IFACE); - return proxy.RootPasswordSet; + const proxy = await this.client.get(USERS_PATH); + return proxy.root.password; } /** @@ -90,16 +91,9 @@ class UsersBaseClient { * @return {Promise} returns an object with the result and the issues found if error */ async setUser(user) { - const proxy = await this.client.proxy(USERS_IFACE); - const [result, issues] = await proxy.SetFirstUser( - user.fullName, - user.userName, - user.password, - user.autologin, - {} - ); - - return { result, issues }; + const result = await this.client.put("/users/user", user); + + return { result, issues: [] }; // TODO: check how to handle issues and result. Maybe separate call to validate? } /** @@ -108,9 +102,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeUser() { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.RemoveFirstUser(); - return result === 0; + return this.client.delete("/users/user"); } /** @@ -120,9 +112,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async setRootPassword(password) { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.SetRootPassword(password, false); - return result === 0; + return this.client.put("/users/root_password", { value: password, encrypted: false }); } /** @@ -131,9 +121,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeRootPassword() { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.RemoveRootPassword(); - return result === 0; + return this.client.delete("/users/root_password"); } /** @@ -142,8 +130,8 @@ class UsersBaseClient { * @return {Promise} SSH public key or an empty string if it is not set */ async getRootSSHKey() { - const proxy = await this.client.proxy(USERS_IFACE); - return proxy.RootSSHKey; + const proxy = await this.client.get(USERS_PATH); + return proxy.root.password || ""; } /** @@ -153,9 +141,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async setRootSSHKey(key) { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.SetRootSSHKey(key); - return result === 0; + return this.client.put("/users/root_sshkey", key); } /** @@ -165,15 +151,15 @@ class UsersBaseClient { * @return {import ("./dbus").RemoveFn} function to disable the callback */ onUsersChange(handler) { - return this.client.onObjectChanged(USERS_PATH, USERS_IFACE, changes => { - if (changes.RootPasswordSet) { + return this.client.ws.onEvent((event) => { + if (event.type === "RootPasswordChanged") { // @ts-ignore - return handler({ rootPasswordSet: changes.RootPasswordSet.v }); - } else if (changes.RootSSHKey) { - return handler({ rootSSHKey: changes.RootSSHKey.v.toString() }); - } else if (changes.FirstUser) { + return handler({ rootPasswordSet: event.password_is_set }); + } else if (event.type === "RootSSHKeyChanged") { + return handler({ rootSSHKey: event.key.toString() }); + } else if (event.type === "FirstUserChanged") { // @ts-ignore - const [fullName, userName, password, autologin] = changes.FirstUser.v; + const { fullName, userName, password, autologin } = event; return handler({ firstUser: { fullName, userName, password, autologin } }); } }); @@ -183,6 +169,6 @@ class UsersBaseClient { /** * Client to interact with the Agama users service */ -class UsersClient extends WithValidation(UsersBaseClient, USERS_PATH) { } +class UsersClient extends WithValidation(UsersBaseClient, "users/validation", "/org/opensuse/Agama/Users1") { } export { UsersClient }; diff --git a/web/src/components/overview/OverviewPage.jsx b/web/src/components/overview/OverviewPage.jsx index 6c18505320..096aece16d 100644 --- a/web/src/components/overview/OverviewPage.jsx +++ b/web/src/components/overview/OverviewPage.jsx @@ -68,6 +68,7 @@ export default function OverviewPage() { > + ); } From 3689fd45241681a1dd784b630961506a35879e80 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 2 Apr 2024 22:40:15 +0200 Subject: [PATCH 09/19] fix UI and also backend --- rust/agama-server/src/users/web.rs | 2 +- web/src/client/index.js | 4 ++-- web/src/client/users.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index e1f0205cfe..f857c8919c 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -120,7 +120,7 @@ async fn root_ssh_key_changed_stream( /// Sets up and returns the axum service for the software module. pub async fn users_service(dbus: zbus::Connection) -> Result { - const DBUS_SERVICE: &str = "org.opensuse.Agama.Users1"; + const DBUS_SERVICE: &str = "org.opensuse.Agama.Manager1"; const DBUS_PATH: &str = "/org/opensuse/Agama/Users1"; let users = UsersClient::new(dbus.clone()).await?; diff --git a/web/src/client/index.js b/web/src/client/index.js index 206365b695..8d820e192e 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -80,7 +80,7 @@ const createClient = (url) => { // const network = new NetworkClient(address); // const software = new SoftwareClient(address); // const storage = new StorageClient(address); - // const users = new UsersClient(address); + const users = new UsersClient(client); // const questions = new QuestionsClient(address); /** @@ -141,7 +141,7 @@ const createClient = (url) => { // network, // software, // storage, - // users, + users, // questions, // issues, onIssuesChange, diff --git a/web/src/client/users.js b/web/src/client/users.js index 8a0315050d..2d3f2fa28d 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -66,7 +66,8 @@ class UsersBaseClient { */ async getUser() { const proxy = await this.client.get(USERS_PATH); - if (proxy.user === undefined) { + console.log(proxy.user); + if (proxy.user === null) { return { fullName: "", userName: "", password: "", autologin: false }; } From 52dc81d01fe8425eb3cdafa1addeef97836aa777 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 3 Apr 2024 13:57:47 +0200 Subject: [PATCH 10/19] another bunch of fixes --- rust/agama-lib/src/users/client.rs | 1 + rust/agama-server/src/users/web.rs | 18 +++++++++--------- rust/agama-server/src/web/common.rs | 2 +- rust/agama-server/src/web/event.rs | 4 ++-- web/src/client/users.js | 16 ++++++++-------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 37a1badb16..979b788e3c 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -8,6 +8,7 @@ use zbus::Connection; /// Represents the settings for the first user #[derive(Serialize, Deserialize, Clone, Debug, Default, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct FirstUser { /// First user's full name pub full_name: String, diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index f857c8919c..d134db0ac2 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -13,7 +13,7 @@ use crate::{ }; use agama_lib::{ error::ServiceError, - users::{proxies::Users1Proxy, FirstUser, FirstUserSettings, UsersClient}, + users::{proxies::Users1Proxy, FirstUser, UsersClient}, }; use axum::{ extract::State, @@ -67,11 +67,12 @@ async fn first_user_changed_stream( .await .then(|change| async move { if let Ok(user) = change.get().await { - let user_struct = FirstUserSettings { - full_name: Some(user.0), - user_name: Some(user.1), - password: Some(user.2), - autologin: Some(user.3), + let user_struct = FirstUser { + full_name: user.0, + user_name: user.1, + password: user.2, + autologin: user.3, + data: user.4 }; return Some(Event::FirstUserChanged(user_struct)); } @@ -128,8 +129,7 @@ pub async fn users_service(dbus: zbus::Connection) -> Result( ) -> Result, ServiceError> { let proxy = build_validation_proxy(dbus, destination, path).await?; let state = ValidationState { proxy }; - Ok(Router::new().route("/", get(validation)).with_state(state)) + Ok(Router::new().route("/validation", get(validation)).with_state(state)) } #[derive(Clone, Serialize, utoipa::ToSchema)] diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 422497e57d..86c3625668 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,6 +1,6 @@ use crate::l10n::web::LocaleConfig; use agama_lib::{ - manager::InstallationPhase, progress::Progress, software::SelectedBy, users::FirstUserSettings, + manager::InstallationPhase, progress::Progress, software::SelectedBy, users::FirstUser, }; use serde::Serialize; use std::collections::HashMap; @@ -23,7 +23,7 @@ pub enum Event { ProductChanged { id: String, }, - FirstUserChanged(FirstUserSettings), + FirstUserChanged(FirstUser), RootPasswordChanged { password_is_set: bool, }, diff --git a/web/src/client/users.js b/web/src/client/users.js index 2d3f2fa28d..bc7f89cd9b 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -23,7 +23,7 @@ import { WithValidation } from "./mixins"; -const USERS_PATH = "/users/info"; // TODO: it should be /users/ when routing in rs is solved +const USERS_PATH = "/users/config"; /** * @typedef {object} UserResult @@ -37,6 +37,7 @@ const USERS_PATH = "/users/info"; // TODO: it should be /users/ when routing in * @property {string} userName - userName * @property {string} [password] - user password * @property {boolean} autologin - Whether autologin is enabled + * @property {object} data - additional user data */ /** @@ -66,13 +67,12 @@ class UsersBaseClient { */ async getUser() { const proxy = await this.client.get(USERS_PATH); - console.log(proxy.user); + if (proxy.user === null) { - return { fullName: "", userName: "", password: "", autologin: false }; + return { fullName: "", userName: "", password: "", autologin: false, data: {} }; } - const [fullName, userName, password, autologin] = proxy.user; - return { fullName, userName, password, autologin }; + return proxy.user; } /** @@ -132,7 +132,7 @@ class UsersBaseClient { */ async getRootSSHKey() { const proxy = await this.client.get(USERS_PATH); - return proxy.root.password || ""; + return proxy.root.ssh_key || ""; } /** @@ -160,8 +160,8 @@ class UsersBaseClient { return handler({ rootSSHKey: event.key.toString() }); } else if (event.type === "FirstUserChanged") { // @ts-ignore - const { fullName, userName, password, autologin } = event; - return handler({ firstUser: { fullName, userName, password, autologin } }); + const { fullName, userName, password, autologin, data } = event; + return handler({ firstUser: { fullName, userName, password, autologin, data } }); } }); } From 205b287708b6d36288f14371b5329be5590a78d2 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 3 Apr 2024 14:30:05 +0200 Subject: [PATCH 11/19] add hints for developing with two machines and debugging hints --- web/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/web/README.md b/web/README.md index e903923adc..c9b54bcd96 100644 --- a/web/README.md +++ b/web/README.md @@ -35,6 +35,26 @@ running Agama server instance. This is especially useful if you use the Live ISO which does not contain any development tools, you can develop the web frontend easily from your workstation. +Example of running from different machine: + +``` + # backend machine + # using ip of machine instead of localhost is important to be network accessible + agama-web-server serve --address 10.100.1.1:3030 + + # frontend machine + # ESLINT=0 is useful during development when there is some styling issues + ESLINT=0 AGAMA_SERVER=10.100.1.1:3000 npm run server +``` + +### Debugging Hints + +There are more locations where to look when something does not work and require debugging. +The first location to check when something does not work is browser console which can give +some hints. The second location to check for errors or warnings is output of `npm run server` +where some issues when communicating with backend can be seen. And last but on least is +journal on backend machine where is logged backend activity `journalctl -b`. + ### Special Environment Variables `AGAMA_SERVER` - When running the development server set up a proxy to From 7149061c2f2d00cbb9193bef6c52ff641d4ba717 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 3 Apr 2024 14:34:43 +0200 Subject: [PATCH 12/19] format rust code --- rust/agama-server/src/users/web.rs | 2 +- rust/agama-server/src/web/common.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index d134db0ac2..28981e7b0e 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -72,7 +72,7 @@ async fn first_user_changed_stream( user_name: user.1, password: user.2, autologin: user.3, - data: user.4 + data: user.4, }; return Some(Event::FirstUserChanged(user_struct)); } diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index c64905fffc..d0b7672354 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -389,7 +389,9 @@ pub async fn validation_router( ) -> Result, ServiceError> { let proxy = build_validation_proxy(dbus, destination, path).await?; let state = ValidationState { proxy }; - Ok(Router::new().route("/validation", get(validation)).with_state(state)) + Ok(Router::new() + .route("/validation", get(validation)) + .with_state(state)) } #[derive(Clone, Serialize, utoipa::ToSchema)] From 56d735e5ee0b4ce1321f4190572b9b68e4059498 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 3 Apr 2024 21:32:19 +0200 Subject: [PATCH 13/19] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa --- rust/agama-server/src/users/web.rs | 2 +- rust/agama-server/src/web/common.rs | 2 +- web/README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 28981e7b0e..4fed705013 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -119,7 +119,7 @@ async fn root_ssh_key_changed_stream( Ok(stream) } -/// Sets up and returns the axum service for the software module. +/// Sets up and returns the axum service for the users module. pub async fn users_service(dbus: zbus::Connection) -> Result { const DBUS_SERVICE: &str = "org.opensuse.Agama.Manager1"; const DBUS_PATH: &str = "/org/opensuse/Agama/Users1"; diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index d0b7672354..0c50cd7e95 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -415,7 +415,7 @@ struct ValidationState<'a> { proxy: ValidationProxy<'a>, } -/// Builds a stream of the changes in the the `org.opensuse.Agama1.Issues` +/// Builds a stream of the changes in the the `org.opensuse.Agama1.Validation` /// interface of the given D-Bus object. /// /// * `dbus`: D-Bus connection. diff --git a/web/README.md b/web/README.md index c9b54bcd96..94ba63a57a 100644 --- a/web/README.md +++ b/web/README.md @@ -43,16 +43,16 @@ Example of running from different machine: agama-web-server serve --address 10.100.1.1:3030 # frontend machine - # ESLINT=0 is useful during development when there is some styling issues + # ESLINT=0 is useful to ignore linter problems during development ESLINT=0 AGAMA_SERVER=10.100.1.1:3000 npm run server ``` ### Debugging Hints -There are more locations where to look when something does not work and require debugging. -The first location to check when something does not work is browser console which can give +There are several places to look when something does not work and requires debugging. +The first place is the browser's console which can give some hints. The second location to check for errors or warnings is output of `npm run server` -where some issues when communicating with backend can be seen. And last but on least is +where you can find issues when communicating with the backend. And last but on least is journal on backend machine where is logged backend activity `journalctl -b`. ### Special Environment Variables From dd4af425b302ec335f9d3affd85af6bf9e74ab53 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 3 Apr 2024 21:40:10 +0200 Subject: [PATCH 14/19] changes from review --- rust/agama-lib/src/users/settings.rs | 2 +- rust/agama-server/src/users/web.rs | 4 ++-- rust/agama-server/src/web/docs.rs | 3 +-- web/src/client/mixins.js | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index c902896156..afc75ec317 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -17,7 +17,7 @@ pub struct UserSettings { /// First user settings /// /// Holds the settings for the first user. -#[derive(Clone, Debug, Default, Settings, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Clone, Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FirstUserSettings { /// First user's full name diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 4fed705013..0fd578dec4 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -129,7 +129,7 @@ pub async fn users_service(dbus: zbus::Connection) -> Result>) -> Result, Error> { +async fn get_config(State(state): State>) -> Result, Error> { let mut result = UsersInfo::default(); let first_user = state.users.first_user().await?; if first_user.user_name.is_empty() { diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 7405b946ae..7d476344fb 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -12,7 +12,6 @@ use utoipa::OpenApi; crate::network::web::connections, crate::software::web::get_config, crate::software::web::patterns, - crate::software::web::patterns, crate::software::web::set_config, crate::manager::web::probe_action, crate::manager::web::install_action, @@ -20,7 +19,7 @@ use utoipa::OpenApi; crate::manager::web::installer_status, crate::questions::web::list_questions, crate::questions::web::answer, - crate::users::web::get_info, + crate::users::web::get_config, crate::users::web::set_first_user, crate::users::web::remove_first_user, crate::users::web::set_root_password, diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 9cc02f9c90..6e4fb3d851 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -252,15 +252,15 @@ const WithValidation = (superclass, validation_path, service_name) => class exte * @return {Promise} */ async getValidationErrors() { - let errors; + let response; try { - errors = await this.client.get(validation_path); + response = await this.client.get(validation_path); } catch (error) { console.error(`Could not get validation errors for ${validation_path}`, error); } - return errors.errors.map(createError); + return response.errors.map(createError); } /** From a5e4c02a08404150dee077428f204be41b3e0a9d Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 4 Apr 2024 15:09:27 +0200 Subject: [PATCH 15/19] reduce number of events for root user change --- rust/agama-server/src/users/web.rs | 10 +++++----- rust/agama-server/src/web/event.rs | 8 +++----- web/src/client/users.js | 13 +++++++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 0fd578dec4..fbdfe83b8e 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -40,6 +40,9 @@ pub async fn users_streams( const FIRST_USER_ID: &str = "first_user"; const ROOT_PASSWORD_ID: &str = "root_password"; const ROOT_SSHKEY_ID: &str = "root_sshkey"; + // here we have three streams, but only two events. Reason is + // that we have three streams from dbus about property change + // and munify two root user properties into single event to http API let result: Vec<(&str, Pin + Send>>)> = vec![ ( FIRST_USER_ID, @@ -91,9 +94,7 @@ async fn root_password_changed_stream( .await .then(|change| async move { if let Ok(is_set) = change.get().await { - return Some(Event::RootPasswordChanged { - password_is_set: is_set, - }); + return Some(Event::RootChanged { password: Some(is_set), sshkey: None }); } None }) @@ -110,8 +111,7 @@ async fn root_ssh_key_changed_stream( .await .then(|change| async move { if let Ok(key) = change.get().await { - let value = if key.is_empty() { None } else { Some(key) }; - return Some(Event::RootSSHKeyChanged { key: value }); + return Some(Event::RootChanged { password: None, sshkey: Some(key) }); } None }) diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 86c3625668..01776da513 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -24,11 +24,9 @@ pub enum Event { id: String, }, FirstUserChanged(FirstUser), - RootPasswordChanged { - password_is_set: bool, - }, - RootSSHKeyChanged { - key: Option, + RootChanged { + password: Option, + sshkey: Option }, // TODO: it should include the full software proposal or, at least, // all the relevant changes. diff --git a/web/src/client/users.js b/web/src/client/users.js index bc7f89cd9b..73195af559 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -153,11 +153,16 @@ class UsersBaseClient { */ onUsersChange(handler) { return this.client.ws.onEvent((event) => { - if (event.type === "RootPasswordChanged") { + if (event.type === "RootChanged") { + const res = {}; + if (event.password !== null) { + res.rootPasswordSet = event.password; + } + if (event.sshkey !== null) { + res.rootSSHKey = event.sshkey; + } // @ts-ignore - return handler({ rootPasswordSet: event.password_is_set }); - } else if (event.type === "RootSSHKeyChanged") { - return handler({ rootSSHKey: event.key.toString() }); + return handler(res); } else if (event.type === "FirstUserChanged") { // @ts-ignore const { fullName, userName, password, autologin, data } = event; From f5f95bf59b8bf90da10254bdf3f17404c76c345b Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 4 Apr 2024 17:20:13 +0200 Subject: [PATCH 16/19] modify routing as agreed. Client part is WIP --- rust/agama-server/src/users/web.rs | 142 ++++++++++++----------------- rust/agama-server/src/web/docs.rs | 13 +-- rust/agama-server/src/web/event.rs | 2 +- 3 files changed, 65 insertions(+), 92 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index fbdfe83b8e..326469e84f 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -15,11 +15,7 @@ use agama_lib::{ error::ServiceError, users::{proxies::Users1Proxy, FirstUser, UsersClient}, }; -use axum::{ - extract::State, - routing::{get, put}, - Json, Router, -}; +use axum::{extract::State, routing::get, Json, Router}; use serde::{Deserialize, Serialize}; use std::pin::Pin; use tokio_stream::{Stream, StreamExt}; @@ -94,7 +90,10 @@ async fn root_password_changed_stream( .await .then(|change| async move { if let Ok(is_set) = change.get().await { - return Some(Event::RootChanged { password: Some(is_set), sshkey: None }); + return Some(Event::RootChanged { + password: Some(is_set), + sshkey: None, + }); } None }) @@ -111,7 +110,10 @@ async fn root_ssh_key_changed_stream( .await .then(|change| async move { if let Ok(key) = change.get().await { - return Some(Event::RootChanged { password: None, sshkey: Some(key) }); + return Some(Event::RootChanged { + password: None, + sshkey: Some(key), + }); } None }) @@ -129,16 +131,13 @@ pub async fn users_service(dbus: zbus::Connection) -> Result Result>) -> Result<(), Er Ok(()) } -#[utoipa::path(put, path = "/users/user", responses( +#[utoipa::path(put, path = "/users/first", responses( (status = 200, description = "User values are set"), (status = 400, description = "The D-Bus service could not perform the action"), ))] @@ -167,87 +166,64 @@ async fn set_first_user( Ok(()) } -#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -pub struct RootPasswordSettings { - pub value: String, - pub encrypted: bool, -} - -#[utoipa::path(delete, path = "/users/root_password", responses( - (status = 200, description = "Removes the root password"), - (status = 400, description = "The D-Bus service could not perform the action"), -))] -async fn remove_root_password(State(state): State>) -> Result<(), Error> { - state.users.remove_root_password().await?; - Ok(()) -} - -#[utoipa::path(put, path = "/users/root_password", responses( - (status = 200, description = "Root password is set"), +#[utoipa::path(get, path = "/users/first", responses( + (status = 200, description = "Configuration for the first user", body = FirstUser), (status = 400, description = "The D-Bus service could not perform the action"), ))] -async fn set_root_password( - State(state): State>, - Json(config): Json, -) -> Result<(), Error> { - state - .users - .set_root_password(&config.value, config.encrypted) - .await?; - Ok(()) +async fn get_user_config(State(state): State>) -> Result, Error> { + Ok(Json(state.users.first_user().await?)) } -#[utoipa::path(delete, path = "/users/root_sshkey", responses( - (status = 200, description = "Removes the root SSH key"), - (status = 400, description = "The D-Bus service could not perform the action"), -))] -async fn remove_root_sshkey(State(state): State>) -> Result<(), Error> { - state.users.set_root_sshkey("").await?; - Ok(()) -} - -#[utoipa::path(put, path = "/users/root_sshkey", responses( - (status = 200, description = "Root SSH Key is set"), +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RootPatchSettings { + /// empty string here means remove ssh key for root + pub sshkey: Option, + /// empty string here means remove password for root + pub password: Option, + /// specify if patched password is provided in encrypted form + pub password_encrypted: Option, +} + +#[utoipa::path(patch, path = "/users/root", responses( + (status = 200, description = "Root configuration is modified", body = RootPatchSettings), (status = 400, description = "The D-Bus service could not perform the action"), ))] -async fn set_root_sshkey( +async fn patch_root( State(state): State>, - Json(key): Json, + Json(config): Json, ) -> Result<(), Error> { - state.users.set_root_sshkey(key.as_str()).await?; + if let Some(key) = config.sshkey { + state.users.set_root_sshkey(&key).await?; + } + if let Some(password) = config.password { + if password.is_empty() { + state.users.remove_root_password().await?; + } else { + state + .users + .set_root_password(&password, config.password_encrypted == Some(true)) + .await?; + } + } Ok(()) } #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -pub struct RootInfo { +pub struct RootConfig { + /// returns if password for root is set or not password: bool, - sshkey: Option, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -pub struct UsersInfo { - user: Option, - root: RootInfo, + /// empty string mean no sshkey is specified + sshkey: String, } -#[utoipa::path(put, path = "/users/config", responses( - (status = 200, description = "Configuration for users including root and the first user", body = UsersInfo), +#[utoipa::path(get, path = "/users/root", responses( + (status = 200, description = "Configuration for the root user", body = RootConfig), (status = 400, description = "The D-Bus service could not perform the action"), ))] -async fn get_config(State(state): State>) -> Result, Error> { - let mut result = UsersInfo::default(); - let first_user = state.users.first_user().await?; - if first_user.user_name.is_empty() { - result.user = None; - } else { - result.user = Some(first_user); - } - result.root.password = state.users.is_root_password().await?; - let ssh_key = state.users.root_ssh_key().await?; - if ssh_key.is_empty() { - result.root.sshkey = None; - } else { - result.root.sshkey = Some(ssh_key); - } - Ok(Json(result)) +async fn get_root_config(State(state): State>) -> Result, Error> { + let password = state.users.is_root_password().await?; + let sshkey = state.users.root_ssh_key().await?; + let config = RootConfig { password, sshkey }; + Ok(Json(config)) } diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 7d476344fb..8d625b0c7e 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -19,13 +19,11 @@ use utoipa::OpenApi; crate::manager::web::installer_status, crate::questions::web::list_questions, crate::questions::web::answer, - crate::users::web::get_config, + crate::users::web::get_root_config, + crate::users::web::get_user_config, crate::users::web::set_first_user, crate::users::web::remove_first_user, - crate::users::web::set_root_password, - crate::users::web::remove_root_password, - crate::users::web::set_root_sshkey, - crate::users::web::remove_root_sshkey, + crate::users::web::patch_root, super::http::ping, ), components( @@ -50,9 +48,8 @@ use utoipa::OpenApi; schemas(crate::questions::web::GenericAnswer), schemas(crate::questions::web::PasswordAnswer), schemas(agama_lib::users::FirstUser), - schemas(crate::users::web::RootPasswordSettings), - schemas(crate::users::web::RootInfo), - schemas(crate::users::web::UsersInfo), + schemas(crate::users::web::RootConfig), + schemas(crate::users::web::RootPatchSettings), schemas(super::http::PingResponse), ) )] diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 01776da513..3f23090659 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -26,7 +26,7 @@ pub enum Event { FirstUserChanged(FirstUser), RootChanged { password: Option, - sshkey: Option + sshkey: Option, }, // TODO: it should include the full software proposal or, at least, // all the relevant changes. From 50e20732c1be3e46cee516dfd398922dc52f9b44 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 5 Apr 2024 16:10:34 +0200 Subject: [PATCH 17/19] adapt UI code to new http api --- web/src/client/http.js | 33 ++++++++++++++++++++------------- web/src/client/users.js | 28 ++++++++++++++-------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/web/src/client/http.js b/web/src/client/http.js index bb9a242795..c8972ae5d0 100644 --- a/web/src/client/http.js +++ b/web/src/client/http.js @@ -142,29 +142,36 @@ class HTTPClient { }, }); - try { - return await response.json(); - } catch (e) { - console.warn("Expecting a JSON response", e); - return response.status === 200; - } + return response; } /** * @param {string} url - Endpoint URL (e.g., "/l10n/config"). - * @return {Promise} Server response. + * @return {Promise} Server response. */ async delete(url) { const response = await fetch(`${this.baseUrl}/${url}`, { method: "DELETE", }); - try { - return await response.json(); - } catch (e) { - console.warn("Expecting a JSON response", e); - return response.status === 200; - } + return response; + } + + /** + * @param {string} url - Endpoint URL (e.g., "/l10n/config"). + * @param {object} data - Data to submit + * @return {Promise} Server response. + */ + async patch(url, data) { + const response = await fetch(`${this.baseUrl}/${url}`, { + method: "PATCH", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }); + + return response; } /** diff --git a/web/src/client/users.js b/web/src/client/users.js index 73195af559..729bc08491 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -23,8 +23,6 @@ import { WithValidation } from "./mixins"; -const USERS_PATH = "/users/config"; - /** * @typedef {object} UserResult * @property {boolean} result - whether the action succeeded or not @@ -66,13 +64,13 @@ class UsersBaseClient { * @return {Promise} */ async getUser() { - const proxy = await this.client.get(USERS_PATH); + const user = await this.client.get("/users/first"); - if (proxy.user === null) { + if (user === null) { return { fullName: "", userName: "", password: "", autologin: false, data: {} }; } - return proxy.user; + return user; } /** @@ -81,8 +79,8 @@ class UsersBaseClient { * @return {Promise} */ async isRootPasswordSet() { - const proxy = await this.client.get(USERS_PATH); - return proxy.root.password; + const proxy = await this.client.get("/users/root"); + return proxy.password; } /** @@ -94,7 +92,7 @@ class UsersBaseClient { async setUser(user) { const result = await this.client.put("/users/user", user); - return { result, issues: [] }; // TODO: check how to handle issues and result. Maybe separate call to validate? + return { result: result.ok, issues: [] }; // TODO: check how to handle issues and result. Maybe separate call to validate? } /** @@ -103,7 +101,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeUser() { - return this.client.delete("/users/user"); + return (await this.client.delete("/users/user")).ok; } /** @@ -113,7 +111,8 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async setRootPassword(password) { - return this.client.put("/users/root_password", { value: password, encrypted: false }); + const response = await this.client.patch("/users/root", { password, password_encrypted: false }); + return response.ok; } /** @@ -122,7 +121,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeRootPassword() { - return this.client.delete("/users/root_password"); + return this.setRootPassword(""); } /** @@ -131,8 +130,8 @@ class UsersBaseClient { * @return {Promise} SSH public key or an empty string if it is not set */ async getRootSSHKey() { - const proxy = await this.client.get(USERS_PATH); - return proxy.root.ssh_key || ""; + const proxy = await this.client.get("/users/root"); + return proxy.sshkey; } /** @@ -142,7 +141,8 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async setRootSSHKey(key) { - return this.client.put("/users/root_sshkey", key); + const response = await this.client.patch("/users/root", { sshkey: key }); + return response.ok; } /** From 5bfad0462422595f2b3865d3cb2d85d6ad15a437 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Sun, 7 Apr 2024 21:36:47 +0200 Subject: [PATCH 18/19] fixes from testing --- web/src/client/http.js | 15 --------------- web/src/client/software.js | 4 ++-- web/src/client/users.js | 6 +++--- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/web/src/client/http.js b/web/src/client/http.js index 639223a212..844c6545a1 100644 --- a/web/src/client/http.js +++ b/web/src/client/http.js @@ -174,21 +174,6 @@ class HTTPClient { return response; } - /** - * @param {string} url - Endpoint URL (e.g., "/l10n/config"). - * @param {object} data - Data to submit - * @return {Promise} Server response. - */ - async patch(url, data) { - await fetch(`${this.baseUrl}/${url}`, { - method: "PATCH", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - }, - }); - } - /** * Registers a handler for a given type of events. * diff --git a/web/src/client/software.js b/web/src/client/software.js index eb4996b40d..e4f6989837 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -277,7 +277,7 @@ class SoftwareClient extends WithIssues( "/software/progress", SOFTWARE_SERVICE, ), - "software/issues/software", + "/software/issues/software", "/org/opensuse/Agama/Software1", ) {} @@ -334,6 +334,6 @@ class ProductBaseClient { } class ProductClient - extends WithIssues(ProductBaseClient, "software/issues/product", PRODUCT_PATH) {} + extends WithIssues(ProductBaseClient, "/software/issues/product", PRODUCT_PATH) {} export { ProductClient, SelectedBy, SoftwareClient }; diff --git a/web/src/client/users.js b/web/src/client/users.js index 729bc08491..ac570aad53 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -90,7 +90,7 @@ class UsersBaseClient { * @return {Promise} returns an object with the result and the issues found if error */ async setUser(user) { - const result = await this.client.put("/users/user", user); + const result = await this.client.put("/users/first", user); return { result: result.ok, issues: [] }; // TODO: check how to handle issues and result. Maybe separate call to validate? } @@ -101,7 +101,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeUser() { - return (await this.client.delete("/users/user")).ok; + return (await this.client.delete("/users/first")).ok; } /** @@ -175,6 +175,6 @@ class UsersBaseClient { /** * Client to interact with the Agama users service */ -class UsersClient extends WithValidation(UsersBaseClient, "users/validation", "/org/opensuse/Agama/Users1") { } +class UsersClient extends WithValidation(UsersBaseClient, "/users/validation", "/org/opensuse/Agama/Users1") { } export { UsersClient }; From c561a3319437569901085ec87dcc6c33c61d3368 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 8 Apr 2024 11:05:15 +0200 Subject: [PATCH 19/19] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa --- rust/agama-server/src/users/web.rs | 4 ++-- web/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 326469e84f..161d9a2bd5 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -38,7 +38,7 @@ pub async fn users_streams( const ROOT_SSHKEY_ID: &str = "root_sshkey"; // here we have three streams, but only two events. Reason is // that we have three streams from dbus about property change - // and munify two root user properties into single event to http API + // and unify two root user properties into single event to http API let result: Vec<(&str, Pin + Send>>)> = vec![ ( FIRST_USER_ID, @@ -155,7 +155,7 @@ async fn remove_first_user(State(state): State>) -> Result<(), Er } #[utoipa::path(put, path = "/users/first", responses( - (status = 200, description = "User values are set"), + (status = 200, description = "Sets the first user"), (status = 400, description = "The D-Bus service could not perform the action"), ))] async fn set_first_user( diff --git a/web/README.md b/web/README.md index 94ba63a57a..746d1012e6 100644 --- a/web/README.md +++ b/web/README.md @@ -40,7 +40,7 @@ Example of running from different machine: ``` # backend machine # using ip of machine instead of localhost is important to be network accessible - agama-web-server serve --address 10.100.1.1:3030 + agama-web-server serve --address 10.100.1.1:3000 # frontend machine # ESLINT=0 is useful to ignore linter problems during development