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

Add more REST methods
leon_tries_rust
Trivernis 4 years ago committed by GitHub
commit 4f73e1ee60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run audit
uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v2
- name: Run audit
uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

@ -13,16 +13,13 @@ jobs:
- name: Copy Repo Files
uses: actions/checkout@master
-
name: Set up QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to GitHub Container Registry
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
@ -36,8 +33,7 @@ jobs:
username: ${{ secrets.PORTUS_USERNAME }}
password: ${{ secrets.PORTUS_PASSWORD }}
-
name: Build and push
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .

@ -15,20 +15,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache build data
uses: actions/cache@v2
with:
path: |
target
~/.cargo/
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v2
- name: Cache build data
uses: actions/cache@v2
with:
path: |
target
~/.cargo/
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

@ -1,20 +0,0 @@
dist: bionic
language: bash
services:
- docker
git:
submodules: false
before_install:
- echo -e "machine github.com\n login $GIT_USER\n password $GIT_PW" > ~/.netrc
- git submodule set-url msg-rpc https://github.com/flotte-goes-smart/msg-rpc.git
- git submodule init
- git submodule update
script:
- docker login -u="$DOCKER_USER" -p="$DOCKER_PW" https://flotte-docker-registry.spdns.org/
- docker build -t flotte-user-managment .
- docker tag flotte-user-managment flotte-docker-registry.spdns.org/flotte-user-managment
- docker push flotte-docker-registry.spdns.org/flotte-user-managment:latest

227
Cargo.lock generated

@ -1,5 +1,11 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "adler"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.2.0"
@ -101,6 +107,16 @@ dependencies = [
"getrandom",
]
[[package]]
name = "bincode"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
dependencies = [
"byteorder",
"serde",
]
[[package]]
name = "bitflags"
version = "1.2.1"
@ -349,6 +365,12 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dyn-clone"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55796afa1b20c2945ca8eabfc421839f2b766619209f1ede813cf2484f31804"
[[package]]
name = "env_logger"
version = "0.7.1"
@ -380,9 +402,21 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "flate2"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129"
dependencies = [
"cfg-if 1.0.0",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "flotte-user-management"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"base64 0.12.3",
"bcrypt",
@ -392,6 +426,7 @@ dependencies = [
"crossbeam-utils",
"dotenv",
"env_logger",
"lazy_static",
"log 0.4.11",
"mime 0.3.16",
"msgrpc",
@ -401,16 +436,25 @@ dependencies = [
"r2d2",
"r2d2_postgres",
"rand 0.7.3",
"regex",
"rmp",
"rmp-serde",
"rouille",
"scheduled-thread-pool",
"schemars",
"serde",
"serde_json",
"serde_postgres",
"syntect",
"zeroize",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@ -558,6 +602,12 @@ dependencies = [
"crc32fast",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "hermit-abi"
version = "0.1.17"
@ -603,6 +653,16 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.8"
@ -643,12 +703,33 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
[[package]]
name = "lock_api"
version = "0.4.1"
@ -721,6 +802,16 @@ dependencies = [
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
dependencies = [
"adler",
"autocfg 1.0.1",
]
[[package]]
name = "mio"
version = "0.6.22"
@ -844,6 +935,28 @@ version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
[[package]]
name = "onig"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a155d13862da85473665694f4c05d77fb96598bdceeaf696433c84ea9567e20"
dependencies = [
"bitflags",
"lazy_static",
"libc",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bff06597a6b17855040955cae613af000fc0bfc8ad49ea68b9479a74e59292d"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "opaque-debug"
version = "0.3.0"
@ -977,6 +1090,26 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "plist"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b336d94e8e4ce29bf15bba393164629764744c567e8ad306cc1fdd0119967fd"
dependencies = [
"base64 0.12.3",
"chrono",
"indexmap",
"line-wrap",
"serde",
"xml-rs",
]
[[package]]
name = "postgres"
version = "0.17.5"
@ -1379,6 +1512,15 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.5"
@ -1388,6 +1530,30 @@ dependencies = [
"parking_lot",
]
[[package]]
name = "schemars"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763f667253711994847f7e73befe859d6fff7bea2b7a7f01669d2c5b60765c37"
dependencies = [
"dyn-clone",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1d457e2e37415f32b7628ddc5a7fea06ef63bd029ed180d65166e87ca25ce21"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1414,6 +1580,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_derive_internals"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.59"
@ -1517,6 +1694,28 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "syntect"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3978df05b5850c839a6b352d3c35ce0478944a4be689be826b53cf75363e88"
dependencies = [
"bincode",
"bitflags",
"flate2",
"fnv",
"lazy_static",
"lazycell",
"onig",
"plist",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"walkdir",
"yaml-rust",
]
[[package]]
name = "tempdir"
version = "0.3.7"
@ -1729,6 +1928,17 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "walkdir"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
dependencies = [
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
@ -1794,6 +2004,21 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "xml-rs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"
[[package]]
name = "yaml-rust"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zeroize"
version = "1.1.1"

@ -1,6 +1,6 @@
[package]
name = "flotte-user-management"
version = "0.1.0"
version = "0.2.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
license = "GPL-3.0"
@ -12,10 +12,10 @@ msgrpc = "0.1.0"
postgres = "0.17.5"
serde_postgres = "0.2.0"
dotenv = "0.15.0"
serde = {version = "1.0.115", features = ["serde_derive"]}
serde = { version = "1.0.115", features = ["serde_derive"] }
rand = "0.7.3"
bcrypt = "0.8.2"
zeroize = {version = "1.1.0", features = ["zeroize_derive"]}
zeroize = { version = "1.1.0", features = ["zeroize_derive"] }
byteorder = "1.3.4"
rmp-serde = "0.14.4"
rmp = "0.8.9"
@ -32,4 +32,8 @@ r2d2 = "0.8.9"
r2d2_postgres = "0.16.0"
scheduled-thread-pool = "0.2.5"
num_cpus = "1.13.0"
parking_lot = "0.11.0"
parking_lot = "0.11.0"
regex = "1.4.2"
lazy_static = "1.4.0"
schemars = "0.8.0"
syntect = "4.4.0"

@ -1,13 +1,19 @@
use crate::database::permissions::Permissions;
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use dotenv;
use postgres::NoTls;
use r2d2::Pool;
use r2d2_postgres::PostgresConnectionManager;
use crate::database::models::CreatePermissionsEntry;
use crate::database::permissions::{Permissions, DEFAULT_PERMISSIONS};
use crate::database::role_permissions::RolePermissions;
use crate::database::roles::Roles;
use crate::database::user_roles::UserRoles;
use crate::database::users::Users;
use crate::utils::error::DatabaseResult;
use dotenv;
use postgres::NoTls;
use r2d2::Pool;
use r2d2_postgres::PostgresConnectionManager;
pub mod models;
pub mod permissions;
@ -24,7 +30,7 @@ const DEFAULT_ADMIN_PASSWORD: &str = "flotte-admin";
const DEFAULT_ADMIN_EMAIL: &str = "admin@flotte-berlin.de";
const ENV_ADMIN_PASSWORD: &str = "ADMIN_PASSWORD";
const ENV_ADMIN_EMAIL: &str = "ADMIN_EMAIL";
const ADMIN_ROLE_NAME: &str = "SUPERADMIN";
pub(crate) const ADMIN_ROLE_NAME: &str = "SUPERADMIN";
pub trait Table {
fn new(pool: PostgresPool) -> Self;
@ -87,6 +93,15 @@ impl Database {
) {
log::debug!("Failed to create admin role {}", e.to_string())
}
self.permissions.create_permissions(
DEFAULT_PERMISSIONS
.iter()
.map(|(name, description)| CreatePermissionsEntry {
name: name.to_string(),
description: description.to_string(),
})
.collect(),
)?;
log::info!("Database fully initialized!");
Ok(())

@ -1,3 +1,7 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use postgres::Row;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
@ -27,7 +31,7 @@ impl UserRecord {
/// A row of the permission table that can be serialized and sent
/// via the rcp connection
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct Permission {
pub id: i32,
pub name: String,
@ -36,7 +40,7 @@ pub struct Permission {
/// A row of the role table that can be serialized and sent
/// via the rcp connection
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct Role {
pub id: i32,
pub name: String,

@ -1,6 +1,22 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use crate::database::models::{CreatePermissionsEntry, Permission};
use crate::database::{DatabaseResult, PostgresPool, Table, ADMIN_ROLE_NAME};
use crate::utils::error::DBError;
use std::collections::HashSet;
use std::iter::FromIterator;
pub(crate) const VIEW_ROLE_PERMISSION: &str = "ROLE_VIEW";
pub(crate) const CREATE_ROLE_PERMISSION: &str = "ROLE_CREATE";
pub(crate) const UPDATE_ROLE_PERMISSION: &str = "ROLE_UPDATE";
pub(crate) const DELETE_ROLE_PERMISSION: &str = "ROLE_DELETE";
pub(crate) const DEFAULT_PERMISSIONS: &[(&'static str, &'static str)] = &[
(CREATE_ROLE_PERMISSION, "Allows the user to create roles"),
(UPDATE_ROLE_PERMISSION, "Allows the user to update roles"),
(DELETE_ROLE_PERMISSION, "Allows the user to delete roles"),
(VIEW_ROLE_PERMISSION, "Allows to see information for roles"),
];
/// The permissions table that stores defined
#[derive(Clone)]
@ -14,16 +30,15 @@ impl Table for Permissions {
}
fn init(&self) -> DatabaseResult<()> {
self.pool
.get()?
.batch_execute(
"CREATE TABLE IF NOT EXISTS permissions (
self.pool.get()?.batch_execute(
"CREATE TABLE IF NOT EXISTS permissions (
id SERIAL PRIMARY KEY,
name VARCHAR(128) UNIQUE NOT NULL,
description VARCHAR(512)
);",
)
.map_err(DBError::from)
)?;
Ok(())
}
}
@ -73,4 +88,24 @@ impl Permissions {
Ok(created_permissions)
}
/// Returns a list of permission IDs that don't exist in the database
pub fn get_not_existing(&self, permissions_vec: &Vec<i32>) -> DatabaseResult<Vec<i32>> {
let permissions = HashSet::from_iter(permissions_vec.iter().cloned());
let mut connection = self.pool.get()?;
let rows = connection.query(
"SELECT id FROM permissions WHERE id = ANY($1)",
&[permissions_vec],
)?;
let existing_perms = rows
.into_iter()
.map(|row| -> i32 { row.get(0) })
.collect::<HashSet<i32>>();
let not_existing_perms = permissions
.difference(&existing_perms)
.cloned()
.collect::<Vec<i32>>();
Ok(not_existing_perms)
}
}

@ -1,3 +1,7 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use crate::database::models::Permission;
use crate::database::{DatabaseResult, PostgresPool, Table};
use crate::utils::error::DBError;
@ -34,7 +38,7 @@ impl RolePermissions {
pub fn by_role(&self, role_id: i32) -> DatabaseResult<Vec<Permission>> {
let mut connection = self.pool.get()?;
let rows = connection.query(
"SELECT * FROM role_permissions, permissions WHERE role_id = $1 AND role_permissions.permission_id = permissions.id",
"SELECT * FROM role_permissions, permissions WHERE role_id = $1 AND role_permissions.permission_id = permissions.id",
&[&role_id])?;
serde_postgres::from_rows(&rows).map_err(DBError::from)

@ -1,7 +1,15 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use crate::database::models::Role;
use crate::database::role_permissions::RolePermissions;
use crate::database::{DatabaseResult, PostgresPool, Table, DEFAULT_ADMIN_EMAIL, ENV_ADMIN_EMAIL};
use crate::database::{
DatabaseResult, PostgresPool, Table, ADMIN_ROLE_NAME, DEFAULT_ADMIN_EMAIL, ENV_ADMIN_EMAIL,
};
use crate::utils::error::DBError;
use std::collections::HashSet;
use std::iter::FromIterator;
/// The role table that stores
/// all defined roles
@ -44,53 +52,142 @@ impl Roles {
description: Option<String>,
permissions: Vec<i32>,
) -> DatabaseResult<Role> {
let permissions: HashSet<i32> = HashSet::from_iter(permissions.into_iter());
let mut connection = self.pool.get()?;
let exists = connection.query_opt("SELECT id FROM roles WHERE name = $1", &[&name])?;
if exists.is_some() {
return Err(DBError::RecordExists);
}
log::trace!("Preparing transaction");
let admin_email = dotenv::var(ENV_ADMIN_EMAIL).unwrap_or(DEFAULT_ADMIN_EMAIL.to_string());
let mut transaction = connection.transaction()?;
let result: DatabaseResult<Role> = {
let row = transaction.query_one(
"INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING *",
&[&name, &description],
let row = transaction.query_one(
"INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING *",
&[&name, &description],
)?;
let role: Role = serde_postgres::from_row(&row)?;
for permission in permissions {
transaction.execute(
"INSERT INTO role_permissions (role_id, permission_id) VALUES ($1, $2);",
&[&role.id, &permission],
)?;
let role: Role = serde_postgres::from_row(&row)?;
for permission in permissions {
transaction.execute(
"INSERT INTO role_permissions (role_id, permission_id) VALUES ($1, $2);",
&[&role.id, &permission],
)?;
}
if let Err(e) = transaction.execute(
"INSERT INTO user_roles (user_id, role_id) VALUES ((SELECT id FROM users WHERE email = $1), $2)",
&[&admin_email, &role.id],
) {
log::debug!("Failed to add role to admin user: {}", e);
}
Ok(role)
};
match result {
Err(e) => {
log::warn!("Failed to create role {}: {}", name, e);
log::trace!("Rolling back...");
transaction.rollback()?;
log::trace!("Rolled back!");
Err(e)
}
Ok(role) => {
log::debug!("Successfully created role {} with id {}", name, role.id);
log::trace!("Committing...");
transaction.commit()?;
log::trace!("Committed!");
Ok(role)
}
}
if let Err(e) = transaction.execute(
"INSERT INTO user_roles (user_id, role_id) VALUES ((SELECT id FROM users WHERE email = $1), $2)",
&[&admin_email, &role.id],
) {
log::debug!("Failed to add role to admin user: {}", e);
}
transaction.commit()?;
Ok(role)
}
/// Returns information for a role
pub fn get_role(&self, name: String) -> DatabaseResult<Role> {
let mut connection = self.pool.get()?;
let result = connection.query_opt("SELECT * FROM roles WHERE roles.name = $1", &[&name])?;
if let Some(row) = result {
Ok(serde_postgres::from_row::<Role>(&row)?)
} else {
Err(DBError::RecordDoesNotExist)
}
}
/// Returns a list of all roles
pub fn get_roles(&self) -> DatabaseResult<Vec<Role>> {
let mut connection = self.pool.get()?;
let results = connection.query("SELECT * FROM roles", &[])?;
let mut roles = Vec::new();
for row in results {
roles.push(serde_postgres::from_row::<Role>(&row)?);
}
Ok(roles)
}
pub fn update_role(
&self,
old_name: String,
name: String,
description: Option<String>,
permissions: Vec<i32>,
) -> DatabaseResult<Role> {
if old_name == ADMIN_ROLE_NAME {
return Err(DBError::GenericError(
"The admin role can't be altered!".to_string(),
));
}
let permissions = HashSet::from_iter(permissions.into_iter());
let mut connection = self.pool.get()?;
let mut transaction = connection.transaction()?;
let id: i32 = transaction
.query_opt("SELECT id FROM roles WHERE name = $1", &[&old_name])?
.ok_or(DBError::RecordDoesNotExist)?
.get(0);
let name_exists =
transaction.query_opt("SELECT id FROM roles WHERE name = $1", &[&name])?;
if name_exists.is_some() {
return Err(DBError::GenericError(format!(
"A role with the name {} already exists!",
name
)));
}
let update_result = transaction.query_one(
"UPDATE roles SET name = $3, description = $2 WHERE id = $1 RETURNING *",
&[&id, &description, &name],
)?;
let current_permissions = transaction
.query(
"SELECT permission_id from role_permissions WHERE role_id = $1",
&[&id],
)?
.into_iter()
.map(|r| -> i32 { r.get(0) })
.collect::<HashSet<i32>>();
let new_permissions = permissions.difference(&current_permissions);
let deleted_permissions = current_permissions.difference(&permissions);
for new in new_permissions {
transaction.query(
"INSERT INTO role_permissions (role_id, permission_id) VALUES ($1, $2)",
&[&id, new],
)?;
}
for deleted in deleted_permissions {
transaction.query(
"DELETE FROM role_permissions WHERE role_id = $1 AND permission_id = $2",
&[&id, deleted],
)?;
}
transaction.commit()?;
Ok(serde_postgres::from_row::<Role>(&update_result)?)
}
/// Deletes a role if it exists
pub fn delete_role(&self, name: &String) -> DatabaseResult<()> {
if name == ADMIN_ROLE_NAME {
return Err(DBError::GenericError(
"The admin role can't be altered!".to_string(),
));
}
let mut connection = self.pool.get()?;
let result = connection.query_opt("SELECT id FROM roles WHERE name = $1", &[name])?;
if result.is_none() {
Err(DBError::RecordDoesNotExist)
} else {
connection.query("DELETE FROM roles WHERE name = $1", &[name])?;
Ok(())
}
}
}

@ -1,15 +1,21 @@
use crate::utils::{create_user_token, get_user_id_from_token, TOKEN_LENGTH};
use serde::Serialize;
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::cmp::{max, min};
use std::collections::HashMap;
use std::time::Instant;
use serde::Serialize;
use zeroize::Zeroize;
use crate::utils::{create_user_token, get_user_id_from_token, TOKEN_LENGTH};
const REQUEST_TOKEN_EXPIRE_SECONDS: u32 = 60 * 10;
const REFRESH_TOKEN_EXPIRE_SECONDS: u32 = 60 * 60 * 24;
/// A struct to store session tokens of a user in a API-readable format
#[derive(Clone, Debug, Zeroize, Serialize)]
#[derive(Clone, Debug, Zeroize, Serialize, JsonSchema)]
#[zeroize(drop)]
pub struct SessionTokens {
pub request_token: String,

@ -1,3 +1,7 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use crate::database::models::Role;
use crate::database::{DatabaseResult, PostgresPool, Table};
use crate::utils::error::DBError;

@ -1,3 +1,12 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::sync::Arc;
use parking_lot::Mutex;
use zeroize::{Zeroize, Zeroizing};
use crate::database::models::UserRecord;
use crate::database::tokens::{SessionTokens, TokenStore};
use crate::database::user_roles::UserRoles;
@ -5,10 +14,6 @@ use crate::database::{DatabaseResult, PostgresPool, Table};
use crate::utils::error::DBError;
use crate::utils::{create_salt, hash_password};
use parking_lot::Mutex;
use std::sync::Arc;
use zeroize::{Zeroize, Zeroizing};
/// Table that stores users with their email addresses and hashed passwords
#[derive(Clone)]
pub struct Users {
@ -146,6 +151,23 @@ impl Users {
}
}
/// Returns if the user has the given permission
pub fn has_permission(&self, id: i32, permission: &str) -> DatabaseResult<bool> {
let mut connection = self.pool.get()?;
let row = connection.query_opt(
"\
SELECT * FROM user_roles, role_permissions, permissions
WHERE user_roles.user_id = $1
AND user_roles.role_id = role_permissions.role_id
AND role_permissions.permission_id = permissions.id
AND permissions.name = $2
LIMIT 1
",
&[&id, &permission],
)?;
Ok(row.is_some())
}
/// Validates the login data of the user by creating the hash for the given password
/// and comparing it with the database entry
fn validate_login(&self, email: &String, password: &String) -> DatabaseResult<bool> {

@ -1,6 +1,13 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
#[macro_use]
extern crate rouille;
#[macro_use]
extern crate schemars;
pub mod database;
pub mod server;
pub mod utils;

@ -1,13 +1,19 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::thread;
use std::thread::Builder;
use chrono::Local;
use colored::Colorize;
use crossbeam_utils::sync::WaitGroup;
use env_logger::Env;
use log::Level;
use flotte_user_management::database::Database;
use flotte_user_management::server::http_server::UserHttpServer;
use flotte_user_management::server::user_rpc::UserRpcServer;
use log::Level;
use std::thread;
use std::thread::Builder;
fn main() {
init_logger();

@ -0,0 +1,82 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use schemars::JsonSchema;
use std::collections::HashMap;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
pub struct RESTDocumentation {
paths: HashMap<String, String>,
base_path: String,
}
impl RESTDocumentation {
pub fn new(base_path: &str) -> Self {
Self {
paths: HashMap::new(),
base_path: base_path.to_string(),
}
}
pub fn get(&self, path: String) -> String {
log::trace!("Rendering help for {}.", path);
format!(
"<html><head><style type='text/css'>{}</style></head><body>{}</body></html>",
include_str!("style.css"),
self.paths.get(&path).unwrap_or(&self.landing())
)
}
fn landing(&self) -> String {
let mut keys = self.paths.keys().cloned().collect::<Vec<String>>();
keys.sort();
let types = keys.into_iter().fold("".to_string(), |a, b| {
format!("{}<br><a href='{}?path={2}'>{2}</a>", a, self.base_path, b)
});
format!("<h1>Paths</h1><br>{}", types)
}
pub fn add_path<I: JsonSchema, O: JsonSchema>(
&mut self,
path: &str,
method: &str,
description: &str,
) -> Result<(), serde_json::error::Error> {
let input_schema = schema_for!(I);
let output_schema = schema_for!(O);
let input_json = highlight_json(serde_json::to_string_pretty(&input_schema)?);
let output_json = highlight_json(serde_json::to_string_pretty(&output_schema)?);
let content = format!(
"\
<a href={}>Back</a>
<h1><code>{}: {}</code></h1>
<p>{}</p>
<h2>Input</h2>
<code>{}</code>
<h2>Output</h2>
<code>{}</code>
",
self.base_path, method, path, description, input_json, output_json
);
self.paths.insert(path.to_string(), content);
Ok(())
}
}
fn highlight_json(input: String) -> String {
lazy_static::lazy_static! { static ref PS: SyntaxSet = SyntaxSet::load_defaults_nonewlines(); }
lazy_static::lazy_static! { static ref TS: ThemeSet = ThemeSet::load_defaults(); }
highlighted_html_for_string(
input.as_str(),
&PS,
PS.find_syntax_by_token("json").unwrap(),
&TS.themes["InspiredGitHub"],
)
}

@ -0,0 +1,26 @@
/*
* flotte-user-management server for managing users, roles and permissions
* Copyright (C) 2020 trivernis
* See LICENSE for more information
*/
body {
font-family: "Fira Sans", "Noto Sans", sans-serif;
}
code {
font-family: "Fira Code", "DejaVu Sans Mono", monospace;
}
code > pre {
font-family: "Fira Code", "DejaVu Sans Mono", monospace;
max-width: 100%;
padding: 2em;
border: 2px solid #EEE;
overflow: auto;
}
a {
font-weight: bold;
font-size: 1.5em;
}

@ -1,13 +1,39 @@
use crate::database::Database;
use crate::server::messages::{LoginMessage, LogoutConfirmation, LogoutMessage, RefreshMessage};
use crate::utils::error::DBError;
use rouille::{Request, Response, Server};
use serde::export::Formatter;
use serde::Serialize;
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::error::Error;
use std::fmt::{self, Display};
use std::io::Read;
use regex::Regex;
use rouille::{Request, Response, Server};
use serde::export::Formatter;
use serde::Serialize;
use crate::database::models::Role;
use crate::database::permissions::{
CREATE_ROLE_PERMISSION, DELETE_ROLE_PERMISSION, UPDATE_ROLE_PERMISSION, VIEW_ROLE_PERMISSION,
};
use crate::database::tokens::SessionTokens;
use crate::database::Database;
use crate::server::documentation::RESTDocumentation;
use crate::server::messages::{
DeleteRoleResponse, ErrorMessage, FullRoleData, LoginMessage, LogoutConfirmation,
LogoutMessage, ModifyRoleRequest, RefreshMessage,
};
use crate::utils::error::DBError;
use crate::utils::get_user_id_from_token;
macro_rules! require_permission {
($database:expr,$request:expr,$permission:expr) => {
let (_token, id) = validate_request_token($request, $database)?;
if !$database.users.has_permission(id, $permission)? {
return Err(HTTPError::new("Insufficient permissions".to_string(), 403));
}
};
}
const LISTEN_ADDRESS: &str = "HTTP_SERVER_ADDRESS";
const DEFAULT_LISTEN_ADDRESS: &str = "127.0.0.1:8080";
const ENV_ENABLE_CORS: &str = "ENABLE_CORS";
@ -29,6 +55,7 @@ impl Display for HTTPError {
write!(f, "{}", self.message)
}
}
impl Error for HTTPError {}
impl From<DBError> for HTTPError {
@ -73,6 +100,9 @@ impl UserHttpServer {
let database = Database::clone(&self.database);
let server = Server::new(&listen_address, move |request| {
let mut response = router!(request,
(GET) (/info) => {
Self::info(request).unwrap_or_else(HTTPError::into)
},
(POST) (/login) => {
Self::login(&database, request).unwrap_or_else(HTTPError::into)
},
@ -82,6 +112,21 @@ impl UserHttpServer {
(POST) (/logout) => {
Self::logout(&database, request).unwrap_or_else(HTTPError::into)
},
(GET) (/roles/{name: String}) => {
Self::get_role(&database, request, name).unwrap_or_else(HTTPError::into)
},
(GET) (/roles) => {
Self::get_roles(&database, request).unwrap_or_else(HTTPError::into)
},
(POST) (/roles/create) => {
Self::create_role(&database, request).unwrap_or_else(HTTPError::into)
},
(POST) (/roles/{name:String}/update) => {
Self::update_role(&database, request, name).unwrap_or_else(HTTPError::into)
},
(POST) (/roles/{name: String}/delete) => {
Self::delete_role(&database, request, name).unwrap_or_else(HTTPError::into)
},
_ => if request.method() == "OPTIONS" {
Response::empty_204()
} else {
@ -113,6 +158,56 @@ impl UserHttpServer {
server.run()
}
fn build_docs() -> Result<RESTDocumentation, serde_json::Error> {
let mut doc = RESTDocumentation::new("/info");
doc.add_path::<LoginMessage, SessionTokens>(
"/login",
"POST",
"Returns request and refresh tokens",
)?;
doc.add_path::<RefreshMessage, SessionTokens>(
"/new-token",
"POST",
"Returns a new request token",
)?;
doc.add_path::<LogoutMessage, LogoutConfirmation>(
"/logout",
"POST",
"Invalidates the refresh and request tokens",
)?;
doc.add_path::<(), FullRoleData>(
"/roles/{name:String}",
"GET",
"Returns the role with the given name",
)?;
doc.add_path::<(), Vec<Role>>("/roles", "GET", "Returns a list of all roles")?;
doc.add_path::<ModifyRoleRequest, FullRoleData>(
"/roles/create",
"POST",
"Creates a new role",
)?;
doc.add_path::<ModifyRoleRequest, FullRoleData>(
"/roles/{name:String}/update",
"POST",
"Updates an existing role",
)?;
doc.add_path::<(), DeleteRoleResponse>(
"/roles/{name:String}/delete",
"POST",
"Deletes a role",
)?;
Ok(doc)
}
fn info(request: &Request) -> HTTPResult<Response> {
lazy_static::lazy_static! {static ref DOCS: RESTDocumentation = UserHttpServer::build_docs().unwrap();}
Ok(Response::html(
DOCS.get(request.get_param("path").unwrap_or("/".to_string())),
))
}
/// Handles the login part of the REST api
fn login(database: &Database, request: &Request) -> HTTPResult<Response> {
let login_request: LoginMessage =
@ -143,6 +238,94 @@ impl UserHttpServer {
Ok(Response::json(&LogoutConfirmation { success }).with_status_code(205))
}
/// Returns the data for a given role
fn get_role(database: &Database, request: &Request, name: String) -> HTTPResult<Response> {
require_permission!(database, request, VIEW_ROLE_PERMISSION);
let role = database.roles.get_role(name)?;
let permissions = database.role_permission.by_role(role.id)?;
Ok(Response::json(&FullRoleData {
id: role.id,
name: role.name,
permissions,
}))
}
/// Returns a list of all roles
fn get_roles(database: &Database, request: &Request) -> HTTPResult<Response> {
require_permission!(database, request, VIEW_ROLE_PERMISSION);
let roles = database.roles.get_roles()?;
Ok(Response::json(&roles))
}
fn create_role(database: &Database, request: &Request) -> HTTPResult<Response> {
require_permission!(database, request, CREATE_ROLE_PERMISSION);
let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str())
.map_err(|e| HTTPError::new(e.to_string(), 400))?;
let not_existing = database
.permissions
.get_not_existing(&message.permissions)?;
if !not_existing.is_empty() {
return Ok(Response::json(&ErrorMessage::new(format!(
"The permissions {:?} don't exist",
not_existing
)))
.with_status_code(400));
}
let role =
database
.roles
.create_role(message.name, message.description, message.permissions)?;
let permissions = database.role_permission.by_role(role.id)?;
Ok(Response::json(&FullRoleData {
id: role.id,
permissions,
name: role.name,
})
.with_status_code(201))
}
fn update_role(database: &Database, request: &Request, name: String) -> HTTPResult<Response> {
require_permission!(database, request, UPDATE_ROLE_PERMISSION);
let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str())
.map_err(|e| HTTPError::new(e.to_string(), 400))?;
let not_existing = database
.permissions
.get_not_existing(&message.permissions)?;
if !not_existing.is_empty() {
return Ok(Response::json(&ErrorMessage::new(format!(
"The permissions {:?} don't exist",
not_existing
)))
.with_status_code(400));
}
let role = database.roles.update_role(
name,
message.name,
message.description,
message.permissions,
)?;
let permissions = database.role_permission.by_role(role.id)?;
Ok(Response::json(&FullRoleData {
id: role.id,
permissions,
name: role.name,
}))
}
fn delete_role(database: &Database, request: &Request, role: String) -> HTTPResult<Response> {
require_permission!(database, request, DELETE_ROLE_PERMISSION);
database.roles.delete_role(&role)?;
Ok(Response::json(&DeleteRoleResponse {
success: true,
role,
}))
}
}
/// Parses the body of a http request into a string representation
@ -156,3 +339,22 @@ fn parse_string_body(request: &Request) -> HTTPResult<String> {
Ok(string_body)
}
/// Parses and validates the request token from the http header
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();}
let token = request
.header("authorization")
.ok_or(HTTPError::new("401 Unauthorized".to_string(), 401))?;
let token = BEARER_REGEX.replace(token, "");
let (valid, _) = database.users.validate_request_token(&token.to_string())?;
if !valid {
Err(HTTPError::new("Invalid request token".to_string(), 401))
} else {
Ok((
token.to_string(),
get_user_id_from_token(&token.to_string())
.ok_or(HTTPError::new("Invalid request token".to_string(), 401))?,
))
}
}

@ -1,12 +1,18 @@
use crate::database::models::CreatePermissionsEntry;
use crate::utils::error::DBError;
use serde::export::Formatter;
use serde::{Deserialize, Serialize};
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::error::Error;
use std::fmt;
use std::fmt::Display;
use serde::export::Formatter;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
use crate::database::models::{CreatePermissionsEntry, Permission};
use crate::utils::error::DBError;
#[derive(Deserialize)]
pub struct TokenRequest {
pub token: String,
@ -28,6 +34,7 @@ impl Display for ErrorMessage {
write!(f, "{}", self.message)
}
}
impl Error for ErrorMessage {}
impl From<DBError> for ErrorMessage {
@ -64,8 +71,8 @@ pub struct GetPermissionsRequest {
pub roles: Vec<i32>,
}
#[derive(Deserialize)]
pub struct CreateRoleRequest {
#[derive(Deserialize, JsonSchema)]
pub struct ModifyRoleRequest {
pub name: String,
pub description: Option<String>,
pub permissions: Vec<i32>,
@ -76,26 +83,39 @@ pub struct CreatePermissionsRequest {
pub permissions: Vec<CreatePermissionsEntry>,
}
#[derive(Deserialize, Zeroize)]
#[derive(Deserialize, Zeroize, JsonSchema)]
#[zeroize(drop)]
pub struct LoginMessage {
pub email: String,
pub password: String,
}
#[derive(Deserialize, Zeroize)]
#[derive(Deserialize, Zeroize, JsonSchema)]
#[zeroize(drop)]
pub struct RefreshMessage {
pub refresh_token: String,
}
#[derive(Deserialize, Zeroize)]
#[derive(Deserialize, Zeroize, JsonSchema)]
#[zeroize(drop)]
pub struct LogoutMessage {
pub request_token: String,
}
#[derive(Serialize)]
#[derive(Serialize, JsonSchema)]
pub struct LogoutConfirmation {
pub success: bool,
}
#[derive(Serialize, JsonSchema)]
pub struct FullRoleData {
pub id: i32,
pub name: String,
pub permissions: Vec<Permission>,
}
#[derive(Serialize, JsonSchema)]
pub struct DeleteRoleResponse {
pub success: bool,
pub role: String,
}

@ -1,3 +1,8 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
pub mod documentation;
pub mod http_server;
pub mod messages;
pub mod rpc_methods;

@ -1,4 +1,9 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
#![allow(dead_code)]
pub(crate) const NULL: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
pub(crate) const ERROR: [u8; 4] = [0x0F, 0x0F, 0x0F, 0x0F];
pub(crate) const INFO: [u8; 4] = [0x49, 0x4e, 0x46, 0x4f];

@ -1,18 +1,25 @@
use super::rpc_methods::*;
use crate::database::Database;
use crate::server::messages::{
CreatePermissionsRequest, CreateRoleRequest, ErrorMessage, GetPermissionsRequest, InfoEntry,
TokenRequest,
};
use crate::utils::get_user_id_from_token;
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::collections::HashMap;
use std::sync::Arc;
use std::thread::Builder;
use msgrpc::message::Message;
use msgrpc::server::RpcServer;
use rmp_serde::Deserializer;
use scheduled_thread_pool::ScheduledThreadPool;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread::Builder;
use crate::database::Database;
use crate::server::messages::{
CreatePermissionsRequest, ErrorMessage, GetPermissionsRequest, InfoEntry, ModifyRoleRequest,
TokenRequest,
};
use crate::utils::get_user_id_from_token;
use super::rpc_methods::*;
const RPC_SERVER_ADDRESS: &str = "RPC_SERVER_ADDRESS";
const DEFAULT_SERVER_ADDRESS: &str = "127.0.0.1:5555";
@ -178,7 +185,7 @@ impl UserRpcServer {
/// Handles the requests for creating new roles
fn handle_create_role(database: Database, data: &Vec<u8>) -> RpcResult<Message> {
log::trace!("Create Role");
let message = CreateRoleRequest::deserialize(&mut Deserializer::new(&mut data.as_slice()))
let message = ModifyRoleRequest::deserialize(&mut Deserializer::new(&mut data.as_slice()))
.map_err(|e| ErrorMessage::new(e.to_string()))?;
let role =
database

@ -1,13 +1,19 @@
use r2d2::Error;
use serde_postgres::DeError;
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::error;
use std::fmt::{self, Display, Formatter};
use r2d2::Error;
use serde_postgres::DeError;
#[derive(Debug)]
pub enum DBError {
Postgres(PostgresError),
Pool(r2d2::Error),
RecordExists,
RecordDoesNotExist,
BCryptError,
DeserializeError(serde_postgres::DeError),
GenericError(String),
@ -25,11 +31,12 @@ impl DBError {
pub fn to_string(&self) -> String {
match self {
DBError::GenericError(g) => g.clone(),
DBError::RecordExists => "Record Exists".to_string(),
DBError::RecordExists => "Record exists".to_string(),
DBError::Postgres(p) => p.to_string(),
DBError::DeserializeError(de) => de.to_string(),
DBError::BCryptError => "BCrypt Hash creation error".to_string(),
DBError::Pool(p) => p.to_string(),
DBError::RecordDoesNotExist => "Record does not exist".to_string(),
}
}
}

@ -1,7 +1,12 @@
// flotte-user-management server for managing users, roles and permissions
// Copyright (C) 2020 trivernis
// See LICENSE for more information
use std::panic;
use bcrypt::DEFAULT_COST;
use byteorder::{BigEndian, ByteOrder};
use rand::Rng;
use std::panic;
pub mod error;

Loading…
Cancel
Save