From f829cc69816a37ecd1fd80bdbb6987c56face4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 16 Feb 2024 12:29:07 +0000 Subject: [PATCH 01/22] rust: add support for service configuration --- rust/agama-dbus-server/Cargo.toml | 2 ++ .../share/server-example.yaml | 2 ++ rust/agama-dbus-server/src/web/service.rs | 35 ++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 rust/agama-dbus-server/share/server-example.yaml diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 326394ffc9..aaaeb156c1 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -36,6 +36,8 @@ tracing = "0.1.40" clap = { version = "4.5.0", features = ["derive", "wrap_help"] } tower = "0.4.13" utoipa = { version = "4.2.0", features = ["axum_extras"] } +config = "0.14.0" +rand = "0.8.5" [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-dbus-server/share/server-example.yaml b/rust/agama-dbus-server/share/server-example.yaml new file mode 100644 index 0000000000..d0d8c4f399 --- /dev/null +++ b/rust/agama-dbus-server/share/server-example.yaml @@ -0,0 +1,2 @@ +--- +jwt_key: "replace-me" diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index 01dac81b28..31789b76ae 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -1,9 +1,16 @@ use axum::{routing::get, Router}; +use config::{Config, ConfigError, File}; +use rand::distributions::{Alphanumeric, DistString}; +use serde::Deserialize; use tower_http::trace::TraceLayer; /// Returns a service that implements the web-based Agama API. pub fn service(dbus_connection: zbus::Connection) -> Router { - let state = ServiceState { dbus_connection }; + let config = ServiceConfig::load().unwrap(); + let state = ServiceState { + config, + dbus_connection, + }; Router::new() .route("/ping", get(super::http::ping)) .route("/ws", get(super::ws::ws_handler)) @@ -11,7 +18,33 @@ pub fn service(dbus_connection: zbus::Connection) -> Router { .with_state(state) } +/// Web service state. +/// +/// It holds the service configuration and the current D-Bus connection. #[derive(Clone)] pub struct ServiceState { + pub config: ServiceConfig, pub dbus_connection: zbus::Connection, } + +/// Web service configuration. +#[derive(Clone, Debug, Deserialize)] +pub struct ServiceConfig { + /// Key to sign the JSON Web Tokens. + pub jwt_key: String, +} + +impl ServiceConfig { + pub fn load() -> Result { + const JWT_SIZE: usize = 30; + let jwt_key: String = Alphanumeric.sample_string(&mut rand::thread_rng(), JWT_SIZE); + + let config = Config::builder() + .set_default("jwt_key", jwt_key)? + .add_source(File::with_name("/usr/etc/agama.d/server").required(false)) + .add_source(File::with_name("/etc/agama.d/server").required(false)) + .add_source(File::with_name("agama-dbus-server/share/server.yaml").required(false)) + .build()?; + config.try_deserialize() + } +} From e60cc3e7b43fbe30f515c7d7c2ebaa4b36c1771f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 16 Feb 2024 13:27:30 +0000 Subject: [PATCH 02/22] rust: generate a JWT * It does not perform any kind of authentication yet. * It does not include any information to identify the client. --- rust/Cargo.lock | 377 +++++++++++++++++++++- rust/agama-dbus-server/Cargo.toml | 1 + rust/agama-dbus-server/src/web/http.rs | 30 +- rust/agama-dbus-server/src/web/service.rs | 6 +- rust/agama-dbus-server/tests/service.rs | 20 +- 5 files changed, 427 insertions(+), 7 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3e7b177d68..94a1bb3fdc 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -52,11 +52,14 @@ dependencies = [ "axum", "cidr", "clap", + "config", "gettext-rs", "http-body-util", + "jsonwebtoken", "log", "macaddr", "once_cell", + "rand", "regex", "serde", "serde_json", @@ -474,6 +477,9 @@ name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -506,6 +512,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "bumpalo" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" + [[package]] name = "bytecount" version = "0.6.7" @@ -635,6 +647,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "console" version = "0.15.7" @@ -648,6 +680,26 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -684,6 +736,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -761,6 +819,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1009,8 +1076,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1058,6 +1127,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1189,7 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", "serde", ] @@ -1241,6 +1316,26 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonschema" version = "0.16.1" @@ -1268,6 +1363,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1310,6 +1420,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1643,6 +1759,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -1697,12 +1823,73 @@ dependencies = [ "regex", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.7", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pest_meta" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.2" @@ -1845,7 +2032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -1968,6 +2155,42 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2080,6 +2303,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2145,6 +2377,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "simplelog" version = "0.12.1" @@ -2197,6 +2441,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2352,6 +2602,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2434,11 +2693,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.6", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2448,7 +2722,20 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.28", +] + +[[package]] +name = "toml_edit" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.1", ] [[package]] @@ -2590,6 +2877,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uds_windows" version = "1.1.0" @@ -2640,6 +2933,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -2734,6 +3033,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + [[package]] name = "winapi" version = "0.3.9" @@ -2972,6 +3325,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d90f4e0f530c4c69f62b80d839e9ef3855edc9cba471a160c4d692deed62b401" +dependencies = [ + "memchr", +] + [[package]] name = "xdg-home" version = "1.0.0" @@ -2982,6 +3344,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zbus" version = "3.14.1" diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index aaaeb156c1..1fa667c7c3 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -38,6 +38,7 @@ tower = "0.4.13" utoipa = { version = "4.2.0", features = ["axum_extras"] } config = "0.14.0" rand = "0.8.5" +jsonwebtoken = "9.2.0" [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index 9635638bb9..2e0c547bab 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -1,9 +1,12 @@ //! Implements the handlers for the HTTP-based API. -use axum::Json; -use serde::Serialize; +use axum::{extract::State, Json}; +use jsonwebtoken::{EncodingKey, Header}; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::service::ServiceState; + #[derive(Serialize, ToSchema)] pub struct PingResponse { /// API status @@ -18,3 +21,26 @@ pub async fn ping() -> Json { status: "success".to_string(), }) } + +#[derive(Debug, Default, Serialize, Deserialize)] +struct Claims { + exp: usize, +} + +#[derive(Serialize)] +pub struct Auth { + token: String, +} + +pub async fn authenticate(State(state): State) -> Json { + let claims = Claims { exp: 3600 }; + + let secret = &state.config.jwt_key; + let token = jsonwebtoken::encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_ref()), + ) + .unwrap(); + Json(Auth { token }) +} diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index 31789b76ae..4ca478afa2 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -1,4 +1,7 @@ -use axum::{routing::get, Router}; +use axum::{ + routing::{get, post}, + Router, +}; use config::{Config, ConfigError, File}; use rand::distributions::{Alphanumeric, DistString}; use serde::Deserialize; @@ -14,6 +17,7 @@ pub fn service(dbus_connection: zbus::Connection) -> Router { Router::new() .route("/ping", get(super::http::ping)) .route("/ws", get(super::ws::ws_handler)) + .route("/authenticate", post(super::http::authenticate)) .layer(TraceLayer::new_for_http()) .with_state(state) } diff --git a/rust/agama-dbus-server/tests/service.rs b/rust/agama-dbus-server/tests/service.rs index 93e60e42fe..407ef86ebb 100644 --- a/rust/agama-dbus-server/tests/service.rs +++ b/rust/agama-dbus-server/tests/service.rs @@ -4,7 +4,7 @@ use self::common::DBusServer; use agama_dbus_server::service; use axum::{ body::Body, - http::{Request, StatusCode}, + http::{Method, Request, StatusCode}, }; use http_body_util::BodyExt; use std::error::Error; @@ -29,3 +29,21 @@ async fn test_ping() -> Result<(), Box> { assert_eq!(&body, "{\"status\":\"success\"}"); Ok(()) } + +#[test] +async fn test_authenticate() -> Result<(), Box> { + let dbus_server = DBusServer::new().start().await?; + let web_server = service(dbus_server.connection()); + let request = Request::builder() + .uri("/authenticate") + .method(Method::POST) + .body(Body::empty()) + .unwrap(); + + let response = web_server.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = body_to_string(response.into_body()).await; + assert!(body.starts_with("{\"token\":")); + Ok(()) +} From fad373cc24b5ff4fd6f7786e4690d8cbd510f482 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 19 Feb 2024 08:55:19 +0000 Subject: [PATCH 03/22] Added pam dependency --- rust/agama-dbus-server/Cargo.toml | 1 + rust/agama-dbus-server/src/web/http.rs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 1fa667c7c3..cfdf5547d0 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -39,6 +39,7 @@ utoipa = { version = "4.2.0", features = ["axum_extras"] } config = "0.14.0" rand = "0.8.5" jsonwebtoken = "9.2.0" +pam = "0.8.0" [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index 2e0c547bab..877acc738d 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -2,6 +2,7 @@ use axum::{extract::State, Json}; use jsonwebtoken::{EncodingKey, Header}; +use pam::Client; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -32,7 +33,14 @@ pub struct Auth { token: String, } -pub async fn authenticate(State(state): State) -> Json { +pub async fn authenticate( + State(state): State, + Json(password): Json, +) -> Json { + let mut client = Client::with_password("cockpit").expect("failed to open PAM!"); + client.conversation_mut().set_credentials("root", password); + client.authenticate().expect("failed authentication!"); + let claims = Claims { exp: 3600 }; let secret = &state.config.jwt_key; From bd53abf53318d5b9e3b368743d92b1da09ef7806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 19 Feb 2024 14:44:57 +0000 Subject: [PATCH 04/22] rust: validate the JWT token * Add a configuration mechanism. * Split the code into smaller and more focused modules. --- rust/Cargo.lock | 231 +++++++++++++++++++++- rust/agama-dbus-server/Cargo.toml | 7 + rust/agama-dbus-server/src/web.rs | 3 + rust/agama-dbus-server/src/web/auth.rs | 85 ++++++++ rust/agama-dbus-server/src/web/config.rs | 38 ++++ rust/agama-dbus-server/src/web/http.rs | 42 ++-- rust/agama-dbus-server/src/web/service.rs | 40 +--- rust/agama-dbus-server/src/web/state.rs | 12 ++ rust/agama-dbus-server/src/web/ws.rs | 2 +- 9 files changed, 400 insertions(+), 60 deletions(-) create mode 100644 rust/agama-dbus-server/src/web/auth.rs create mode 100644 rust/agama-dbus-server/src/web/config.rs create mode 100644 rust/agama-dbus-server/src/web/state.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 94a1bb3fdc..d79e816f4c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -50,6 +50,8 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-extra", + "chrono", "cidr", "clap", "config", @@ -59,6 +61,7 @@ dependencies = [ "log", "macaddr", "once_cell", + "pam", "rand", "regex", "serde", @@ -155,6 +158,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.11" @@ -424,6 +442,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -451,6 +491,26 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.4.1", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -545,6 +605,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -553,11 +622,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "windows-targets 0.52.0", ] [[package]] @@ -591,6 +663,16 @@ dependencies = [ "serde", ] +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", +] + [[package]] name = "clap" version = "4.5.0" @@ -709,6 +791,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -828,6 +916,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1108,6 +1202,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.4.2" @@ -1139,6 +1239,30 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1247,6 +1371,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1310,6 +1457,15 @@ dependencies = [ "nom", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -1384,6 +1540,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.151" @@ -1785,6 +1947,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pam" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba" +dependencies = [ + "libc", + "memchr", + "pam-macros", + "pam-sys", + "users", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b" +dependencies = [ + "bindgen", + "libc", +] + [[package]] name = "parking" version = "2.2.0" @@ -2197,6 +2393,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.37.27" @@ -2368,6 +2570,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2950,6 +3158,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -3118,6 +3336,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index cfdf5547d0..85f8753a9e 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -39,6 +39,13 @@ utoipa = { version = "4.2.0", features = ["axum_extras"] } config = "0.14.0" rand = "0.8.5" jsonwebtoken = "9.2.0" +axum-extra = { version = "0.9.2", features = ["typed-header"] } +chrono = { version = "0.4.34", default-features = false, features = [ + "now", + "std", + "alloc", + "clock", +] } pam = "0.8.0" [[bin]] diff --git a/rust/agama-dbus-server/src/web.rs b/rust/agama-dbus-server/src/web.rs index cfc44ef4d4..babc00766a 100644 --- a/rust/agama-dbus-server/src/web.rs +++ b/rust/agama-dbus-server/src/web.rs @@ -4,9 +4,12 @@ //! * Emit relevant events via websocket. //! * Serve the code for the web user interface (not implemented yet). +mod auth; +mod config; mod docs; mod http; mod service; +mod state; mod ws; pub use docs::ApiDoc; diff --git a/rust/agama-dbus-server/src/web/auth.rs b/rust/agama-dbus-server/src/web/auth.rs new file mode 100644 index 0000000000..57bab8e559 --- /dev/null +++ b/rust/agama-dbus-server/src/web/auth.rs @@ -0,0 +1,85 @@ +//! Contains the code to handle access authorization. + +use super::state::ServiceState; +use async_trait::async_trait; +use axum::{ + extract::FromRequestParts, + http::{request, StatusCode}, + response::{IntoResponse, Response}, + RequestPartsExt, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Represents an authentication error. +#[derive(Error, Debug)] +pub enum AuthError { + /// The authentication error is not included in the headers. + #[error("Missing authentication token")] + MissingToken, + /// The authentication error is invalid. + #[error("Invalid authentication token: {0}")] + InvalidToken(#[from] jsonwebtoken::errors::Error), +} + +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } +} + +/// Claims that are included in the token. +/// +/// See https://datatracker.ietf.org/doc/html/rfc7519 for reference. +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenClaims { + exp: i64, +} + +impl Default for TokenClaims { + fn default() -> Self { + let exp = Utc::now() + Duration::days(1); + Self { + exp: exp.timestamp(), + } + } +} + +#[async_trait] +impl FromRequestParts for TokenClaims { + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut request::Parts, + state: &ServiceState, + ) -> Result { + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|_| AuthError::MissingToken)?; + + let decoding = DecodingKey::from_secret(state.config.jwt_secret.as_ref()); + let token_data = jsonwebtoken::decode(bearer.token(), &decoding, &Validation::default())?; + + Ok(token_data.claims) + } +} + +/// Generates a JWT. +/// +/// - `secret`: secret to encrypt/sign the token. +pub fn generate_token(secret: &str) -> String { + let claims = TokenClaims::default(); + jsonwebtoken::encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_ref()), + ) + .unwrap() +} diff --git a/rust/agama-dbus-server/src/web/config.rs b/rust/agama-dbus-server/src/web/config.rs new file mode 100644 index 0000000000..4f6e9a5ff7 --- /dev/null +++ b/rust/agama-dbus-server/src/web/config.rs @@ -0,0 +1,38 @@ +//! Handles Agama web server configuration. +//! +//! The configuration can be written in YAML or JSON formats, although we plan to choose just one +//! of them in the future. It is read from the following locations: +//! +//! * `/usr/etc/agama.d/server.{json/yaml}` +//! * `/etc/agama.d/server.{json/yaml}` +//! * `./agama-dbus-server/share/server.{json/yaml}` +//! +//! All the settings are merged into a single configuration. The values in the latter locations +//! take precedence. + +use config::{Config, ConfigError, File}; +use rand::distributions::{Alphanumeric, DistString}; +use serde::Deserialize; + +/// Web service configuration. +#[derive(Clone, Debug, Deserialize)] +pub struct ServiceConfig { + /// Key to sign the JSON Web Tokens. + pub jwt_secret: String, +} + +impl ServiceConfig { + pub fn load() -> Result { + const JWT_SECRET_SIZE: usize = 30; + let jwt_secret: String = + Alphanumeric.sample_string(&mut rand::thread_rng(), JWT_SECRET_SIZE); + + let config = Config::builder() + .set_default("jwt_secret", jwt_secret)? + .add_source(File::with_name("/usr/etc/agama.d/server").required(false)) + .add_source(File::with_name("/etc/agama.d/server").required(false)) + .add_source(File::with_name("agama-dbus-server/share/server").required(false)) + .build()?; + config.try_deserialize() + } +} diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index 877acc738d..489cee33db 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -1,13 +1,11 @@ //! Implements the handlers for the HTTP-based API. +use super::{auth::generate_token, state::ServiceState}; use axum::{extract::State, Json}; -use jsonwebtoken::{EncodingKey, Header}; use pam::Client; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use utoipa::ToSchema; -use super::service::ServiceState; - #[derive(Serialize, ToSchema)] pub struct PingResponse { /// API status @@ -23,32 +21,30 @@ pub async fn ping() -> Json { }) } -#[derive(Debug, Default, Serialize, Deserialize)] -struct Claims { - exp: usize, +// TODO: remove this route (as it is just for teting) as soon as we have a legit protected one +pub async fn protected() -> String { + "OK".to_string() } #[derive(Serialize)] -pub struct Auth { +pub struct AuthResponse { + /// Bearer token to use on subsequent calls token: String, } +#[utoipa::path(get, path = "/authenticate", responses( + (status = 200, description = "The user have been successfully authenticated", body = AuthResponse) +))] pub async fn authenticate( State(state): State, Json(password): Json, -) -> Json { - let mut client = Client::with_password("cockpit").expect("failed to open PAM!"); - client.conversation_mut().set_credentials("root", password); - client.authenticate().expect("failed authentication!"); - - let claims = Claims { exp: 3600 }; - - let secret = &state.config.jwt_key; - let token = jsonwebtoken::encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_ref()), - ) - .unwrap(); - Json(Auth { token }) +) -> Json { + let mut pam_client = Client::with_password("cockpit").expect("failed to open PAM!"); + pam_client + .conversation_mut() + .set_credentials("root", password); + pam_client.authenticate().expect("failed authentication!"); + + let token = generate_token(&state.config.jwt_secret); + Json(AuthResponse { token }) } diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index 4ca478afa2..2c9168d58d 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -1,10 +1,9 @@ +use super::{auth::TokenClaims, config::ServiceConfig, state::ServiceState}; use axum::{ + middleware, routing::{get, post}, Router, }; -use config::{Config, ConfigError, File}; -use rand::distributions::{Alphanumeric, DistString}; -use serde::Deserialize; use tower_http::trace::TraceLayer; /// Returns a service that implements the web-based Agama API. @@ -15,40 +14,13 @@ pub fn service(dbus_connection: zbus::Connection) -> Router { dbus_connection, }; Router::new() + .route("/protected", get(super::http::protected)) + .route_layer(middleware::from_extractor_with_state::( + state.clone(), + )) .route("/ping", get(super::http::ping)) .route("/ws", get(super::ws::ws_handler)) .route("/authenticate", post(super::http::authenticate)) .layer(TraceLayer::new_for_http()) .with_state(state) } - -/// Web service state. -/// -/// It holds the service configuration and the current D-Bus connection. -#[derive(Clone)] -pub struct ServiceState { - pub config: ServiceConfig, - pub dbus_connection: zbus::Connection, -} - -/// Web service configuration. -#[derive(Clone, Debug, Deserialize)] -pub struct ServiceConfig { - /// Key to sign the JSON Web Tokens. - pub jwt_key: String, -} - -impl ServiceConfig { - pub fn load() -> Result { - const JWT_SIZE: usize = 30; - let jwt_key: String = Alphanumeric.sample_string(&mut rand::thread_rng(), JWT_SIZE); - - let config = Config::builder() - .set_default("jwt_key", jwt_key)? - .add_source(File::with_name("/usr/etc/agama.d/server").required(false)) - .add_source(File::with_name("/etc/agama.d/server").required(false)) - .add_source(File::with_name("agama-dbus-server/share/server.yaml").required(false)) - .build()?; - config.try_deserialize() - } -} diff --git a/rust/agama-dbus-server/src/web/state.rs b/rust/agama-dbus-server/src/web/state.rs new file mode 100644 index 0000000000..49e16c4e22 --- /dev/null +++ b/rust/agama-dbus-server/src/web/state.rs @@ -0,0 +1,12 @@ +//! Implements the web service state. + +use super::config::ServiceConfig; + +/// Web service state. +/// +/// It holds the service configuration and the current D-Bus connection. +#[derive(Clone)] +pub struct ServiceState { + pub config: ServiceConfig, + pub dbus_connection: zbus::Connection, +} diff --git a/rust/agama-dbus-server/src/web/ws.rs b/rust/agama-dbus-server/src/web/ws.rs index c1d3cb2770..3cfd9061c1 100644 --- a/rust/agama-dbus-server/src/web/ws.rs +++ b/rust/agama-dbus-server/src/web/ws.rs @@ -1,6 +1,6 @@ //! Implements the websocket handling. -use super::service::ServiceState; +use super::state::ServiceState; use agama_lib::progress::{Progress, ProgressMonitor, ProgressPresenter}; use async_trait::async_trait; use axum::{ From d0c6c17ca0b6e3d57ef407357ef4fcb5ac619b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 19 Feb 2024 15:22:10 +0000 Subject: [PATCH 05/22] Add clang-devel to the Rust CI workflow --- .github/workflows/ci-rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index a4576120bd..4757f5b974 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -69,6 +69,7 @@ jobs: - name: Install required packages run: zypper --non-interactive install + clang-devel jq libopenssl-3-devel openssl-3 From 31fef63403efc9890caf7b78dfdba0674a156c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 19 Feb 2024 16:03:03 +0000 Subject: [PATCH 06/22] rust: improve failed authentication handling --- rust/agama-dbus-server/src/web/auth.rs | 12 ++++++++++-- rust/agama-dbus-server/src/web/http.rs | 25 +++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/rust/agama-dbus-server/src/web/auth.rs b/rust/agama-dbus-server/src/web/auth.rs index 57bab8e559..5674bdc753 100644 --- a/rust/agama-dbus-server/src/web/auth.rs +++ b/rust/agama-dbus-server/src/web/auth.rs @@ -6,7 +6,7 @@ use axum::{ extract::FromRequestParts, http::{request, StatusCode}, response::{IntoResponse, Response}, - RequestPartsExt, + Json, RequestPartsExt, }; use axum_extra::{ headers::{authorization::Bearer, Authorization}, @@ -14,7 +14,9 @@ use axum_extra::{ }; use chrono::{Duration, Utc}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; +use pam::PamError; use serde::{Deserialize, Serialize}; +use serde_json::json; use thiserror::Error; /// Represents an authentication error. @@ -26,11 +28,17 @@ pub enum AuthError { /// The authentication error is invalid. #[error("Invalid authentication token: {0}")] InvalidToken(#[from] jsonwebtoken::errors::Error), + /// The authentication failed (most probably the password is wrong) + #[error("Authentication via PAM failed: {0}")] + Failed(#[from] PamError), } impl IntoResponse for AuthError { fn into_response(self) -> Response { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() + let body = json!({ + "error": self.to_string() + }); + (StatusCode::BAD_REQUEST, Json(body)).into_response() } } diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index 489cee33db..9126049a38 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -1,9 +1,12 @@ //! Implements the handlers for the HTTP-based API. -use super::{auth::generate_token, state::ServiceState}; +use super::{ + auth::{generate_token, AuthError}, + state::ServiceState, +}; use axum::{extract::State, Json}; use pam::Client; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, ToSchema)] @@ -32,19 +35,25 @@ pub struct AuthResponse { token: String, } +#[derive(Deserialize)] +pub struct LoginRequest { + /// User password + pub password: String, +} + #[utoipa::path(get, path = "/authenticate", responses( (status = 200, description = "The user have been successfully authenticated", body = AuthResponse) ))] pub async fn authenticate( State(state): State, - Json(password): Json, -) -> Json { - let mut pam_client = Client::with_password("cockpit").expect("failed to open PAM!"); + Json(login): Json, +) -> Result, AuthError> { + let mut pam_client = Client::with_password("cockpit")?; pam_client .conversation_mut() - .set_credentials("root", password); - pam_client.authenticate().expect("failed authentication!"); + .set_credentials("root", login.password); + pam_client.authenticate()?; let token = generate_token(&state.config.jwt_secret); - Json(AuthResponse { token }) + Ok(Json(AuthResponse { token })) } From 8ff2b32e082e35798500511671eb978115131d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 07:05:50 +0000 Subject: [PATCH 07/22] rust: inject the config into the web service --- rust/agama-dbus-server/src/agama-web-server.rs | 4 +++- rust/agama-dbus-server/src/web.rs | 2 ++ rust/agama-dbus-server/src/web/config.rs | 8 ++++++++ rust/agama-dbus-server/src/web/service.rs | 3 +-- rust/agama-dbus-server/tests/service.rs | 17 +++++++++++------ 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/rust/agama-dbus-server/src/agama-web-server.rs b/rust/agama-dbus-server/src/agama-web-server.rs index 4e45bfacfd..9df6dfac0c 100644 --- a/rust/agama-dbus-server/src/agama-web-server.rs +++ b/rust/agama-dbus-server/src/agama-web-server.rs @@ -36,7 +36,9 @@ async fn serve_command(address: &str) { .unwrap_or_else(|_| panic!("could not listen on {}", address)); let dbus_connection = connection().await.unwrap(); - axum::serve(listener, web::service(dbus_connection)) + let config = web::ServiceConfig::load().unwrap(); + let service = web::service(config, dbus_connection); + axum::serve(listener, service) .await .expect("could not mount app on listener"); } diff --git a/rust/agama-dbus-server/src/web.rs b/rust/agama-dbus-server/src/web.rs index babc00766a..b3e1490f9b 100644 --- a/rust/agama-dbus-server/src/web.rs +++ b/rust/agama-dbus-server/src/web.rs @@ -12,5 +12,7 @@ mod service; mod state; mod ws; +pub use auth::generate_token; +pub use config::ServiceConfig; pub use docs::ApiDoc; pub use service::service; diff --git a/rust/agama-dbus-server/src/web/config.rs b/rust/agama-dbus-server/src/web/config.rs index 4f6e9a5ff7..0292d6d361 100644 --- a/rust/agama-dbus-server/src/web/config.rs +++ b/rust/agama-dbus-server/src/web/config.rs @@ -36,3 +36,11 @@ impl ServiceConfig { config.try_deserialize() } } + +impl Default for ServiceConfig { + fn default() -> Self { + Self { + jwt_secret: "".to_string(), + } + } +} diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index 2c9168d58d..cf7763c16d 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -7,8 +7,7 @@ use axum::{ use tower_http::trace::TraceLayer; /// Returns a service that implements the web-based Agama API. -pub fn service(dbus_connection: zbus::Connection) -> Router { - let config = ServiceConfig::load().unwrap(); +pub fn service(config: ServiceConfig, dbus_connection: zbus::Connection) -> Router { let state = ServiceState { config, dbus_connection, diff --git a/rust/agama-dbus-server/tests/service.rs b/rust/agama-dbus-server/tests/service.rs index 407ef86ebb..1fda09a3b0 100644 --- a/rust/agama-dbus-server/tests/service.rs +++ b/rust/agama-dbus-server/tests/service.rs @@ -1,7 +1,7 @@ mod common; use self::common::DBusServer; -use agama_dbus_server::service; +use agama_dbus_server::{service, web::generate_token, web::ServiceConfig}; use axum::{ body::Body, http::{Method, Request, StatusCode}, @@ -19,7 +19,7 @@ async fn body_to_string(body: Body) -> String { #[test] async fn test_ping() -> Result<(), Box> { let dbus_server = DBusServer::new().start().await?; - let web_server = service(dbus_server.connection()); + let web_server = service(ServiceConfig::default(), dbus_server.connection()); let request = Request::builder().uri("/ping").body(Body::empty()).unwrap(); let response = web_server.oneshot(request).await.unwrap(); @@ -33,10 +33,15 @@ async fn test_ping() -> Result<(), Box> { #[test] async fn test_authenticate() -> Result<(), Box> { let dbus_server = DBusServer::new().start().await?; - let web_server = service(dbus_server.connection()); + let config = ServiceConfig { + jwt_secret: "nots3cr3t".to_string(), + }; + let web_server = service(config, dbus_server.connection()); + let token = generate_token("nots3cr3t"); let request = Request::builder() - .uri("/authenticate") - .method(Method::POST) + .uri("/protected") + .method(Method::GET) + .header("Authorization", format!("Bearer {}", token)) .body(Body::empty()) .unwrap(); @@ -44,6 +49,6 @@ async fn test_authenticate() -> Result<(), Box> { assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; - assert!(body.starts_with("{\"token\":")); + assert_eq!(body, "OK"); Ok(()) } From b1aaa56447ee5840ff8263a85faad4a86cd0a6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 07:29:23 +0000 Subject: [PATCH 08/22] rust: add a test for TokenClaims extraction --- rust/agama-dbus-server/src/web/auth.rs | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/rust/agama-dbus-server/src/web/auth.rs b/rust/agama-dbus-server/src/web/auth.rs index 5674bdc753..629dfcc74e 100644 --- a/rust/agama-dbus-server/src/web/auth.rs +++ b/rust/agama-dbus-server/src/web/auth.rs @@ -91,3 +91,41 @@ pub fn generate_token(secret: &str) -> String { ) .unwrap() } + +#[cfg(test)] +mod tests { + use super::{generate_token, AuthError, TokenClaims}; + use crate::web::{state::ServiceState, ServiceConfig}; + use axum::extract::{FromRequestParts, Request}; + use tokio::test; + + async fn try_extract_claims(token: &str, password: &str) -> Result { + let request = Request::builder() + .uri("/test") + .header("Authorization", format!("Bearer {}", token)) + .body(()) + .unwrap(); + let (mut parts, _) = request.into_parts(); + let state = ServiceState { + config: ServiceConfig { + jwt_secret: password.to_string(), + }, + dbus_connection: zbus::Connection::session().await.unwrap(), + }; + TokenClaims::from_request_parts(&mut parts, &state).await + } + + #[test] + async fn test_extract_claims() { + let token = generate_token("nots3cr3t"); + let claims = try_extract_claims(&token, "nots3cr3t").await; + assert!(claims.is_ok()); + } + + #[test] + async fn test_extract_claims_failed() { + let token = generate_token("nots3cr3t"); + let claims = try_extract_claims(&token, "nots3cr3t").await; + assert!(claims.is_err()); + } +} From 02b2412f57c9372c226a0d648a84fd2a92beb1c0 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 20 Feb 2024 09:36:09 +0000 Subject: [PATCH 09/22] Added agama pam configuration file --- rust/package/agama-cli.spec | 1 + rust/share/agama.pam | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 rust/share/agama.pam diff --git a/rust/package/agama-cli.spec b/rust/package/agama-cli.spec index 9a6fdd25ea..11e561f1a1 100644 --- a/rust/package/agama-cli.spec +++ b/rust/package/agama-cli.spec @@ -85,6 +85,7 @@ install -D -d -m 0755 %{buildroot}%{_bindir} install -m 0755 %{_builddir}/agama/target/release/agama %{buildroot}%{_bindir}/agama install -m 0755 %{_builddir}/agama/target/release/agama-dbus-server %{buildroot}%{_bindir}/agama-dbus-server install -m 0755 %{_builddir}/agama/target/release/agama-web-server %{buildroot}%{_bindir}/agama-web-server +install -p -m 644 %{_builddir}/agama/share/agama.pam $RPM_BUILD_ROOT%{_pam_vendordir}/agama install -D -d -m 0755 %{buildroot}%{_datadir}/agama-cli install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildroot}%{_datadir}/agama-cli install --directory %{buildroot}%{_datadir}/dbus-1/agama-services diff --git a/rust/share/agama.pam b/rust/share/agama.pam new file mode 100644 index 0000000000..7bf2ed28b6 --- /dev/null +++ b/rust/share/agama.pam @@ -0,0 +1,8 @@ +#%PAM-1.0 +auth substack common-auth +account required pam_nologin.so +account include common-account +password include common-password +session required pam_loginuid.so +session optional pam_keyinit.so force revoke +session include common-session From 1e93a4a2650e494f1e8902637b4b487b2d4cfaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 09:56:45 +0000 Subject: [PATCH 10/22] Add pam-devel to the Rust CI workflow --- .github/workflows/ci-rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 4757f5b974..41c3a6eaa3 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -73,6 +73,7 @@ jobs: jq libopenssl-3-devel openssl-3 + pam-devel python-langtable-data timezone From 4c40c7922fc86f4c0d45ffb43719e318aa5610c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 10:36:50 +0000 Subject: [PATCH 11/22] rust: fix tests --- .github/workflows/ci-rust.yml | 3 +++ rust/agama-dbus-server/src/web/auth.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 41c3a6eaa3..0b25242756 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -77,6 +77,9 @@ jobs: python-langtable-data timezone + - name: Copy the PAM configuration + run: cp share/agama.pam /usr/lib/pam.d/agama + - name: Install Rust toolchains run: rustup toolchain install stable diff --git a/rust/agama-dbus-server/src/web/auth.rs b/rust/agama-dbus-server/src/web/auth.rs index 629dfcc74e..c4565a2933 100644 --- a/rust/agama-dbus-server/src/web/auth.rs +++ b/rust/agama-dbus-server/src/web/auth.rs @@ -125,7 +125,7 @@ mod tests { #[test] async fn test_extract_claims_failed() { let token = generate_token("nots3cr3t"); - let claims = try_extract_claims(&token, "nots3cr3t").await; + let claims = try_extract_claims(&token, "wrong").await; assert!(claims.is_err()); } } From dca5c2a6361d66eb66f3f9dc5833a306d3289ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 12:11:10 +0000 Subject: [PATCH 12/22] rust: fix CI tests --- rust/agama-dbus-server/src/web/auth.rs | 38 ------------------------- rust/agama-dbus-server/tests/service.rs | 31 ++++++++++++++++---- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/rust/agama-dbus-server/src/web/auth.rs b/rust/agama-dbus-server/src/web/auth.rs index c4565a2933..5674bdc753 100644 --- a/rust/agama-dbus-server/src/web/auth.rs +++ b/rust/agama-dbus-server/src/web/auth.rs @@ -91,41 +91,3 @@ pub fn generate_token(secret: &str) -> String { ) .unwrap() } - -#[cfg(test)] -mod tests { - use super::{generate_token, AuthError, TokenClaims}; - use crate::web::{state::ServiceState, ServiceConfig}; - use axum::extract::{FromRequestParts, Request}; - use tokio::test; - - async fn try_extract_claims(token: &str, password: &str) -> Result { - let request = Request::builder() - .uri("/test") - .header("Authorization", format!("Bearer {}", token)) - .body(()) - .unwrap(); - let (mut parts, _) = request.into_parts(); - let state = ServiceState { - config: ServiceConfig { - jwt_secret: password.to_string(), - }, - dbus_connection: zbus::Connection::session().await.unwrap(), - }; - TokenClaims::from_request_parts(&mut parts, &state).await - } - - #[test] - async fn test_extract_claims() { - let token = generate_token("nots3cr3t"); - let claims = try_extract_claims(&token, "nots3cr3t").await; - assert!(claims.is_ok()); - } - - #[test] - async fn test_extract_claims_failed() { - let token = generate_token("nots3cr3t"); - let claims = try_extract_claims(&token, "wrong").await; - assert!(claims.is_err()); - } -} diff --git a/rust/agama-dbus-server/tests/service.rs b/rust/agama-dbus-server/tests/service.rs index 1fda09a3b0..c825fbbe43 100644 --- a/rust/agama-dbus-server/tests/service.rs +++ b/rust/agama-dbus-server/tests/service.rs @@ -5,6 +5,7 @@ use agama_dbus_server::{service, web::generate_token, web::ServiceConfig}; use axum::{ body::Body, http::{Method, Request, StatusCode}, + response::Response, }; use http_body_util::BodyExt; use std::error::Error; @@ -30,14 +31,12 @@ async fn test_ping() -> Result<(), Box> { Ok(()) } -#[test] -async fn test_authenticate() -> Result<(), Box> { - let dbus_server = DBusServer::new().start().await?; +async fn access_protected_route(token: &str, jwt_secret: &str) -> Response { + let dbus_server = DBusServer::new().start().await.unwrap(); let config = ServiceConfig { - jwt_secret: "nots3cr3t".to_string(), + jwt_secret: jwt_secret.to_string(), }; let web_server = service(config, dbus_server.connection()); - let token = generate_token("nots3cr3t"); let request = Request::builder() .uri("/protected") .method(Method::GET) @@ -45,10 +44,30 @@ async fn test_authenticate() -> Result<(), Box> { .body(Body::empty()) .unwrap(); - let response = web_server.oneshot(request).await.unwrap(); + web_server.oneshot(request).await.unwrap() +} + +// TODO: The following test should belong to `auth.rs`. However, we need a working +// D-Bus connection which is not available on containers. Let's keep the test +// here until by now. +#[test] +async fn test_access_protected_route() -> Result<(), Box> { + let token = generate_token("nots3cr3t"); + let response = access_protected_route(&token, "nots3cr3t").await; assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; assert_eq!(body, "OK"); Ok(()) } + +// TODO: The following test should belong to `auth.rs`. However, we need a working +// D-Bus connection which is not available on containers. Let's keep the test +// here until by now. +#[test] +async fn test_access_protected_route_failed() -> Result<(), Box> { + let token = generate_token("nots3cr3t"); + let response = access_protected_route(&token, "wrong").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + Ok(()) +} From 4128d69e4b26bf7d7dfc50b516750c5e6c2419b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 12:31:25 +0000 Subject: [PATCH 13/22] rust: add some development notes --- rust/WEB-SERVER.md | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 rust/WEB-SERVER.md diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md new file mode 100644 index 0000000000..3bb436a696 --- /dev/null +++ b/rust/WEB-SERVER.md @@ -0,0 +1,102 @@ +# Web server development notes + +This document includes some notes about the usage and development of the new Agama web server. + +## Installing Rust and related tools + +It is recommended to use Rustup to install Rust and related tools. In openSUSE distributions, rustup +is available as a package. After rustup is installed, you can proceed to install the toolchain: + +``` +zypper --non-interactive in rustup +rustup install stable +``` + +In addition to the Rust compiler, the previous command would install some additionall components +like `cargo`, `clippy`, `rustfmt`, documentation, etc. + +Another interesting addition might be +[cargo-binstall](https://github.com/cargo-bins/cargo-binstall), which allows to install Rust +binaries. If you are fine with this approach, just run: + +``` +cargo install cargo-binstall +``` + +## Setting up PAM + +The web sever will use PAM for authentication. For some reason, you might want to +copy the `share/agama.pam` file to `/usr/lib/pam.d/agama` + +``` +cp share/agama.pam /usr/lib/pam.d/agama +``` + +## Running the server + +NOTE: in order to run the server, you need to start Agama's D-Bus daemon. So you can either start +the Agama service or just start the D-Bus daemon (`sudo bundle exec bin/agamactl -f` from the +`service/` directory). + +You need to run the server as `root`, so you cannot use `cargo run` directly. Instead, just do: + +``` +$ cargo build +$ sudo ./target/debug/agama-web-server serve +``` + +If it fails to compile, please check whether `clang-devel` and `pam-devel` are installed. + +You can add a `--listen` flag if you want to use a different port: + +``` +$ sudo ./target/debug/agama-web-server serve --listen 0.0.0.0:5678 +``` + +## Trying the server + +You can check whether the server is up and running by just performing a ping: + +``` +$ curl http://localhost:3000/ping +``` + +### Authentication + +The web server uses a bearer token for HTTP authentication. You can get the token by providing your +password to the `/authenticate` endpoint. + +``` +$ curl http://localhost:3000/authenticate \ + -H "Content-Type: application/json" \ + -d '{"password": "your-password"}' +{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U"}⏎ +``` + +Now you can access protected routes by including the token in the header: + +``` +$ curl -X GET http://localhost:3000/protected \ + -H "Accept: application/json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" +``` + +### Connecting to the websocket + +You can use `websocat` to connect to the websocket. To install the tool, just run: + +``` +$ cargo binstall websocat +``` + +If you did not install `binstall`, you can do: + +``` +$ cargo install websocat +``` + +Now, you can use the following command to connect: + +``` +$ websocat ws://localhost:3000/ws +``` From 1d4a9b79642396517f860d1d8212d7e711dc48b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 12:33:26 +0000 Subject: [PATCH 14/22] rust: improved format of development notes --- rust/WEB-SERVER.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md index 3bb436a696..e08db8f938 100644 --- a/rust/WEB-SERVER.md +++ b/rust/WEB-SERVER.md @@ -34,9 +34,9 @@ cp share/agama.pam /usr/lib/pam.d/agama ## Running the server -NOTE: in order to run the server, you need to start Agama's D-Bus daemon. So you can either start -the Agama service or just start the D-Bus daemon (`sudo bundle exec bin/agamactl -f` from the -`service/` directory). +> [!NOTE] +> The web server needs to connect to Agama's D-Bus daemon. So you can either start the Agama service +> or just start the D-Bus daemon (`sudo bundle exec bin/agamactl -f` from the `service/` directory). You need to run the server as `root`, so you cannot use `cargo run` directly. Instead, just do: @@ -89,7 +89,7 @@ You can use `websocat` to connect to the websocket. To install the tool, just ru $ cargo binstall websocat ``` -If you did not install `binstall`, you can do: +If you did not install `cargo-binstall`, you can do: ``` $ cargo install websocat From 0e1226127a0317e3ce2d777a67e31ffa7ec16027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 12:54:41 +0000 Subject: [PATCH 15/22] rust: protect the ws route --- rust/WEB-SERVER.md | 5 +++-- rust/agama-dbus-server/src/web/service.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md index e08db8f938..c5e1bbb717 100644 --- a/rust/WEB-SERVER.md +++ b/rust/WEB-SERVER.md @@ -77,8 +77,8 @@ Now you can access protected routes by including the token in the header: ``` $ curl -X GET http://localhost:3000/protected \ - -H "Accept: application/json" \ - -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" + -H "Accept: application/json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" ``` ### Connecting to the websocket @@ -99,4 +99,5 @@ Now, you can use the following command to connect: ``` $ websocat ws://localhost:3000/ws + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" ``` diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index cf7763c16d..2c30d24212 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -14,11 +14,11 @@ pub fn service(config: ServiceConfig, dbus_connection: zbus::Connection) -> Rout }; Router::new() .route("/protected", get(super::http::protected)) + .route("/ws", get(super::ws::ws_handler)) .route_layer(middleware::from_extractor_with_state::( state.clone(), )) .route("/ping", get(super::http::ping)) - .route("/ws", get(super::ws::ws_handler)) .route("/authenticate", post(super::http::authenticate)) .layer(TraceLayer::new_for_http()) .with_state(state) From 33b4afe8d83415551c1d456aa6de9fd445a85d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 13:40:47 +0000 Subject: [PATCH 16/22] Do not copy the PAM configuration in GitHub actions --- .github/workflows/ci-rust.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 0b25242756..41c3a6eaa3 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -77,9 +77,6 @@ jobs: python-langtable-data timezone - - name: Copy the PAM configuration - run: cp share/agama.pam /usr/lib/pam.d/agama - - name: Install Rust toolchains run: rustup toolchain install stable From e5beead1a308407255cbb64adbf0b5b5bbc8a943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 14:07:40 +0000 Subject: [PATCH 17/22] Extend the WEB-SERVER.md file with information about PAM --- rust/WEB-SERVER.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md index c5e1bbb717..07d80b0b24 100644 --- a/rust/WEB-SERVER.md +++ b/rust/WEB-SERVER.md @@ -25,8 +25,10 @@ cargo install cargo-binstall ## Setting up PAM -The web sever will use PAM for authentication. For some reason, you might want to -copy the `share/agama.pam` file to `/usr/lib/pam.d/agama` +The web sever will use [Pluggable Authentication Modules +(PAM)](https://github.com/linux-pam/linux-pam) for authentication. For that +reason, you need to copy the `agama` service definition for PAM to `/usr/lib/pam.d`. Otherwise, PAM +would not know how to authenticate the service: ``` cp share/agama.pam /usr/lib/pam.d/agama From 4268066b21118d3a4e8a5a0097b8497705c82aed Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 20 Feb 2024 14:32:00 +0000 Subject: [PATCH 18/22] Create pam.d directory if do not exist --- rust/package/agama-cli.spec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/package/agama-cli.spec b/rust/package/agama-cli.spec index 11e561f1a1..534cfd59d7 100644 --- a/rust/package/agama-cli.spec +++ b/rust/package/agama-cli.spec @@ -34,6 +34,7 @@ BuildRequires: timezone BuildRequires: dbus-1-common # required by agama-dbus-server integration tests BuildRequires: dbus-1-daemon +BuildRequires: pkgconfig(pam) Requires: jsonnet Requires: lshw # required by "agama logs store" @@ -85,7 +86,7 @@ install -D -d -m 0755 %{buildroot}%{_bindir} install -m 0755 %{_builddir}/agama/target/release/agama %{buildroot}%{_bindir}/agama install -m 0755 %{_builddir}/agama/target/release/agama-dbus-server %{buildroot}%{_bindir}/agama-dbus-server install -m 0755 %{_builddir}/agama/target/release/agama-web-server %{buildroot}%{_bindir}/agama-web-server -install -p -m 644 %{_builddir}/agama/share/agama.pam $RPM_BUILD_ROOT%{_pam_vendordir}/agama +install -D -p -m 644 %{_builddir}/agama/share/agama.pam $RPM_BUILD_ROOT%{_pam_vendordir}/agama install -D -d -m 0755 %{buildroot}%{_datadir}/agama-cli install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildroot}%{_datadir}/agama-cli install --directory %{buildroot}%{_datadir}/dbus-1/agama-services @@ -107,6 +108,7 @@ install -m 0644 --target-directory=%{buildroot}%{_datadir}/dbus-1/agama-services %files -n agama-dbus-server %{_bindir}/agama-dbus-server %{_datadir}/dbus-1/agama-services +%{_pam_vendordir}/agama %files -n agama-web-server %{_bindir}/agama-web-server From 38fd041e9402b583f14674cb10d184002eaff9df Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 20 Feb 2024 14:36:34 +0000 Subject: [PATCH 19/22] Added clang devel dependency --- rust/package/agama-cli.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/package/agama-cli.spec b/rust/package/agama-cli.spec index 534cfd59d7..b180f26aa7 100644 --- a/rust/package/agama-cli.spec +++ b/rust/package/agama-cli.spec @@ -34,6 +34,7 @@ BuildRequires: timezone BuildRequires: dbus-1-common # required by agama-dbus-server integration tests BuildRequires: dbus-1-daemon +BuildRequires: clang-devel BuildRequires: pkgconfig(pam) Requires: jsonnet Requires: lshw From bee3e01867aee8635929b559317f10bb1453c46e Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 20 Feb 2024 14:50:39 +0000 Subject: [PATCH 20/22] Use agama pam service --- rust/agama-dbus-server/src/web/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index 9126049a38..f6131044db 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -48,7 +48,7 @@ pub async fn authenticate( State(state): State, Json(login): Json, ) -> Result, AuthError> { - let mut pam_client = Client::with_password("cockpit")?; + let mut pam_client = Client::with_password("agama")?; pam_client .conversation_mut() .set_credentials("root", login.password); From 157f7ba0112007755e7fcc4b14fbd0f148924dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 20 Feb 2024 14:52:32 +0000 Subject: [PATCH 21/22] rust: update PAM configuration --- rust/share/agama.pam | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rust/share/agama.pam b/rust/share/agama.pam index 7bf2ed28b6..76d077affe 100644 --- a/rust/share/agama.pam +++ b/rust/share/agama.pam @@ -1,8 +1,3 @@ #%PAM-1.0 -auth substack common-auth -account required pam_nologin.so +auth include common-auth account include common-account -password include common-password -session required pam_loginuid.so -session optional pam_keyinit.so force revoke -session include common-session From f10ffd1afc7308ef16a80a850b263141f7792661 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 20 Feb 2024 15:51:33 +0000 Subject: [PATCH 22/22] Link 'Added link to PAM authentication doc --- rust/WEB-SERVER.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md index 07d80b0b24..75534d03ee 100644 --- a/rust/WEB-SERVER.md +++ b/rust/WEB-SERVER.md @@ -34,6 +34,8 @@ would not know how to authenticate the service: cp share/agama.pam /usr/lib/pam.d/agama ``` +For further information, see [Authenticating with PAM](https://doc.opensuse.org/documentation/leap/security/single-html/book-security/index.html#cha-pam). + ## Running the server > [!NOTE]