diff --git a/Cargo.toml b/Cargo.toml index 7e185ae..c2543f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,5 @@ mongodb-cursor-pagination = "0.3.2" tonic = "0.8" json = "0.12.4" log = "0.4.20" -simple_logger = "4.3.3" \ No newline at end of file +simple_logger = "4.3.3" +serde_json = "1.0.113" diff --git a/src/authentication.rs b/src/authentication.rs new file mode 100644 index 0000000..c48ee3a --- /dev/null +++ b/src/authentication.rs @@ -0,0 +1,79 @@ +use async_graphql::{Context, Error, Result}; +use axum::http::HeaderMap; +use bson::Uuid; +use serde::Deserialize; + +/// Authorized-User HTTP header. +#[derive(Deserialize, Debug)] +pub struct AuthorizedUserHeader { + id: Uuid, + roles: Vec, +} + +/// Extraction of AuthorizedUserHeader from HeaderMap. +impl TryFrom<&HeaderMap> for AuthorizedUserHeader { + type Error = Error; + + /// Tries to extract the AuthorizedUserHeader from a HeaderMap. + /// + /// Returns a GraphQL Error if the extraction fails. + fn try_from(header_map: &HeaderMap) -> Result { + if let Some(authenticate_user_header_value) = header_map.get("Authorized-User") { + if let Ok(authenticate_user_header_str) = authenticate_user_header_value.to_str() { + let authenticate_user_header: AuthorizedUserHeader = + serde_json::from_str(authenticate_user_header_str)?; + return Ok(authenticate_user_header); + } + } + Err(Error::new("Authorized-User header could not be parsed.")) + } +} + +/// Role of user. +#[derive(Deserialize, Debug, PartialEq, Clone, Copy)] +#[serde(rename_all = "snake_case")] +enum Role { + Buyer, + Admin, + Employee, +} + +impl Role { + /// Defines if user has a permissive role. + fn is_permissive(self) -> bool { + match self { + Self::Buyer => false, + Self::Admin => true, + Self::Employee => true, + } + } +} + +/// Authenticate user of UUID for a Context. +pub fn authenticate_user(ctx: &Context, id: Uuid) -> Result<()> { + match ctx.data::() { + Ok(authenticate_user_header) => check_permissions(&authenticate_user_header, id), + Err(_) => Err(Error::new("Authorized-User header could not be parsed.")), + } +} + +/// Check if user of UUID has a valid permission according to the AuthorizedUserHeader. +/// +/// Permission is valid if the user has `Role::Buyer` and the same UUID as provided in the function parameter. +/// Permission is valid if the user has a permissive role: `user.is_permissive() == true`, regardless of the users UUID. +pub fn check_permissions(authenticate_user_header: &AuthorizedUserHeader, id: Uuid) -> Result<()> { + if authenticate_user_header + .roles + .iter() + .any(|r| r.is_permissive()) + || authenticate_user_header.id == id + { + return Ok(()); + } else { + let message = format!( + "Authentication failed for user of UUID: `{}`. Operation not permitted.", + authenticate_user_header.id + ); + return Err(Error::new(message)); + } +} diff --git a/src/http_event_service.rs b/src/http_event_service.rs index b4b180e..abca096 100644 --- a/src/http_event_service.rs +++ b/src/http_event_service.rs @@ -18,7 +18,7 @@ pub struct Pubsub { /// Reponse data to send to Dapr when receiving an event. #[derive(Serialize)] pub struct TopicEventResponse { - pub status: i32, + pub status: u8, } /// Default status is `0` -> Ok, according to Dapr specs. diff --git a/src/main.rs b/src/main.rs index 805ac65..bcd61cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,11 @@ use std::{collections::HashSet, env, fs::File, io::Write}; use async_graphql::{ extensions::Logger, http::GraphiQLSource, EmptySubscription, SDLExportOptions, Schema, }; -use async_graphql_axum::GraphQL; +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use authentication::AuthorizedUserHeader; use axum::{ + extract::State, + http::HeaderMap, response::{self, IntoResponse}, routing::{get, post}, Router, Server, @@ -29,6 +32,8 @@ use mutation::Mutation; mod user; use user::User; +mod authentication; + mod http_event_service; use http_event_service::{list_topic_subscriptions, on_topic_event, HttpEventServiceState}; @@ -119,6 +124,22 @@ async fn main() -> std::io::Result<()> { Ok(()) } +/// Describes the handler for GraphQL requests. +/// +/// Parses the "Authenticate-User" header and writes it in the context data of the specfic request. +/// Then executes the GraphQL schema with the request. +async fn graphql_handler( + State(schema): State>, + headers: HeaderMap, + req: GraphQLRequest, +) -> GraphQLResponse { + let mut req = req.into_inner(); + if let Ok(authenticate_user_header) = AuthorizedUserHeader::try_from(&headers) { + req = req.data(authenticate_user_header); + } + schema.execute(req).await.into() +} + /// Starts shoppingcart service on port 8000. async fn start_service() { let client = db_connection().await; @@ -130,7 +151,9 @@ async fn start_service() { .enable_federation() .finish(); - let graphiql = Router::new().route("/", get(graphiql).post_service(GraphQL::new(schema))); + let graphiql = Router::new() + .route("/", get(graphiql).post(graphql_handler)) + .with_state(schema); let dapr_router = build_dapr_router(db_client).await; let app = Router::new().merge(graphiql).merge(dapr_router); diff --git a/src/mutation.rs b/src/mutation.rs index f24f6db..095f308 100644 --- a/src/mutation.rs +++ b/src/mutation.rs @@ -8,12 +8,14 @@ use mongodb::{ Collection, Database, }; -use crate::mutation_input_structs::ShoppingCartItemInput; -use crate::mutation_input_structs::UpdateShoppingCartItemInput; use crate::query::query_shoppingcart_item; use crate::query::query_shoppingcart_item_by_product_variant_id_and_user_id; use crate::shoppingcart_item::ShoppingCartItem; -use crate::{mutation_input_structs::AddShoppingCartItemInput, query::query_user}; +use crate::{authentication::authenticate_user, mutation_input_structs::ShoppingCartItemInput}; +use crate::{mutation_input_structs::CreateShoppingCartItemInput, query::query_user}; +use crate::{ + mutation_input_structs::UpdateShoppingCartItemInput, query::query_shoppingcart_item_user, +}; use crate::user::User; use crate::{ @@ -34,7 +36,8 @@ impl Mutation { ctx: &Context<'a>, #[graphql(desc = "UpdateShoppingCartInput")] input: UpdateShoppingCartInput, ) -> Result { - let db_client = ctx.data_unchecked::(); + authenticate_user(&ctx, input.id)?; + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); let product_variant_collection: Collection = db_client.collection::("product_variants"); @@ -53,12 +56,13 @@ impl Mutation { /// Adds shoppingcart item to a shopping cart. /// /// Queries for existing item, otherwise adds new shoppingcart item. - async fn add_shoppingcart_item<'a>( + async fn create_shoppingcart_item<'a>( &self, ctx: &Context<'a>, - #[graphql(desc = "AddShoppingCartItemInput")] input: AddShoppingCartItemInput, + #[graphql(desc = "CreateShoppingCartItemInput")] input: CreateShoppingCartItemInput, ) -> Result { - let db_client = ctx.data_unchecked::(); + authenticate_user(&ctx, input.id)?; + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); let product_variant_collection: Collection = db_client.collection::("product_variants"); @@ -85,8 +89,10 @@ impl Mutation { ctx: &Context<'a>, #[graphql(desc = "UpdateShoppingCartItemInput")] input: UpdateShoppingCartItemInput, ) -> Result { - let db_client = ctx.data_unchecked::(); + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); + let user = query_shoppingcart_item_user(&collection, input.id).await?; + authenticate_user(&ctx, user._id)?; if let Err(_) = collection .update_one( doc! {"shoppingcart.internal_shoppingcart_items._id": input.id }, @@ -111,8 +117,10 @@ impl Mutation { ctx: &Context<'a>, #[graphql(desc = "UUID of shoppingcart item to delete.")] id: Uuid, ) -> Result { - let db_client = ctx.data_unchecked::(); + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); + let user = query_shoppingcart_item_user(&collection, id).await?; + authenticate_user(&ctx, user._id)?; if let Err(_) = collection .update_one( doc! {"shoppingcart.internal_shoppingcart_items._id": id }, @@ -203,10 +211,10 @@ async fn validate_shopping_cart_items( /// Adds shoppingcart item to MongoDB collection. /// /// * `collection` - MongoDB collection to add the shoppingcart item to. -/// * `input` - `AddShoppingCartItemInput`. +/// * `input` - `CreateShoppingCartItemInput`. async fn add_shoppingcart_item_to_monogdb( collection: &Collection, - input: AddShoppingCartItemInput, + input: CreateShoppingCartItemInput, ) -> Result { let current_timestamp = DateTime::now(); let shoppingcart_item = ShoppingCartItem { diff --git a/src/mutation_input_structs.rs b/src/mutation_input_structs.rs index 82e4ce0..6fbd4a1 100644 --- a/src/mutation_input_structs.rs +++ b/src/mutation_input_structs.rs @@ -19,7 +19,7 @@ pub struct ShoppingCartItemInput { } #[derive(SimpleObject, InputObject)] -pub struct AddShoppingCartItemInput { +pub struct CreateShoppingCartItemInput { /// UUID of user owning the shopping cart. pub id: Uuid, /// ShoppingCartItem in shoppingcart to update diff --git a/src/query.rs b/src/query.rs index a234176..947ee94 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,11 +1,12 @@ -use crate::{shoppingcart_item::ShoppingCartItem, user::User, ShoppingCart}; +use crate::{ + authentication::authenticate_user, shoppingcart_item::ShoppingCartItem, user::User, + ShoppingCart, +}; use async_graphql::{Context, Error, Object, Result}; use bson::Uuid; use mongodb::{bson::doc, options::FindOneOptions, Collection, Database}; -use serde::{Deserialize, Serialize}; - /// Describes GraphQL shoppingcart queries. pub struct Query; @@ -18,7 +19,7 @@ impl Query { ctx: &Context<'a>, #[graphql(desc = "UUID of user to retrieve.")] id: Uuid, ) -> Result { - let db_client = ctx.data_unchecked::(); + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); query_user(&collection, id).await } @@ -29,9 +30,11 @@ impl Query { ctx: &Context<'a>, #[graphql(desc = "UUID of shoppingcart to retrieve.")] id: Uuid, ) -> Result { - let db_client = ctx.data_unchecked::(); + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); - query_shoppingcart_item(&collection, id).await + let user = query_shoppingcart_item_user(&collection, id).await?; + authenticate_user(&ctx, user._id)?; + project_user_to_shopping_cart_item(user) } /// Entity resolver for shoppingcart item of specific id. @@ -41,9 +44,10 @@ impl Query { ctx: &Context<'a>, #[graphql(key, desc = "UUID of shoppingcart to retrieve.")] id: Uuid, ) -> Result { - let db_client = ctx.data_unchecked::(); + let db_client = ctx.data::()?; let collection: Collection = db_client.collection::("users"); - query_shoppingcart_item(&collection, id).await + let user = query_shoppingcart_item_user(&collection, id).await?; + project_user_to_shopping_cart_item(user) } } @@ -56,49 +60,32 @@ pub async fn query_shoppingcart(collection: &Collection, id: Uuid) -> Resu Ok(maybe_user) => match maybe_user { Some(user) => Ok(user.shoppingcart), None => { - let message = format!("ShoppingCart with UUID id: `{}` not found.", id); + let message = format!("ShoppingCart with UUID: `{}` not found.", id); Err(Error::new(message)) } }, Err(_) => { - let message = format!("ShoppingCart with UUID id: `{}` not found.", id); + let message = format!("ShoppingCart with UUID: `{}` not found.", id); Err(Error::new(message)) } } } -/// Helper struct for MongoDB projection. -#[derive(Serialize, Deserialize)] -struct ProjectedShoppingCart { - #[serde(rename = "shoppingcart")] - projected_inner_shoppingcart: ProjectedInnerShoppingCart, -} - -/// Helper struct for MongoDB projection. -#[derive(Serialize, Deserialize)] -struct ProjectedInnerShoppingCart { - internal_shoppingcart_items: Vec, -} - /// Shared function to query a shoppingcart item from a MongoDB collection of users. +/// Returns User which only contains the queried shoppingcart item. /// /// * `connection` - MongoDB database connection. /// * `id` - UUID of shoppingcart item. -/// -/// Specifies options with projection. -pub async fn query_shoppingcart_item( - collection: &Collection, - id: Uuid, -) -> Result { +pub async fn query_shoppingcart_item_user(collection: &Collection, id: Uuid) -> Result { let find_options = FindOneOptions::builder() .projection(Some(doc! { "shoppingcart.internal_shoppingcart_items.$": 1, - "_id": 0 + "shoppingcart.last_updated_at": 1, + "_id": 1 })) .build(); - let projected_collection = collection.clone_with_type::(); - let message = format!("ShoppingCartItem of UUID id: `{}` not found.", id); - match projected_collection + let message = format!("ShoppingCartItem of UUID: `{}` not found.", id); + match collection .find_one( doc! {"shoppingcart.internal_shoppingcart_items": { "$elemMatch": { @@ -109,39 +96,70 @@ pub async fn query_shoppingcart_item( ) .await { - Ok(maybe_shoppingcart_projection) => maybe_shoppingcart_projection - .and_then(|projection| { - projection - .projected_inner_shoppingcart - .internal_shoppingcart_items - .first() - .cloned() - }) - .ok_or_else(|| Error::new(message.clone())), - Err(_) => Err(Error::new(message)), + Ok(maybe_user) => maybe_user.ok_or(Error::new(message.clone())), + Err(e) => Err(e.into()), } } -/// Shared function to query a shoppingcart item from a MongoDB collection of users. +/// Projects result of shoppingcart item query, which is of type User, to the contained ShoppingCartItem. +pub fn project_user_to_shopping_cart_item(user: User) -> Result { + let message = format!("Projection failed, shoppingcart item could not be extracted from user."); + user.shoppingcart + .internal_shoppingcart_items + .iter() + .next() + .cloned() + .ok_or(Error::new(message.clone())) +} + +/// Queries shoppingcart item user and applies projection directly. /// /// * `connection` - MongoDB database connection. /// * `id` - UUID of user. +pub async fn query_shoppingcart_item( + collection: &Collection, + id: Uuid, +) -> Result { + let user = query_shoppingcart_item_user(&collection, id).await?; + project_user_to_shopping_cart_item(user) +} + +/// Queries shoppingcart item user and applies projection directly. /// -/// Specifies options with projection. +/// * `connection` - MongoDB database connection. +/// * `id` - UUID of user. pub async fn query_shoppingcart_item_by_product_variant_id_and_user_id( collection: &Collection, product_variant_id: Uuid, user_id: Uuid, ) -> Result { + let user = query_shoppingcart_item_user_by_product_variant_id_and_user_id( + &collection, + product_variant_id, + user_id, + ) + .await?; + project_user_to_shopping_cart_item(user) +} + +/// Shared function to query a shoppingcart item from a MongoDB collection of users. +/// Returns User which only contains the queried shoppingcart item. +/// +/// * `connection` - MongoDB database connection. +/// * `id` - UUID of user. +pub async fn query_shoppingcart_item_user_by_product_variant_id_and_user_id( + collection: &Collection, + product_variant_id: Uuid, + user_id: Uuid, +) -> Result { let find_options = FindOneOptions::builder() .projection(Some(doc! { "shoppingcart.internal_shoppingcart_items.$": 1, "_id": 0 })) .build(); - let projected_collection = collection.clone_with_type::(); let message = format!("ShoppingCartItem referencing product variant of UUID: `{}` in shopping cart of user with UUID: `{}` not found.", product_variant_id, user_id); - match projected_collection + match collection .find_one( doc! {"_id": user_id, "shoppingcart.internal_shoppingcart_items": { "$elemMatch": { @@ -152,15 +170,7 @@ pub async fn query_shoppingcart_item_by_product_variant_id_and_user_id( ) .await { - Ok(maybe_shoppingcart_projection) => maybe_shoppingcart_projection - .and_then(|projection| { - projection - .projected_inner_shoppingcart - .internal_shoppingcart_items - .first() - .cloned() - }) - .ok_or_else(|| Error::new(message.clone())), + Ok(maybe_user) => maybe_user.ok_or(Error::new(message.clone())), Err(_) => Err(Error::new(message)), } } @@ -174,12 +184,12 @@ pub async fn query_user(collection: &Collection, id: Uuid) -> Result Ok(maybe_user) => match maybe_user { Some(user) => Ok(user), None => { - let message = format!("User with UUID id: `{}` not found.", id); + let message = format!("User with UUID: `{}` not found.", id); Err(Error::new(message)) } }, Err(_) => { - let message = format!("User with UUID id: `{}` not found.", id); + let message = format!("User with UUID: `{}` not found.", id); Err(Error::new(message)) } }