Make secret more logsafe, implement add and edit for links

This commit is contained in:
2021-08-12 15:48:02 +02:00
committed by dietrich
parent d503d49917
commit b782d97920
18 changed files with 909 additions and 158 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ use clap::{
ArgMatches, SubCommand,
};
use dotenv::dotenv;
use shared::datatypes::User;
use shared::datatypes::{Secret, User};
use sqlx::{migrate::Migrator, Pool, Sqlite};
use std::{
fs::File,
@@ -151,7 +151,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
} else {
secret
};
let secret = pslink::Secret::new(secret);
let secret = Secret::new(secret);
let db = config
.value_of("database")
.expect(concat!(
+24 -25
View File
@@ -11,6 +11,7 @@ use actix_web::HttpResponse;
use actix_web::{web, App, HttpServer};
use fluent_templates::{static_loader, FluentLoader};
use qrcode::types::QrError;
use shared::datatypes::Secret;
use sqlx::{Pool, Sqlite};
use std::{fmt::Display, path::PathBuf, str::FromStr};
use tera::Tera;
@@ -156,30 +157,6 @@ impl FromStr for Protocol {
}
}
#[derive(Clone)]
pub struct Secret {
secret: String,
}
impl Secret {
#[must_use]
pub const fn new(secret: String) -> Self {
Self { secret }
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub secret: Secret,
@@ -209,7 +186,13 @@ impl ServerConfig {
"# If it is changed all existing passwords are invalid.\n"
)
.to_owned(),
format!("PSLINK_SECRET=\"{}\"\n", self.secret.secret),
format!(
"PSLINK_SECRET=\"{}\"\n",
self.secret
.secret
.as_ref()
.expect("A Secret was not specified!")
),
]
}
}
@@ -370,6 +353,18 @@ pub async fn webservice(
.service(
web::scope("/json")
.route("/list_links/", web::post().to(views::index_json))
.route(
"/create_link/",
web::post().to(views::process_create_link_json),
)
.route(
"/edit_link/",
web::post().to(views::process_update_link_json),
)
.route(
"/delete_link/",
web::post().to(views::process_delete_link_json),
)
.route("/list_users/", web::post().to(views::index_users_json))
.route(
"/create_user/",
@@ -378,6 +373,10 @@ pub async fn webservice(
.route(
"/update_user/",
web::post().to(views::process_update_user_json),
)
.route(
"/get_logged_user/",
web::post().to(views::get_logged_user_json),
),
)
// login to the admin area
+86 -14
View File
@@ -5,7 +5,12 @@ use async_trait::async_trait;
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
use shared::datatypes::{Count, Link, User};
use shared::{
apirequests::links::LinkDelta,
datatypes::{Count, Link, User},
};
use sqlx::Row;
use tracing::{error, info, instrument};
#[async_trait]
pub trait UserDbOperations<T> {
@@ -24,10 +29,19 @@ pub trait UserDbOperations<T> {
#[async_trait]
impl UserDbOperations<Self> for User {
#[instrument()]
async fn get_user(id: i64, server_config: &ServerConfig) -> Result<Self, ServerError> {
let user = sqlx::query_as!(Self, "Select * from users where id = ? ", id)
let user = sqlx::query!("Select * from users where id = ? ", id)
.fetch_one(&server_config.db_pool)
.await;
.await
.map(|row| Self {
id: row.id,
username: row.username,
email: row.email,
password: Secret::new(row.password),
role: row.role,
language: row.language,
});
user.map_err(ServerError::Database)
}
@@ -35,23 +49,46 @@ impl UserDbOperations<Self> for User {
///
/// # Errors
/// fails with [`ServerError`] if the user does not exist or the database cannot be acessed.
#[instrument()]
async fn get_user_by_name(
name: &str,
server_config: &ServerConfig,
) -> Result<Self, ServerError> {
let user = sqlx::query_as!(Self, "Select * from users where username = ? ", name)
let user = sqlx::query!("Select * from users where username = ? ", name)
.fetch_one(&server_config.db_pool)
.await;
.await
.map(|row| Self {
id: row.id,
username: row.username,
email: row.email,
password: Secret::new(row.password),
role: row.role,
language: row.language,
});
user.map_err(ServerError::Database)
}
#[instrument()]
async fn get_all_users(server_config: &ServerConfig) -> Result<Vec<Self>, ServerError> {
let user = sqlx::query_as!(Self, "Select * from users")
let user = sqlx::query("Select * from users")
.fetch_all(&server_config.db_pool)
.await;
.await
.map(|row| {
row.into_iter()
.map(|r| Self {
id: r.get("id"),
username: r.get("username"),
email: r.get("email"),
password: Secret::new(r.get("password")),
role: r.get("role"),
language: r.get("language"),
})
.collect()
});
user.map_err(ServerError::Database)
}
#[instrument()]
async fn update_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!(
"UPDATE users SET
@@ -61,7 +98,7 @@ impl UserDbOperations<Self> for User {
role = ? where id = ?",
self.username,
self.email,
self.password,
self.password.secret,
self.role,
self.id
)
@@ -69,10 +106,12 @@ impl UserDbOperations<Self> for User {
.await?;
Ok(())
}
/// Change an admin user to normal user and a normal user to admin
///
/// # Errors
/// fails with [`ServerError`] if the database cannot be acessed. (the user should exist)
#[instrument()]
async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> {
let new_role = 2 - (self.role + 1) % 2;
sqlx::query!("UPDATE users SET role = ? where id = ?", new_role, self.id)
@@ -81,6 +120,7 @@ impl UserDbOperations<Self> for User {
Ok(())
}
#[instrument()]
async fn set_language(
self,
server_config: &ServerConfig,
@@ -102,6 +142,7 @@ impl UserDbOperations<Self> for User {
///
/// # Errors
/// fails with [`ServerError`] if the database cannot be acessed.
#[instrument()]
async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> {
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
.fetch_one(&server_config.db_pool)
@@ -122,6 +163,7 @@ impl NewUser {
///
/// # Errors
/// fails with [`ServerError`] if the password could not be encrypted.
#[instrument()]
pub fn new(
username: String,
email: String,
@@ -137,12 +179,13 @@ impl NewUser {
})
}
#[instrument()]
pub(crate) fn hash_password(password: &str, secret: &Secret) -> Result<String, ServerError> {
dotenv().ok();
let hash = Hasher::default()
.with_password(password)
.with_secret_key(&secret.secret)
.with_secret_key(secret.secret.as_ref().expect("A secret key was not given"))
.hash()?;
Ok(hash)
@@ -152,6 +195,7 @@ impl NewUser {
///
/// # Errors
/// fails with [`ServerError`] if the database cannot be acessed.
#[instrument()]
pub async fn insert_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!(
"Insert into users (
@@ -178,6 +222,7 @@ pub struct LoginUser {
#[async_trait]
pub trait LinkDbOperations<T> {
async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result<T, ServerError>;
async fn get_link_by_id(id: i64, server_config: &ServerConfig) -> Result<T, ServerError>;
async fn delete_link_by_code(
code: &str,
server_config: &ServerConfig,
@@ -187,6 +232,7 @@ pub trait LinkDbOperations<T> {
#[async_trait]
impl LinkDbOperations<Self> for Link {
#[instrument()]
async fn get_link_by_code(
code: &str,
server_config: &ServerConfig,
@@ -197,7 +243,16 @@ impl LinkDbOperations<Self> for Link {
tracing::info!("Found link: {:?}", &link);
link.map_err(ServerError::Database)
}
#[instrument()]
async fn get_link_by_id(id: i64, server_config: &ServerConfig) -> Result<Self, ServerError> {
let link = sqlx::query_as!(Self, "Select * from links where id = ? ", id)
.fetch_one(&server_config.db_pool)
.await;
tracing::info!("Found link: {:?}", &link);
link.map_err(ServerError::Database)
}
#[instrument()]
async fn delete_link_by_code(
code: &str,
server_config: &ServerConfig,
@@ -207,8 +262,11 @@ impl LinkDbOperations<Self> for Link {
.await?;
Ok(())
}
#[instrument()]
async fn update_link(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!(
info!("{:?}", self);
let qry = sqlx::query!(
"UPDATE links SET
title = ?,
target = ?,
@@ -221,10 +279,15 @@ impl LinkDbOperations<Self> for Link {
self.author,
self.created_at,
self.id
)
.execute(&server_config.db_pool)
.await?;
Ok(())
);
match qry.execute(&server_config.db_pool).await {
Ok(_) => Ok(()),
Err(e) => {
//error!("{}", qry);
error!("{}", e);
Err(e.into())
}
}
}
}
@@ -247,6 +310,15 @@ impl NewLink {
created_at: chrono::Local::now().naive_utc(),
}
}
pub(crate) fn from_link_delta(link: LinkDelta, uid: i64) -> Self {
Self {
title: link.title,
target: link.target,
code: link.code,
author: uid,
created_at: chrono::Local::now().naive_utc(),
}
}
pub(crate) async fn insert(self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!(
+103 -6
View File
@@ -5,10 +5,10 @@ use serde::Serialize;
use shared::{
apirequests::{
general::{EditMode, Filter, Operation, Ordering},
links::{LinkOverviewColumns, LinkRequestForm},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
users::{UserDelta, UserOverviewColumns, UserRequestForm},
},
datatypes::{Count, FullLink, Link, User},
datatypes::{Count, FullLink, Link, Secret, User},
};
use sqlx::Row;
use tracing::{info, instrument, warn};
@@ -127,7 +127,7 @@ pub async fn list_all_allowed(
id: v.get("usid"),
username: v.get("usern"),
email: v.get("uemail"),
password: "invalid".to_owned(),
password: Secret::new("invalid".to_string()),
role: v.get("urole"),
language: v.get("ulang"),
},
@@ -240,7 +240,7 @@ pub async fn list_users(
id: v.get("id"),
username: v.get("username"),
email: v.get("email"),
password: "invalid".to_owned(),
password: Secret::new("".to_string()),
role: v.get("role"),
language: v.get("language"),
})
@@ -458,7 +458,9 @@ pub async fn update_user_json(
Role::Admin { .. } | Role::Regular { .. } => {
info!("Updating userinfo: ");
let password = match &data.password {
Some(password) => NewUser::hash_password(password, &server_config.secret)?,
Some(password) => {
Secret::new(NewUser::hash_password(password, &server_config.secret)?)
}
None => unmodified_user.password,
};
let new_user = User {
@@ -510,7 +512,10 @@ pub async fn update_user(
Role::Admin { .. } | Role::Regular { .. } => {
info!("Updating userinfo: ");
let password = if data.password.len() > 3 {
NewUser::hash_password(&data.password, &server_config.secret)?
Secret::new(NewUser::hash_password(
&data.password,
&server_config.secret,
)?)
} else {
unmodified_user.password
};
@@ -629,6 +634,28 @@ pub async fn get_link(
}
}
/// Get one link if permissions are accordingly.
///
/// # Errors
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
#[instrument(skip(id))]
pub async fn get_link_by_id(
id: &Identity,
lid: i64,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
match authenticate(id, server_config).await? {
Role::Admin { user } | Role::Regular { user } => {
let link = Link::get_link_by_id(lid, server_config).await?;
Ok(Item { user, item: link })
}
Role::Disabled | Role::NotAuthenticated => {
warn!("User could not be authenticated!");
Err(ServerError::User("Not Allowed".to_owned()))
}
}
}
/// Get link **without authentication**
///
/// # Errors
@@ -743,3 +770,73 @@ pub async fn create_link(
}
}
}
/// Create a new link
///
/// # Errors
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
#[instrument(skip(id))]
pub async fn create_link_json(
id: &Identity,
data: web::Json<LinkDelta>,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
let auth = authenticate(id, server_config).await?;
match auth {
Role::Admin { user } | Role::Regular { user } => {
let code = data.code.clone();
info!("Creating link for: {}", &code);
let new_link = NewLink::from_link_delta(data.into_inner(), user.id);
info!("Creating link for: {:?}", &new_link);
new_link.insert(server_config).await?;
let new_link: Link = get_link_simple(&code, server_config).await?;
Ok(Item {
user,
item: new_link,
})
}
Role::Disabled | Role::NotAuthenticated => {
Err(ServerError::User("Permission denied!".to_owned()))
}
}
}
/// Update a link if the user is admin or it is its own link.
///
/// # Errors
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
#[instrument(skip(ident))]
pub async fn update_link_json(
ident: &Identity,
data: web::Json<LinkDelta>,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
let auth = authenticate(ident, server_config).await?;
match auth {
Role::Admin { .. } | Role::Regular { .. } => {
if let Some(id) = data.id {
let query: Item<Link> = get_link_by_id(ident, id, server_config).await?;
if auth.admin_or_self(query.item.author) {
let mut link = query.item;
let LinkDelta {
title,
target,
code,
..
} = data.into_inner();
link.code = code.clone();
link.target = target;
link.title = title;
link.update_link(server_config).await?;
get_link(ident, &code, server_config).await
} else {
Err(ServerError::User("Invalid Request".to_owned()))
}
} else {
Err(ServerError::User("Not Allowed".to_owned()))
}
}
Role::Disabled | Role::NotAuthenticated => Err(ServerError::User("Not Allowed".to_owned())),
}
}
+78 -18
View File
@@ -15,8 +15,8 @@ use image::{DynamicImage, ImageOutputFormat, Luma};
use qrcode::{render::svg, QrCode};
use queries::{authenticate, Role};
use shared::apirequests::{
general::SuccessMessage,
links::LinkRequestForm,
general::{Message, Status},
links::{LinkDelta, LinkRequestForm},
users::{UserDelta, UserRequestForm},
};
use tera::{Context, Tera};
@@ -171,6 +171,19 @@ pub async fn index_users_json(
}
}
pub async fn get_logged_user_json(
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let user = authenticate(&id, &config).await?;
match user {
Role::NotAuthenticated | Role::Disabled => {
Err(ServerError::User("User not logged in!".to_string()))
}
Role::Regular { user } | Role::Admin { user } => Ok(HttpResponse::Ok().json2(&user)),
}
}
#[instrument(skip(id, tera))]
pub async fn view_link_empty(
tera: web::Data<Tera>,
@@ -358,9 +371,9 @@ pub async fn process_create_user_json(
) -> Result<HttpResponse, ServerError> {
info!("Listing Users to Json api");
match queries::create_user_json(&id, &form, &config).await {
Ok(item) => Ok(HttpResponse::Ok().json2(&SuccessMessage {
Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully saved user: {}", item.item.username),
})),
}))),
Err(e) => Err(e),
}
}
@@ -373,9 +386,9 @@ pub async fn process_update_user_json(
) -> Result<HttpResponse, ServerError> {
info!("Listing Users to Json api");
match queries::update_user_json(&id, &form, &config).await {
Ok(item) => Ok(HttpResponse::Ok().json2(&SuccessMessage {
Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully saved user: {}", item.item.username),
})),
}))),
Err(e) => Err(e),
}
}
@@ -447,20 +460,26 @@ pub async fn process_login(
match user {
Ok(u) => {
let secret = &config.secret;
let valid = Verifier::default()
.with_hash(&u.password)
.with_password(&data.password)
.with_secret_key(&secret.secret)
.verify()?;
if let Some(hash) = u.password.secret {
let secret = &config.secret;
let valid = Verifier::default()
.with_hash(hash)
.with_password(&data.password)
.with_secret_key(secret.secret.as_ref().expect("No secret available"))
.verify()?;
if valid {
info!("Log-in of user: {}", &u.username);
let session_token = u.username;
id.remember(session_token);
Ok(redirect_builder("/admin/index/"))
if valid {
info!("Log-in of user: {}", &u.username);
let session_token = u.username;
id.remember(session_token);
Ok(redirect_builder("/admin/index/"))
} else {
Ok(redirect_builder("/admin/login/"))
}
} else {
Ok(redirect_builder("/admin/login/"))
Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message {
message: "Failed to Login".to_string(),
})))
}
}
Err(e) => {
@@ -515,6 +534,36 @@ pub async fn redirect_empty(
Ok(redirect_builder(&config.empty_forward_url))
}
#[instrument(skip(id))]
pub async fn process_create_link_json(
config: web::Data<crate::ServerConfig>,
data: web::Json<LinkDelta>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let new_link = queries::create_link_json(&id, data, &config).await;
match new_link {
Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully saved link: {}", item.item.code),
}))),
Err(e) => Err(e),
}
}
#[instrument(skip(id))]
pub async fn process_update_link_json(
config: web::Data<crate::ServerConfig>,
data: web::Json<LinkDelta>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let new_link = queries::update_link_json(&id, data, &config).await;
match new_link {
Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully updated link: {}", item.item.code),
}))),
Err(e) => Err(e),
}
}
#[instrument(skip(id))]
pub async fn create_link(
tera: web::Data<Tera>,
@@ -591,3 +640,14 @@ pub async fn process_link_delete(
queries::delete_link(&id, &link_code.0, &config).await?;
Ok(redirect_builder("/admin/login/"))
}
#[instrument(skip(id))]
pub async fn process_delete_link_json(
id: Identity,
config: web::Data<crate::ServerConfig>,
data: web::Json<LinkDelta>,
) -> Result<HttpResponse, ServerError> {
queries::delete_link(&id, &data.code, &config).await?;
Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully deleted link: {}", &data.code),
})))
}
+9
View File
@@ -43,6 +43,7 @@ form {
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 10px;
}
th,
@@ -184,4 +185,12 @@ div.message {
background-color: aliceblue;
border: 5px solid rgb(90, 90, 90);
height: auto;
}
a {
cursor: pointer
}
img.trashicon {
width: 0.5cm;
}
+19
View File
@@ -0,0 +1,19 @@
<svg width="99.857mm" height="134.86mm" version="1.1" viewBox="0 0 99.857 134.86" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-58.021 -74.624)">
<path d="m65.101 94.716h84.426l-10.99 112.77h-62.431z" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:4;stroke:#000"/>
<path d="m107.57 113.88v74.637" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
<path d="m86.154 113.62 5.4485 74.438" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
<path d="m129.73 114.36-5.5378 74.432" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
<path d="m59.882 87.653h96.136" style="fill:none;stroke-linecap:round;stroke-width:3.721;stroke:#000"/>
<path d="m93.588 87.521c13.139-19.351 20.218-10.383 27.029-0.1697" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+3 -1
View File
@@ -1,3 +1,5 @@
use shared::datatypes::Secret;
#[test]
fn test_help_of_command_for_breaking_changes() {
let output = test_bin::get_test_bin("pslink")
@@ -208,7 +210,7 @@ async fn run_server() {
);
let server_config = pslink::ServerConfig {
secret: pslink::Secret::new("abcdefghijklmnopqrstuvw".to_string()),
secret: Secret::new("abcdefghijklmnopqrstuvw".to_string()),
db: std::path::PathBuf::from("links.db"),
db_pool,
public_url: "localhost:8080".to_string(),