Add auth to delete endpoints

This commit is contained in:
Tyler Hallada 2020-07-30 01:09:29 -04:00
parent 79b45551fd
commit 68b04b4f4c
10 changed files with 136 additions and 25 deletions

View File

@ -6,7 +6,7 @@ pub fn migration() -> String {
m.create_table("owners", |t| { m.create_table("owners", |t| {
t.add_column("id", types::primary().indexed(true)); t.add_column("id", types::primary().indexed(true));
t.add_column("name", types::varchar(255)); t.add_column("name", types::varchar(255));
t.add_column("api_key", types::uuid()); t.add_column("api_key", types::uuid().indexed(true));
t.add_column("ip_address", types::custom("inet").nullable(true)); t.add_column("ip_address", types::custom("inet").nullable(true));
t.add_column("mod_version", types::varchar(25)); t.add_column("mod_version", types::varchar(25));
t.add_column("created_at", types::custom("timestamp(3)")); t.add_column("created_at", types::custom("timestamp(3)"));
@ -37,6 +37,7 @@ pub fn migration() -> String {
m.create_table("merchandise", |t| { m.create_table("merchandise", |t| {
t.add_column("id", types::primary().indexed(true)); t.add_column("id", types::primary().indexed(true));
t.add_column("shop_id", types::foreign("shops", "id").indexed(true)); t.add_column("shop_id", types::foreign("shops", "id").indexed(true));
t.add_column("owner_id", types::foreign("owners", "id").indexed(true));
t.add_column("mod_name", types::varchar(255)); t.add_column("mod_name", types::varchar(255));
t.add_column("local_form_id", types::integer()); t.add_column("local_form_id", types::integer());
t.add_column("quantity", types::integer()); t.add_column("quantity", types::integer());
@ -51,6 +52,7 @@ pub fn migration() -> String {
m.create_table("transactions", |t| { m.create_table("transactions", |t| {
t.add_column("id", types::primary().indexed(true)); t.add_column("id", types::primary().indexed(true));
t.add_column("shop_id", types::foreign("shops", "id").indexed(true)); t.add_column("shop_id", types::foreign("shops", "id").indexed(true));
t.add_column("owner_id", types::foreign("owners", "id").indexed(true));
t.add_column("merchandise_id", types::foreign("merchandise", "id")); t.add_column("merchandise_id", types::foreign("merchandise", "id"));
t.add_column("customer_name", types::varchar(255)); t.add_column("customer_name", types::varchar(255));
t.add_column("is_customer_npc", types::boolean()); t.add_column("is_customer_npc", types::boolean());
@ -63,6 +65,7 @@ pub fn migration() -> String {
m.create_table("interior_ref_lists", |t| { m.create_table("interior_ref_lists", |t| {
t.add_column("id", types::primary().indexed(true)); t.add_column("id", types::primary().indexed(true));
t.add_column("shop_id", types::foreign("shops", "id").indexed(true)); t.add_column("shop_id", types::foreign("shops", "id").indexed(true));
t.add_column("owner_id", types::foreign("owners", "id").indexed(true));
t.add_column("ref_list", types::custom("jsonb")); t.add_column("ref_list", types::custom("jsonb"));
t.add_column("created_at", types::custom("timestamp(3)")); t.add_column("created_at", types::custom("timestamp(3)"));
t.add_column("updated_at", types::custom("timestamp(3)")); t.add_column("updated_at", types::custom("timestamp(3)"));

View File

@ -56,6 +56,7 @@ pub fn delete_shop(
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path::param() warp::path::param()
.and(warp::delete()) .and(warp::delete())
.and(warp::header::optional("api-key"))
.and(with_env(env)) .and(with_env(env))
.and_then(handlers::delete_shop) .and_then(handlers::delete_shop)
} }
@ -91,6 +92,7 @@ pub fn delete_owner(
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path::param() warp::path::param()
.and(warp::delete()) .and(warp::delete())
.and(warp::header::optional("api-key"))
.and(with_env(env)) .and(with_env(env))
.and_then(handlers::delete_owner) .and_then(handlers::delete_owner)
} }
@ -127,6 +129,7 @@ pub fn delete_interior_ref_list(
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path::param() warp::path::param()
.and(warp::delete()) .and(warp::delete())
.and(warp::header::optional("api-key"))
.and(with_env(env)) .and(with_env(env))
.and_then(handlers::delete_interior_ref_list) .and_then(handlers::delete_interior_ref_list)
} }

View File

@ -1,13 +1,35 @@
use anyhow::{anyhow, Result};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use sqlx::postgres::PgPool;
use std::net::SocketAddr; use std::net::SocketAddr;
use uuid::Uuid;
use warp::http::StatusCode; use warp::http::StatusCode;
use warp::reply::{json, with_header, with_status}; use warp::reply::{json, with_header, with_status};
use warp::{Rejection, Reply}; use warp::{Rejection, Reply};
use super::models::{InteriorRefList, ListParams, Model, Owner, Shop}; use super::models::{InteriorRefList, ListParams, Model, Owner, Shop};
use super::problem::reject_anyhow; use super::problem::{forbidden_no_api_key, forbidden_no_owner, reject_anyhow};
use super::Environment; use super::Environment;
pub async fn authenticate(api_key: Option<Uuid>, db: &PgPool) -> Result<i32> {
if let Some(api_key) = api_key {
Ok(
sqlx::query!("SELECT id FROM owners WHERE api_key = $1", api_key)
.fetch_one(db)
.await
.map_err(|error| {
if let sqlx::Error::RowNotFound = error {
return forbidden_no_owner();
}
anyhow!(error)
})?
.id,
)
} else {
Err(forbidden_no_api_key())
}
}
pub async fn get_shop(id: i32, env: Environment) -> Result<impl Reply, Rejection> { pub async fn get_shop(id: i32, env: Environment) -> Result<impl Reply, Rejection> {
let shop = Shop::get(&env.db, id).await.map_err(reject_anyhow)?; let shop = Shop::get(&env.db, id).await.map_err(reject_anyhow)?;
let reply = json(&shop); let reply = json(&shop);
@ -36,8 +58,18 @@ pub async fn create_shop(shop: Shop, env: Environment) -> Result<impl Reply, Rej
Ok(reply) Ok(reply)
} }
pub async fn delete_shop(id: i32, env: Environment) -> Result<impl Reply, Rejection> { pub async fn delete_shop(
Shop::delete(&env.db, id).await.map_err(reject_anyhow)?; id: i32,
api_key: Option<Uuid>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(api_key, &env.db)
.await
.map_err(reject_anyhow)?;
dbg!(owner_id);
Shop::delete(&env.db, owner_id, id)
.await
.map_err(reject_anyhow)?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@ -80,8 +112,18 @@ pub async fn create_owner(
Ok(reply) Ok(reply)
} }
pub async fn delete_owner(id: i32, env: Environment) -> Result<impl Reply, Rejection> { pub async fn delete_owner(
Owner::delete(&env.db, id).await.map_err(reject_anyhow)?; id: i32,
api_key: Option<Uuid>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(api_key, &env.db)
.await
.map_err(reject_anyhow)?;
dbg!(owner_id);
Owner::delete(&env.db, owner_id, id)
.await
.map_err(reject_anyhow)?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@ -123,8 +165,16 @@ pub async fn create_interior_ref_list(
Ok(reply) Ok(reply)
} }
pub async fn delete_interior_ref_list(id: i32, env: Environment) -> Result<impl Reply, Rejection> { pub async fn delete_interior_ref_list(
InteriorRefList::delete(&env.db, id) id: i32,
api_key: Option<Uuid>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(api_key, &env.db)
.await
.map_err(reject_anyhow)?;
dbg!(owner_id);
InteriorRefList::delete(&env.db, owner_id, id)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)

View File

@ -8,6 +8,7 @@ use tracing::instrument;
use super::ListParams; use super::ListParams;
use super::Model; use super::Model;
use crate::problem::forbidden_permission;
// sqlx queries for this model need to be `query_as_unchecked!` because `query_as!` does not // sqlx queries for this model need to be `query_as_unchecked!` because `query_as!` does not
// support user-defined types (`ref_list` Json field). // support user-defined types (`ref_list` Json field).
@ -31,6 +32,7 @@ pub struct InteriorRef {
pub struct InteriorRefList { pub struct InteriorRefList {
pub id: Option<i32>, pub id: Option<i32>,
pub shop_id: i32, pub shop_id: i32,
pub owner_id: i32,
pub ref_list: Json<Vec<InteriorRef>>, pub ref_list: Json<Vec<InteriorRef>>,
pub created_at: Option<NaiveDateTime>, pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
@ -64,10 +66,11 @@ impl Model for InteriorRefList {
Ok(sqlx::query_as_unchecked!( Ok(sqlx::query_as_unchecked!(
Self, Self,
"INSERT INTO interior_ref_lists "INSERT INTO interior_ref_lists
(shop_id, ref_list, created_at, updated_at) (shop_id, owner_id, ref_list, created_at, updated_at)
VALUES ($1, $2, now(), now()) VALUES ($1, $2, $3, now(), now())
RETURNING *", RETURNING *",
self.shop_id, self.shop_id,
self.owner_id,
self.ref_list, self.ref_list,
) )
.fetch_one(db) .fetch_one(db)
@ -75,12 +78,20 @@ impl Model for InteriorRefList {
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, id: i32) -> Result<u64> { async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
Ok( let interior_ref_list =
sqlx::query!("SELECT owner_id FROM interior_ref_lists WHERE id = $1", id)
.fetch_one(db)
.await?;
if interior_ref_list.owner_id == owner_id {
return Ok(
sqlx::query!("DELETE FROM interior_ref_lists WHERE id = $1", id) sqlx::query!("DELETE FROM interior_ref_lists WHERE id = $1", id)
.execute(db) .execute(db)
.await?, .await?,
) );
} else {
return Err(forbidden_permission());
}
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]

View File

@ -24,6 +24,6 @@ where
} }
async fn get(db: &PgPool, id: i32) -> Result<Self>; async fn get(db: &PgPool, id: i32) -> Result<Self>;
async fn save(self, db: &PgPool) -> Result<Self>; async fn save(self, db: &PgPool) -> Result<Self>;
async fn delete(db: &PgPool, id: i32) -> Result<u64>; async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64>;
async fn list(db: &PgPool, list_params: ListParams) -> Result<Vec<Self>>; async fn list(db: &PgPool, list_params: ListParams) -> Result<Vec<Self>>;
} }

View File

@ -9,6 +9,7 @@ use uuid::Uuid;
use super::ListParams; use super::ListParams;
use super::Model; use super::Model;
use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Owner { pub struct Owner {
@ -58,10 +59,17 @@ impl Model for Owner {
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, id: i32) -> Result<u64> { async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
let owner = sqlx::query!("SELECT id FROM owners WHERE id = $1", id)
.fetch_one(db)
.await?;
if owner.id == owner_id {
Ok(sqlx::query!("DELETE FROM owners WHERE id = $1", id) Ok(sqlx::query!("DELETE FROM owners WHERE id = $1", id)
.execute(db) .execute(db)
.await?) .await?)
} else {
return Err(forbidden_permission());
}
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]

View File

@ -7,6 +7,7 @@ use tracing::instrument;
use super::ListParams; use super::ListParams;
use super::Model; use super::Model;
use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Shop { pub struct Shop {
@ -63,10 +64,17 @@ impl Model for Shop {
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, id: i32) -> Result<u64> { async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
Ok(sqlx::query!("DELETE FROM shops WHERE id = $1", id) let shop = sqlx::query!("SELECT owner_id FROM shops WHERE id = $1", id)
.fetch_one(db)
.await?;
if shop.owner_id == owner_id {
return Ok(sqlx::query!("DELETE FROM shops WHERE shops.id = $1", id)
.execute(db) .execute(db)
.await?) .await?);
} else {
return Err(forbidden_permission());
}
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(db))]

View File

@ -1,8 +1,30 @@
use anyhow::{anyhow, Error};
use http_api_problem::HttpApiProblem; use http_api_problem::HttpApiProblem;
use tracing::error; use tracing::error;
use warp::http::StatusCode; use warp::http::StatusCode;
use warp::{reject, Rejection, Reply}; use warp::{reject, Rejection, Reply};
pub fn forbidden_permission() -> Error {
anyhow!(
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,)
.set_detail("Api-Key does not have required permissions")
)
}
pub fn forbidden_no_owner() -> Error {
anyhow!(
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,)
.set_detail("Api-Key not recognized")
)
}
pub fn forbidden_no_api_key() -> Error {
anyhow!(
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,)
.set_detail("Api-Key header not present")
)
}
pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem { pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
let error = match error.downcast::<HttpApiProblem>() { let error = match error.downcast::<HttpApiProblem>() {
Ok(problem) => return problem, Ok(problem) => return problem,

View File

@ -1,5 +1,6 @@
{ {
"shop_id": 1, "shop_id": 1,
"owner_id": 1,
"ref_list": [ "ref_list": [
{ {
"mod_name": "Skyrim.esm", "mod_name": "Skyrim.esm",

5
test_data/owner2.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Test Owner 2",
"api_key": "84f03de0-225f-44c1-894d-d5a8c2f74803",
"mod_version": "0.0.1"
}