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_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",

View File

@ -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"

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::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<Context>, req: Self::Req, user: Option<Player>) -> Result<Self::Resp>;
fn user_is_authorized(user: &Option<Player>) -> 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<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 {
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<T: Command>(ctx: Arc<Context>, req: T::Req) -> Result<T::R
log::debug!("Request: {:?}", 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),
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))
} else {
let e = reply.err().unwrap();
log::warn!("Got error when processing command: {:?}", e);
warp::reply::json(&APIResponse::<T::Resp>::Error(e))
let msg = e.to_string();
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_models::models::parameters::CommandRequest;
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>> {
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)
}
}
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 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<T> = std::result::Result<T, GeoffreyAPIError>;
@ -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<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]
@ -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),
};
}

View File

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

View File

@ -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<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)]
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()
}
}

View File

@ -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<UserID> {
None
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

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

View File

@ -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<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)]
pub struct Token {
pub id: Option<u64>,
permission: u64,
pub created: DateTime<Utc>,
pub modified: DateTime<Utc>,
pub secret: String,
}
impl Token {
@ -43,6 +60,7 @@ impl Default for Token {
permission: 0,
created: Utc::now(),
modified: Utc::now(),
secret: String::new(),
}
}
}