Skip to content

Commit

Permalink
Adapt the users service to the HTTP/JSON API (#1117)
Browse files Browse the repository at this point in the history
## Problem

Users section in architecture_2024 is disabled


## Solution

Readd it using http API.


## Testing

- *Tested manually*

Only issue found during testing is that some put/delete request does not
respond body in this PR and js part complain as it always expect json.


## Screenshots



![new_users_overview](https://github.com/openSUSE/agama/assets/478871/f5eac20a-fd93-4ae3-8bd3-12cbafa2fff6)

![new_users](https://github.com/openSUSE/agama/assets/478871/3152560b-abdc-411b-8015-5f5ff375adc4)
  • Loading branch information
jreidinger authored Apr 8, 2024
2 parents 3a8c46a + c561a33 commit 5e1d4b2
Show file tree
Hide file tree
Showing 18 changed files with 482 additions and 82 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>;
}
2 changes: 1 addition & 1 deletion rust/agama-lib/src/users.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Implements support for handling the users settings

mod client;
mod proxies;
pub mod proxies;
mod settings;
mod store;

Expand Down
14 changes: 12 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,12 @@
use super::proxies::{FirstUser as FirstUserFromDBus, Users1Proxy};
use crate::error::ServiceError;
use agama_settings::{settings::Settings, SettingValue, SettingsError};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use zbus::Connection;

/// Represents the settings for the first user
#[derive(Serialize, Debug, Default)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FirstUser {
/// First user's full name
pub full_name: String,
Expand Down Expand Up @@ -66,6 +67,7 @@ impl Settings for FirstUser {
}

/// D-Bus client for the users service
#[derive(Clone)]
pub struct UsersClient<'a> {
users_proxy: Users1Proxy<'a>,
}
Expand All @@ -91,6 +93,10 @@ impl<'a> UsersClient<'a> {
Ok(self.users_proxy.set_root_password(value, encrypted).await?)
}

pub async fn remove_root_password(&self) -> Result<u32, ServiceError> {
Ok(self.users_proxy.remove_root_password().await?)
}

/// Whether the root password is set or not
pub async fn is_root_password(&self) -> Result<bool, ServiceError> {
Ok(self.users_proxy.root_password_set().await?)
Expand Down Expand Up @@ -121,4 +127,8 @@ impl<'a> UsersClient<'a> {
)
.await
}

pub async fn remove_first_user(&self) -> zbus::Result<bool> {
Ok(self.users_proxy.remove_first_user().await? == 0)
}
}
2 changes: 1 addition & 1 deletion rust/agama-lib/src/users/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
#[serde(rename_all = "camelCase")]
pub struct FirstUserSettings {
/// First user's full name
Expand Down
1 change: 1 addition & 0 deletions rust/agama-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ pub mod manager;
pub mod network;
pub mod questions;
pub mod software;
pub mod users;
pub mod web;
pub use web::service;
2 changes: 2 additions & 0 deletions rust/agama-server/src/users.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod web;
pub use web::{users_service, users_streams};
229 changes: 229 additions & 0 deletions rust/agama-server/src/users/web.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//!
//! 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 crate::{
error::Error,
web::{
common::{service_status_router, validation_router},
Event,
},
};
use agama_lib::{
error::ServiceError,
users::{proxies::Users1Proxy, FirstUser, UsersClient},
};
use axum::{extract::State, routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use tokio_stream::{Stream, StreamExt};

#[derive(Clone)]
struct UsersState<'a> {
users: UsersClient<'a>,
}

/// 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.
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";
// here we have three streams, but only two events. Reason is
// that we have three streams from dbus about property change
// and unify two root user properties into single event to http API
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,
) -> Result<impl Stream<Item = Event> + Send, 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 = FirstUser {
full_name: user.0,
user_name: user.1,
password: user.2,
autologin: user.3,
data: user.4,
};
return Some(Event::FirstUserChanged(user_struct));
}
None
})
.filter_map(|e| e);
Ok(stream)
}

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::RootChanged {
password: Some(is_set),
sshkey: None,
});
}
None
})
.filter_map(|e| e);
Ok(stream)
}

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 {
return Some(Event::RootChanged {
password: None,
sshkey: Some(key),
});
}
None
})
.filter_map(|e| e);
Ok(stream)
}

/// Sets up and returns the axum service for the users module.
pub async fn users_service(dbus: zbus::Connection) -> Result<Router, ServiceError> {
const DBUS_SERVICE: &str = "org.opensuse.Agama.Manager1";
const DBUS_PATH: &str = "/org/opensuse/Agama/Users1";

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(
"/first",
get(get_user_config)
.put(set_first_user)
.delete(remove_first_user),
)
.route("/root", get(get_root_config).patch(patch_root))
.merge(validation_router)
.merge(status_router)
.with_state(state);
Ok(router)
}

/// Removes the first user settings
#[utoipa::path(delete, path = "/users/first", 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/first", responses(
(status = 200, description = "Sets the first user"),
(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.users.set_first_user(&config).await?;
Ok(())
}

#[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 get_user_config(State(state): State<UsersState<'_>>) -> Result<Json<FirstUser>, Error> {
Ok(Json(state.users.first_user().await?))
}

#[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<String>,
/// empty string here means remove password for root
pub password: Option<String>,
/// specify if patched password is provided in encrypted form
pub password_encrypted: Option<bool>,
}

#[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 patch_root(
State(state): State<UsersState<'_>>,
Json(config): Json<RootPatchSettings>,
) -> Result<(), Error> {
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 RootConfig {
/// returns if password for root is set or not
password: bool,
/// empty string mean no sshkey is specified
sshkey: String,
}

#[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_root_config(State(state): State<UsersState<'_>>) -> Result<Json<RootConfig>, 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))
}
8 changes: 6 additions & 2 deletions rust/agama-server/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, users_streams},
web::common::{issues_stream, progress_stream, service_status_stream},
};
use axum::Router;
Expand Down Expand Up @@ -61,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)
Expand Down Expand Up @@ -105,7 +107,9 @@ 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, user_stream);
}
stream.insert("software", software_stream(dbus.clone()).await?);
stream.insert(
"software-status",
Expand Down
Loading

0 comments on commit 5e1d4b2

Please sign in to comment.