From 22ffe7542210f1e7d8d886ea7c3a135a9145cdb4 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 31 Oct 2021 11:00:07 -0600 Subject: [PATCH] Added API token handling + Tokens can be added by the add_token command or via the CLI + Tokens are checked for authorization before a command is run + Added an error message field to the GeoffreyApiResponse + Made tokens strings instead of u64s + Tokens are randomly generated 64 char long alphanumeric strings + Clippy + Fmt --- Cargo.lock | 9 +-- geoffrey_api/Cargo.toml | 1 + geoffrey_api/src/commands/add_token.rs | 48 +++++++++++++ geoffrey_api/src/commands/mod.rs | 38 +++++++--- geoffrey_api/src/helper/mod.rs | 7 ++ geoffrey_api/src/main.rs | 70 +++++++++++++++---- geoffrey_models/src/models/mod.rs | 3 +- .../models/parameters/add_location_params.rs | 8 +-- .../src/models/parameters/add_token_params.rs | 20 ++++++ .../src/models/parameters/find_params.rs | 6 +- geoffrey_models/src/models/parameters/mod.rs | 3 +- .../src/models/parameters/register_params.rs | 6 +- .../src/models/response/api_error.rs | 24 ++++++- geoffrey_models/src/models/response/mod.rs | 5 +- geoffrey_models/src/models/token.rs | 18 +++++ 15 files changed, 225 insertions(+), 41 deletions(-) create mode 100644 geoffrey_api/src/commands/add_token.rs create mode 100644 geoffrey_models/src/models/parameters/add_token_params.rs diff --git a/Cargo.lock b/Cargo.lock index 8838656..82917a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,6 +311,7 @@ dependencies = [ "geoffrey_db", "geoffrey_models", "log", + "rand 0.8.4", "serde 1.0.124", "serde_json", "simple_logger", @@ -863,9 +864,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", "rand_chacha 0.3.0", @@ -1184,7 +1185,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if", "libc", - "rand 0.8.3", + "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", @@ -1359,7 +1360,7 @@ dependencies = [ "httparse", "input_buffer", "log", - "rand 0.8.3", + "rand 0.8.4", "sha-1", "url", "utf-8", diff --git a/geoffrey_api/Cargo.toml b/geoffrey_api/Cargo.toml index 7afd971..18a81b2 100644 --- a/geoffrey_api/Cargo.toml +++ b/geoffrey_api/Cargo.toml @@ -17,3 +17,4 @@ config = "0.11.0" structopt = "0.3.21" log = "0.4.14" simple_logger = "1.13.0" +rand = "0.8.4" \ No newline at end of file diff --git a/geoffrey_api/src/commands/add_token.rs b/geoffrey_api/src/commands/add_token.rs new file mode 100644 index 0000000..888821b --- /dev/null +++ b/geoffrey_api/src/commands/add_token.rs @@ -0,0 +1,48 @@ +use crate::commands::{Command, RequestType}; +use crate::context::Context; +use crate::Result; +use geoffrey_models::models::parameters::add_token_params::AddTokenParams; +use geoffrey_models::models::player::Player; +use geoffrey_models::models::response::api_error::GeoffreyAPIError; +use geoffrey_models::models::token::Token; +use geoffrey_models::models::CommandLevel; +use rand::distributions::Alphanumeric; +use rand::Rng; +use std::sync::Arc; + +pub struct AddToken {} + +impl Command for AddToken { + type Req = AddTokenParams; + type Resp = Token; + + fn command_name() -> String { + "add_token".to_string() + } + + fn request_type() -> RequestType { + RequestType::POST + } + + fn command_level() -> CommandLevel { + CommandLevel::ADMIN + } + + fn run_command(ctx: Arc, req: Self::Req, _: Option) -> Result { + let mut token = Token::default(); + + let secret: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(64) + .map(char::from) + .collect(); + + token.secret = secret; + + for permission in req.permissions { + token.set_permission(permission); + } + + ctx.db.insert(token).map_err(GeoffreyAPIError::from) + } +} diff --git a/geoffrey_api/src/commands/mod.rs b/geoffrey_api/src/commands/mod.rs index 187829c..4569288 100644 --- a/geoffrey_api/src/commands/mod.rs +++ b/geoffrey_api/src/commands/mod.rs @@ -2,12 +2,13 @@ use crate::commands::add_location::AddLocation; use crate::commands::find::FindCommand; use crate::commands::register::Register; use crate::context::Context; -use crate::helper::get_player_from_req; +use crate::helper::{get_player_from_req, get_token_from_req}; use crate::Result; use geoffrey_models::models::parameters::CommandRequest; use geoffrey_models::models::player::Player; use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::APIResponse; +use geoffrey_models::models::token::{Permissions, Token}; use geoffrey_models::models::CommandLevel; use serde::de::DeserializeOwned; use serde::Serialize; @@ -17,6 +18,7 @@ use warp::filters::BoxedFilter; use warp::Filter; pub mod add_location; +pub mod add_token; pub mod find; pub mod register; @@ -36,17 +38,29 @@ pub trait Command { fn command_level() -> CommandLevel; fn run_command(ctx: Arc, req: Self::Req, user: Option) -> Result; - fn user_is_authorized(user: &Option) -> Result<()> { - if Self::command_level() == CommandLevel::ALL { - Ok(()) - } else if let Some(user) = user { - if user.auth_level >= Self::command_level() { + fn user_is_authorized(token: &Option, user: &Option) -> Result<()> { + if let Some(token) = token { + if !match Self::command_level() { + CommandLevel::MOD => token.check_permission(Permissions::ModCommand), + CommandLevel::ADMIN => token.check_permission(Permissions::Admin), + _ => token.check_permission(Permissions::Command), + } { + return Err(GeoffreyAPIError::TokenNotAuthorized); + } + + if Self::command_level() == CommandLevel::ALL { Ok(()) + } else if let Some(user) = user { + if user.auth_level >= Self::command_level() { + Ok(()) + } else { + Err(GeoffreyAPIError::PermissionInsufficient) + } } else { - Err(GeoffreyAPIError::PermissionInsufficient) + Err(GeoffreyAPIError::PlayerNotRegistered) } } else { - Err(GeoffreyAPIError::PlayerNotRegistered) + Err(GeoffreyAPIError::TokenNotAuthorized) } } } @@ -56,8 +70,9 @@ pub fn handle_command(ctx: Arc, req: T::Req) -> Result T::run_command(ctx, req, user), Err(e) => Err(e), } @@ -75,8 +90,9 @@ pub fn create_command_filter(ctx: Arc) -> BoxedFilter<(impl warp::reply::json(&APIResponse::Response::(reply)) } else { let e = reply.err().unwrap(); - log::warn!("Got error when processing command: {:?}", e); - warp::reply::json(&APIResponse::::Error(e)) + let msg = e.to_string(); + log::warn!("Got error when processing command '{:?}': {}", e, msg); + warp::reply::json(&APIResponse::::Error { error: e, msg }) } }); diff --git a/geoffrey_api/src/helper/mod.rs b/geoffrey_api/src/helper/mod.rs index e43580c..b52c2ce 100644 --- a/geoffrey_api/src/helper/mod.rs +++ b/geoffrey_api/src/helper/mod.rs @@ -2,6 +2,7 @@ use crate::Result; use geoffrey_db::database::Database; use geoffrey_models::models::parameters::CommandRequest; use geoffrey_models::models::player::Player; +use geoffrey_models::models::token::Token; pub fn get_player_from_req(db: &Database, req: &T) -> Result> { if let Some(user_id) = req.user_id() { @@ -12,3 +13,9 @@ pub fn get_player_from_req(db: &Database, req: &T) -> Result< Ok(None) } } + +pub fn get_token_from_req(db: &Database, req: &T) -> Result> { + Ok(db + .filter(|_, token: &Token| token.secret == req.token())? + .next()) +} diff --git a/geoffrey_api/src/main.rs b/geoffrey_api/src/main.rs index e10c4a2..f41fd23 100644 --- a/geoffrey_api/src/main.rs +++ b/geoffrey_api/src/main.rs @@ -4,14 +4,19 @@ mod context; mod helper; mod logging; -use crate::commands::command_filter; +use crate::commands::add_token::AddToken; +use crate::commands::{command_filter, Command}; use crate::config::GeoffreyAPIConfig; use crate::context::Context; use crate::logging::{init_logging, LogLevel}; +use geoffrey_models::models::parameters::add_token_params::AddTokenParams; use geoffrey_models::models::response::api_error::GeoffreyAPIError; +use geoffrey_models::models::token::Permissions; +use std::convert::TryFrom; use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; +use std::sync::Arc; use structopt::StructOpt; pub type Result = std::result::Result; @@ -29,6 +34,54 @@ struct Args { default_value = "Info" )] log_level: LogLevel, + + #[structopt(subcommand)] + command: GeoffreyApiCommand, +} + +#[derive(Debug, PartialEq, StructOpt, Clone)] +enum GeoffreyApiCommand { + Run, + CreateAdminToken(CreateTokenCommand), +} + +#[derive(Debug, StructOpt, PartialEq, Clone)] +struct CreateTokenCommand { + #[structopt(parse(try_from_str = Permissions::try_from))] + pub permissions: Vec, +} + +async fn run_server(ctx: Arc) { + let socket_addr = match SocketAddr::from_str(ctx.cfg.host.as_str()) { + Ok(socket_addr) => socket_addr, + Err(e) => { + log::warn!("Error parsing {} as address: {}", ctx.cfg.host, e); + return; + } + }; + + let api = command_filter(ctx.clone()); + + warp::serve(api).run(socket_addr).await; +} + +fn create_token(ctx: Arc, perms: Vec) { + match AddToken::run_command( + ctx, + AddTokenParams { + token: "".to_string(), + permissions: perms, + }, + None, + ) { + Ok(token) => { + // Don't log this to keep tokens out of the log + println!("Added admin token with secret: {}", token.secret) + } + Err(e) => { + log::warn!("Unable to create admin token: {}", e) + } + } } #[tokio::main] @@ -48,17 +101,10 @@ async fn main() { } }; - let socket_addr = match SocketAddr::from_str(cfg.host.as_str()) { - Ok(socket_addr) => socket_addr, - Err(e) => { - log::warn!("Error parsing {} as address: {}", cfg.host, e); - return; - } - }; - let ctx = Context::new(cfg).unwrap(); - let api = command_filter(ctx.clone()); - - warp::serve(api).run(socket_addr).await; + match args.command { + GeoffreyApiCommand::Run => run_server(ctx).await, + GeoffreyApiCommand::CreateAdminToken(perms) => create_token(ctx, perms.permissions), + }; } diff --git a/geoffrey_models/src/models/mod.rs b/geoffrey_models/src/models/mod.rs index 933b40d..d4ee6d5 100644 --- a/geoffrey_models/src/models/mod.rs +++ b/geoffrey_models/src/models/mod.rs @@ -52,5 +52,6 @@ pub struct Tunnel { pub enum CommandLevel { ALL = 0, REGISTERED = 1, - ADMIN = 2, + MOD = 2, + ADMIN = 3, } diff --git a/geoffrey_models/src/models/parameters/add_location_params.rs b/geoffrey_models/src/models/parameters/add_location_params.rs index 731911c..f037c49 100644 --- a/geoffrey_models/src/models/parameters/add_location_params.rs +++ b/geoffrey_models/src/models/parameters/add_location_params.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AddLocationParams { - token: u64, - user_id: UserID, + pub token: String, + pub user_id: UserID, pub name: String, pub position: Position, pub loc_type: LocationType, @@ -15,8 +15,8 @@ pub struct AddLocationParams { } impl CommandRequest for AddLocationParams { - fn token(&self) -> u64 { - self.token + fn token(&self) -> String { + self.token.clone() } fn user_id(&self) -> Option { diff --git a/geoffrey_models/src/models/parameters/add_token_params.rs b/geoffrey_models/src/models/parameters/add_token_params.rs new file mode 100644 index 0000000..f550b22 --- /dev/null +++ b/geoffrey_models/src/models/parameters/add_token_params.rs @@ -0,0 +1,20 @@ +use crate::models::parameters::CommandRequest; +use crate::models::player::UserID; +use crate::models::token::Permissions; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AddTokenParams { + pub token: String, + pub permissions: Vec, +} + +impl CommandRequest for AddTokenParams { + fn token(&self) -> String { + self.token.clone() + } + + fn user_id(&self) -> Option { + None + } +} diff --git a/geoffrey_models/src/models/parameters/find_params.rs b/geoffrey_models/src/models/parameters/find_params.rs index d873017..f1dd514 100644 --- a/geoffrey_models/src/models/parameters/find_params.rs +++ b/geoffrey_models/src/models/parameters/find_params.rs @@ -3,12 +3,12 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FindParams { - pub token: u64, + pub token: String, pub query: String, } impl CommandRequest for FindParams { - fn token(&self) -> u64 { - self.token + fn token(&self) -> String { + self.token.clone() } } diff --git a/geoffrey_models/src/models/parameters/mod.rs b/geoffrey_models/src/models/parameters/mod.rs index 1ef316c..fc8cbd5 100644 --- a/geoffrey_models/src/models/parameters/mod.rs +++ b/geoffrey_models/src/models/parameters/mod.rs @@ -1,4 +1,5 @@ pub mod add_location_params; +pub mod add_token_params; pub mod find_params; pub mod register_params; @@ -10,7 +11,7 @@ use serde::Serialize; use std::fmt::Debug; pub trait CommandRequest: Serialize + DeserializeOwned + Debug + Clone + Send + 'static { - fn token(&self) -> u64; + fn token(&self) -> String; fn user_id(&self) -> Option { None } diff --git a/geoffrey_models/src/models/parameters/register_params.rs b/geoffrey_models/src/models/parameters/register_params.rs index 2d83369..16b9c9a 100644 --- a/geoffrey_models/src/models/parameters/register_params.rs +++ b/geoffrey_models/src/models/parameters/register_params.rs @@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RegisterParameters { - pub token: u64, + pub token: String, pub new_user_id: UserID, pub username: String, } impl CommandRequest for RegisterParameters { - fn token(&self) -> u64 { - self.token + fn token(&self) -> String { + self.token.clone() } } diff --git a/geoffrey_models/src/models/response/api_error.rs b/geoffrey_models/src/models/response/api_error.rs index 34700dc..176d95f 100644 --- a/geoffrey_models/src/models/response/api_error.rs +++ b/geoffrey_models/src/models/response/api_error.rs @@ -1,10 +1,32 @@ use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub enum GeoffreyAPIError { PlayerNotRegistered, EntryNotFound, PermissionInsufficient, EntryNotUnique, DatabaseError(String), + TokenNotAuthorized, +} + +impl Display for GeoffreyAPIError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let string = match self { + GeoffreyAPIError::PlayerNotRegistered => "Player has not been registered.".to_string(), + GeoffreyAPIError::EntryNotFound => { + "An entry that matches that query was not found.".to_string() + } + GeoffreyAPIError::PermissionInsufficient => { + "Insufficient permission to preform that action".to_string() + } + GeoffreyAPIError::EntryNotUnique => "Entry not unique in the DB.".to_string(), + GeoffreyAPIError::DatabaseError(e) => format!("Database Error: {}", e), + GeoffreyAPIError::TokenNotAuthorized => { + "Token supplied in request is not authorized".to_string() + } + }; + write!(f, "{}", string) + } } diff --git a/geoffrey_models/src/models/response/mod.rs b/geoffrey_models/src/models/response/mod.rs index 6f95cf2..5a01db4 100644 --- a/geoffrey_models/src/models/response/mod.rs +++ b/geoffrey_models/src/models/response/mod.rs @@ -6,5 +6,8 @@ pub mod api_error; #[derive(Debug, Serialize, Deserialize)] pub enum APIResponse { Response(T), - Error(GeoffreyAPIError), + Error { + error: GeoffreyAPIError, + msg: String, + }, } diff --git a/geoffrey_models/src/models/token.rs b/geoffrey_models/src/models/token.rs index 592800a..e7978a4 100644 --- a/geoffrey_models/src/models/token.rs +++ b/geoffrey_models/src/models/token.rs @@ -1,7 +1,9 @@ use crate::GeoffreyDatabaseModel; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +#[derive(Serialize, Deserialize, Debug, Clone, PartialOrd, PartialEq)] pub enum Permissions { ModelGet = 0, ModelPost, @@ -10,12 +12,27 @@ pub enum Permissions { Admin, } +impl TryFrom<&str> for Permissions { + type Error = String; + fn try_from(s: &str) -> Result { + Ok(match s { + "ModelGet" => Self::ModelGet, + "ModelPost" => Self::ModelPost, + "Command" => Self::Command, + "ModCommand" => Self::ModCommand, + "Admin" => Self::Admin, + _ => return Err(format!("Unable to parse '{}' as a permission", s)), + }) + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Token { pub id: Option, permission: u64, pub created: DateTime, pub modified: DateTime, + pub secret: String, } impl Token { @@ -43,6 +60,7 @@ impl Default for Token { permission: 0, created: Utc::now(), modified: Utc::now(), + secret: String::new(), } } }