Skip to content

Commit

Permalink
Merge pull request #3 from nwrenger/authentication
Browse files Browse the repository at this point in the history
Authentication
  • Loading branch information
nwrenger authored May 18, 2023
2 parents 9e68194 + 14e8224 commit 2551aa6
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 254 deletions.
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sndm"
version = "2.2.1"
version = "2.3.0"
edition = "2021"
license = "MIT"

Expand All @@ -15,4 +15,6 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["rocket"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"
dotenv = "0.15"
simplelog = "0.12"
dotenv = "0.15"
base64 = "0.21"
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The latest builds can be downloaded from the [releases page](https://github.com/

### Usage

Just run the binary/exceutable file provided in the release. Make sure it's in the same Diractory as the dummy data file (benutzer.txt), otherwise it won't start. Run it with sudo/admin permission (because of server port being http://0.0.0.0:80). The path for the Swagger-UI is http://0.0.0.0/swagger-ui/ / http://localhost/swagger-ui/. In addition, you have to set the Admin Key (ADMIN_KEY), Employment Key (EMPLOYMENT_KEY), Police Key (POLICE_KEY) and the Writing Key (WRITING_KEY) using the environment file keys.env.
Just run the binary/exceutable file provided in the release. Make sure it's in the same Diractory as the dummy data file (benutzer.txt) and admin.env file otherwise it won't start. Run it with sudo/admin permission (because of server port being http://0.0.0.0:80). The path for the Swagger-UI is http://0.0.0.0/swagger-ui/ / http://localhost/swagger-ui/. In addition, using the admin.env file you can define your admin, which can't be deleted. This admin can add other User and their permissions. Without those permissions you are unauthorised and can't interact with the Server/Database.

## Architecture - Including SNDI

Expand Down Expand Up @@ -40,27 +40,23 @@ The Server calls are:

Swagger UI integrated via Utoipa:

<img src="images/server_routes.png" alt="Database Schema" width=400 />
<img src="images/server_routes.png" alt="Database Schema" width=500 />

Schemas:

<img src="images/schemas.png" alt="Database Schema" width=400 />
<img src="images/schemas.png" alt="Database Schema" width=500 />

Security:

- Writing Key -> a Key for Writing/Changing Data (POST, PUT, DELETE) of Criminal and Absence
- Police Key -> a Key for accessing Criminal
- Employment Key -> a Key for accessing Absence
- Employment Key/Police Key -> a Key for accessing Users
- Adming Key -> a Key with Admin Permissions -> can do everything and even more than the keys above like changing User data
- the keys are not encrypted
- logging every Successful Server call (excluding Swagger UI) to seperate file called 'log.txt'
- User System, an Admin, definded throught the admin.env file
- Admin can add User with Permissions what they can do cannot do like: Reading/Writing for each Data Type (User, Absence, Criminal)
- logging every Server call (excluding Swagger UI) to seperate file called 'log.txt' with Information who did what

### Database Layer

The [SQLite](https://sqlite.org/index.html) database has the following schema:

<img src="images/sqlite_dia.png" alt="Database Schema" width=400 />
<img src="images/sqlite_dia.png" alt="Database Schema" width=600 />

(The bold printed texts are the primary keys!)

Expand Down
2 changes: 2 additions & 0 deletions admin.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SNDM_USER=max.mustermann
SNDM_PASSWORD=1234
Binary file modified images/schemas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/server_routes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/sqlite_dia.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 0 additions & 4 deletions keys.env

This file was deleted.

136 changes: 136 additions & 0 deletions src/db/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use crate::db::project::{Database, Error, FromRow, Result};
use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
use serde::Deserialize;
use utoipa::ToSchema;

#[repr(i64)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ToSchema)]
pub enum Permission {
None,
ReadOnly,
Write,
}

impl FromSql for Permission {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
i64::column_result(value).and_then(|value| match value {
0 => Ok(Permission::None),
1 => Ok(Permission::ReadOnly),
2 => Ok(Permission::Write),
_ => Err(rusqlite::types::FromSqlError::OutOfRange(2)),
})
}
}

impl ToSql for Permission {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok((*self as i64).into())
}
}

#[derive(Deserialize, PartialEq, Debug, ToSchema)]
pub struct Login {
pub user: String,
pub password: String,
pub access_user: Permission,
pub access_absence: Permission,
pub access_criminal: Permission,
}

impl Login {
pub fn is_valid(&self) -> bool {
!self.user.trim().is_empty() && !self.user.contains(':')
}
}

impl FromRow for Login {
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Login> {
Ok(Login {
user: row.get("user")?,
password: row.get("password")?,
access_user: row.get("access_user")?,
access_absence: row.get("access_absence")?,
access_criminal: row.get("access_criminal")?,
})
}
}

/// Returns the login with the given `user` and `password`.
pub fn fetch(db: &Database, user: &str, password: &str) -> Result<Login> {
let mut stmt = db.con.prepare(
"select \
user, \
password, \
access_user, \
access_absence, \
access_criminal \
from login \
where user=? and password=?
limit 1",
)?;
let mut result = stmt.query([user, password])?;
Ok(Login::from_row(result.next()?.ok_or(Error::NothingFound)?)?)
}

/// Adds a new date with presenters.
pub fn add(db: &Database, login: &Login) -> Result<()> {
if !login.is_valid() {
return Err(Error::InvalidUser);
}
db.con.execute(
"INSERT INTO login VALUES (?, ?, ?, ?, ?)",
rusqlite::params![
login.user.trim(),
login.password.trim(),
login.access_user,
login.access_absence,
login.access_criminal
],
)?;
Ok(())
}

/// Deletes the login by user.
pub fn delete(db: &Database, user: &str) -> Result<()> {
let user = user.trim();
if user.is_empty() {
return Err(Error::InvalidUser);
}
// remove non-admin users
db.con
.execute("delete from login where user=?", rusqlite::params![user])?;
Ok(())
}

#[cfg(test)]
mod tests {
//TODO: Tests

use crate::db::login::{self, Login, Permission};
use crate::db::project::{create, Database};

#[test]
fn fetch_add_delete_logins() {
let db = Database::memory().unwrap();
create(&db).unwrap();

let login = Login {
user: "nils.wrenger".into(),
password: "123456".into(),
access_user: Permission::ReadOnly,
access_absence: Permission::Write,
access_criminal: Permission::None,
};
login::add(&db, &login).unwrap();

let result = login::fetch(&db, &login.user, &login.password);
assert!(result.is_ok());
assert_eq!(result.unwrap(), login);

login::delete(&db, &login.user).unwrap();

let result = login::fetch(&db, &login.user, &login.password);
println!("{result:?}");
assert!(result.is_err());
}
}
1 change: 1 addition & 0 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod absence;
pub mod criminal;
pub mod login;
pub mod project;
pub mod stats;
pub mod user;
23 changes: 15 additions & 8 deletions src/db/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::io::BufReader;

use chrono::NaiveDate;

use rusqlite::Connection;
use rusqlite::{types::FromSql, Connection};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

Expand Down Expand Up @@ -94,18 +94,18 @@ impl From<std::io::Error> for Error {
}
}

impl From<Error> for serde_json::Value {
fn from(e: Error) -> Self {
serde_json::json!({ "Err": e })
}
}

pub type Result<T> = std::result::Result<T, Error>;

pub trait FromRow: Sized {
fn from_row(stmt: &rusqlite::Row) -> rusqlite::Result<Self>;
}

impl<T: FromSql> FromRow for T {
fn from_row(stmt: &rusqlite::Row) -> rusqlite::Result<Self> {
stmt.get(1)
}
}

impl<'a, T: FromRow> Iterator for DBIter<'a, T> {
type Item = Result<T>;
fn next(&mut self) -> Option<Self::Item> {
Expand Down Expand Up @@ -221,7 +221,14 @@ pub fn create(db: &Database) -> Result<()> {
\
create table criminal ( \
account text not null primary key, \
data text not null); \
data text not null);
\
create table login ( \
user text not null primary key, \
password text not null, \
access_user int default 0, \
access_absence int default 0, \
access_criminal int default 0); \
";

let transaction = db.transaction()?;
Expand Down
50 changes: 39 additions & 11 deletions src/db/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,22 @@ pub fn fetch(db: &Database, id: &str) -> Result<User> {
)?)
}

/// Parameters for the advanced search
///
/// Adding the '%' char allows every number of every character in this place
#[derive(Debug, Clone, Default)]
pub struct UserSearch<'a> {
pub name: &'a str,
pub role: &'a str,
}
impl<'a> UserSearch<'a> {
pub fn new(name: &'a str, role: &'a str) -> UserSearch<'a> {
Self { name, role }
}
}

/// Performes a simple user search with the given `text`.
pub fn search<'a>(db: &'a Database, text: &'a str) -> Result<Vec<User>> {
pub fn search(db: &Database, params: UserSearch, offset: usize) -> Result<Vec<User>> {
let mut stmt = db.con.prepare(
"select \
account, \
Expand All @@ -44,13 +58,18 @@ pub fn search<'a>(db: &'a Database, text: &'a str) -> Result<Vec<User>> {
role \
\
from user \
where account like '%'||?1||'%' \
or forename like '%'||?1||'%' \
or surname like '%'||?1||'%' \
or role like '%'||?1||'%' \
order by account",
where (account like '%'||?1||'%' \
or forename like '%'||?1||'%' \
or surname like '%'||?1||'%') \
and role like ?2 \
order by account \
limit 100 offset ?3",
)?;
let rows = stmt.query([text.trim()])?;
let rows = stmt.query(rusqlite::params![
params.name.trim(),
params.role.trim(),
offset
])?;
DBIter::new(rows).collect()
}

Expand All @@ -70,6 +89,7 @@ pub fn add(db: &Database, user: &User) -> Result<()> {
)?;
Ok(())
}

/// Updates the user and all references if its account changes.
pub fn update(db: &Database, previous_account: &str, user: &User) -> Result<()> {
let previous_account = previous_account.trim();
Expand Down Expand Up @@ -101,6 +121,12 @@ pub fn update(db: &Database, previous_account: &str, user: &User) -> Result<()>
[user.account.trim(), previous_account],
)?;

// update login
transaction.execute(
"update login set user=? where user=?",
[user.account.trim(), previous_account],
)?;

transaction.commit()?;
Ok(())
}
Expand All @@ -120,6 +146,8 @@ pub fn delete(db: &Database, account: &str) -> Result<()> {
transaction.execute("delete from absence where account=?", [account])?;
//remove from criminal
transaction.execute("delete from criminal where account=?", [account])?;
//remove from login
transaction.execute("delete from login where user=?", [account])?;
transaction.commit()?;

Ok(())
Expand All @@ -128,7 +156,7 @@ pub fn delete(db: &Database, account: &str) -> Result<()> {
#[cfg(test)]
mod tests {
use crate::db::project::{create, Database, User};
use crate::db::user;
use crate::db::user::{self, UserSearch};
#[test]
fn add_update_remove_users() {
let db = Database::memory().unwrap();
Expand All @@ -142,7 +170,7 @@ mod tests {
};
user::add(&db, &user).unwrap();

let result = user::search(&db, "").unwrap();
let result = user::search(&db, UserSearch::new("%", "%"), 0).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], user);

Expand All @@ -155,12 +183,12 @@ mod tests {
},
)
.unwrap();
let result = user::search(&db, "").unwrap();
let result = user::search(&db, UserSearch::new("%foo%", "%"), 0).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].role, "Teacher");

user::delete(&db, &user.account).unwrap();
let result = user::search(&db, "").unwrap();
let result = user::search(&db, UserSearch::new("no one", "%"), 0).unwrap();
assert_eq!(result.len(), 0);
}
}
Loading

0 comments on commit 2551aa6

Please sign in to comment.