Add authentication to more handlers
This commit is contained in:
parent
eb7706974e
commit
d1c933e1ea
@ -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"));
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)?;
|
||||||
|
@ -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"));
|
||||||
|
@ -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>,
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user