From 13163847d3862c8887029da8f7595cf1e63135cc Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 27 Mar 2024 23:06:27 +0100 Subject: [PATCH] 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;