Load a list of clients

This commit is contained in:
Pierre HUBERT 2022-04-06 17:18:06 +02:00
parent f6403afa34
commit da6a494875
14 changed files with 184 additions and 28 deletions

28
Cargo.lock generated
View File

@ -395,6 +395,7 @@ dependencies = [
"mime_guess", "mime_guess",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"urlencoding", "urlencoding",
"uuid", "uuid",
] ]
@ -952,6 +953,12 @@ version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]] [[package]]
name = "local-channel" name = "local-channel"
version = "0.1.2" version = "0.1.2"
@ -1356,6 +1363,18 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha-1" name = "sha-1"
version = "0.10.0" version = "0.10.0"
@ -1733,6 +1752,15 @@ version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" 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]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.5.4" version = "1.5.4"

View File

@ -13,6 +13,7 @@ clap = { version = "3.1.6", features = ["derive", "env"] }
include_dir = "0.7.2" include_dir = "0.7.2"
log = "0.4.16" log = "0.4.16"
serde_json = "1.0.79" serde_json = "1.0.79"
serde_yaml = "0.8.23"
env_logger = "0.9.0" env_logger = "0.9.0"
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }
bcrypt = "0.12.1" bcrypt = "0.12.1"

View File

@ -3,6 +3,9 @@ use std::time::Duration;
/// File in storage containing users list /// File in storage containing users list
pub const USERS_LIST_FILE: &str = "users.json"; 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 /// Default built-in credentials
pub const DEFAULT_ADMIN_USERNAME: &str = "admin"; pub const DEFAULT_ADMIN_USERNAME: &str = "admin";
pub const DEFAULT_ADMIN_PASSWORD: &str = "admin"; pub const DEFAULT_ADMIN_PASSWORD: &str = "admin";

View File

@ -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<Client>,
}
pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>) -> impl Responder {
HttpResponse::Ok().body(ClientsListTemplate {
_parent: BaseSettingsPage::get(
"Clients list",
&user,
None,
None,
),
clients: clients.cloned(),
}.render().unwrap())
}

View File

@ -1,4 +1,5 @@
pub mod assets_controller; pub mod assets_controller;
pub mod base_controller; pub mod base_controller;
pub mod login_controller; pub mod login_controller;
pub mod settings_controller; pub mod settings_controller;
pub mod admin_controller;

View File

@ -12,17 +12,17 @@ use crate::data::user::User;
#[derive(Template)] #[derive(Template)]
#[template(path = "settings/base_settings_page.html")] #[template(path = "settings/base_settings_page.html")]
struct BaseSettingsPage { pub(crate) struct BaseSettingsPage {
danger_message: Option<String>, pub danger_message: Option<String>,
success_message: Option<String>, pub success_message: Option<String>,
page_title: &'static str, pub page_title: &'static str,
app_name: &'static str, pub app_name: &'static str,
is_admin: bool, pub is_admin: bool,
user_name: String, pub user_name: String,
} }
impl BaseSettingsPage { impl BaseSettingsPage {
async fn get(page_title: &'static str, user: &User, pub fn get(page_title: &'static str, user: &User,
danger_message: Option<String>, success_message: Option<String>) -> BaseSettingsPage { danger_message: Option<String>, success_message: Option<String>) -> BaseSettingsPage {
Self { Self {
danger_message, danger_message,
@ -58,7 +58,7 @@ pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder
let user = user.into(); let user = user.into();
HttpResponse::Ok() HttpResponse::Ok()
.body(AccountDetailsPage { .body(AccountDetailsPage {
_parent: BaseSettingsPage::get("Account details", &user, None, None).await, _parent: BaseSettingsPage::get("Account details", &user, None, None),
user_id: user.uid, user_id: user.uid,
first_name: user.first_name, first_name: user.first_name,
last_name: user.last_last, last_name: user.last_last,
@ -121,7 +121,7 @@ pub async fn change_password_route(user: CurrentUser,
HttpResponse::Ok() HttpResponse::Ok()
.body(ChangePasswordPage { .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, min_pwd_len: MIN_PASS_LEN,
}.render().unwrap()) }.render().unwrap())
} }

View File

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use clap::Parser; use clap::Parser;
use crate::constants::USERS_LIST_FILE; use crate::constants::{CLIENTS_LIST_FILE, USERS_LIST_FILE};
/// Basic OIDC provider /// Basic OIDC provider
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
@ -41,4 +41,8 @@ impl AppConfig {
pub fn users_file(&self) -> PathBuf { pub fn users_file(&self) -> PathBuf {
self.storage_path().join(USERS_LIST_FILE) self.storage_path().join(USERS_LIST_FILE)
} }
pub fn clients_file(&self) -> PathBuf {
self.storage_path().join(CLIENTS_LIST_FILE)
}
} }

32
src/data/client.rs Normal file
View File

@ -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<Client>;
impl EntityManager<Client> {
pub fn find_by_id(&self, u: &ClientID) -> Option<Client> {
for entry in self.iter() {
if entry.id.eq(u) {
return Some(entry.clone());
}
}
None
}
}

View File

@ -1,4 +1,5 @@
use std::future::Future; use std::future::Future;
use std::ops::Deref;
use std::pin::Pin; use std::pin::Pin;
use actix::Addr; use actix::Addr;
@ -19,6 +20,14 @@ impl From<CurrentUser> for User {
} }
} }
impl Deref for CurrentUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromRequest for CurrentUser { impl FromRequest for CurrentUser {
type Error = Error; type Error = Error;
type Future = Pin<Box<dyn Future<Output=Result<Self, Self::Error>>>>; type Future = Pin<Box<dyn Future<Output=Result<Self, Self::Error>>>>;

View File

@ -3,14 +3,16 @@ use std::slice::Iter;
use crate::utils::err::Res; use crate::utils::err::Res;
enum FileFormat { Json, Yaml }
pub struct EntityManager<E> { pub struct EntityManager<E> {
file_path: PathBuf, file_path: PathBuf,
list: Vec<E>, list: Vec<E>,
} }
impl<E> EntityManager<E> impl<E> EntityManager<E>
where where
E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone, E: serde::Serialize + serde::de::DeserializeOwned + Eq + Clone,
{ {
/// Open entity /// Open entity
pub fn open_or_create<A: AsRef<Path>>(path: A) -> Res<Self> { pub fn open_or_create<A: AsRef<Path>>(path: A) -> Res<Self> {
@ -23,12 +25,34 @@ where
} }
log::info!("Open existing entity file {:?}", path.as_ref()); log::info!("Open existing entity file {:?}", path.as_ref());
let file_content = std::fs::read_to_string(path.as_ref())?;
Ok(Self { Ok(Self {
file_path: path.as_ref().to_path_buf(), 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 /// Get the number of entries in the list
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.list.len() self.list.len()
@ -38,14 +62,6 @@ where
self.len() == 0 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 /// Insert a new element in the list
pub fn insert(&mut self, el: E) -> Res { pub fn insert(&mut self, el: E) -> Res {
self.list.push(el); self.list.push(el);
@ -54,8 +70,8 @@ where
/// Replace entries in the list that matches a criteria /// Replace entries in the list that matches a criteria
pub fn replace_entries<F>(&mut self, filter: F, el: &E) -> Res pub fn replace_entries<F>(&mut self, filter: F, el: &E) -> Res
where where
F: Fn(&E) -> bool, F: Fn(&E) -> bool,
{ {
for i in 0..self.list.len() { for i in 0..self.list.len() {
if filter(&self.list[i]) { if filter(&self.list[i]) {
@ -70,4 +86,9 @@ where
pub fn iter(&self) -> Iter<'_, E> { pub fn iter(&self) -> Iter<'_, E> {
self.list.iter() self.list.iter()
} }
/// Get a cloned list of entries
pub fn cloned(&self) -> Vec<E> {
self.list.clone()
}
} }

View File

@ -3,5 +3,6 @@ pub mod entity_manager;
pub mod service; pub mod service;
pub mod session_identity; pub mod session_identity;
pub mod user; pub mod user;
pub mod client;
pub mod remote_ip; pub mod remote_ip;
pub mod current_user; pub mod current_user;

View File

@ -12,10 +12,11 @@ use basic_oidc::constants::{
DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION,
SESSION_COOKIE_NAME, SESSION_COOKIE_NAME,
}; };
use basic_oidc::controllers::{admin_controller, settings_controller};
use basic_oidc::controllers::assets_controller::assets_route; use basic_oidc::controllers::assets_controller::assets_route;
use basic_oidc::controllers::login_controller::{login_route, logout_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::app_config::AppConfig;
use basic_oidc::data::client::ClientManager;
use basic_oidc::data::entity_manager::EntityManager; use basic_oidc::data::entity_manager::EntityManager;
use basic_oidc::data::user::{hash_password, User}; use basic_oidc::data::user::{hash_password, User};
use basic_oidc::middlewares::auth_middleware::AuthMiddleware; 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(); let listen_address = config.listen_address.to_string();
HttpServer::new(move || { 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()) let policy = CookieIdentityPolicy::new(config.token_key.as_bytes())
.name(SESSION_COOKIE_NAME) .name(SESSION_COOKIE_NAME)
.secure(config.secure_cookie()) .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(users_actor.clone()))
.app_data(web::Data::new(bruteforce_actor.clone())) .app_data(web::Data::new(bruteforce_actor.clone()))
.app_data(web::Data::new(config.clone())) .app_data(web::Data::new(config.clone()))
.app_data(web::Data::new(clients))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(AuthMiddleware {}) .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", 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::get().to(settings_controller::change_password_route))
.route("/settings/change_password", web::post().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)? .bind(listen_address)?
.run() .run()

View File

@ -30,8 +30,8 @@
<hr/> <hr/>
{% if is_admin %} {% if is_admin %}
<li> <li>
<a href="/admin/apps" class="nav-link link-dark"> <a href="/admin/clients" class="nav-link link-dark">
Applications Clients
</a> </a>
</li> </li>
<li> <li>

View File

@ -0,0 +1,23 @@
{% extends "base_settings_page.html" %}
{% block content %}
<table class="table table-hover" style="max-width: 600px;" aria-describedby="Clients list">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Description</th>
</tr>
</thead>
<tbody>
{% for c in clients %}
<tr>
<td>{{ c.id.0 }}</td>
<td>{{ c.name }}</td>
<td>{{ c.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}