From da6a494875a9417795f80ae27ac168200b6d7f92 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Wed, 6 Apr 2022 17:18:06 +0200 Subject: [PATCH] Load a list of clients --- Cargo.lock | 28 +++++++++++++ Cargo.toml | 1 + src/constants.rs | 3 ++ src/controllers/admin_controller.rs | 25 ++++++++++++ src/controllers/mod.rs | 3 +- src/controllers/settings_controller.rs | 20 ++++----- src/data/app_config.rs | 6 ++- src/data/client.rs | 32 +++++++++++++++ src/data/current_user.rs | 9 +++++ src/data/entity_manager.rs | 47 ++++++++++++++++------ src/data/mod.rs | 1 + src/main.rs | 10 ++++- templates/settings/base_settings_page.html | 4 +- templates/settings/clients_list.html | 23 +++++++++++ 14 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 src/controllers/admin_controller.rs create mode 100644 src/data/client.rs create mode 100644 templates/settings/clients_list.html diff --git a/Cargo.lock b/Cargo.lock index 0b57bc7..62e2d72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,7 @@ dependencies = [ "mime_guess", "serde", "serde_json", + "serde_yaml", "urlencoding", "uuid", ] @@ -952,6 +953,12 @@ version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "local-channel" version = "0.1.2" @@ -1356,6 +1363,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "sha-1" version = "0.10.0" @@ -1733,6 +1752,15 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +[[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 = "zeroize" version = "1.5.4" diff --git a/Cargo.toml b/Cargo.toml index a068d31..ec1afb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ clap = { version = "3.1.6", features = ["derive", "env"] } include_dir = "0.7.2" log = "0.4.16" serde_json = "1.0.79" +serde_yaml = "0.8.23" env_logger = "0.9.0" serde = { version = "1.0.136", features = ["derive"] } bcrypt = "0.12.1" diff --git a/src/constants.rs b/src/constants.rs index 3509afd..a20e0cc 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,6 +3,9 @@ use std::time::Duration; /// File in storage containing users list pub const USERS_LIST_FILE: &str = "users.json"; +/// File in storage containing clients list +pub const CLIENTS_LIST_FILE: &str = "clients.yaml"; + /// Default built-in credentials pub const DEFAULT_ADMIN_USERNAME: &str = "admin"; pub const DEFAULT_ADMIN_PASSWORD: &str = "admin"; diff --git a/src/controllers/admin_controller.rs b/src/controllers/admin_controller.rs new file mode 100644 index 0000000..176dad5 --- /dev/null +++ b/src/controllers/admin_controller.rs @@ -0,0 +1,25 @@ +use actix_web::{HttpResponse, Responder, web}; +use askama::Template; + +use crate::controllers::settings_controller::BaseSettingsPage; +use crate::data::client::{Client, ClientManager}; +use crate::data::current_user::CurrentUser; + +#[derive(Template)] +#[template(path = "settings/clients_list.html")] +struct ClientsListTemplate { + _parent: BaseSettingsPage, + clients: Vec, +} + +pub async fn clients_route(user: CurrentUser, clients: web::Data) -> impl Responder { + HttpResponse::Ok().body(ClientsListTemplate { + _parent: BaseSettingsPage::get( + "Clients list", + &user, + None, + None, + ), + clients: clients.cloned(), + }.render().unwrap()) +} \ No newline at end of file diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index 1b479ac..2ac2a2f 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,4 +1,5 @@ pub mod assets_controller; pub mod base_controller; pub mod login_controller; -pub mod settings_controller; \ No newline at end of file +pub mod settings_controller; +pub mod admin_controller; \ No newline at end of file diff --git a/src/controllers/settings_controller.rs b/src/controllers/settings_controller.rs index e3bd398..f68916a 100644 --- a/src/controllers/settings_controller.rs +++ b/src/controllers/settings_controller.rs @@ -12,17 +12,17 @@ use crate::data::user::User; #[derive(Template)] #[template(path = "settings/base_settings_page.html")] -struct BaseSettingsPage { - danger_message: Option, - success_message: Option, - page_title: &'static str, - app_name: &'static str, - is_admin: bool, - user_name: String, +pub(crate) struct BaseSettingsPage { + pub danger_message: Option, + pub success_message: Option, + pub page_title: &'static str, + pub app_name: &'static str, + pub is_admin: bool, + pub user_name: String, } impl BaseSettingsPage { - async fn get(page_title: &'static str, user: &User, + pub fn get(page_title: &'static str, user: &User, danger_message: Option, success_message: Option) -> BaseSettingsPage { Self { danger_message, @@ -58,7 +58,7 @@ pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder let user = user.into(); HttpResponse::Ok() .body(AccountDetailsPage { - _parent: BaseSettingsPage::get("Account details", &user, None, None).await, + _parent: BaseSettingsPage::get("Account details", &user, None, None), user_id: user.uid, first_name: user.first_name, last_name: user.last_last, @@ -121,7 +121,7 @@ pub async fn change_password_route(user: CurrentUser, HttpResponse::Ok() .body(ChangePasswordPage { - _parent: BaseSettingsPage::get("Change password", &user, danger, success).await, + _parent: BaseSettingsPage::get("Change password", &user, danger, success), min_pwd_len: MIN_PASS_LEN, }.render().unwrap()) } \ No newline at end of file diff --git a/src/data/app_config.rs b/src/data/app_config.rs index 463e0ef..69a80c7 100644 --- a/src/data/app_config.rs +++ b/src/data/app_config.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use clap::Parser; -use crate::constants::USERS_LIST_FILE; +use crate::constants::{CLIENTS_LIST_FILE, USERS_LIST_FILE}; /// Basic OIDC provider #[derive(Parser, Debug, Clone)] @@ -41,4 +41,8 @@ impl AppConfig { pub fn users_file(&self) -> PathBuf { self.storage_path().join(USERS_LIST_FILE) } + + pub fn clients_file(&self) -> PathBuf { + self.storage_path().join(CLIENTS_LIST_FILE) + } } diff --git a/src/data/client.rs b/src/data/client.rs new file mode 100644 index 0000000..1c6f909 --- /dev/null +++ b/src/data/client.rs @@ -0,0 +1,32 @@ +use crate::data::entity_manager::EntityManager; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +pub struct ClientID(pub String); + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct Client { + pub id: ClientID, + pub name: String, + pub description: String, +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Eq for Client {} + +pub type ClientManager = EntityManager; + +impl EntityManager { + pub fn find_by_id(&self, u: &ClientID) -> Option { + for entry in self.iter() { + if entry.id.eq(u) { + return Some(entry.clone()); + } + } + None + } +} \ No newline at end of file diff --git a/src/data/current_user.rs b/src/data/current_user.rs index 7d424a4..5fa455a 100644 --- a/src/data/current_user.rs +++ b/src/data/current_user.rs @@ -1,4 +1,5 @@ use std::future::Future; +use std::ops::Deref; use std::pin::Pin; use actix::Addr; @@ -19,6 +20,14 @@ impl From for User { } } +impl Deref for CurrentUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl FromRequest for CurrentUser { type Error = Error; type Future = Pin>>>; diff --git a/src/data/entity_manager.rs b/src/data/entity_manager.rs index c67ab37..94e2fff 100644 --- a/src/data/entity_manager.rs +++ b/src/data/entity_manager.rs @@ -3,14 +3,16 @@ use std::slice::Iter; use crate::utils::err::Res; +enum FileFormat { Json, Yaml } + pub struct EntityManager { file_path: PathBuf, list: Vec, } impl EntityManager -where - E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone, + where + E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone, { /// Open entity pub fn open_or_create>(path: A) -> Res { @@ -23,12 +25,34 @@ where } log::info!("Open existing entity file {:?}", path.as_ref()); + let file_content = std::fs::read_to_string(path.as_ref())?; Ok(Self { file_path: path.as_ref().to_path_buf(), - list: serde_json::from_str(&std::fs::read_to_string(path.as_ref())?)?, + list: match Self::file_format(path.as_ref()) { + FileFormat::Json => serde_json::from_str(&file_content)?, + FileFormat::Yaml => serde_yaml::from_str(&file_content)? + }, }) } + /// Save the list + fn save(&self) -> Res { + Ok(std::fs::write( + &self.file_path, + match Self::file_format(self.file_path.as_ref()) { + FileFormat::Json => serde_json::to_string(&self.list)?, + FileFormat::Yaml => serde_yaml::to_string(&self.list)?, + }, + )?) + } + + fn file_format(p: &Path) -> FileFormat { + match p.to_string_lossy().ends_with(".json") { + true => FileFormat::Json, + false => FileFormat::Yaml + } + } + /// Get the number of entries in the list pub fn len(&self) -> usize { self.list.len() @@ -38,14 +62,6 @@ where self.len() == 0 } - /// Save the list - fn save(&self) -> Res { - Ok(std::fs::write( - &self.file_path, - serde_json::to_string(&self.list)?, - )?) - } - /// Insert a new element in the list pub fn insert(&mut self, el: E) -> Res { self.list.push(el); @@ -54,8 +70,8 @@ where /// Replace entries in the list that matches a criteria pub fn replace_entries(&mut self, filter: F, el: &E) -> Res - where - F: Fn(&E) -> bool, + where + F: Fn(&E) -> bool, { for i in 0..self.list.len() { if filter(&self.list[i]) { @@ -70,4 +86,9 @@ where pub fn iter(&self) -> Iter<'_, E> { self.list.iter() } + + /// Get a cloned list of entries + pub fn cloned(&self) -> Vec { + self.list.clone() + } } diff --git a/src/data/mod.rs b/src/data/mod.rs index 98ff0e9..0c9e04f 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -3,5 +3,6 @@ pub mod entity_manager; pub mod service; pub mod session_identity; pub mod user; +pub mod client; pub mod remote_ip; pub mod current_user; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 18b78a7..00801b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,10 +12,11 @@ use basic_oidc::constants::{ DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, }; +use basic_oidc::controllers::{admin_controller, settings_controller}; use basic_oidc::controllers::assets_controller::assets_route; use basic_oidc::controllers::login_controller::{login_route, logout_route}; -use basic_oidc::controllers::settings_controller; use basic_oidc::data::app_config::AppConfig; +use basic_oidc::data::client::ClientManager; use basic_oidc::data::entity_manager::EntityManager; use basic_oidc::data::user::{hash_password, User}; use basic_oidc::middlewares::auth_middleware::AuthMiddleware; @@ -71,6 +72,9 @@ async fn main() -> std::io::Result<()> { let listen_address = config.listen_address.to_string(); HttpServer::new(move || { + let clients = ClientManager::open_or_create(config.clients_file()) + .expect("Failed to load clients list!"); + let policy = CookieIdentityPolicy::new(config.token_key.as_bytes()) .name(SESSION_COOKIE_NAME) .secure(config.secure_cookie()) @@ -82,6 +86,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(users_actor.clone())) .app_data(web::Data::new(bruteforce_actor.clone())) .app_data(web::Data::new(config.clone())) + .app_data(web::Data::new(clients)) .wrap(Logger::default()) .wrap(AuthMiddleware {}) @@ -108,6 +113,9 @@ async fn main() -> std::io::Result<()> { .route("/settings", web::get().to(settings_controller::account_settings_details_route)) .route("/settings/change_password", web::get().to(settings_controller::change_password_route)) .route("/settings/change_password", web::post().to(settings_controller::change_password_route)) + + // Admin routes + .route("/admin/clients", web::get().to(admin_controller::clients_route)) }) .bind(listen_address)? .run() diff --git a/templates/settings/base_settings_page.html b/templates/settings/base_settings_page.html index d05dc01..d0d259b 100644 --- a/templates/settings/base_settings_page.html +++ b/templates/settings/base_settings_page.html @@ -30,8 +30,8 @@
{% if is_admin %}
  • - - Applications + + Clients
  • diff --git a/templates/settings/clients_list.html b/templates/settings/clients_list.html new file mode 100644 index 0000000..708c6e6 --- /dev/null +++ b/templates/settings/clients_list.html @@ -0,0 +1,23 @@ +{% extends "base_settings_page.html" %} +{% block content %} + + + + + + + + + + + {% for c in clients %} + + + + + + {% endfor %} + +
    IDNameDescription
    {{ c.id.0 }}{{ c.name }}{{ c.description }}
    + +{% endblock content %}