Extended login system, added member selection and database query to get full member (incl. permissions)

This commit is contained in:
Keanu D?lle 2020-05-28 19:05:38 +02:00
parent d569d134e7
commit b9f084b2ec
28 changed files with 303 additions and 25 deletions

1
Cargo.lock generated
View File

@ -205,6 +205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_derives 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -13,7 +13,7 @@ serde = "1.0.104"
log = "0.4.8"
env_logger = "0.7.1"
rocket = "0.4.3"
diesel = { version = "1.4", features = ["postgres", "uuid"] }
diesel = { version = "1.4", features = ["postgres", "uuid", "chrono"] }
diesel_geometry = "1.4.0"
uuid = { version = "0.6", features = ["serde"] } #Only version 0.6 is supported by diesel 1.4
rust-argon2 = "0.8"

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table permissions;

View File

@ -0,0 +1,8 @@
-- Your SQL goes here
create table permissions
(
permission text not null
constraint permissions_pk
primary key,
description text
);

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table roles;

View File

@ -0,0 +1,8 @@
-- Your SQL goes here
create table roles
(
id text not null
constraint roles_pk
primary key,
description text
);

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table roles_permissions;

View File

@ -0,0 +1,14 @@
-- Your SQL goes here
create table roles_permissions
(
role_id text not null
constraint roles_permissions_roles_id_fk
references roles
on update cascade on delete cascade,
permission_id text not null
constraint roles_permissions_permissions_permission_fk
references permissions
on update cascade on delete cascade,
constraint roles_permissions_pk
primary key (role_id, permission_id)
);

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table members_roles;

View File

@ -0,0 +1,14 @@
-- Your SQL goes here
create table members_roles
(
member_id uuid not null
constraint members_roles_entities_entity_id_fk
references entities
on update cascade on delete cascade,
role_id text not null
constraint members_roles_roles_id_fk
references roles
on update cascade on delete cascade,
constraint members_roles_pk
primary key (member_id, role_id)
);

View File

@ -1,13 +1,18 @@
use crate::database::model::members::Sex;
use crate::database::controller::connector::establish_connection;
use crate::database::model::members::{RawMember, Sex};
use crate::diesel::prelude::*;
use crate::helper::settings::Settings;
use crate::modules::member_management::model::member::Member;
use diesel::backend::Backend;
use diesel::deserialize;
use diesel::deserialize::FromSql;
use diesel::sql_types::Integer;
use diesel::sql_types::{Integer, SmallInt};
use diesel::{deserialize, JoinOnDsl, QueryDsl};
use rocket::State;
impl<DB> FromSql<Integer, DB> for Sex
impl<DB> FromSql<SmallInt, DB> for Sex
where
DB: Backend,
i16: FromSql<Integer, DB>,
i16: FromSql<SmallInt, DB>,
{
fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
match i16::from_sql(bytes)? {
@ -19,3 +24,99 @@ where
}
}
}
/// Aggregates all permissions a member has (from all assigned roles)
///
/// #Arguments
/// * 'uuid' - Member's uuid, type uuid::Uuid
/// * 'settings' - Settings, as managed State
pub fn get_member_permissions_by_uuid(
uuid: uuid::Uuid,
settings: &State<Settings>,
) -> Option<Vec<String>> {
use crate::schema::members::dsl::*;
use crate::schema::members_roles::dsl::*;
use crate::schema::roles_permissions::dsl::permission_id;
use crate::schema::roles_permissions::dsl::role_id as role_id2;
use crate::schema::roles_permissions::dsl::roles_permissions;
let connection = establish_connection(settings);
let data = members
.left_join(members_roles.on(member_id.eq(entity_id)))
.left_join(roles_permissions.on(role_id2.eq(role_id)))
.select(permission_id.nullable())
.filter(entity_id.eq(uuid))
.load::<Option<String>>(&connection);
let data = match data {
Ok(data) => data,
Err(e) => {
error!(
"Error occured while checking permissions for member {}: {}",
uuid, e
);
return None;
}
};
let mut permissions: Vec<String> = vec![];
for permission in data {
match permission {
Some(permission) => permissions.push(permission),
None => (),
}
}
if (!permissions.is_empty()) {
Some(permissions)
} else {
None
}
}
/// Get vector of all members (database::model::members::RawMember) for a user uuid
///
/// #Arguments
/// * 'uuid' - User's uuid, type uuid::Uuid
/// * 'settings' - Settings, as managed State
pub fn get_raw_members_by_uuid(uuid: uuid::Uuid, settings: &State<Settings>) -> Vec<RawMember> {
use crate::schema::members::dsl::*;
use diesel::debug_query;
let connection = establish_connection(settings);
let member: Result<Vec<RawMember>, diesel::result::Error> =
members.filter(users_id.eq(uuid)).load(&connection);
match member {
Ok(member) => member,
Err(e) => {
error!(
"Error occured while loading data for member {}: {}",
uuid, e
);
vec![]
}
}
}
/// Get member_management::model::member::Member
///
/// #Arguments
/// * 'uuid' - Users's uuid, type uuid::Uuid
/// * 'settings' - Settings, as managed State
pub fn get_member_by_uuid(uuid: uuid::Uuid, settings: &State<Settings>) -> Vec<Member> {
let raw_members: Vec<RawMember> = get_raw_members_by_uuid(uuid, settings);
let mut members: Vec<Member> = vec![];
for raw_member in raw_members {
let mut permissions = get_member_permissions_by_uuid(raw_member.entity_id, settings);
let permissions = permissions.unwrap_or(vec![]);
members.push(Member::from((raw_member, permissions)));
}
members
}

View File

@ -3,6 +3,7 @@ use crate::database::model::users::User;
use crate::diesel::prelude::*;
use crate::diesel::QueryDsl;
use crate::helper::settings::Settings;
use crate::modules::member_management::model::member::Member;
use diesel::ExpressionMethods;
use rocket::State;
@ -10,6 +11,7 @@ pub fn get_user_by_email(email: String, settings: &State<Settings>) -> Option<Us
use crate::schema::users::dsl::*;
let connection = establish_connection(settings);
match users.filter(email.eq(email)).first(&connection) {
Ok(user) => Some(user),
Err(_) => None,

View File

@ -1,7 +1,8 @@
use std::time::SystemTime;
use chrono::NaiveDate;
use diesel::sql_types::SmallInt;
#[repr(i16)]
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Copy, Clone, AsExpression, FromSqlRow, Deserialize)]
#[sql_type = "SmallInt"]
pub enum Sex {
UNKNOWN = 0,
MALE = 1,
@ -9,17 +10,17 @@ pub enum Sex {
OTHER = 9,
}
#[derive(Queryable)]
pub struct Member {
#[derive(Queryable, Clone, Deserialize)]
pub struct RawMember {
pub(crate) entity_id: uuid::Uuid,
pub(crate) users_id: uuid::Uuid,
pub(crate) users_id: Option<uuid::Uuid>,
pub(crate) firstname: String,
pub(crate) lastname: String,
pub(crate) date_of_birth: SystemTime,
pub(crate) sex: Sex,
pub(crate) salutation: String,
pub(crate) place_of_birth: String,
pub(crate) academic_titles: String,
pub(crate) personnel_number: i32,
pub(crate) ui_language: String,
pub(crate) date_of_birth: Option<NaiveDate>,
pub(crate) sex: Option<Sex>,
pub(crate) salutation: Option<String>,
pub(crate) place_of_birth: Option<String>,
pub(crate) academic_titles: Option<String>,
pub(crate) personnel_number: Option<i32>,
pub(crate) ui_language: Option<String>,
}

View File

@ -2,6 +2,7 @@ use crate::database::model::users::User;
use crate::helper::session_cookies::model::{
SessionCookie, SessionCookieError, SessionCookieStorage,
};
use crate::modules::member_management::model::member::Member;
use chrono::{DateTime, Utc};
use core::fmt;
use rand::distributions::Alphanumeric;
@ -27,11 +28,12 @@ impl SessionCookieStorage {
/// Generates cookie and adds it to the SessionCookieStorage
///
/// Generates random id via retrieve_id, creates cookie and adds it to the Storage.
pub fn add(&self, expires: DateTime<Utc>, user: User) -> SessionCookie {
pub fn add(&self, expires: DateTime<Utc>, user: User, member: Option<Member>) -> SessionCookie {
let item = SessionCookie {
id: self::SessionCookieStorage::retrieve_id(self),
expires,
user,
member,
};
trace!(
"Inserted cookie with id {} into SessionCookieStorage",
@ -48,7 +50,7 @@ impl SessionCookieStorage {
.unwrap()
.clone()
}
//TODO: make user don't change id while updating cookie. This would cause inconsistency
/// Updates cookie with specified id in Storage or adds it if it doesn't exist
pub fn update(&self, cookie: SessionCookie) -> SessionCookie {
trace!(
@ -126,6 +128,7 @@ mod tests {
let inserted_cookie = storage.add(
DateTime::<Utc>::from_utc(NaiveDate::from_ymd(2099, 9, 9).and_hms(0, 00, 00), Utc),
user.clone(),
None,
);
assert!(inserted_cookie.expires.eq(&DateTime::<Utc>::from_utc(
@ -150,6 +153,7 @@ mod tests {
let inserted_cookie = storage.add(
DateTime::<Utc>::from_utc(NaiveDate::from_ymd(2099, 9, 9).and_hms(0, 00, 00), Utc),
user.clone(),
None,
);
let mut changed_cookie = inserted_cookie.clone();
@ -175,6 +179,7 @@ mod tests {
let inserted_cookie = storage.add(
DateTime::<Utc>::from_utc(NaiveDate::from_ymd(2000, 9, 9).and_hms(0, 00, 00), Utc),
user.clone(),
None,
);
let id = inserted_cookie.clone().id;
@ -197,6 +202,7 @@ mod tests {
let inserted_cookie = storage.add(
DateTime::<Utc>::from_utc(NaiveDate::from_ymd(2000, 9, 9).and_hms(0, 00, 00), Utc),
user.clone(),
None,
);
assert_eq!(

View File

@ -1,4 +1,5 @@
use crate::database::model::users::User;
use crate::modules::member_management::model::member::Member;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::sync::RwLock;
@ -19,6 +20,8 @@ pub struct SessionCookie {
/// SessionCookie will get destroyed if called + current DateTime >= expires
pub(crate) expires: DateTime<Utc>,
pub(crate) user: User,
/// contains member data after member was selected
pub(crate) member: Option<Member>,
}
/// Error may occur while try to receive cookie out of SessionCookieStorage

View File

@ -52,6 +52,7 @@ fn main() {
modules::dashboard::view::dashboard,
modules::welcome::view::welcome_get::welcome_get,
modules::welcome::view::welcome_post::welcome_post,
modules::welcome::view::login_select_get::login_select_get,
],
)
.mount("/css", StaticFiles::from("resources/css"))

View File

@ -1,10 +1,19 @@
use super::controller::render::get_context;
use crate::database::controller::members::{
get_member_permissions_by_uuid, get_raw_members_by_uuid,
};
use crate::database::model::users::User;
use crate::helper::session_cookies::model::SessionCookieError;
use crate::helper::settings::Settings;
use rocket::http::Status;
use rocket::State;
use rocket_contrib::templates::Template;
#[get("/portal")]
pub fn dashboard(user: User) -> Result<Template, Status> {
pub fn dashboard(user: User, settings: State<Settings>) -> Result<Template, Status> {
get_raw_members_by_uuid(
"035c34fe-47bc-11ea-af58-a4c3f06b801a".parse().unwrap(),
&settings,
);
Ok(Template::render("module_dashboard", &get_context()))
}

View File

@ -0,0 +1,24 @@
use crate::database::model::members::RawMember;
use crate::modules::member_management::model::member::Member;
impl From<(RawMember, Vec<String>)> for Member {
fn from(rawmember_permissions: (RawMember, Vec<String>)) -> Self {
let raw_member = rawmember_permissions.0;
let permissions = rawmember_permissions.1;
Member {
entity_id: raw_member.entity_id,
users_id: raw_member.users_id,
firstname: raw_member.firstname,
lastname: raw_member.lastname,
date_of_birth: raw_member.date_of_birth,
sex: raw_member.sex,
salutation: raw_member.salutation,
place_of_birth: raw_member.place_of_birth,
academic_titles: raw_member.academic_titles,
personnel_number: raw_member.personnel_number,
ui_language: raw_member.ui_language,
permissions,
}
}
}

View File

@ -0,0 +1 @@
pub mod member_trait;

View File

@ -0,0 +1,3 @@
pub mod controller;
pub mod model;
pub mod view;

View File

@ -0,0 +1,18 @@
use crate::database::model::members::Sex;
use chrono::NaiveDate;
#[derive(Queryable, Clone, Deserialize)]
pub struct Member {
pub(crate) entity_id: uuid::Uuid,
pub(crate) users_id: Option<uuid::Uuid>,
pub(crate) firstname: String,
pub(crate) lastname: String,
pub(crate) date_of_birth: Option<NaiveDate>,
pub(crate) sex: Option<Sex>,
pub(crate) salutation: Option<String>,
pub(crate) place_of_birth: Option<String>,
pub(crate) academic_titles: Option<String>,
pub(crate) personnel_number: Option<i32>,
pub(crate) ui_language: Option<String>,
pub(crate) permissions: Vec<String>,
}

View File

@ -0,0 +1 @@
pub mod member;

View File

@ -27,6 +27,6 @@ pub fn add_session_cookie(
let expires = Utc::now()
.checked_add_signed(Duration::seconds(settings.application.session_timeout))
.expect("Session timeout specified in configuration is too great!");
let cookie = storage.add(expires, user);
let cookie = storage.add(expires, user, None);
cookies.add_private(Cookie::new("session", cookie.id))
}

View File

@ -0,0 +1,51 @@
use crate::database::controller::members::get_member_by_uuid;
use crate::database::model::users::User;
use crate::helper::session_cookies::model::{SessionCookie, SessionCookieStorage};
use crate::helper::settings::Settings;
use rocket::http::Cookies;
use rocket::http::Status;
use rocket::response::Redirect;
use rocket::State;
use rocket_contrib::templates::Template;
use std::error::Error;
#[get("/loginselect")]
pub fn login_select_get(
user: User,
cookie_storage: State<SessionCookieStorage>,
settings: State<Settings>,
mut cookies: Cookies,
) -> Result<Template, Redirect> {
let session_id = match cookies.get_private("session") {
Some(session_id) => session_id.clone(),
None => {
trace!("Couldn't find session id while login_select");
return Err(Redirect::to("/?error=unauthorized"));
}
};
let mut cookie: SessionCookie = match cookie_storage.get(session_id.value().to_string()) {
Ok(cookie) => cookie,
Err(e) => {
trace!(
"Couldn't find session cookie for session_id: {} while login_select: {}",
session_id,
e.to_string()
);
return Err(Redirect::to("/?error=unauthorized"));
}
};
let members = get_member_by_uuid(user.id, &settings);
trace!("Found {} members for user uuid {}", members.len(), user.id);
//TODO: Add selection screen when there is more then 1 member available.
match members.len() {
1 => {
cookie.member = Some(members[0].clone());
cookie_storage.update(cookie);
return Err(Redirect::to("/portal/"));
}
_ => {}
}
return Err(Redirect::to("/?error=notimplemented"));
}

View File

@ -1,2 +1,3 @@
pub mod login_select_get;
pub mod welcome_get;
pub mod welcome_post;

View File

@ -11,6 +11,10 @@ pub fn welcome_get(error: Option<String>) -> Result<Template, Status> {
AlertClass::Warning,
"Session invalid or expired. Please log in again.".to_string(),
)),
"notimplemented" => Some(Alert::new(
AlertClass::Danger,
"Error: Feature not implemented yet.".to_string(),
)),
_ => None,
},
None => None,

View File

@ -29,7 +29,7 @@ pub fn welcome_post(
let alert: Option<Alert> = match user {
Some(user) => {
add_session_cookie(user, &settings, cookieStorage, cookies);
return Ok(Redirect::to("/portal"));
return Ok(Redirect::to("/loginselect"));
}
None => Some(Alert::new(
AlertClass::Danger,

View File

@ -176,7 +176,6 @@ joinable!(members -> users (users_id));
joinable!(members_roles -> entities (member_id));
joinable!(members_roles -> roles (role_id));
joinable!(roles_permissions -> permissions (permission_id));
joinable!(roles_permissions -> roles (role_id));
joinable!(vehicles -> entities (entity_id));
joinable!(vehicles -> vehicle_categories (entity_id));