Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
840e259565 | ||
|
ebbf57f28e |
184
.drone.yml
184
.drone.yml
@ -1,196 +1,28 @@
|
||||
kind: pipeline
|
||||
name: amd64 [debug]
|
||||
name: test-on-amd64
|
||||
|
||||
platform:
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: build [debug]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
- name: test
|
||||
image: rust:alpine
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- cargo build --all
|
||||
|
||||
- name: test [debug]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- cargo test --all
|
||||
|
||||
- name: lint-clippy [debug]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- rustup component add clippy
|
||||
- cargo clippy --all
|
||||
- cargo test --verbose --all
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: arm64 [debug]
|
||||
name: test-on-arm64
|
||||
|
||||
platform:
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: build [debug]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
- name: test
|
||||
image: rust:alpine
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- cargo build --all
|
||||
|
||||
- name: test [debug]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- cargo test --all
|
||||
|
||||
- name: lint-clippy [debug]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- rustup component add clippy
|
||||
- cargo clippy --all
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: amd64 [release]
|
||||
|
||||
platform:
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: build and test [release]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- cargo test --all --release
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: arm64 [release]
|
||||
|
||||
platform:
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: build and test [release]
|
||||
image: rust:1.71.1-slim-bookworm
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- cargo test --all --release
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: rustfmt
|
||||
steps:
|
||||
- name: Format Project with rustfmt
|
||||
image: rust:1.71.1-alpine
|
||||
commands:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
|
||||
# Stupid workarounds, for stupid problems
|
||||
- echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt --all --check && echo "No formatting required, exiting early..." && exit 0
|
||||
- cargo fmt --all --verbose
|
||||
|
||||
# Check for format loop
|
||||
- apk add git
|
||||
- '[ "$(git log -1 --pretty=%B | grep -E ".+")" = "rustfmt" ] && echo "format loop detected, aborting..." && exit 1'
|
||||
|
||||
# Get necessary packages
|
||||
- apk add openssh gpg gpg-agent
|
||||
|
||||
# configure SSH and import private key
|
||||
- mkdir -p "$${HOME}/.ssh"
|
||||
- echo "$SSH_PRIVATE_KEY" > "$${HOME}/.ssh/git"
|
||||
- echo "Host git.libre.moe" >> "$${HOME}/.ssh/config"
|
||||
- echo " User git" >> "$${HOME}/.ssh/config"
|
||||
- echo " IdentityFile $${HOME}/.ssh/git" >> "$${HOME}/.ssh/config"
|
||||
- echo "$GITEA_ED25519_SIG" >> "$${HOME}/.ssh/known_hosts"
|
||||
- echo "$GITEA_RSA_SIG" >> "$${HOME}/.ssh/known_hosts"
|
||||
- echo "$GITEA_ECDSA_SIG" >> "$${HOME}/.ssh/known_hosts"
|
||||
- chmod 700 -R "$${HOME}/.ssh"
|
||||
|
||||
# setup gpg
|
||||
- gpg-agent --daemon
|
||||
# the git config gpg.program absolutely despises anything other than a path, including additional arguments.
|
||||
# so we just put it into it's own shell script and use that later
|
||||
- echo "#!/bin/sh" >> /tmp/gpg.sh
|
||||
- echo gpg --batch --pinentry-mode loopback --passphrase '$GPG_PASSPHRASE' \$@ >> /tmp/gpg.sh
|
||||
- chmod 777 /tmp/gpg.sh
|
||||
|
||||
# import gpg key
|
||||
- echo "$GPG_PRIVKEY" > /tmp/private.key
|
||||
- /tmp/gpg.sh --import /tmp/private.key >> /tmp/import.sh || exit 2
|
||||
|
||||
# configure git
|
||||
- git config --local user.name "WANessa"
|
||||
- git config --local user.email "$GIT_EMAIL_ADDRESS"
|
||||
- git config --local user.signingkey "$GPG_PUBKEY_ID"
|
||||
- git config --local gpg.program "/tmp/gpg.sh" # see above comment
|
||||
# Uncomment below line for ssh debugging
|
||||
# - git config core.sshCommand '/usr/bin/ssh -v'
|
||||
- git remote add ssh "$DRONE_GIT_SSH_URL"
|
||||
|
||||
# commit and push every modified file, does not include new files, because why should it?
|
||||
- GIT_COMMITTER_NAME="WANessa" GIT_COMMITTER_EMAIL="$GIT_EMAIL_ADDRESS" git commit --author "$DRONE_COMMIT_AUTHOR_NAME <$DRONE_COMMIT_AUTHOR_EMAIL>" -S -a -m "rustfmt"
|
||||
- git push ssh
|
||||
|
||||
environment:
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: WANESSA_SSH_PRIVKEY
|
||||
GIT_EMAIL_ADDRESS:
|
||||
from_secret: WANESSA_EMAIL_ADDRESS
|
||||
GITEA_ED25519_SIG:
|
||||
from_secret: GIT_ED25519_SIG
|
||||
GITEA_RSA_SIG:
|
||||
from_secret: GIT_RSA_SIG
|
||||
GITEA_ECDSA_SIG:
|
||||
from_secret: GIT_ECDSA_SIG
|
||||
GPG_PUBKEY_ID:
|
||||
from_secret: WANESSA_GPG_PUBKEY_ID
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: WANESSA_GPG_PASSPHRASE
|
||||
GPG_PRIVKEY:
|
||||
from_secret: WANESSA_GPG_PRIVKEY
|
||||
|
||||
depends_on:
|
||||
- amd64 [debug]
|
||||
- arm64 [debug]
|
||||
- amd64 [release]
|
||||
- arm64 [release]
|
||||
- cargo test --verbose --all
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"./Cargo.toml",
|
||||
"./Cargo.toml"
|
||||
],
|
||||
"rust-analyzer.showUnlinkedFileNotification": false
|
||||
}
|
17
.vscode/tasks.json
vendored
Normal file
17
.vscode/tasks.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
13
Cargo.toml
13
Cargo.toml
@ -6,15 +6,4 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use
|
||||
pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic
|
||||
nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery
|
||||
unwrap_used = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used
|
||||
missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn
|
||||
missing_assert_message = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
missing_errors_doc = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message
|
||||
mysql = "*"
|
||||
|
@ -1 +0,0 @@
|
||||
hard_tabs = true
|
354
src/database.rs
Normal file
354
src/database.rs
Normal 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");
|
||||
}
|
||||
}
|
26
src/main.rs
26
src/main.rs
@ -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
223
src/permission.rs
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user