Add authentication to more handlers

This commit is contained in:
Tyler Hallada 2020-08-01 02:18:31 -04:00
parent eb7706974e
commit d1c933e1ea
13 changed files with 99 additions and 60 deletions

View File

@ -64,6 +64,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));
// TODO make shop_id unique, recover unique_violation
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("owner_id", types::foreign("owners", "id").indexed(true));
t.add_column("ref_list", types::custom("jsonb")); t.add_column("ref_list", types::custom("jsonb"));

View File

@ -48,6 +48,7 @@ pub fn create_shop(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<Shop>()) .and(json_body::<Shop>())
.and(warp::header::optional("api-key"))
.and(with_env(env)) .and(with_env(env))
.and_then(handlers::create_shop) .and_then(handlers::create_shop)
} }
@ -86,6 +87,7 @@ pub fn create_owner(
.and(warp::post()) .and(warp::post())
.and(json_body::<Owner>()) .and(json_body::<Owner>())
.and(warp::addr::remote()) .and(warp::addr::remote())
.and(warp::header::optional("api-key"))
.and(with_env(env)) .and(with_env(env))
.and_then(handlers::create_owner) .and_then(handlers::create_owner)
} }
@ -125,6 +127,7 @@ pub fn create_interior_ref_list(
warp::path::end() warp::path::end()
.and(warp::post()) .and(warp::post())
.and(json_body::<InteriorRefList>()) .and(json_body::<InteriorRefList>())
.and(warp::header::optional("api-key"))
.and(with_env(env)) .and(with_env(env))
.and_then(handlers::create_interior_ref_list) .and_then(handlers::create_interior_ref_list)
} }

View File

@ -1,6 +1,5 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use sqlx::postgres::PgPool;
use std::net::SocketAddr; use std::net::SocketAddr;
use tracing::instrument; use tracing::instrument;
use uuid::Uuid; use uuid::Uuid;
@ -9,26 +8,22 @@ 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::{forbidden_no_api_key, forbidden_no_owner, reject_anyhow}; use super::problem::{reject_anyhow, unauthorized_no_api_key, unauthorized_no_owner};
use super::Environment; use super::Environment;
use crate::caches::Cache;
#[instrument(level = "debug", skip(db, cache, api_key))] #[instrument(level = "debug", skip(env, api_key))]
pub async fn authenticate( pub async fn authenticate(env: &Environment, api_key: Option<Uuid>) -> Result<i32> {
db: &PgPool,
cache: &Cache<Uuid, i32>,
api_key: Option<Uuid>,
) -> Result<i32> {
if let Some(api_key) = api_key { if let Some(api_key) = api_key {
cache env.caches
.owner_ids_by_api_key
.get(api_key, || async { .get(api_key, || async {
Ok( Ok(
sqlx::query!("SELECT id FROM owners WHERE api_key = $1", api_key) sqlx::query!("SELECT id FROM owners WHERE api_key = $1", api_key)
.fetch_one(db) .fetch_one(&env.db)
.await .await
.map_err(|error| { .map_err(|error| {
if let sqlx::Error::RowNotFound = error { if let sqlx::Error::RowNotFound = error {
return forbidden_no_owner(); return unauthorized_no_owner();
} }
anyhow!(error) anyhow!(error)
})? })?
@ -37,8 +32,7 @@ pub async fn authenticate(
}) })
.await .await
} else { } else {
// TODO: this should be 401 status instead Err(unauthorized_no_api_key())
Err(forbidden_no_api_key())
} }
} }
@ -69,10 +63,20 @@ pub async fn list_shops(
.await .await
} }
pub async fn create_shop(shop: Shop, env: Environment) -> Result<impl Reply, Rejection> { pub async fn create_shop(
// TODO: authenticate shop: Shop,
// TODO: return 400 error with message if unique key is violated api_key: Option<Uuid>,
let saved_shop = shop.save(&env.db).await.map_err(reject_anyhow)?; env: Environment,
) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let shop_with_owner_id = Shop {
owner_id: Some(owner_id),
..shop
};
let saved_shop = shop_with_owner_id
.save(&env.db)
.await
.map_err(reject_anyhow)?;
let url = saved_shop.url(&env.api_url).map_err(reject_anyhow)?; let url = saved_shop.url(&env.api_url).map_err(reject_anyhow)?;
let reply = json(&saved_shop); let reply = json(&saved_shop);
let reply = with_header(reply, "Location", url.as_str()); let reply = with_header(reply, "Location", url.as_str());
@ -86,9 +90,7 @@ pub async fn delete_shop(
api_key: Option<Uuid>, api_key: Option<Uuid>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(&env.db, &env.caches.owner_ids_by_api_key, api_key) let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
.await
.map_err(reject_anyhow)?;
Shop::delete(&env.db, owner_id, id) Shop::delete(&env.db, owner_id, id)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
@ -131,23 +133,34 @@ pub async fn list_owners(
pub async fn create_owner( pub async fn create_owner(
owner: Owner, owner: Owner,
remote_addr: Option<SocketAddr>, remote_addr: Option<SocketAddr>,
api_key: Option<Uuid>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
// TODO: authenticate and get api_key from header if let Some(api_key) = api_key {
let owner_with_ip = match remote_addr { let owner_with_ip_and_key = match remote_addr {
Some(addr) => Owner { Some(addr) => Owner {
api_key: Some(api_key),
ip_address: Some(IpNetwork::from(addr.ip())), ip_address: Some(IpNetwork::from(addr.ip())),
..owner ..owner
}, },
None => owner, None => Owner {
api_key: Some(api_key),
..owner
},
}; };
let saved_owner = owner_with_ip.save(&env.db).await.map_err(reject_anyhow)?; let saved_owner = owner_with_ip_and_key
.save(&env.db)
.await
.map_err(reject_anyhow)?;
let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?; let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?;
let reply = json(&saved_owner); let reply = json(&saved_owner);
let reply = with_header(reply, "Location", url.as_str()); let reply = with_header(reply, "Location", url.as_str());
let reply = with_status(reply, StatusCode::CREATED); let reply = with_status(reply, StatusCode::CREATED);
env.caches.list_owners.clear().await; env.caches.list_owners.clear().await;
Ok(reply) Ok(reply)
} else {
Err(reject_anyhow(unauthorized_no_api_key()))
}
} }
pub async fn delete_owner( pub async fn delete_owner(
@ -155,9 +168,7 @@ pub async fn delete_owner(
api_key: Option<Uuid>, api_key: Option<Uuid>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(&env.db, &env.caches.owner_ids_by_api_key, api_key) let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
.await
.map_err(reject_anyhow)?;
Owner::delete(&env.db, owner_id, id) Owner::delete(&env.db, owner_id, id)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
@ -204,10 +215,15 @@ pub async fn list_interior_ref_lists(
pub async fn create_interior_ref_list( pub async fn create_interior_ref_list(
interior_ref_list: InteriorRefList, interior_ref_list: InteriorRefList,
api_key: Option<Uuid>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
// TODO: authenticate let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let saved_interior_ref_list = interior_ref_list let ref_list_with_owner_id = InteriorRefList {
owner_id: Some(owner_id),
..interior_ref_list
};
let saved_interior_ref_list = ref_list_with_owner_id
.save(&env.db) .save(&env.db)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
@ -226,9 +242,7 @@ pub async fn delete_interior_ref_list(
api_key: Option<Uuid>, api_key: Option<Uuid>,
env: Environment, env: Environment,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(&env.db, &env.caches.owner_ids_by_api_key, api_key) let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
.await
.map_err(reject_anyhow)?;
InteriorRefList::delete(&env.db, owner_id, id) InteriorRefList::delete(&env.db, owner_id, id)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;

View File

@ -73,7 +73,7 @@ async fn main() -> Result<()> {
let host = env::var("HOST").expect("`HOST` environment variable not defined"); let host = env::var("HOST").expect("`HOST` environment variable not defined");
let host_url = Url::parse(&host).expect("Cannot parse URL from `HOST` environment variable"); let host_url = Url::parse(&host).expect("Cannot parse URL from `HOST` environment variable");
let api_url = host_url.join("/api/v1")?; let api_url = host_url.join("/api/v1/")?;
let env = Environment::new(api_url).await?; let env = Environment::new(api_url).await?;
let base = warp::path("api").and(warp::path("v1")); let base = warp::path("api").and(warp::path("v1"));

View File

@ -32,7 +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 owner_id: Option<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>,

View File

@ -14,7 +14,7 @@ where
fn pk(&self) -> Option<i32>; fn pk(&self) -> Option<i32>;
fn url(&self, api_url: &Url) -> Result<Url> { fn url(&self, api_url: &Url) -> Result<Url> {
if let Some(pk) = self.pk() { if let Some(pk) = self.pk() {
Ok(api_url.join(&format!("/{}s/{}", Self::resource_name(), pk))?) Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
} else { } else {
Err(anyhow!( Err(anyhow!(
"Cannot get URL for {} with no primary key", "Cannot get URL for {} with no primary key",

View File

@ -15,7 +15,9 @@ use crate::problem::forbidden_permission;
pub struct Owner { pub struct Owner {
pub id: Option<i32>, pub id: Option<i32>,
pub name: String, pub name: String,
pub api_key: Uuid, #[serde(skip_serializing)]
pub api_key: Option<Uuid>,
#[serde(skip_serializing)]
pub ip_address: Option<IpNetwork>, pub ip_address: Option<IpNetwork>,
pub mod_version: String, pub mod_version: String,
pub created_at: Option<NaiveDateTime>, pub created_at: Option<NaiveDateTime>,
@ -40,7 +42,7 @@ impl Model for Owner {
.map_err(Error::new) .map_err(Error::new)
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(self, db))]
async fn save(self, db: &PgPool) -> Result<Self> { async fn save(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,

View File

@ -13,7 +13,7 @@ use crate::problem::forbidden_permission;
pub struct Shop { pub struct Shop {
pub id: Option<i32>, pub id: Option<i32>,
pub name: String, pub name: String,
pub owner_id: i32, pub owner_id: Option<i32>,
pub description: String, pub description: String,
pub is_not_sell_buy: bool, pub is_not_sell_buy: bool,
pub sell_buy_list_id: i32, pub sell_buy_list_id: i32,
@ -41,7 +41,7 @@ impl Model for Shop {
.map_err(Error::new) .map_err(Error::new)
} }
#[instrument(level = "debug", skip(db))] #[instrument(level = "debug", skip(self, db))]
async fn save(self, db: &PgPool) -> Result<Self> { async fn save(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,

View File

@ -11,16 +11,16 @@ pub fn forbidden_permission() -> Error {
) )
} }
pub fn forbidden_no_owner() -> Error { pub fn unauthorized_no_owner() -> Error {
anyhow!( anyhow!(
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,) HttpApiProblem::with_title_and_type_from_status(StatusCode::UNAUTHORIZED,)
.set_detail("Api-Key not recognized") .set_detail("Api-Key not recognized")
) )
} }
pub fn forbidden_no_api_key() -> Error { pub fn unauthorized_no_api_key() -> Error {
anyhow!( anyhow!(
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,) HttpApiProblem::with_title_and_type_from_status(StatusCode::UNAUTHORIZED,)
.set_detail("Api-Key header not present") .set_detail("Api-Key header not present")
) )
} }
@ -42,14 +42,37 @@ pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
db_error.message(), db_error.message(),
db_error.details().unwrap_or("") db_error.details().unwrap_or("")
); );
dbg!(&db_error);
if let Some(code) = db_error.code() { if let Some(code) = db_error.code() {
dbg!(&code);
if let Some(constraint) = db_error.constraint_name() { if let Some(constraint) = db_error.constraint_name() {
dbg!(&constraint);
if code == "23503" && constraint == "shops_owner_id_fkey" { if code == "23503" && constraint == "shops_owner_id_fkey" {
// foreign_key_violation // foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status( return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
) )
.set_detail("Owner does not exist"); .set_detail("Owner does not exist");
} else if code == "23505" && constraint == "owners_api_key_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner with Api-Key already exists");
} else if code == "23505" && constraint == "owners_unique_name_and_api_key"
{
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Duplicate owner with same name and Api-Key exists");
} else if code == "23505" && constraint == "shops_unique_name_and_owner_id"
{
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner already has a shop with that name");
} }
} }
} }

View File

@ -1,5 +1,4 @@
{ {
"name": "Test Owner", "name": "Test Owner",
"api_key": "9972bf37-5154-4e1a-a76e-49c93d7d78df",
"mod_version": "0.0.1" "mod_version": "0.0.1"
} }

View File

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

View File

@ -1,6 +1,5 @@
{ {
"name": "Test Shop", "name": "Test Shop",
"owner_id": 1,
"description": "for testing", "description": "for testing",
"is_not_sell_buy": true, "is_not_sell_buy": true,
"sell_buy_list_id": 1, "sell_buy_list_id": 1,

View File

@ -1,6 +1,5 @@
{ {
"name": "Test Shop", "name": "Test Shop 2",
"owner_id": 2,
"description": "for testing", "description": "for testing",
"is_not_sell_buy": true, "is_not_sell_buy": true,
"sell_buy_list_id": 1, "sell_buy_list_id": 1,