Skip to content

Commit

Permalink
Add validation router and use it in users
Browse files Browse the repository at this point in the history
  • Loading branch information
jreidinger committed Mar 27, 2024
1 parent 9987a22 commit 1316384
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 45 deletions.
11 changes: 11 additions & 0 deletions rust/agama-lib/src/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,14 @@ trait Issues {
#[dbus_proxy(property)]
fn all(&self) -> zbus::Result<Vec<(String, String, u32, u32)>>;
}

#[dbus_proxy(interface = "org.opensuse.Agama1.Validation", assume_defaults = true)]
trait Validation {
/// Errors property
#[dbus_proxy(property)]
fn errors(&self) -> zbus::Result<Vec<String>>;

/// Valid property
#[dbus_proxy(property)]
fn valid(&self) -> zbus::Result<bool>;
}
4 changes: 2 additions & 2 deletions rust/agama-lib/src/users/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
134 changes: 94 additions & 40 deletions rust/agama-server/src/users/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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> {
Expand All @@ -36,19 +37,29 @@ struct UsersState<'a> {
pub async fn users_streams(
dbus: zbus::Connection,
) -> Result<Vec<(&'static str, Pin<Box<dyn Stream<Item = Event> + 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<Box<dyn Stream<Item = Event> + 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<Box<dyn Stream<Item = Event> + 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<impl Stream<Item = Event> + Send, Error> {
let proxy = Users1Proxy::new(&dbus).await?;
let stream = proxy
Expand All @@ -70,35 +81,35 @@ 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<impl Stream<Item = Event> + Send, 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 });
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,
async fn root_ssh_key_changed_stream(
dbus: zbus::Connection,
) -> Result<impl Stream<Item = Event> + Send, 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)
};
let value = if key.is_empty() { None } else { Some(key) };
return Some(Event::RootSSHKeyChanged { key: value });
}
None
Expand All @@ -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<Router, ServiceError> {
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<UsersState<'_>>) -> 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<UsersState<'_>>,
Json(config) : Json<FirstUser>
) -> Result<(), Error> {
State(state): State<UsersState<'_>>,
Json(config): Json<FirstUser>,
) -> Result<(), Error> {
state.users.set_first_user(&config).await?;
Ok(())
}
Expand All @@ -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<UsersState<'_>>) -> 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<UsersState<'_>>,
Json(config) : Json<RootPasswordSettings>
Json(config): Json<RootPasswordSettings>,
) -> 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<UsersState<'_>>) -> 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<UsersState<'_>>,
Json(key) : Json<String>
Json(key): Json<String>,
) -> 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)]
Expand All @@ -178,8 +229,11 @@ pub struct UsersInfo {
root: RootInfo,
}

async fn get_info(State(state): State<UsersState<'_>>)
-> Result<Json<UsersInfo>, 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<UsersState<'_>>) -> Result<Json<UsersInfo>, Error> {
let mut result = UsersInfo::default();
let first_user = state.users.first_user().await?;
if first_user.user_name.is_empty() {
Expand Down
105 changes: 104 additions & 1 deletion rust/agama-server/src/web/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<HelloWorldState>) {};
///
/// #[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<HelloWorldState> = 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<T>(
dbus: &zbus::Connection,
destination: &str,
path: &str,
) -> Result<Router<T>, 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<String>,
}

async fn validation(
State(state): State<ValidationState<'_>>,
) -> Result<Json<ValidationResult>, 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<Pin<Box<dyn Stream<Item = Event> + 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<ValidationProxy<'a>, zbus::Error> {
let proxy = ValidationProxy::builder(dbus)
.destination(destination.to_string())?
.path(path.to_string())?
.build()
.await?;
Ok(proxy)
}
Loading

0 comments on commit 1316384

Please sign in to comment.