Merge pull request #14 from fLotte-meets-HWR-DB/develop

REST methods for user management
leon_tries_rust
Trivernis 4 years ago committed by GitHub
commit 36dae3ad2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,7 +8,7 @@ use r2d2::Pool;
use r2d2_postgres::PostgresConnectionManager; use r2d2_postgres::PostgresConnectionManager;
use crate::database::models::CreatePermissionsEntry; use crate::database::models::CreatePermissionsEntry;
use crate::database::permissions::{Permissions, DEFAULT_PERMISSIONS}; use crate::database::permissions::{Permissions, USER_MANAGEMENT_PERMISSIONS};
use crate::database::role_permissions::RolePermissions; use crate::database::role_permissions::RolePermissions;
use crate::database::roles::Roles; use crate::database::roles::Roles;
use crate::database::user_roles::UserRoles; use crate::database::user_roles::UserRoles;
@ -94,7 +94,7 @@ impl Database {
log::debug!("Failed to create admin role {}", e.to_string()) log::debug!("Failed to create admin role {}", e.to_string())
} }
self.permissions.create_permissions( self.permissions.create_permissions(
DEFAULT_PERMISSIONS USER_MANAGEMENT_PERMISSIONS
.iter() .iter()
.map(|(name, description)| CreatePermissionsEntry { .map(|(name, description)| CreatePermissionsEntry {
name: name.to_string(), name: name.to_string(),

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use zeroize::Zeroize; use zeroize::Zeroize;
/// Record to store data in when retrieving rows from the users table /// Record to store data in when retrieving rows from the users table
#[derive(Clone, Debug, Zeroize)] #[derive(Clone, Debug, Zeroize, Serialize)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct UserRecord { pub struct UserRecord {
pub id: i32, pub id: i32,
@ -55,3 +55,29 @@ pub struct CreatePermissionsEntry {
pub name: String, pub name: String,
pub description: String, pub description: String,
} }
/// Information about the user that doesn't contain any critical information
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct UserInformation {
pub id: i32,
pub name: String,
pub email: String,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct UserFullInformation {
pub id: i32,
pub name: String,
pub email: String,
pub roles: Vec<Role>,
}
impl From<UserRecord> for UserInformation {
fn from(record: UserRecord) -> Self {
Self {
id: record.id.clone(),
name: record.name.clone(),
email: record.email.clone(),
}
}
}

@ -7,15 +7,28 @@ use crate::database::{DatabaseResult, PostgresPool, Table, ADMIN_ROLE_NAME};
use std::collections::HashSet; use std::collections::HashSet;
use std::iter::FromIterator; use std::iter::FromIterator;
pub(crate) const VIEW_ROLE_PERMISSION: &str = "ROLE_VIEW"; pub(crate) const ROLE_VIEW_PERM: &str = "ROLE_VIEW";
pub(crate) const CREATE_ROLE_PERMISSION: &str = "ROLE_CREATE"; pub(crate) const ROLE_CREATE_PERM: &str = "ROLE_CREATE";
pub(crate) const UPDATE_ROLE_PERMISSION: &str = "ROLE_UPDATE"; pub(crate) const ROLE_UPDATE_PERM: &str = "ROLE_UPDATE";
pub(crate) const DELETE_ROLE_PERMISSION: &str = "ROLE_DELETE"; pub(crate) const ROLE_DELETE_PERM: &str = "ROLE_DELETE";
pub(crate) const DEFAULT_PERMISSIONS: &[(&'static str, &'static str)] = &[
(CREATE_ROLE_PERMISSION, "Allows the user to create roles"), pub(crate) const USER_UPDATE_PERM: &str = "USER_UPDATE";
(UPDATE_ROLE_PERMISSION, "Allows the user to update roles"), pub(crate) const USER_VIEW_PERM: &str = "USER_VIEW";
(DELETE_ROLE_PERMISSION, "Allows the user to delete roles"), pub(crate) const USER_CREATE_PERM: &str = "USER_CREATE";
(VIEW_ROLE_PERMISSION, "Allows to see information for roles"), pub(crate) const USER_DELETE_PERM: &str = "USER_DELETE";
pub(crate) const USER_MANAGEMENT_PERMISSIONS: &[(&'static str, &'static str)] = &[
(ROLE_CREATE_PERM, "Allows the user to create roles"),
(ROLE_UPDATE_PERM, "Allows the user to update roles"),
(ROLE_DELETE_PERM, "Allows the user to delete roles"),
(ROLE_VIEW_PERM, "Allows to see information of roles"),
(
USER_UPDATE_PERM,
"Allows changing the name, password and email of a user",
),
(USER_VIEW_PERM, "Allows to see information of users"),
(USER_CREATE_PERM, "Allows the creation of new users"),
(USER_DELETE_PERM, "Allows the deletion of users"),
]; ];
/// The permissions table that stores defined /// The permissions table that stores defined

@ -7,7 +7,7 @@ use std::sync::Arc;
use parking_lot::Mutex; use parking_lot::Mutex;
use zeroize::{Zeroize, Zeroizing}; use zeroize::{Zeroize, Zeroizing};
use crate::database::models::UserRecord; use crate::database::models::{UserInformation, UserRecord};
use crate::database::tokens::{SessionTokens, TokenStore}; use crate::database::tokens::{SessionTokens, TokenStore};
use crate::database::user_roles::UserRoles; use crate::database::user_roles::UserRoles;
use crate::database::{DatabaseResult, PostgresPool, Table}; use crate::database::{DatabaseResult, PostgresPool, Table};
@ -78,6 +78,105 @@ impl Users {
Ok(UserRecord::from_ordered_row(&row)) Ok(UserRecord::from_ordered_row(&row))
} }
pub fn update_user(
&self,
old_email: &String,
name: &String,
email: &String,
password: &Option<String>,
) -> DatabaseResult<UserInformation> {
log::trace!(
"Updating user {} with new entries name: {}, email: {}",
old_email,
name,
email
);
let mut connection = self.pool.get()?;
if connection
.query_opt("SELECT email FROM users WHERE email = $1", &[&old_email])?
.is_none()
{
log::trace!("Failed to create user: Record doesn't exist!");
return Err(DBError::RecordDoesNotExist);
}
if old_email != email
&& connection
.query_opt("SELECT email FROM users WHERE email = $1", &[&email])?
.is_some()
{
log::trace!("Failed to create user: New Record exists!");
return Err(DBError::GenericError(format!(
"A user for the email {} already exists!",
email
)));
}
let new_record = if let Some(password) = password {
let salt = Zeroizing::new(create_salt());
let pw_hash =
hash_password(password.as_bytes(), &*salt).map_err(|e| DBError::GenericError(e))?;
connection.query_one(
"UPDATE users SET name = $1, email = $2, password_hash = $3, salt = $4 WHERE email = $5 RETURNING *",
&[&name, &email, &pw_hash.to_vec(), &salt.to_vec(), &old_email],
)?
} else {
connection.query_one(
"UPDATE users SET name = $1, email = $2 WHERE email = $3 RETURNING *",
&[&name, &email, &old_email],
)?
};
Ok(serde_postgres::from_row::<UserInformation>(&new_record)?)
}
/// Returns information about a user by Id
pub fn get_user(&self, id: i32) -> DatabaseResult<UserInformation> {
log::trace!("Looking up entry for user with id {}", id);
let mut connection = self.pool.get()?;
let result = connection
.query_opt("SELECT id, name, email FROM users WHERE id = $1", &[&id])?
.ok_or(DBError::RecordDoesNotExist)?;
Ok(serde_postgres::from_row::<UserInformation>(&result)?)
}
pub fn get_user_by_email(&self, email: &String) -> DatabaseResult<UserInformation> {
log::trace!("Looking up entry for user with email {}", email);
let mut connection = self.pool.get()?;
let result = connection
.query_opt(
"SELECT id, name, email FROM users WHERE email = $1",
&[email],
)?
.ok_or(DBError::RecordDoesNotExist)?;
Ok(serde_postgres::from_row::<UserInformation>(&result)?)
}
pub fn get_users(&self) -> DatabaseResult<Vec<UserInformation>> {
log::trace!("Returning a list of all users...");
let mut connection = self.pool.get()?;
let results = connection.query("SELECT id, name, email FROM users", &[])?;
let mut users = Vec::new();
for result in results {
users.push(serde_postgres::from_row::<UserInformation>(&result)?);
}
Ok(users)
}
pub fn delete_user(&self, email: &String) -> DatabaseResult<()> {
log::trace!("Deleting user with email {}", email);
let mut connection = self.pool.get()?;
let exists = connection.query_opt("SELECT id FROM users WHERE email = $1", &[email])?;
if exists.is_none() {
return Err(DBError::RecordDoesNotExist);
}
connection.query("DELETE FROM users WHERE email = $1", &[email])?;
Ok(())
}
/// Creates new tokens for a user login that can be used by services /// Creates new tokens for a user login that can be used by services
/// that need those tokens to verify a user login /// that need those tokens to verify a user login
pub fn create_tokens( pub fn create_tokens(
@ -85,6 +184,7 @@ impl Users {
email: &String, email: &String,
password: &String, password: &String,
) -> DatabaseResult<SessionTokens> { ) -> DatabaseResult<SessionTokens> {
log::trace!("Creating new tokens for user with email {}", email);
if self.validate_login(&email, password)? { if self.validate_login(&email, password)? {
let mut connection = self.pool.get()?; let mut connection = self.pool.get()?;
let row = connection.query_one("SELECT id FROM users WHERE email = $1", &[&email])?; let row = connection.query_one("SELECT id FROM users WHERE email = $1", &[&email])?;
@ -170,7 +270,7 @@ impl Users {
/// Validates the login data of the user by creating the hash for the given password /// Validates the login data of the user by creating the hash for the given password
/// and comparing it with the database entry /// and comparing it with the database entry
fn validate_login(&self, email: &String, password: &String) -> DatabaseResult<bool> { pub fn validate_login(&self, email: &String, password: &String) -> DatabaseResult<bool> {
let mut connection = self.pool.get()?; let mut connection = self.pool.get()?;
let row = connection let row = connection
.query_opt( .query_opt(

@ -23,7 +23,6 @@ impl RESTDocumentation {
} }
pub fn get(&self, path: String) -> String { pub fn get(&self, path: String) -> String {
log::trace!("Rendering help for {}.", path);
format!( format!(
"<html><head><style type='text/css'>{}</style></head><body>{}</body></html>", "<html><head><style type='text/css'>{}</style></head><body>{}</body></html>",
include_str!("style.css"), include_str!("style.css"),
@ -47,6 +46,7 @@ impl RESTDocumentation {
method: &str, method: &str,
description: &str, description: &str,
) -> Result<(), serde_json::error::Error> { ) -> Result<(), serde_json::error::Error> {
log::trace!("Prerendering documentation for {}", path);
let input_schema = schema_for!(I); let input_schema = schema_for!(I);
let output_schema = schema_for!(O); let output_schema = schema_for!(O);
@ -65,6 +65,8 @@ impl RESTDocumentation {
self.base_path, method, path, description, input_json, output_json self.base_path, method, path, description, input_json, output_json
); );
self.paths.insert(path.to_string(), content); self.paths.insert(path.to_string(), content);
log::trace!("Documentation for {} rendered", path);
Ok(()) Ok(())
} }
} }

@ -11,19 +11,22 @@ use rouille::{Request, Response, Server};
use serde::export::Formatter; use serde::export::Formatter;
use serde::Serialize; use serde::Serialize;
use crate::database::models::Role; use crate::database::models::{Role, UserFullInformation, UserInformation};
use crate::database::permissions::{ use crate::database::permissions::{
CREATE_ROLE_PERMISSION, DELETE_ROLE_PERMISSION, UPDATE_ROLE_PERMISSION, VIEW_ROLE_PERMISSION, ROLE_CREATE_PERM, ROLE_DELETE_PERM, ROLE_UPDATE_PERM, ROLE_VIEW_PERM, USER_CREATE_PERM,
USER_DELETE_PERM, USER_UPDATE_PERM, USER_VIEW_PERM,
}; };
use crate::database::tokens::SessionTokens; use crate::database::tokens::SessionTokens;
use crate::database::Database; use crate::database::Database;
use crate::server::documentation::RESTDocumentation; use crate::server::documentation::RESTDocumentation;
use crate::server::messages::{ use crate::server::messages::{
DeleteRoleResponse, ErrorMessage, FullRoleData, LoginMessage, LogoutConfirmation, CreateUserRequest, DeleteRoleResponse, DeleteUserRequest, DeleteUserResponse, ErrorMessage,
LogoutMessage, ModifyRoleRequest, RefreshMessage, FullRoleData, LoginMessage, LogoutConfirmation, LogoutMessage, ModifyRoleRequest,
RefreshMessage, UpdateUserRequest,
}; };
use crate::utils::error::DBError; use crate::utils::error::DBError;
use crate::utils::get_user_id_from_token; use crate::utils::get_user_id_from_token;
use serde::de::DeserializeOwned;
macro_rules! require_permission { macro_rules! require_permission {
($database:expr,$request:expr,$permission:expr) => { ($database:expr,$request:expr,$permission:expr) => {
@ -127,6 +130,21 @@ impl UserHttpServer {
(POST) (/roles/{name: String}/delete) => { (POST) (/roles/{name: String}/delete) => {
Self::delete_role(&database, request, name).unwrap_or_else(HTTPError::into) Self::delete_role(&database, request, name).unwrap_or_else(HTTPError::into)
}, },
(GET) (/users/{email: String}) => {
Self::get_user(&database, request, email).unwrap_or_else(HTTPError::into)
},
(GET) (/users) => {
Self::get_users(&database, request).unwrap_or_else(HTTPError::into)
},
(POST) (/users/create) => {
Self::create_user(&database, request).unwrap_or_else(HTTPError::into)
},
(POST) (/users/{email: String}/update) => {
Self::update_user(&database, request, email).unwrap_or_else(HTTPError::into)
},
(POST) (/users/{email: String}/delete) => {
Self::delete_user(&database, request, email).unwrap_or_else(HTTPError::into)
},
_ => if request.method() == "OPTIONS" { _ => if request.method() == "OPTIONS" {
Response::empty_204() Response::empty_204()
} else { } else {
@ -196,6 +214,31 @@ impl UserHttpServer {
"POST", "POST",
"Deletes a role", "Deletes a role",
)?; )?;
doc.add_path::<UpdateUserRequest, UserInformation>(
"/users/{email:String}/update",
"POST",
"Change user information",
)?;
doc.add_path::<(), UserFullInformation>(
"/users/{email:String}",
"GET",
"See user information",
)?;
doc.add_path::<(), Vec<UserInformation>>(
"/users",
"GET",
"Returns information for all users",
)?;
doc.add_path::<CreateUserRequest, UserInformation>(
"/users/create",
"POST",
"Creates a new user",
)?;
doc.add_path::<DeleteUserRequest, DeleteUserResponse>(
"/users/{email:String}/delete",
"POST",
"Deletes a user",
)?;
Ok(doc) Ok(doc)
} }
@ -241,7 +284,7 @@ impl UserHttpServer {
/// Returns the data for a given role /// Returns the data for a given role
fn get_role(database: &Database, request: &Request, name: String) -> HTTPResult<Response> { fn get_role(database: &Database, request: &Request, name: String) -> HTTPResult<Response> {
require_permission!(database, request, VIEW_ROLE_PERMISSION); require_permission!(database, request, ROLE_VIEW_PERM);
let role = database.roles.get_role(name)?; let role = database.roles.get_role(name)?;
let permissions = database.role_permission.by_role(role.id)?; let permissions = database.role_permission.by_role(role.id)?;
@ -254,14 +297,15 @@ impl UserHttpServer {
/// Returns a list of all roles /// Returns a list of all roles
fn get_roles(database: &Database, request: &Request) -> HTTPResult<Response> { fn get_roles(database: &Database, request: &Request) -> HTTPResult<Response> {
require_permission!(database, request, VIEW_ROLE_PERMISSION); require_permission!(database, request, ROLE_VIEW_PERM);
let roles = database.roles.get_roles()?; let roles = database.roles.get_roles()?;
Ok(Response::json(&roles)) Ok(Response::json(&roles))
} }
/// Creates a new role with the given permissions
fn create_role(database: &Database, request: &Request) -> HTTPResult<Response> { fn create_role(database: &Database, request: &Request) -> HTTPResult<Response> {
require_permission!(database, request, CREATE_ROLE_PERMISSION); require_permission!(database, request, ROLE_CREATE_PERM);
let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str()) let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str())
.map_err(|e| HTTPError::new(e.to_string(), 400))?; .map_err(|e| HTTPError::new(e.to_string(), 400))?;
let not_existing = database let not_existing = database
@ -288,10 +332,11 @@ impl UserHttpServer {
.with_status_code(201)) .with_status_code(201))
} }
/// Updates information for a single role
fn update_role(database: &Database, request: &Request, name: String) -> HTTPResult<Response> { fn update_role(database: &Database, request: &Request, name: String) -> HTTPResult<Response> {
require_permission!(database, request, UPDATE_ROLE_PERMISSION); require_permission!(database, request, ROLE_UPDATE_PERM);
let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str()) let message: ModifyRoleRequest = deserialize_body(&request)?;
.map_err(|e| HTTPError::new(e.to_string(), 400))?;
let not_existing = database let not_existing = database
.permissions .permissions
.get_not_existing(&message.permissions)?; .get_not_existing(&message.permissions)?;
@ -317,8 +362,9 @@ impl UserHttpServer {
})) }))
} }
/// Deletes a role from the database
fn delete_role(database: &Database, request: &Request, role: String) -> HTTPResult<Response> { fn delete_role(database: &Database, request: &Request, role: String) -> HTTPResult<Response> {
require_permission!(database, request, DELETE_ROLE_PERMISSION); require_permission!(database, request, ROLE_DELETE_PERM);
database.roles.delete_role(&role)?; database.roles.delete_role(&role)?;
Ok(Response::json(&DeleteRoleResponse { Ok(Response::json(&DeleteRoleResponse {
@ -326,6 +372,96 @@ impl UserHttpServer {
role, role,
})) }))
} }
/// Returns information for a single user
fn get_user(database: &Database, request: &Request, email: String) -> HTTPResult<Response> {
require_permission!(database, request, USER_VIEW_PERM);
let user = database.users.get_user_by_email(&email)?;
let roles = database.user_roles.by_user(user.id)?;
Ok(Response::json(&UserFullInformation {
id: user.id,
name: user.name,
email: user.email,
roles,
}))
}
/// Returns a list of all users
fn get_users(database: &Database, request: &Request) -> HTTPResult<Response> {
require_permission!(database, request, USER_VIEW_PERM);
let users = database.users.get_users()?;
Ok(Response::json(&users))
}
/// Creates a new user
fn create_user(database: &Database, request: &Request) -> HTTPResult<Response> {
require_permission!(database, request, USER_CREATE_PERM);
let message = deserialize_body::<CreateUserRequest>(&request)?;
let result = database.users.create_user(
message.name.clone(),
message.email.clone(),
message.password.clone(),
)?;
Ok(Response::json(&UserInformation::from(result)).with_status_code(201))
}
/// Updates the information of a user. This requires the operating user to revalidate his password
fn update_user(database: &Database, request: &Request, email: String) -> HTTPResult<Response> {
let (_, id) = validate_request_token(request, database)?;
let message = deserialize_body::<UpdateUserRequest>(&request)?;
let logged_in_user = database.users.get_user(id)?;
if !database
.users
.validate_login(&logged_in_user.email, &message.own_password)?
{
return Err(HTTPError::new(
"Invalid authentication data".to_string(),
401,
));
}
if logged_in_user.email != email {
require_permission!(database, request, USER_UPDATE_PERM);
}
let user_record = database.users.get_user_by_email(&email)?;
let record = database.users.update_user(
&email,
&message.name.clone().unwrap_or(user_record.name),
&message.email.clone().unwrap_or(user_record.email),
&message.password,
)?;
Ok(Response::json(&record))
}
/// Deletes a user completely
fn delete_user(database: &Database, request: &Request, email: String) -> HTTPResult<Response> {
let (_, id) = validate_request_token(request, database)?;
let message = deserialize_body::<DeleteUserRequest>(&request)?;
let logged_in_user = database.users.get_user(id)?;
if !database
.users
.validate_login(&logged_in_user.email, &message.own_password)?
{
return Err(HTTPError::new(
"Invalid authentication data".to_string(),
401,
));
}
if !database.users.has_permission(id, USER_DELETE_PERM)? {
return Err(HTTPError::new("Insufficient permissions".to_string(), 403));
}
database.users.delete_user(&email)?;
Ok(Response::json(&DeleteUserResponse {
success: true,
email,
}))
}
} }
/// Parses the body of a http request into a string representation /// Parses the body of a http request into a string representation
@ -340,6 +476,12 @@ fn parse_string_body(request: &Request) -> HTTPResult<String> {
Ok(string_body) Ok(string_body)
} }
/// Deserialized a json body into the given type
fn deserialize_body<T: DeserializeOwned>(request: &Request) -> HTTPResult<T> {
serde_json::from_str(parse_string_body(request)?.as_str())
.map_err(|e| HTTPError::new(e.to_string(), 400))
}
/// Parses and validates the request token from the http header /// Parses and validates the request token from the http header
fn validate_request_token(request: &Request, database: &Database) -> HTTPResult<(String, i32)> { fn validate_request_token(request: &Request, database: &Database) -> HTTPResult<(String, i32)> {
lazy_static::lazy_static! {static ref BEARER_REGEX: Regex = Regex::new(r"^[bB]earer\s+").unwrap();} lazy_static::lazy_static! {static ref BEARER_REGEX: Regex = Regex::new(r"^[bB]earer\s+").unwrap();}

@ -119,3 +119,32 @@ pub struct DeleteRoleResponse {
pub success: bool, pub success: bool,
pub role: String, pub role: String,
} }
#[derive(Deserialize, JsonSchema, Zeroize)]
#[zeroize(drop)]
pub struct UpdateUserRequest {
pub name: Option<String>,
pub email: Option<String>,
pub password: Option<String>,
pub own_password: String,
}
#[derive(Deserialize, JsonSchema, Zeroize)]
#[zeroize(drop)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
pub password: String,
}
#[derive(Deserialize, JsonSchema, Zeroize)]
#[zeroize(drop)]
pub struct DeleteUserRequest {
pub own_password: String,
}
#[derive(Serialize, JsonSchema)]
pub struct DeleteUserResponse {
pub email: String,
pub success: bool,
}

Loading…
Cancel
Save