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
main
Joey Hines 2021-10-31 11:00:07 -06:00
parent b79db6029e
commit 22ffe75422
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
15 changed files with 225 additions and 41 deletions

9
Cargo.lock generated
View File

@ -311,6 +311,7 @@ dependencies = [
"geoffrey_db", "geoffrey_db",
"geoffrey_models", "geoffrey_models",
"log", "log",
"rand 0.8.4",
"serde 1.0.124", "serde 1.0.124",
"serde_json", "serde_json",
"simple_logger", "simple_logger",
@ -863,9 +864,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha 0.3.0", "rand_chacha 0.3.0",
@ -1184,7 +1185,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"rand 0.8.3", "rand 0.8.4",
"redox_syscall", "redox_syscall",
"remove_dir_all", "remove_dir_all",
"winapi", "winapi",
@ -1359,7 +1360,7 @@ dependencies = [
"httparse", "httparse",
"input_buffer", "input_buffer",
"log", "log",
"rand 0.8.3", "rand 0.8.4",
"sha-1", "sha-1",
"url", "url",
"utf-8", "utf-8",

View File

@ -17,3 +17,4 @@ config = "0.11.0"
structopt = "0.3.21" structopt = "0.3.21"
log = "0.4.14" log = "0.4.14"
simple_logger = "1.13.0" simple_logger = "1.13.0"
rand = "0.8.4"

View File

@ -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<Context>, req: Self::Req, _: Option<Player>) -> Result<Self::Resp> {
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)
}
}

View File

@ -2,12 +2,13 @@ use crate::commands::add_location::AddLocation;
use crate::commands::find::FindCommand; use crate::commands::find::FindCommand;
use crate::commands::register::Register; use crate::commands::register::Register;
use crate::context::Context; 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 crate::Result;
use geoffrey_models::models::parameters::CommandRequest; use geoffrey_models::models::parameters::CommandRequest;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::response::APIResponse; use geoffrey_models::models::response::APIResponse;
use geoffrey_models::models::token::{Permissions, Token};
use geoffrey_models::models::CommandLevel; use geoffrey_models::models::CommandLevel;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
@ -17,6 +18,7 @@ use warp::filters::BoxedFilter;
use warp::Filter; use warp::Filter;
pub mod add_location; pub mod add_location;
pub mod add_token;
pub mod find; pub mod find;
pub mod register; pub mod register;
@ -36,7 +38,16 @@ pub trait Command {
fn command_level() -> CommandLevel; fn command_level() -> CommandLevel;
fn run_command(ctx: Arc<Context>, req: Self::Req, user: Option<Player>) -> Result<Self::Resp>; fn run_command(ctx: Arc<Context>, req: Self::Req, user: Option<Player>) -> Result<Self::Resp>;
fn user_is_authorized(user: &Option<Player>) -> Result<()> { fn user_is_authorized(token: &Option<Token>, user: &Option<Player>) -> 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 { if Self::command_level() == CommandLevel::ALL {
Ok(()) Ok(())
} else if let Some(user) = user { } else if let Some(user) = user {
@ -48,6 +59,9 @@ pub trait Command {
} else { } else {
Err(GeoffreyAPIError::PlayerNotRegistered) Err(GeoffreyAPIError::PlayerNotRegistered)
} }
} else {
Err(GeoffreyAPIError::TokenNotAuthorized)
}
} }
} }
@ -56,8 +70,9 @@ pub fn handle_command<T: Command>(ctx: Arc<Context>, req: T::Req) -> Result<T::R
log::debug!("Request: {:?}", req); log::debug!("Request: {:?}", req);
let user = get_player_from_req(&ctx.db, &req)?; let user = get_player_from_req(&ctx.db, &req)?;
let token = get_token_from_req(&ctx.db, &req)?;
match T::user_is_authorized(&user) { match T::user_is_authorized(&token, &user) {
Ok(_) => T::run_command(ctx, req, user), Ok(_) => T::run_command(ctx, req, user),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -75,8 +90,9 @@ pub fn create_command_filter<T: Command>(ctx: Arc<Context>) -> BoxedFilter<(impl
warp::reply::json(&APIResponse::Response::<T::Resp>(reply)) warp::reply::json(&APIResponse::Response::<T::Resp>(reply))
} else { } else {
let e = reply.err().unwrap(); let e = reply.err().unwrap();
log::warn!("Got error when processing command: {:?}", e); let msg = e.to_string();
warp::reply::json(&APIResponse::<T::Resp>::Error(e)) log::warn!("Got error when processing command '{:?}': {}", e, msg);
warp::reply::json(&APIResponse::<T::Resp>::Error { error: e, msg })
} }
}); });

View File

@ -2,6 +2,7 @@ use crate::Result;
use geoffrey_db::database::Database; use geoffrey_db::database::Database;
use geoffrey_models::models::parameters::CommandRequest; use geoffrey_models::models::parameters::CommandRequest;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::token::Token;
pub fn get_player_from_req<T: CommandRequest>(db: &Database, req: &T) -> Result<Option<Player>> { pub fn get_player_from_req<T: CommandRequest>(db: &Database, req: &T) -> Result<Option<Player>> {
if let Some(user_id) = req.user_id() { if let Some(user_id) = req.user_id() {
@ -12,3 +13,9 @@ pub fn get_player_from_req<T: CommandRequest>(db: &Database, req: &T) -> Result<
Ok(None) Ok(None)
} }
} }
pub fn get_token_from_req<T: CommandRequest>(db: &Database, req: &T) -> Result<Option<Token>> {
Ok(db
.filter(|_, token: &Token| token.secret == req.token())?
.next())
}

View File

@ -4,14 +4,19 @@ mod context;
mod helper; mod helper;
mod logging; 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::config::GeoffreyAPIConfig;
use crate::context::Context; use crate::context::Context;
use crate::logging::{init_logging, LogLevel}; 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::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::token::Permissions;
use std::convert::TryFrom;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
pub type Result<T> = std::result::Result<T, GeoffreyAPIError>; pub type Result<T> = std::result::Result<T, GeoffreyAPIError>;
@ -29,6 +34,54 @@ struct Args {
default_value = "Info" default_value = "Info"
)] )]
log_level: LogLevel, 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<Permissions>,
}
async fn run_server(ctx: Arc<Context>) {
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<Context>, perms: Vec<Permissions>) {
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] #[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 ctx = Context::new(cfg).unwrap();
let api = command_filter(ctx.clone()); match args.command {
GeoffreyApiCommand::Run => run_server(ctx).await,
warp::serve(api).run(socket_addr).await; GeoffreyApiCommand::CreateAdminToken(perms) => create_token(ctx, perms.permissions),
};
} }

View File

@ -52,5 +52,6 @@ pub struct Tunnel {
pub enum CommandLevel { pub enum CommandLevel {
ALL = 0, ALL = 0,
REGISTERED = 1, REGISTERED = 1,
ADMIN = 2, MOD = 2,
ADMIN = 3,
} }

View File

@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AddLocationParams { pub struct AddLocationParams {
token: u64, pub token: String,
user_id: UserID, pub user_id: UserID,
pub name: String, pub name: String,
pub position: Position, pub position: Position,
pub loc_type: LocationType, pub loc_type: LocationType,
@ -15,8 +15,8 @@ pub struct AddLocationParams {
} }
impl CommandRequest for AddLocationParams { impl CommandRequest for AddLocationParams {
fn token(&self) -> u64 { fn token(&self) -> String {
self.token self.token.clone()
} }
fn user_id(&self) -> Option<UserID> { fn user_id(&self) -> Option<UserID> {

View File

@ -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<Permissions>,
}
impl CommandRequest for AddTokenParams {
fn token(&self) -> String {
self.token.clone()
}
fn user_id(&self) -> Option<UserID> {
None
}
}

View File

@ -3,12 +3,12 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FindParams { pub struct FindParams {
pub token: u64, pub token: String,
pub query: String, pub query: String,
} }
impl CommandRequest for FindParams { impl CommandRequest for FindParams {
fn token(&self) -> u64 { fn token(&self) -> String {
self.token self.token.clone()
} }
} }

View File

@ -1,4 +1,5 @@
pub mod add_location_params; pub mod add_location_params;
pub mod add_token_params;
pub mod find_params; pub mod find_params;
pub mod register_params; pub mod register_params;
@ -10,7 +11,7 @@ use serde::Serialize;
use std::fmt::Debug; use std::fmt::Debug;
pub trait CommandRequest: Serialize + DeserializeOwned + Debug + Clone + Send + 'static { pub trait CommandRequest: Serialize + DeserializeOwned + Debug + Clone + Send + 'static {
fn token(&self) -> u64; fn token(&self) -> String;
fn user_id(&self) -> Option<UserID> { fn user_id(&self) -> Option<UserID> {
None None
} }

View File

@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RegisterParameters { pub struct RegisterParameters {
pub token: u64, pub token: String,
pub new_user_id: UserID, pub new_user_id: UserID,
pub username: String, pub username: String,
} }
impl CommandRequest for RegisterParameters { impl CommandRequest for RegisterParameters {
fn token(&self) -> u64 { fn token(&self) -> String {
self.token self.token.clone()
} }
} }

View File

@ -1,10 +1,32 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub enum GeoffreyAPIError { pub enum GeoffreyAPIError {
PlayerNotRegistered, PlayerNotRegistered,
EntryNotFound, EntryNotFound,
PermissionInsufficient, PermissionInsufficient,
EntryNotUnique, EntryNotUnique,
DatabaseError(String), 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)
}
} }

View File

@ -6,5 +6,8 @@ pub mod api_error;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum APIResponse<T> { pub enum APIResponse<T> {
Response(T), Response(T),
Error(GeoffreyAPIError), Error {
error: GeoffreyAPIError,
msg: String,
},
} }

View File

@ -1,7 +1,9 @@
use crate::GeoffreyDatabaseModel; use crate::GeoffreyDatabaseModel;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
#[derive(Serialize, Deserialize, Debug, Clone, PartialOrd, PartialEq)]
pub enum Permissions { pub enum Permissions {
ModelGet = 0, ModelGet = 0,
ModelPost, ModelPost,
@ -10,12 +12,27 @@ pub enum Permissions {
Admin, Admin,
} }
impl TryFrom<&str> for Permissions {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
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)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Token { pub struct Token {
pub id: Option<u64>, pub id: Option<u64>,
permission: u64, permission: u64,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
pub modified: DateTime<Utc>, pub modified: DateTime<Utc>,
pub secret: String,
} }
impl Token { impl Token {
@ -43,6 +60,7 @@ impl Default for Token {
permission: 0, permission: 0,
created: Utc::now(), created: Utc::now(),
modified: Utc::now(), modified: Utc::now(),
secret: String::new(),
} }
} }
} }