Compare commits

...
This repository has been archived on 2024-10-20. You can view files and clone it, but cannot push or open issues or pull requests.

2 Commits

Author SHA1 Message Date
Hendrik
840e259565 Added create DB function, helper functions, moved rec. perm. getter
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-15 03:54:46 +01:00
Hendrik
ebbf57f28e Added database and permission modules
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-13 19:32:52 +01:00
6 changed files with 627 additions and 3 deletions

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"rust-analyzer.linkedProjects": [
"./Cargo.toml",
"./Cargo.toml"
],
"rust-analyzer.showUnlinkedFileNotification": false
}

17
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"command": "build",
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
},
"label": "rust: cargo build"
}
]
}

View File

@ -1,8 +1,9 @@
[package]
name = "WANessa"
name = "wanessa"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
mysql = "*"

354
src/database.rs Normal file
View File

@ -0,0 +1,354 @@
//!
//!
//! The database module to use WANessa's MySQL database
//!
//! This module provides abstraction for the networking and SQL queries
//! to simplify the database access and provide structures to represent
//! the database's content. The following functionalities are provided:
//! - Creating a new WANessa database from scratch
//! - Reading localized texts from the database
//! - Getting info about permission spaces
//! - Getting and setting info about a user's permissions in specific spaces
//!
//! Note that this module is not responsible for ANY safety features. Even
//! simple attacks like SQL injections are not checked for.
//! DO NOT PASS ANY UNCHECKED USER INPUT TO ANY OF THESE FUNCTIONS!
//!
use mysql::{*, prelude::{Queryable, FromRow}};
use std::fmt;
/// The 'TextType' enum represents the type of a LocalizedText.
/// It is used to categorize the meaning of the text's content
/// (is it a greeting, an affirmation, an insult, ...)
pub enum TextType {
/// The text is some type of greeting
Greeting = 0,
/// The text is some kind of an affirmative statement
Ok = 1,
/// The text is some kind of a negative statement
NotOk = 2,
/// The text is a translated name for a role
RoleName = 3,
}
impl TextType {
#[allow(dead_code)]
pub fn fmt(&self, tt: &mut fmt::Formatter) -> fmt::Result {
let s: &str = match self {
TextType::Greeting => "Greeting",
TextType::Ok => "Ok",
TextType::NotOk => "NotOk",
TextType::RoleName => "RoleName"
};
tt.write_str(s)?;
Ok(())
}
fn from(item: u32) -> Self {
match item {
0 => TextType::Greeting,
1 => TextType::Ok,
2 => TextType::NotOk,
3 => TextType::RoleName,
_ => panic!("Invalid value"),
}
}
}
/// A representation of a tuple stored in the 'LocalizedText' relation of the database
pub struct LocalizedText {
/// The ID used to identify the specific text in the database. Note
/// that the same text translated to different languages still has
/// the same ID
pub id: usize,
/// The language the text is translated in
pub language_id: usize,
/// The actual text content
pub content: String,
/// The type of the text
pub text_type: TextType
}
impl FromRow for LocalizedText {
fn from_row_opt(row: Row) -> std::result::Result<Self, FromRowError> {
let (id, language_id, content, text_type): (usize, usize, String, u32) = from_row_opt(row)?;
let text_type = TextType::from(text_type);
Ok(Self { id, language_id, content, text_type })
}
}
/// A representation of a tuple stored in the 'Space' relation of the database
pub struct Space {
pub id: usize,
pub name: String,
pub parent_id: Option<usize>
}
impl FromRow for Space {
fn from_row_opt(row: Row) -> std::result::Result<Self, FromRowError> {
let (id, name, parent_id): (usize, String, Option<usize>) = from_row_opt(row)?;
Ok(Self { id, name, parent_id})
}
}
/// A representation of a tuple stored in the 'Role' relation of the database
pub struct Role {
pub id: usize,
pub localized_name_id: usize,
pub permissions: String
}
impl FromRow for Role {
fn from_row_opt(row: Row) -> std::result::Result<Self, FromRowError> {
let (id, localized_name_id, permissions): (usize, usize, String) = from_row_opt(row)?;
Ok(Self { id, localized_name_id, permissions})
}
}
/// A representation of a tuple stored in the 'User' relation of the database
pub struct User {
/// Represents the internal ID for this user
pub id: String,
/// Represents the display name of the user
pub name: String,
}
impl FromRow for User {
fn from_row_opt(row: Row) -> std::result::Result<Self, FromRowError> {
let (id, name): (String, String) = from_row_opt(row)?;
Ok(Self { id, name})
}
}
///
/// A non-public helper function that performs a query and returns the result as an option value
/// Note that the value enclosed in the option needs to implement FromRow.
///
fn query_and_handle_error<RetType: FromRow>(connection: &mut PooledConn, query: String) -> Option<RetType> {
match connection.query_first(query) {
Ok(opt) => {
match opt {
Some(lt) => Some(lt),
None => None,
}
},
Err(e) => {
println!("ERR! {}", e.to_string());
None
},
}
}
/// Initializes the database. Tries to establish a connection to the url specified
/// and returns true if the initialization succeeded and false otherwise.
#[allow(dead_code)]
pub fn initialize(url: &str, port: u16, dbname: &str, username: &str, password: &str) -> Result<PooledConn, Error> {
let full_url: String = format!("mysql://{}:{}@{}:{}/{}", username, password, url, port, dbname);
let pool = Pool::new(full_url.as_str())?;
let conn = pool.get_conn()?;
Ok(conn)
}
/// A helper function for more readable code. Initializes the database and panics
/// if any error occurs during the function.
#[allow(dead_code)]
pub fn initialize_or_panic(url: &str, port: u16, dbname: &str, username: &str, password: &str) -> PooledConn {
match initialize(url, port, dbname, username, password) {
Ok(c) => {
return c;
},
Err(e) => {
// Panic at this point as this is a critical error that doesn't allow the program
// to be run safely
panic!("Database initialization error! (\"{}\")", e.to_string());
}
}
}
/// Retrieves a LocalizedText from the database and returns an instance
/// of Option<LocalizedText>. See LocalizedText definition for more information.
///
/// connection is the database connection returned from the initialize function.
///
/// text_id is the ID of the text as described in the "LocalizedText"
/// documentation. This will return an empty option if no text with the ID
/// specified is present in the database.
///
/// language_id is the target language in which the text
/// should be returned. Note that this will fall back to any other
/// language if the text is not present for the specified language in the
/// database.
#[allow(dead_code)]
pub fn localized_text(connection: &mut PooledConn, text_id: usize, language_id: usize) -> Option<LocalizedText> {
let query: String = format!("SELECT * FROM LocalizedText WHERE ID={} AND LanguageID={}" , text_id, language_id);
return query_and_handle_error(connection, query);
}
/// Retrieves a Space from the database and returns an instance of Option<Space>, which holds a space instance
/// if the space was found and no error occured and None otherwise.
///
/// connection is the database connection returned from the initialize function.
///
/// space_id is the ID of the space that should be retrieved from the database.
#[allow(dead_code)]
pub fn get_space(connection: &mut PooledConn, space_id: usize) -> Option<Space> {
let query: String = format!("SELECT * FROM Space WHERE ID={}" , space_id);
return query_and_handle_error(connection, query);
}
/// Retrieves the permission description string for the specified user from the database and returns
/// an instance of Option<String>. Note that this function will not take into account the parent spaces
/// (if the parent space defines "Allow" for a permission and the specified space defined "Disallow", this
/// function will return only the space-local settings, which in this case would be "Disallow").
/// See the get_user_permissions_recurse function to recursively get the permission string that actually
/// determines what a user can do in the specified space.
///
/// connection is the database connection returned from the initialize function.
///
/// user_id is the ID of the user whose permissions should be returned.
///
/// space_id is the ID for the space, for that the permissions should be evaluated.
#[allow(dead_code)]
pub fn get_user_permissions(connection: &mut PooledConn, user_id: &str, space_id: usize) -> Option<String> {
let query: String = format!(
"SELECT Permissions FROM UserRoles JOIN Role ON UserRoles.RoleID=Role.RoleID WHERE SpaceID={} AND UserID='{}'",
space_id,
user_id);
return query_and_handle_error(connection, query);
}
/// Updates the permissions of a specific user in a specific space
#[allow(dead_code)]
pub fn set_role_permissions(connection: &mut PooledConn, role_id: usize, new_perms: &str) {
let query: String = format!("UPDATE Role SET Permissions='{}' WHERE RoleID={}", new_perms, role_id);
let _: Option<String> = query_and_handle_error(connection, query);
}
/// Returns the permissions of a specific role
#[allow(dead_code)]
pub fn get_role_permissions(connection: &mut PooledConn, role_id: usize) -> Option<String> {
let query: String = format!("SELECT Permissions FROM Role WHERE RoleID={}", role_id);
return query_and_handle_error(connection, query);
}
/// Returns the username for the user with the specified ID
#[allow(dead_code)]
pub fn get_username(connection: &mut PooledConn, user_id: String) -> Option<String> {
let query: String = format!("SELECT Name FROM User WHERE ID={}", user_id);
return query_and_handle_error(connection, query);
}
/// Creates the WANessa database from scratch.
#[allow(dead_code)]
pub fn create_db(connection: &mut PooledConn) {
// Create the LocalizedText relation
let _: Option<String> = query_and_handle_error(connection, String::from("CREATE TABLE LocalizedText (\
ID INT PRIMARY KEY,\
LanguageID INT NOT NULL,\
Content varchar(200) NOT NULL,\
Type INT NOT NULL CHECK (Type >= 0 AND Type <= 3))"));
// Create the Role relation
let _: Option<String> = query_and_handle_error(connection, String::from("CREATE TABLE Role (\
RoleID INT PRIMARY KEY,\
LocalizedNameID INT references LocalizedText(ID) NOT NULL,\
Permissions varchar(60) NOT NULL"));
// Create the Space relation
let _: Option<String> = query_and_handle_error(connection, String::from("CREATE TABLE Space (\
ID INT PRIMARY KEY,\
Name varchar(80) NOT NULL,\
ParentSpaceID INT references Space(ID))"));
// Create the User relation
let _: Option<String> = query_and_handle_error(connection, String::from("CREATE TABLE User (\
ID varchar(50) PRIMARY KEY,\
Name varchar(80) NOT NULL)"));
// Create the UserRoles relation
let _: Option<String> = query_and_handle_error(connection, String::from("CREATE TABLE UserRoles (\
UserID varchar(50) references User(ID) NOT NULL,\
SpaceID INT references Space(ID) NOT NULL,\
RoleID INT references Role(RoleID) NOT NULL,\
PRIMARY KEY (UserID, SpaceID, RoleID)\
)"));
}
///
/// The test module for the database
///
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connect_and_get_text() {
// Test settings
let url = "localhost";
let port = 3306;
let dbname = "WANessaDB";
let username = "testuser";
let password = "12345";
// Query settings
let text_id = 1;
let lang_id = 1;
// Perform test
let mut c = initialize_or_panic(url, port, dbname, username, password);
match localized_text(&mut c, text_id, lang_id) {
Some(t) => println!("Got text from database: {}", t.content),
None => println!("No text found for {} in lang {}!", text_id, lang_id)
}
}
#[test]
fn test_connect_and_get_perms() {
// Test settings
let url = "localhost";
let port = 3306;
let dbname = "WANessaDB";
let username = "testuser";
let password = "12345";
// Query settings
let target_user = "testuser";
let space_id = 1;
// Perform test
let mut c = initialize_or_panic(url, port, dbname, username, password);
match get_user_permissions(&mut c, target_user, space_id) {
Some(t) => println!("Got permissions from database: {}", t.as_str()),
None => println!("No permissions found for user {}!", target_user)
}
}
#[test]
fn test_permission_set() {
let mut db_conn = initialize_or_panic("localhost",
3306,
"WANessaDB",
"testuser",
"12345");
set_role_permissions(&mut db_conn, 1, "p");
}
}

View File

@ -1,3 +1,25 @@
fn main() {
println!("Hello, world!");
use mysql::PooledConn;
mod database;
mod permission;
fn main()
{
// Database initialization and some example code
let mut db_conn: PooledConn = database::initialize_or_panic(
"localhost",
3306,
"WANessaDB",
"testuser",
"12345");
let text = database::localized_text(&mut db_conn, 1, 1);
match text {
Some(_) => {
println!("Got the text!");
},
None => {
println!("Got no text :(");
}
}
}

223
src/permission.rs Normal file
View File

@ -0,0 +1,223 @@
//!
//!
//! The permission module handles everything regarding permission management.
//! This module's tasks are:
//! - Providing structures and enums related to permission handling to other modules
//! - Granting / Revoking permissions from/to user(s)
//!
//!
use std::{collections::HashMap, str::FromStr, hash::Hash};
use mysql::*;
use crate::database::*;
use crate::database;
/// This enum contains all permissions
#[derive(PartialEq, Eq, Hash)]
#[allow(dead_code)]
pub enum Permission
{
UseCommands = 0,
}
impl Permission {
#[allow(dead_code)]
fn is_set(self, permstring: String) -> bool {
match permstring.chars().nth(self as usize) {
Some(c) => c == 'p',
None => panic!("The permstring provided does not contain all permissions and is therefore invalid!")
}
}
}
/// This enum describes the state of a permission for a PermissionTable. Any Permission
/// can be allowed, disallowed or passed through to a subsidiary PermissionTable (don't care state).
#[allow(dead_code)]
pub enum PermState
{
Allow = 2,
DontCare = 1,
Disallow = 0
}
impl FromStr for PermState {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"p" => Ok(PermState::Allow),
"n" => Ok(PermState::Disallow),
"x" => Ok(PermState::DontCare),
_ => Err(()),
}
}
}
/// Definition of a PermissionTable. A permission table is a map
/// that provides one of the three PermStates for every permission.
#[allow(dead_code)]
pub struct PermissionTable {
map: HashMap<Permission, PermState>
}
impl PermissionTable {
fn new() -> Self {
PermissionTable { map: HashMap::new() }
}
fn insert(&mut self, perm: Permission, state: PermState) {
self.map.insert(perm, state);
}
fn from_str(s: &str) -> Result<Self, ()> {
let mut table = PermissionTable::new();
for (i, state) in s.chars().enumerate() {
if let Ok(state) = PermState::from_str(&state.to_string()) {
let permission = match i {
0 => Permission::UseCommands,
_ => return Err(()),
};
table.insert(permission, state);
} else {
return Err(());
}
}
Ok(table)
}
fn get_permstring(self) -> &'static str {
return ""
}
}
/// Sets the state of the specified Permission for the specified role to the specified state.
#[allow(dead_code)]
pub fn set_permission(db_connection: &mut PooledConn, role_id: usize, perm: Permission, state: PermState) -> bool {
match database::get_role_permissions(db_connection, role_id) {
Some(permstring) => {
match PermissionTable::from_str(permstring.as_str()) {
Ok(mut permtable) => {
permtable.insert(perm, state);
database::set_role_permissions(db_connection, role_id, permtable.get_permstring());
return true;
},
Err(_) => return false
}
},
None => return false
}
}
/// Retrieves the actual permissions of a user from the database. This function also applies all the parent space's rules,
/// so that the result of this function can be used to determine, if a user has a specific permission in a specific space.
/// This function returns an Option, which will be empty if an error occured (e.g. database inconsistency). If no error
/// occurs, the Option will contain a String that only contains 'p' for positive permissions and 'n' for negative ones.
/// Permissions that are globally defined as "Don't care" will be treated as "Disallow" by this function.
#[allow(dead_code)]
pub fn get_permissions(db_connection: &mut PooledConn, user_id: &str, space_id: usize) -> Option<String> {
let mut permstr_opt: Option<String> = get_user_permissions(db_connection, user_id, space_id);
let space_opt: Option<Space> = get_space(db_connection, space_id);
match permstr_opt {
Some(mut permstr) => {
let mut space: Space = match space_opt {
Some(s) => s,
None => return None // The initial space could not be retrieved from the database
};
loop {
// query the current space in order to get the parent space ID
let parent_id: usize;
space = match space.parent_id {
Some(parent_space_id) => {
parent_id = parent_space_id;
match get_space(db_connection, parent_space_id) {
Some(parent_space) => parent_space,
None => return None // The parent space specified in the database could not be retrieved
}
},
None => break // The top of the space hierarchy has been reached. The permission string is complete.
};
permstr_opt = get_user_permissions(db_connection, user_id, parent_id);
permstr = match permstr_opt {
Some(n) => {
// Here we have the permission string from the sub-space ('permstr') and the permission string from the
// super-space ('n'). Now the sub-space string will be overridden by every non-dont-care state in the
// super-space string, because super-space rules are always prioritized higher than the sub-space rules.
let mut newpermstr = String::from("");
if n.len() != permstr.len() {
return None; // There is a database inconsistency where not all permstrings are equally long.
}
for (super_char, sub_char) in n.chars().zip(permstr.chars()) {
match super_char {
'p' => newpermstr += "p",
'n' => newpermstr += "n",
'x' => newpermstr += sub_char.to_string().as_str(),
_ => return None // There is a database inconsistency where not all permstrings consist of 'p', 'n' or 'x'.
}
}
newpermstr
},
None => {
// No explicit entry for the permissions at that level exist. This is valid and every permission entry
// is therefore assumed as "Don't care". Because a string consisting of just "Don't care" states will not
// change the permissions in the sub-space, the permstr will stay the same in this case.
permstr
}
};
};
// Check the characters for validity due to the top-most layer not being checked in the loop
for c in permstr.chars() {
if c != 'p' && c != 'x' && c != 'n' {
return None // The permission string in the top-most layer contains invalid states
}
}
// At this point, permstr contains all the permissions of the user while only considering the database entries. However,
// some permissions could be declared as "Don't care" on every level. These "Don't care" states will now be replaced by
// "Disallow" states.
permstr = permstr.chars().map(|c| if c == 'x' { 'n' } else { c }).collect();
Some(permstr)
},
None => None // The permission query for the user in the space failed
}
}
/// Checks if the user has the specified permission in the specified space
#[allow(dead_code)]
pub fn has_permission(db_connection: &mut PooledConn, user_id: &str, space_id: usize, perm: Permission) -> Option<bool> {
let perms = get_permissions(db_connection, user_id, space_id);
match perms {
Some(permstring) => Some(perm.is_set(permstring)),
None => None
}
}
#[cfg(test)]
mod tests {
use crate::database;
use super::*;
#[test]
fn test_permission_check() {
let mut db_conn = database::initialize_or_panic("localhost",
3306,
"WANessaDB",
"testuser",
"12345");
let hasperm = has_permission(&mut db_conn, "testuser", 0, Permission::UseCommands);
match hasperm {
Some(hasres) => {
match hasres {
true => println!("The user has the specified permission"),
false => println!("The user does not have the specified permission")
}
},
None => panic!("An error occured. has_permission returned no value.")
}
}
}