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| {
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("owner_id", types::foreign("owners", "id").indexed(true));
t.add_column("ref_list", types::custom("jsonb"));

View File

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

View File

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

View File

@ -32,7 +32,7 @@ pub struct InteriorRef {
pub struct InteriorRefList {
pub id: Option<i32>,
pub shop_id: i32,
pub owner_id: i32,
pub owner_id: Option<i32>,
pub ref_list: Json<Vec<InteriorRef>>,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,

View File

@ -14,7 +14,7 @@ where
fn pk(&self) -> Option<i32>;
fn url(&self, api_url: &Url) -> Result<Url> {
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 {
Err(anyhow!(
"Cannot get URL for {} with no primary key",

View File

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

View File

@ -13,7 +13,7 @@ use crate::problem::forbidden_permission;
pub struct Shop {
pub id: Option<i32>,
pub name: String,
pub owner_id: i32,
pub owner_id: Option<i32>,
pub description: String,
pub is_not_sell_buy: bool,
pub sell_buy_list_id: i32,
@ -41,7 +41,7 @@ impl Model for Shop {
.map_err(Error::new)
}
#[instrument(level = "debug", skip(db))]
#[instrument(level = "debug", skip(self, db))]
async fn save(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as!(
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!(
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,)
HttpApiProblem::with_title_and_type_from_status(StatusCode::UNAUTHORIZED,)
.set_detail("Api-Key not recognized")
)
}
pub fn forbidden_no_api_key() -> Error {
pub fn unauthorized_no_api_key() -> Error {
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")
)
}
@ -42,14 +42,37 @@ pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
db_error.message(),
db_error.details().unwrap_or("")
);
dbg!(&db_error);
if let Some(code) = db_error.code() {
dbg!(&code);
if let Some(constraint) = db_error.constraint_name() {
dbg!(&constraint);
if code == "23503" && constraint == "shops_owner_id_fkey" {
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.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",
"api_key": "9972bf37-5154-4e1a-a76e-49c93d7d78df",
"mod_version": "0.0.1"
}

View File

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

View File

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

View File

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